链接器的作用就是为符号转换成地址,一般来说可以分为三种情况:
- 生成二进制可执行文件的过程中。这种情况称为静态链接;
- 在二进制文件被加载进内存时。这种情况是在二进制文件保留符号,在加载时再把符号解析成真实的内存地址,这种被称为动态链接;
- 在运行期间解析符号。这种情况会把符号的解析延迟到最后不得不做时才去做符号的解析,这也是动态链接的一种。
CPU 在执行程序代码的时候,并不理解符号的概念,它所能理解的只有内存地址的概念。不管是读数据,调用函数还是读指令,对于 CPU 而言都是一个个的内存地址。因此,这里就需要一个连接 CPU 与程序员之间的桥梁,把程序中的符号转换成 CPU 执行时的内存地址。这个桥梁就是链接器,它负责将符号转换为地址。
链接器的第一个作用就是把多个中间文件合并成一个可执行文件。
链接器的第二个任务:重定位。所谓重定位,就是当被调用者的地址变化了,要让调用者知道新的地址是什么。
链接器的工作流程也主要分为两步:
- 链接器需要对编译器生成的多个目标(.o) 文件进行合并,一般采取的策略是相似段的合并,最终生成共享文件 (.so) 或者可执行文件。
- 链接器会对整个文件再进行第二遍扫描,这一阶段,会利用第一遍扫描得到的符号表信息,依次对文件中每个符号引用的地方进行地址替换。也就是对符号的解析以及重定位过程。
各种符号的处理方式:
- 局部变量在程序中的地址,都是基于 %rbp 的偏移这种形式,rbp 寄存器存放的是当前函数栈帧的基地址。这些局部变量的内存分配与释放,都是在运行时通过 %rbp 的改变来进行的,因此,局部变量的内存地址不需要链接器来操心。
- 静态函数,是唯一不需要重定位的类型,静态函数的调用地址在编译阶段就可以确定下来。
- 外部变量、全局变量以及静态变量的处理,都生成了一条 mov 0x0(%rip),%eax 的指令。这是因为在这个时候,编译器还无法确定这三个变量的地址,因此,这里先通过 0 来进行占位,以后链接器会将真正的地址回填在这里。
- 对于 extern_func 和 global_func 的调用,call 指令同样是通过 0 来进行占位,这和全局变量的处理方式一样。
此文章为7月Day6学习笔记,内容来源于极客时间《编程高手必学的内存知识》