目录
编译一道程序时,编译器会经过哪些过程呢?
这个过程绝大多数人都耳熟能详,什么预编译,编译,汇编,链接,最后在运行。事实上,确实是这几个步骤,但是这几个步骤分别干了些什么呢?
简单说来:
- 预编译:注释的替换,宏的展开
- 编译:生成指令,将源代码变成汇编代码,并进行语法分析,词法分析等
- 汇编:将汇编代码生成二进制文件,即生成目标文件(Windows下为 *.obj,Linux下为 *.o)
- 链接:将目标文件与所需要的库中的文件组合起来,形成可执行文件(Windows下为.exe,Linux下为.out)。
在gcc下编译每一步的命令如下
- 预编译:gcc -E hello.c -o hello.i
- 编译: gcc -S hello.i -o hello.s
- 汇编: gcc -c hello.s -o hello.o
- 链接: gcc -o hello hello.o
gcc下直接生成可执行文件:gcc -o hello hello.c
gcc下直接生成 .o 文件:gcc -c hello.c
我们通常vs或预编译者gcc编译一道程序时,觉得很快就完了,然后就可以执行了。但是殊不知,编译器却在背后做了这么多事。
一、预编译
gcc下,首先会将C文件生成 .i 文件,C++文件生成 .ii 文 件。除此之外,预编译过程还做了以下事情:
- 将所有的 "#define" 删除,并且展开所有的宏定义
- 处理所有条件预编译指令,比如"#if","#ifdef","#endif",但是除过"pragma",需要保留所有的#pragma编译器指令至链接时期
- 处理"#include"预编译指令,将被包含的文件插入到该预编译指令的位置。该过程递归进行,因为被包含的文件还有可能包含 其 他文件。
- 删除所有的注释
- 添加行号和文件名标识
预编译后的文件不包含任何宏的定义,所以可以通过预编译后的文件来检查宏定义是否正确。
C语言的宏替换和文件包含等工作一般不归入编译器的范围而交给一个独立的预处理器
二、编译
编译阶段主要完成的是把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化。
1、词法分析
将源代码的字符序列分割成一系列的记号。可分为:关键字、标识符、字面量(数字、字符串)和特殊符号(加号、等号) 等
实现:lex程序
2、语法分析
对由扫描器产生的记号进行语法分析,从而产生语法树(以表达式为节点的树),在语法分析的同时很多运算符号的优先级和含义被确定下来。分析过程采用“上下文无关语法”的分析手段。如果出现表达式不合法,括号不匹配等,那么编译器所报的错误就是语法分析阶段的错误。
工具:yacc
3、语义分析
语义分析器完成。语法分析仅仅完成了对表达式的语法层面的分析,但是不能分析出这个语句是否有意义。编译器可以分析静态语义(是指在编译期间可以确定的语义),静态语义通常包括生命和类型的匹配、类型的转换。动态语义(只有在运行时期才能确定的语义),0作为除数是运行期语义错误。
语义分析阶段以后,整个语法树的表达式都被标识了类型,如果有隐式转换,那么,语义分析程序会在语法树中插入相应的转换节点。
4、源代码优化
源代码级优化器优化过程中会将整个语法树转换成中间代码,它是语法树的顺序表示。中间代码:三地址码和P-代码。中间代码使得编译器可以被分为前端和后端,前端负责产生机器无关的中间代码,后端将中间代码转换成目标机器代码。
源代码级优化器产生中间代码标志着下面的过程都属于编译器后端。编译器后端主要包括代码生成器和目标代码优化器。
5、目标代码生成与优化
代码生成器将中间代码转换成目标机器代码。目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算,删除多余的指令等。
三、汇编
汇编最主要的任务就是,生成二进制可重定位的目标文件。
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。汇编过程也可以调用汇编器as来完成。
四、链接
写代码时,我们不可能将所有的代码实现在一个模块中。而在平时的编程中,我们要尽可能的做到模块化。但是模块化了之后,怎么把它们结合在一起来形成最终的可执行文件,这就是链接要做的事情了。
链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确的衔接。链接可以分为静态链接和动态链接。相应的也有静态库和动态库来供链接使用。
静态链接:源代码文件经编译器编译成目标文件(.o或.obj),目标文件和静态库一起链接形成最终的可执行文件。
- 步骤一:所有 .o 文件段的合并;符号表合并后,进行符号解析
- 步骤二:符号的重定位(重定向)
1、段的合并是根据什么来合并的?
段的合并是根据各个段的属性来进行合并,比如data段合并到data段,text合并到 text 段等。
2、什么是符号解析?
符号解析:所有对符号的引用,都有找到该符号定义的地方。常见错误:符号重定义,符号未定义。
3、什么是重定位?
《程序员自我修养》这本书中给重定位的定义是:在编译多个目标文件时,无法确定变量/函数地址的情况下,会将其目标地址置为0,等到链接时将目标地址进行修改。这个地址修正的过程就被叫做重定位。就是给程序中的每个绝对地址引用的位置打补丁,使它们指向正确的位置。
因为在编译过程中符号是不分配虚拟地址的,但是编译过程指令已经生成。所以先把符号的地址都置为0,等到链接时符号解析成功以后,给所有的符号分配虚拟地址,写入指令中置0的地方,称之为符号的重定向。
可以发现目标文件和最终的可执行文件格式上来说是很像的,但是目标文件不能执行的原因之一就是因为其还不确定符号的地址。必须得等到链接时对其地址进行重定位。
五、ELF文件格式
PC平台的可执行文件格式主要包括Windows下的PE和Linux下的ELF。目标文件和可执行文件的格式也是相似的,不光是可执行文件按可执行文件的格式存储,动态链接库和静态链接库也都按照可执行文件格式存储。
1、ELF文件类型
ELF文件类型 | 说 明 | 实例 |
可重定位文件 | 包含代码和数据,可以被用来链接成可执行文件或共享目标文件, 静态链接库也可以归为这一类 | .o .obj |
可执行文件 | 包含可以直接执行的程序,它的代表就是ELF可执行文件,一般没有扩展名 | .exe /bin/bash |
共享目标文件 | 包含代码和数据,使用情况可以分为两种,一是链接器可以使用这种文件跟其他的可 重定位文件和共享目标文件链接,产生新的目标文件;二是动态链接器可以将这几个 共享目标文件与可执行文件结合,作为进程映像的一部分来运行 | .so .dll |
核心转储文件 | 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储 到核心转储文件 | Linux下的core dump |
2、ELF文件存储形式
目标文件中至少包括编译后的机器指令代码,数据,符号表,调试信息,字符串等。将这些信息按不同的属性来以 “段” 的形式存储。
- 源代码编译后的机器指令 ---- 代码段(.text)
- 已初始化的全局变量和局部静态变量 ---- 数据段(.data)
- 未初始化的全局变量和局部静态变量 ---- 数据段(.bss)。
.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。未初始化的全局变量会依据编译器决定是否存放在bss段
段表:ELF文件中有很多段,段表就是用来保存这些段的基本属性的结构。它描述了各个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其它属性。编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性。
在Linux下可以使用命令来查看段表信息
一些别的段
3、ELF文件格式
ELF文件格式中比较重要的部分是ELF Header
现在可以看到代码的入口地址是0,但是在链接重定位以后,会写入一个虚拟地址来作为代码的入口。这也就是为什么代码知道从main函数开始执行,就是因为在文件头记录了代码入口地址。
4、可执行文件和目标文件格式上的区别
可执行文件与.o文件都是各种段组成。可执行文件的段来源于 .o 文件各个段的合并。
可执行文件较目标文件多了一个progrma headers段。它有两个load,作用是告诉系统运行这个程序时把哪些段加载到内存中。一般是代码段和数据段。
5、符号表
在链接中,目标文件之间相互组合实际上就是目标文件之间对地址的引用,即对函数和变量的地址的引用。将函数和变量统称为符号,函数名或变量名就是符号名。
符号表:是ELF文件中的一个段,记录目标文件中所用到的所有符号。每个定义的符号都有一个对应的值—符号值。对于变量和函数来说,符号值就是它们的地址。符号分类如下:
- 定义在本目标文件的全局符号,可以被其它目标文件引用
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,称作外部符号
- 段名,由编译器产生,它的值就是该段的起始地址
- 局部符号,只在编译单元内部可见
- 行号信息,即目标文件指令与源代码中代码行的对应关系
符号表的结构:
typedef struct
{
Elf32_Word st_name; //符号名
Elf32_Addr st_value; //符号相对应的值
Elf32_Word st_size; //符号大小
unsigned char st_info; //符号类型和绑定信息
unsigned char st_other; //为0,目前不使用
Elf32_Half st_shndx; //符号所在的段
}Elf32_Sym;
查看符号表的命令:readelf、objdump -t、nm