CSAPP阅读笔记-第七章

CSAPP 第七章 链接

  1. 编译器驱动程序
  2. 静态链接
  3. 目标文件
  4. 可重定位目标文件
  5. 符号和符号表
  6. 符号解析
  7. 重定位
  8. 可执行目标文件
  9. 加载可执行目标文件
  10. 动态链接共享库
  11. 从应用程序中加载和连接共享库
  12. 与位置无关的代码
  13. 处理目标的工具

读前感:

说实话看目录上这些标题,我大多数都了解过,但是具体细节不甚了解,我对程序生成整个过程的理解是,先是程序员编码,然后是预编译阶段,预编译会打开宏和预编译指令,比如#include的文件会展开,预编译会生成一个.i文件,之后是编译阶段,编译阶段检查词法语法,如无错误的话,生成汇编程序.s文件,之后汇编器对.s文件做汇编,生成目标文件,一般是.obj文件,但是到这函数可以不绑定它的实现,然后最后一步链接器会把目标文件和对应的静态库或者动态库链接成一个二进制可执行程序,windows下就是exe格式文件。操作系统是“认识”可执行程序的,可以把它们load到内存来执行。

读后感:

  • 编译器驱动程序
    看完之后第一感觉,就是gcc么,等于是预编译处理器+编译器+汇编器+链接器的集合,是一个工具集。
  • 静态链接

  • 目标文件和可重定位目标文件
    目标文件分三种,可重定位目标文件(.o文件,汇编器的输出)、可执行目标文件(elf文件,链接器的输出)、共享目标文件。elf可重定位目标文件格式如下:

    • .text 代码段
    • .rodata 只读数据段。
    • .data 初始化的全局变量数据段。
    • .bss 未初始化的全局变量数据段。
    • .symtab 符号表。
    • .rel.text 重定位代码段。
    • .rel.data 重定位数据段。
    • .debug 调试信息段。-g才会有。
    • .line 源码和.text的行对应关系。-g才会有这个段。
    • .strtab 字符串表。

以上这些所有的节(section)都有一个固定大小的表项(entry)。 elf可执行目标文件是链接器合并这些.o文件生成的可执行文件,用于被OS加载到内存中运行的,所以默认是不需要.rel.text和.rel.data这些重定位段的,其他大体和上述相同,但是会在.text段之前多一个表头和.init段。

  • 链接器(linker)到底做了什么
    经过了编译器+汇编器处理后的可重定位目标文件(.o)离能被直接加载到系统内存中运行的程序还差两点:
    1. 符号表的解析
    2. 重定位

很好理解这两点其实,关于符号表,直观的理解就是extern int a关键字,比如在source1.c中引用了source2.c中定义的变量a,那么source1.o(source1.c编译汇编的结果)中是无法知道解析这个a的,链接器需要把source1.o中的a关联到source2.o上,不止全局变量,还有函数,以及所有1中找不到的符号引用,对于C++而言,函数重载给编译增加了难度,编译器会给每个同名不同函数签名的函数做一个名字编码,术语叫做“毁坏”,

对于同名的符号,linker有它自己的规则,大致可以理解为选择最优的最强的,其次是弱的。这里的强弱是这样定义的:初始化的全局变量和函数是强,而未初始化的全局变量是弱。所以如果有两个相同的强符号,那么链接器会报错,也就是初始化的全局变量和函数当然只能有一份啊。如果同名符号,一个强一个弱,那么选择强符号;如果同名符号都是弱,那么随机选一个。这里有个很有意思的问题,就是如果一个强一个弱,但是类型不一样,虽然链接器选了强符号,但是弱符号对应变量操作的类型是弱符号定义的类型!其实很正确,因为对于链接器的符号解析来说并没有类型的概念,所以这种写法很可能产生隐藏的bug,也就是说我写了个int a = 0,但是别的函数操作的时候居然有可能操作的是个float a,因为 可能它的源码中定义了个float a,然后没有初始化!所以论名字空间的重要性!论全局变量的滥用后果!还是能尽量少用全局变量就少用,尤其是c没有一套很好的名字空间管理方法和包的机制的前提下。符号解析还有一个有意思的事情是,系统函数的调用。比如你的代码里调用了printf,可是你并没有在链接的时候指定一个printf.o或者system_func.o之类的可重定位目标文件,因为你会发现“编译器驱动程序”默认传一个libc.a给linker,这个libc.a就是静态库,archive格式文件,就是.o文件的合集,这个libc.a里包含了各种C的基本函数,包括IO,字符串操作等,当然也有你需要的printf,而且linker会“聪明”地查找到libc.a中的printf对应的可重定位目标模块并且只包含它到最终的可执行文件。不过linker的查找是有顺序依赖的,所以一般把静态库放到linker参数的最后。符号解析如果有找不到的符号,那么linker会报一个错误,提示不认识这个symbol,如果符号解析成功,那么linker就可以开始下一步:重定位。暂停回顾一下,到这里为止,linker已经知道了代码中出现所有的符号引用和对应的符号表的表项,所有的符号都找到了自己的“归属”,再没有一个“野孩子”了,但是距离我们的能被OS加载到内存运行的可执行目标文件还差一点,真是内存地址。我们所有的地址都是偏移量,那么重定位这一步就是合并所有输入的.o,分配运行时内存地址。很简单,分两步

  1. 重定位节和符号定义
  2. 重定位节中的符号引用

重定位节很好理解了,合并所有输入的.o中相同的节为新的可执行目标文件的节,并附给运行时内存地址,包括节中的符号的地址。重定位符号引用我理解就是解决多文件程序的各个输入模块中那些引用其他模块的符号引用,计算它们的运行时地址,写入到可执行文件中,也就是根据.rel.text和.rel.data的重定位表来做,所以elf可执行文件中并没有这两个节。典型elf可执行文件结构如下图:

  • 动态链接
    除了上述这种linker外,还有一种动态链接器,可以动态加载share object(也就是.so文件)中的函数调用,这种使用so文件的好处很明显,首先节约内存,因为静态库的使用最终实际上是代码和数据的复制,那么不同进程使用相同静态库的话,就每个进程都有一份该静态库的拷贝,而且运行时随该进程加载到内存中。其次,便于部署和升级,假设你发布了了一个自己开发的软件,之后想要升级其中某项功能,使用动态库的话你只需要发布新的动态库(so或者dll)替换以前的就可以了,是非常方便的,而且便于模块化你的程序,解耦。使用共享库的示例:

另给出一篇博文参考,我谷歌了半天觉得这篇博客写得很好,用自己的理解说清楚了都。