编译是把高级语言写的源文件翻译成二进制文件的过程。大家知道,机器只能识别1和0。最初的时候,程序是用二进制写的。后来有人发明了汇编语言,再后来有人发明了C/C++等高级语言。显然我们能够更容易理解和掌握更接近自然语言的高级语言。高级语言能够被用来更好的描述和解决问题。当然了,鱼和熊掌不可兼得。越高级的语言,往往要牺牲某些方面(比如效率)以改善易用性。有的时候还不得不适用低级语言,常见的情况是,时间敏感性非常高的场景。总之,没有万能的语言,各有所长。
发展至今,机器还是只能认得0和1。高级语言写的程序必须经过编译才能变成可执行的二进制程序。
分层是解决复杂问题的有效方法。编译也不例外。编译过程大致可分为如下步骤(以GCC为例):
1. 预处理
预处理器(cpp)将源文件a.c翻译成一个ASCII码的中间文件a.i。单独的编译命令如下:
cpp [otherarguments] a.c ./a.i
这一步主要完成的工作包括:扩展头文件、宏替换、条件编译。
扩展头文件:把包含的头文件的内容插入源码文件中,做为源码的一部分。
宏替换:例如有宏定义 #define PI 3.1415926
在预处理阶段会把源文件中的PI全都替换为3.1415926
条件编译:删除不满足条件的程序段
例如a.c中有这样一段
#if0
inta, b;
a =b+3;
#endif
在预处理阶段,该段代码将被删除。
另外源码中的注释部分也将被删除。
2. 编译
C编译器(cc1)把a.i翻译成一个ASCII码的汇编语言文件a.s。单独的编译命令如下:
cc1 a.i a.c [other arguments] –o a.s
cc1是编译器的核心模块。
一个简单的.c文件:
void main()
{
int a,b;
a = b+3;
}
被翻译成如下的汇编语言文件
.file "main.i"
.text
.globl main
.type main,@function
main:
.LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp,%ebp
.cfi_def_cfa_register 5
subl $16,%esp
movl -4(%ebp),%eax
addl $3,%eax
movl %eax,-8(%ebp)
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.size main,.-main
.ident "GCC:(Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
.section .note.GNU-stack,"",@progbits
如此也可看出用汇编语言直接写程序是件多么低效的事情。
3. 汇编
汇编器(as)将a.s翻译成一个可重定位的目标文件a.o。单独的编译命令如下:
as [otherarguments] –o a.o a.s
在Linux上a.o是一个ELF文件。
4. 链接
链接器(ld)将相关的.o文件以及一些必要的系统目标文件组合起来,创建一个可执行的目标文件。
ld –o a.out[system objects files and args] a.o
链接器的主要任务是:
• 符号解析,目的是将每个符号引用刚好和一个符号定义联系起来。
• 重定位。把对符号的引用转换成存储器地址。
shell中输入a.out执行程序。shell调用一个叫做加载器的函数,它拷贝可执行文件a.out中的代码和数据到存储器,然后将控制代码转移到这个程序的开头。
目标文件
目标文件分为三种形式
▪ 可重定位目标文件。也就是.o文件和.a文件(实际上是多个.o文件的集合)。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并,创建一个可执行目标文件。
▪ 可执行文件。也就是可以直接拷贝到内存运行的文件。
▪ 共享目标文件。即动态库。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载到存储器并链接。
各个系统的目标文件格式是不相同的。所有不能把Linux的可执行程序放到Windows上面运行。Linux系统上的目标文件格式是ELF(Executableand Linkable Format)。目标文件除了包含可执行代码和数据外,好包含系统为执行程序需要的一些信息:生成该文件的系统的字的大小和字节顺序、目标文件的类型、机器类型等。