从程序源代码到最终可执行文件需要四步:
一、预编译(生成.i文件)
预编译过程主要处理那些源代码文件中的一“#”开始的预编译指令。入:“#define”、“#clude”等。
1、将所有的“#define”删除,并且展开所有的宏定义。
2、处理所有条件预编译指令,比如:“#if”“#ifdef”“#elif”“#else”“#endif”
3、处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。注意:这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
4、删除所有的注释“//”和“/* */”。
5、添加行号和文件标识,比如#2“hello.c”2,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或者警告时能够显示行号。
6、保留所有的#program编译器指令,因为编译器须要使用它们。
经过编译后的.i文件不包含任何宏定义,因为所有的宏已展开,并且包含的文件也已经被插到.i文件中。所以我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定。
二、编译(生成.s)
1、词法分析
2、语法分析
3、语义分析
4、中间语言生成
5、目标代码生成与优化
三、汇编(生成.obj(windows下)或者 .o(linux下)文件,可重定位的二进制目标文件)
as指令和gcc -c指令
汇编器是是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。
重定位:重新计算各个目标的地址过程。
目标文件:由段组成(两个段)
1、.text 代码段:存放指令
2、.data .bss: 数据段
四、链接(生成.exe,可执行文件)
链接过程只关心全局符号
1)
空间与地址分配
每个.o文件中都有自己的代码段和数据段,在多个文件合并后,这些指令和数据如何组织?通常是采用相似段合并的方式,将多个.o文件中的代码段合并为一个代码段,数据段合并为一个数据段。
这样做的好处是可以空间消耗(因为操作系统在分配空间时最小单位是页),并且可以提高内存访问的命中率。
在进行相似段合并时,链接器会根据.o中每个段的长度、属性和位置等,计算出合并后的长度与位置,然后并每一个.o的中的符号合并到全局符号表,记录每一个符号的新的位置。
2)
符号解析与重定位(链接阶段80%错误出现在符号解析)
这一步是链接过程中核心,链接器通过重定位表,获取到每一个需要进行重定位的指令的位置以及符号,然后通过符号表查询符号的地址,修改指令中访问的地址。
如果在这个过程中,无法在符号表中找到需要重定位的符号,就会出现链接失败,也就是我们常见的”undefined reference to ‘xxx’”错误。
在符号表中,除了全局符号外,COMMON类型的符号也需要进行重定位,又称为COMMON块。COMMON块通常用来处理弱符号。
编译器将未初始化的全局变量定义为弱符号。当多个源代码文件中存在相同的弱符号时,链接器采用如下策略决定弱符号的大小:
强符号:全局已初始化的变量。
弱符号:全局未初始化的变量。
(1) 存在两个强符号时,报重定义错误。
(2) 存在一个强符号一个弱符号时,选择强符号。
(3) 存在两个弱符号时,选择数据较大的,即(数据长度较大的)
(4) 接完成后,所有的强弱符号都在bss段
程序加载运行过程:
1、建立虚拟地址空间到物理内存的映射(创建内核映射结构体)
2、创建页目录和页表。
3、加载指令和数据。
4、程序的入口地址写入PC寄存器(PC寄存器指向下一条指令的地址)
(内核部分)