一、预处理
(1) 将所有的#define删除,并且展开所有的宏定义
(2) 处理所有的条件预编译指令,如#if、#ifdef
(3) 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
(4) 过滤所有的注释
(5) 添加行号和文件名标识。
需要注意的是,对于#pragma预编译指令不一定是在这个阶段执行的,因为#pragma link就是在链接阶段才会执行的。
二、编译
(1) 词法分析:将源代码的字符序列分割成一系列的记号。
(2) 语法分析:对记号进行语法分析,产生语法树。
(3) 语义分析:判断表达式是否有意义。
(4) 代码优化:比如内联函数、合并代码分支、公共子表达式消除等。
(5) 目标代码生成:生成汇编代码并且进行优化。
三、汇编
将汇编代码转变成机器可以执行的指令(全部在.text段里面),最终产生.o文件或者.obj文件等二进制可重定位的目标文件。
四、链接
将不同的源文件产生的目标文件和静态库文件进行链接,从而形成一个可以执行的程序。
链接的过程主要分为两部分
(1)将所有.o文件的文件段合并,比如main.o的.text和sum.o的.text合并为.exe的.text,然后合并符号表段(.section table段),并进行符号解析,如果解析成功,就给对应的符号分配虚拟地址,否则就会报错(符号未定义或者符号重定义),需要注意的是,如果从一个.cpp文件通过extern引用另一个.cpp文件的变量或者函数,那么符号解析也会将这个函数和变量写入这个.cpp对应的.o文件的符号表中,但是段位置会显示为undefine而不是.text或者.data等文件段。
(2)进行符号的重定位,由于.cpp文件在汇编阶段就已经产生了.o文件,也就是说此时的.text中已经存在了汇编指令,.section table也有了符号,但是由于符号表中的符号没有分配虚拟内存,所以此时符号表中所有的符号地址默认为0,经历了过程一之后,符号分配了虚拟地址,这时候就需要对这些.text段的汇编指令里面对应符号的地址从0重定位到分配的虚拟地址。
实例说明
比如我们有一个main.cpp
#include<iostream>
using namespace std;
extern int globaldata;
int sum(int,int);
int localdata=10;
int main()
{
int a=globaldata;
int b=localdata;
int ret=sum(a,b);
cout<<ret<<endl;
return 0;
}
还有一个sum.cpp
int globaldata=10;
int sum(int a,int b)
{
return a+b;
}
很显然,我们通过extern在main.cpp中引用了sum.cpp的全局变量globaldata和函数sum。
在Linux上进行编译,可以生成对应的.o文件。
g++ -c main.cpp sum.cpp
然后可以通过objdump指令查看这些.o文件的具体信息。
比如查看.o文件的符号表信息
objdump -t sum.o
从这里可以发现,我们的globaldata和sum函数在符号表中都有对应的符号 ,比如sum函数对应的符号就是_Z3sumii,这个是通过sum函数的名称和它的参数列表共同生成的,具体生成机制大家可以自己去了解哈。
这个是main.o的符号表。
就和我们上面所说的一样,由于globaldata和sum函数是main.cpp从sum.cpp外部引用过来的,所以它们的 段位置都是UND(undefine),而其他main自己的符号都是有对应的文件段的,而且我们可以发现main函数和localdata都是全局符号(g),代表它们可以其他文件引用。
除此之外,我们还可以通过objdump指令,查看一个.o文件由哪些文件段组成。
objdump -h sum.o
main.o的段组成。
可以发现.o文件的组成格式是统一的,这也是为什么链接步骤的第一步的合并文件段可以进行,正是因为.o文件都有相对应的文件段所以可以合并啊,当然合并之后就是.out文件了。
通过g++将main.cpp和sum.cpp编译链接为可执行文件test.out。
再查看.out文件的组成格式。
readelf -t test
显然.out文件同样是有.text和.data等文件段的,这些就是通过.o文件对应的文件段合并而来的。
查看.out文件的elf文件头
readelf -h test
这个入口点地址就是main函数的第一行代码的偏移地址,也就是0x4006a0。
查看test.out的汇编指令
objdump -S test
找到.text的汇编指令段,我们知道.text段存放的就是程序代码的汇编指令,所以main函数什么的都在这里面,这个4006a0就是main函数的第一行代码地址,也是我们程序的入口地址。
其实从这个链接过程再结合进程的虚拟内存地址,我们基本可以推断出.o/.obj文件和.out/.exe文件的基本组成格式了。
.obj文件又叫作二进制可重定位的目标文件,它是由一个elf文件头和很多个文件段组成的,比如.text和.data等等,由于.exe文件是由若干个.obj文件链接而成的,所以.exe文件的组成和.obj文件基本一致,只是.exe的各个文件段都是.obj文件对应的文件段合并而来的,但是有一点不同的是,.exe文件它有一个program headers的文件段,这个文件段规定了.exe文件要把哪些文件段加载到内存中,毕竟不是所有文件段都需要加载进入内存,像符号表什么的就不需要,但是.text和.data等就需要,另外需要注意的是,一个cpp程序之所以会从main函数的第一行代码开始执行,是因为.exe文件的elf文件头规定了程序的入口地址,这个入口地址就是main函数第一行代码的地址。