我们可以在linux系统下对下列代码是如何进行编译进行一个理解:
main.cpp
//引用sum.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 o;
}
sum.cpp
int gdata = 10;
int sum(int a, int b)
{
return a+b;
}
接下来我们将从以下几个问题展开:
*.o
文件的格式组成是什么样的?可执行文件
的组成格式是什么样子?- 上图中步骤一和步骤二是做的什么事情?
- 符号表的输出里面的符号怎么理解?
- 符号什么时候分配虚拟地址?
预编译(.c -> .i):
#
开头的命令的预处理(除了#pragma lib和#pragma link)- 将所有的“#define”删除,并且展开所有的宏定义。
- 注释的删除和替换
- 头文件的引入。
- 保留所有的#pragma编译器指令,因为编译器须要使用它们。
- 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号;
编译(.i -> .s):
词法分析;语法分析;语义分析;以及优化后生产相应的汇编代码文件。
汇编(.s -> .o):
汇编分为两种x86和AT&T;将汇编代码
转变成机器可以执行的指令
,汇编后生成二进制可重定位的目标文件.o
。
我们可以通过objdump命令来查看.o
或.exe
文件的相关信息。
例如,我们可以是使用objdump -t main.o来查看其中的符号表信息。
我们可以看到在main.o
里面引用了外部文件的gdata
变量和sum
函数,在符号表中都是UND
的,也就是(undefine);这就意味着汇编器生成符号的时候在main.cpp文件中使用到了但是未找到gdata和sum的定义,所以只能暂时存放在UND
段中。
链接(.o -> a.out):
编译完成的所有.o文件+静态库文件进行链接
- 第一个步骤就是将所有
.o
文件段的合并,符号表合并后进行符号解析 - 第二个步骤就是符号的重定位(链接的核心)。
.o文件的格式组成:
elf文件头
.text
.data
.bss
.symbal
.section table
下面我们详解一下.o文件段的合并:
- 首先是所有.o文件段的合并, 也就是
main.o
和sum.o
的.text
、.data
等段合并到一起。 - 其次是符号解析,可以理解为:
所有对符合引用,都要找到该符号定义的地方
;
也就是链接器寻找main.o文件中UND
的gdata和sum符号定义的地方,如果找遍了所有地方都没有找到,那么链接器就会报错:符号未定义!,或者是在多个地方都找到了相同的符号定义,那么也会报错:符号重定义!
对于本例来说,这两个符号会在sum.o的.text和.data段找到符号的定义地方。 - 最后是符号的重定向:
给所有的符号分配虚拟地址,之后去代码段中给所有的符号重定向
。
符号什么时候分配虚拟地址?
链接的第一步符号解析完成后进行分配虚拟地址的
。(编译过程符号是不分配虚拟地址的)
我们二进制可重定位目标文件和可执行文件最大的区别是什么呢?
可执行文件有program headers段。由两个load告诉系统运行这个程序的时候把哪些内容加载到内存中,加载的是代码段和数据段,由下图可以看出:
可执行文件加载的大概过程如下图: