程序编译的过程
#include<stdio.h>
int main(){
printf("Hello, World!\n");
return 0;
}
gcc hello.c
./a.out
- 如上发生了四个过程:预处理、编译、汇编、链接
预处理
gcc -E hello.c -o hello.i
或者
cpp hello.c->hello.i
- 文件后缀:.c==>.i
- 处理过程:
- 将所有的
#define
删除并进行宏展开; - 处理所有的条件预编译指令如
#ifdef
; - 将
#include
包含的文件插入到预编译指令的位置; - 删除所有的注释;
- 添加行号、文件名标识,编译调试时显示错误信息;
- 保留
#program
编译器指令,因为编译时要用到
编译
gcc -S hello.i -o hello.s
- 整个过程包括词法分析、语法分析、语义分析、中间代码生成,目标代码生成及优化
汇编
as hello.s -o hello.o
- 将汇编代码转换成机器指令,每一个汇编语句几乎对应一条机器指令。
链接
- 将可重定位文件、静态库等链接,生成可执行文件.out
编译详解
- 为什么需要编译器:编译器实际上就是个单向翻译器,将高级语言翻译成机器语言。
- 为什么不用机器语言直接写程序:首先是用机器指令或者汇编语言写程序效率低下;其次是低级语言依赖于特定计算机,而高级语言关注程序逻辑本身,很少考虑硬件设备,比如字节、内存大小、通信方式、存储方式。这样便于移植,即用高级语言写一次,在不同机器上用它对应的编译器编译就行,不用更改程序代码。
- 编译过程:词法分析、语法分析、语义分析、中间代码生成,目标代码生成及优化
- 如下对
array[index]=(index+4)*(2+6);
进行分析
词法分析
- 使用扫描器,应用状态机算法,将源程序的字符序列分割成一系列记号。记号可以分为标识符、关键字、字面量、运算符等。将标识符放到符号表,将字面量放到文字表。
- Lex程序可以通过自定义词法规则,完成词法分析,这样就不用再编写词法分析器。

语法分析
- 判断语句写法是否正确,是否有左括号而没有右括号等等问题。
- 使用词法分析得到的记号,利用上下文无关语法的分析手段,生成语法树。
- 语法树就是一个一表达式为结点的树,符号和数字是最小的表达式,也就是叶子结点。
- 在语法分析阶段会报告语法错误。
- yacc和Lex一样,可以通过规则生成语法树,而不必要重新编写一个语法分析器。

语义分析
- 判断语句含义是否正确。比如两个指针相乘,在语法分析中是正确的,但是语义分析就会报错。
- 语义分析分为静态语义分析和动态语义分析。静态语义分析在编译器就可确定,动态语义分析在运行期才能确定。
- 静态语义分析包括:声明和类型是否匹配,类型转换包括将浮点型转换成整型。动态语义分析包括把0作为除数。
- 语义分析的结果是语法树的表达式都被标识了类型,同时也对符号表的符号类型更新。

中间代码生成与优化
- 中间代码是语法树的顺序表示,和目标代码相似,但是与目标机器、运行时环境无关,即不包含数据尺寸、变量地址、寄存器名称等。
- 常见地中间代码有三地址码和P-代码
t1=2+6
t2=index+4
t3=t1+t2
array[index]=t3
- 在这个过程里,2+6可以计算出来,以后直接使用8,从而减少临时变量t1。t3也可以被省掉。
t2=index+4
t2=t2*8
array[index]=t2
- 编译器分为前端和后端,前端负责产生与机器无关的中间代码,后端负责将中间代码生成机器指令。这样跨平台编译器只需要一个前端,多个后端。
目标代码生成与优化
- 编译器后端=代码生成器+目标代码优化器
- 代码生成器依赖于机器的字长、寄存器、整数数据类型、浮点数数据类型等。
movl index,%ecx ;index的值存到ecx
addl $4,%ecx ;ecx=ecx+4
mull $8,%ecx ;ecx=ecx*8
movl index,%eax ;index的值存到eax
movl %ecx,array(,eax,4) ;array[index]=ecx
- 目标代码优化:选择合适的寻址方式、使用位移代替乘法、删除多余的指令
movl index,%edx
leal 32(,%edx,8),%eax ;基址比例变址寻址指令lea完成乘法
movl %eax,array(,%edx,4)
链接器历史
- 最初的程序是纸带打孔,程序确定了绝对地址。比如0001 0100这一条机器指令,前四位代表这是跳转指令,后四位表示绝对地址。
- 抛开这样编写程序复杂不说,如果我在中间加了一条指令,那么原本跳转到0100的指令,就要跳转到0101。
- 汇编语言使用符号帮助人们记忆,比如说0001跳转指令写成jmp,0100地址写成foo。那么上述的跳转指令就是jmp foo
- 这样汇编器在汇编程序的时候,重新计算foo的目标地址就行了,解放了人类手工计算地址的过程。
- 随着程序的发展,程序越来越大,这时我们采用模块编写的思想。各个不同功能的代码卸载不同的模块中。最后模块组合成一个程序。
- 但是各个模块怎么拼接成一个程序,就要看:链接
静态链接
- 各个源代码模块独立编译,然后通过链接组装在一起。
- 链接就是处理各个模块互相引用的部分,使各个模块正确衔接。
- 它和人工修正地址的过程没有本质区别,就是把一些指令对其他符号地址的引用加以修正。
- 链接:地址和空间分配、符号决议、重定位。

- 比如,我们编写了两个模块,一个模块func.c文件中有foo()。另一个模块main.c文件中我们使用到了foo()。首先我们将各个模块独自编译,main.c中此时不知道foo()的地址,暂时将目标地址搁置。在组装成一个可运行程序时,通过链接器,得到foo()的地址,然后修正所有使用到foo()的地址。这样就完成了链接。
参考资料
- 俞甲子. 程序员的自我修养 : 链接、装载与库[M]. 北京 : 电子工业出版社, 2009