程序编译链接过程
我们都知道,一个.cpp源程序需要经过编译链接过程,从一个被人能读懂的高级语言转化为一系列的低级机器语言指令,然后这些指令按照一种称为可执行目标程序的格式打包好,以二进制的形式存放在磁盘上。
(截取自《深入理解计算机系统》)
在普通的编译器上,我们是感受不到源程序编译链接的过程的。我们可以在Linux环境下,通过一系列的命令来分析这个过程。
编译链接过程,大体可分为编译和链接两个阶段,而编译又可细分为预编译、编译和汇编三个阶段。
预编译阶段
在这个阶段,编译器根据以 # 开头的命令、注释,修改原始的源程序,如 #defile
会进行宏替换、 #include
会读取头文件内容、删除注释等,但并不是所有 # 开头的命令都会在预编译阶段处理,如 #pragma lib
、 #pragma link
,在链接阶段才会处理。
编译阶段
编译阶段,编译器会对程序指令进行语法、语义和词法分析,并会对代码进行优化,编译完成后就会生成相应平台的汇编程序代码。
汇编阶段
汇编阶段就是把汇编程序代码翻译成机器认识的二进制代码,也就是二进制可重定位的目标文件。
编译最终生成的二进制可重定位目标文件是不能直接执行的,还需要经过两个步骤:
- 所有的.o可重定位文件段的合并、符号表合并后,进行符号解析。
- 符号重定向
这两个步骤就是链接的过程。
链接
下面用以下两个文件来对链接过程进行分析
main.cpp
extern int gdata;
int sum(int,int);
int data = 20;
int main()
{
int a = gdata;
int b = data;
int ret = sum(a, b);
return 0;
}
sum.cpp
int gdata = 10;
int sum(int a, int b)
{
return a + b;
}
先对这两个源文件分别进行编译
main.o 和 sum.o 就是编程生成的二进制可重定位目标文件。要分析链接过程,我们需要先知道.o文件都有哪些信息。在Linux下,我们可以用objdump
命令来查看可重定位目标文件的详细信息。
-t
参数可以查看符号表信息。
我们的代码在编译时,变量名、数组名、函数名等都会生成符号,这些符号以表的形式存储在文件中,符号的属性有符号名、符号类型、符号的存储类别、符号的作用域等,符号名会根据编译器的规则来生成。
l表示local,链接时只在本文件可见,其他文件不可见,g表示global,全局可见,在连接时用重名符号时会出现链接错误。如我们定义一个静态的全局变量,它的属性就是local,对外不可见。
在我们声明外部文件的变量或函数时,编译器也会给我们生成对应的符号,因为我们只是声明了我们要用这个符号,并不知道是怎么定义的,所以main函数中变量gdata
和函数sum
的属性是 *UND*
undefine(未定义的)。
main
符号的属性是.text
,main函数是在本文件定义了的,编译器知道main
将会在 .text
代码段上存放,data
符号也是如此。
编译除了会生成符号表外,还会生成各个段。
可以看到可重定位目标文件有一个文件头,我们可以用readelf命令加上 -h 参数来查看 .o 文件的头部信息。然后就是程序的各个段了。而各个段的详细信息可以用readelf命令,加上-S参数来打印出来。
链接步骤1
链接的第一步,就是把所有用到的 .o 文件对应的段全部进行合并,合并完成后会进行符号解析。
符号解析,就是找到符号表中属性为 *UND*
的符号定义的地方,找不到或找到多个,都会链接错误,也就是符号未定义和符号重定义。
链接步骤2
符号解析成功后,给所有的符号分配虚拟地址。
分配完虚拟地址后,每个符号都有了对应的虚拟地址,但代码段上的地址还未更新,初始为0地址:
而接下来符号重定向的作用就是更新具体指令上用到符号的虚拟地址。
链接:
链接完成后,每个符号都有对应的虚拟地址,指令用到的符号也有了具体的地址或偏移量,链接就完成,生成的文件就是可执行文件了。