C语言是一种编译型语言,需要把源文件进行编译之后才能运行,它的编译过程是:
预处理:展开头文件、宏替换,去掉注释,条件编译;
编译:检查语法,生成汇编;
汇编:把生成的汇编文件汇编成机器码;
链接:链接到一起生成可执行程序
预编译
处理所有的注释,以空格代替
将所有的 #define 删除,并且展开所有的宏定义
处理条件编译指令 #if, #ifdef, #elif,#else,#endif
处理 #include,展开被包含的文件
保留编译器需要使用的 #pragma 指令
预处理指令示例:gcc -E file.c -o file.i
编译
对预处理文件进行词法分析,语法分析和语义分析
词法分析:分析关键字,标示符,立即数等是否合法
语法分析:分析表达式是否遵循语法规则
语义分析:在语法分析的基础上进一步分析表达式是否合法
分析结束后进行代码优化生成相应的汇编代码文件
编译指令示例:gcc -S file.i -o file.s
汇编
汇编器将汇编代码转变为机器的可以执行指令
每条汇编语句几乎都对应一条机器指令
汇编指令示例:gcc -c file.s -o file.o
链接
链接生成.out文件,Linux的.out是由gcc编译生成的二进制格式文件
//demo.h
/*
This is a header file.
*/
char* p = "Autumn";
int i = 0;
//demo.c
#include "demo.h"
// Begin to define macro
#define GREETING "Hello world!"
#define INC(x) x++
// End
int main()
{
p = GREETING;
INC(i);
return 0;
}
//输入 gcc -E demo.c -o demo.i,然后就生成了 demo.i 文件,如下:
# 1 "demo.c"
# 1 ""
# 1 ""
# 1 "demo.c"
# 1 "demo.h" 1
# 9 "demo.h"
char* p = "Autumn";
int i = 0;
# 2 "demo.c" 2
# 11 "demo.c"
int main()
{
p = "Hello world!";
i++;
return 0;
}
输入gcc -S demo.i -o demo.s,就生成了 demo.o 文件,如下:
.file "demo.c"
.globl p
.section .rodata
.LC0:
.string "Autumn"
.data
.align 4
.type p, @object
.size p, 4
p:
.long .LC0
.globl i
.bss
.align 4
.type i, @object
.size i, 4
i:
.zero 4
.section .rodata
.LC1:
.string "Hello world!"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
movl $.LC1, p
movl i, %eax
addl $1, %eax
movl %eax, i
movl $0, %eax
popl %ebp
ret
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.4.4-14ubuntu5.1) 4.4.5"
.section .note.GNU-stack,"",@progbits
最后输入gcc -c demo.s -o demo.o,这样就生成了一个 demo.o 文件
最后链接器出场了,输入 gcc demo.o,这样就生成一个 a.out 文件:
许多脚本是相当的简单的.
可能的最简单的脚本只含有一个命令: ‘SECTIONS’. 你可以使用’SECTIONS’来描述输出文件的内存布局.
‘SECTIONS’是一个功能很强大的命令. 这里这们会描述一个很简单的使用. 让我们假设你的程序只有代码节, 初始化过的数据节, 和未初始化过的数据节. 这些会存在于’.text’,‘.data’和’.bss’节, 另外, 让我们进一步假设在你的输入文件中只有这些节.
对于这个例子, 我们说代码应当被载入到地址’0x10000’处, 而数据应当从0x8000000处开始. 下面是一个实现这个功能的脚本:
SECTIONS
{
. = 0x10000; #在’SECTIONS’命令的开始处, 定位计数器拥有值’0’,这里重新将定位计数器的值赋为0x10000
.text : { *(.text) } #所有名为XXX.text的数据节输出
. = 0x8000000; #这里重新将定位计数器的值赋为0x8000000
.data : { *(.data) } #所有名为XXX.data的数据节输出
.bss : { *(.bss) } #所有名为XXX.bss的数据节输出
}
你使用关键字’SECTIONS’写了这个SECTIONS命令, 后面跟有一串放在花括号中的符号赋值和输出节描述的内容.
上例中, 在’SECTIONS’命令中的第一行是对一个特殊的符号’.‘赋值, 这是一个定位计数器. 如果你没有以其它的方式指定输出节的地址(其他方式在后面会描述), 那地址值就会被设为定位计数器的现有值. 定位计数器然后被加上输出节的尺寸. 在’SECTIONS’命令的开始处, 定位计数器拥有值’0’.
第二行定义一个输出节,‘.text’. 冒号是语法需要,现在可以被忽略. 节名后面的花括号中,你列出所有应当被放入到这个输出节中的输入节的名字. '‘是一个通配符,匹配任何文件名. 表达式’(.text)‘意思是所有的输入文件中的’.text’输入节.
因为当输出节’.text’定义的时候, 定位计数器的值是’0x10000’,连接器会把输出文件中的’.text’节的地址设为’0x10000’.
余下的内容定义了输出文件中的’.data’节和’.bss’节. 连接器会把’.data’输出节放到地址’0x8000000’处. 连接器放好’.data’输出节之后, 定位计数器的值是’0x8000000’加上’.data’输出节的长度. 得到的结果是连接器会把’.bss’输出节放到紧接’.data’节后面的位置.
连接器会通过在必要时增加定位计数器的值来保证每一个输出节具有它所需的对齐. 在这个例子中, 为’.text’和’.data’节指定的地址会满足对齐约束, 但是连接器可能会需要在’.data’和’.bss’节之间创建一个小的缺口.
就这样,这是一个简单但完整的连接脚本.
每个连接都被一个’连接脚本’所控制. 这个脚本是用连接命令语言书写的
更多命令和详情
指定入口地址,入口地址就是进程执行的第一条用户空间的指令再进程地址空间中的地址,被指定再ELF文件头的ELF32_Ehdr的e_entry成员中。
ld有多种指定程序入口方式:(优先级逐渐降低)
ld -e
entry()
_start符号
.text
0
将文件filename 作为链接过程中的第一个输入文件。
将路径path加入到ld链接器的库查找目录。 ld会根据指定的目录去查找相应的库。
将指定文件作为链接过程中的输入文件
将指定文件包含进本链接脚本。
在链接脚本中定义某个符号。该符号再程序中可以被引用,起始前文提到的特殊符号都是由系统默认的链接脚本通过PROVIDE 命令定义在脚本中的
SECTIONS
{
secname : {contents}
}
secname 表示输出段的段名,后面必须有一个空格,使得输出段名没有歧义,后面根一个冒号和一对大括号。
有一个特殊的段名/DISCARD/如果使用这个名字作为输出,那么所有符合条件的段都丢弃
contents描述了一套规则和条件,表示符合这种条件的输入段合并到输出段中。输出段名必须符号输出文件的要求,
例如:a.out 输出段名不可以是.text.data .bss 之外的名字,因为.a.out 规定只允许这三个名。
contents 规则包含若干个条件,每个条件用空格分开,如果输入段符合任一条件,就表示符合cotents规则
输入规则 :filename(sections),即:所选文件(所选段)
ENTRY(nomain) //指定了程序的入口地址
SECTIONS //链接脚本的主体
{
. = 0x08048000 + SIZEOF_HEADERS; //将当前虚拟地址设置为0x08048000 + sizeof_headers. 其中 “.”表示当前虚拟地址SIZEOF_HEADERS 表示文件的文件头大小
tinytext : {*(.text) *(.data) *(.rodata)} //将所有输入文件中的三个段合并为输出文件中的 tinytext段。
/DISCARD/ : {*(.comment)} //将输入文件的comment段丢弃
}