PS:这个博主连载了四篇关于编译、链接的博文,思路很清晰,推荐看原文,我只是用自己的话做了个总结
https://segmentfault.com/a/1190000016433947
注意:编译包括了使用编译器生成汇编代码,和使用汇编器生成目标文件两部分1.编译:将一个源文件转为目标文件
2.链接:将多个目标文件之间产生关联
一、预处理
预处理不对源代码进行解析,主要是 文本替换,即展开的过程
1.文件包含
2.宏定义
3.条件编译
一、编译与目标文件
1.编译
编译的任务就是将汇编或高级计算机语言源程序转换成目标文件,即机器可以执行的机器指令
源文件被编译后生成的目标文件可以简单的理解为两部分:
代码段:源文件中定义的所有函数翻译成机器指令,存放在目标文件的代码部分
数据段:源文件中定义的全局变量,存放在目标文件的数据部分
(注意:局部变量是函数私有的,所以被放在了代码段中,作为机器指令的操作数)
在遇到外部定义的全局变量或者函数时 只要能在当前文件找到其声明,编译器就认为编译正确
而寻找使用变量定义的这项任务就被留给了链接器,链接器的其中一项任务就是要确定所使用的变量要有其唯一的定义
2.符号表(Symbol table)
在编译过程中每次遇到一个全局变量或者函数名就会在符号表中添加一项,最终编译器会统计出一张符号表
符号表中保存的信息有两部分:
- 目标文件中引用的全局变量以及函数,仅仅只是声明,未定义
- 目标文件中定义的全局变量以及函数
符号表想表达的两件事:
- 我能提供给其它文件使用的符号
- 我需要其它文件提供给我使用的符号
符号表被放在目标文件中
3.目标文件
当前代码中引用,定义在其它源文件中的变量、函数,编译无法确定其内存地址,只有在进行链接时才能够确定这类变量、函数的内存地址
因此在目标文件中对于这些变量、函数的引用,对应的机器指令可能是:call 0x000000
即编译器将不能确定的地址都这设置为空(0x000000),同时编译器还会生成一条记录,告诉链接器在进行链接时要修正这条指令中的内存地址
- 使用外部定义的函数,这个记录就放在了目标文件的.rel.text段中
- 使用外部定义的全局变量,则该记录放在了目标文件的.rel.data段中
即链接器需要在链接过程中根据.rel.data以及.rel.text来填好编译器留下的空白位置(0x000000)
目标文件的内容如下图:
二、链接器
1.符号表决
根据编译后得到的目标文件中的符号表,链接器根据所有目表文件进行符号决议,即 确保所有目标文件中的符号引用都有唯一的定义
1.对于当前目标文件,查找其符号表,并将已定义的符号并添加到已定义符号集合D中
2.对于当前目标文件,查找其符号表,将每一个当前目标文件引用的符号与已定义符号集合D进行对比,如果该符号不在集合D中则将其添加到未定义符合集合U中
3.当所有文件都扫描完成后,如果 未定义符号集合U不为空,则说明当前输入的目标文件集合中有未定义错误,链接器报错,整个编译过程终止
2.重定位
PS:以下关于重定义的分析, 仅限于静态链接,该情况下,代码和数据都合并到可执行文件中,因此需要确定代码和数据的最终位置
编译时,无法确定,定义在其它源文件中的变量、函数的内存地址
链接时,才能确定可执行文件中代码和数据运行时的内存地址
确定程序运行时地址的过程就是重定位,重定位分为两步:
1.链接时,需要对所有目标文件的数据段和代码段的内存地址进行修正,编译得到的目标文件得到的只是相对地址
2.链接器扫描所有的.rel.text以及.rel.data段并找到相应记录的最终内存地址,并将机器指令中的0x000000
修正为最终内存地址
PS:最后那个博主提出了一个问题,如果运行两次可执行文件,会不会出现内存内存占用的问题,而两个程序之间产生影响
回答肯定是不会的,从 操作系统层面来解释,从虚拟内存里面看到的内存地址是一样的,但是物理地址不可能一样