【深入理解计算机系统】第七章-链接
这一章重读读了比较久。按照书里参考文献说明这一栏的说明,链接处在编译器、计算机体系结构和操作系统的交叉点上,要求理解代码生成、机器语言编程、程序实例化和虚拟存储器。这一章和上一章的风格相去甚远,上一章给我留下的就是不断的计算、更优的计算,这一章记忆为主。现在重读,感觉有些工具是可以记录一下的。这一章里,我比较关注实际工具的使用,以及命令本身。对于编译原理不做深究。
- 静态链接过程
-
ASCII码源文件*.cpp----(预处理器cpp)---->
ASCII码的中间文件*.i----(编译器cc1)---->
ASCII汇编语言文件*.s----(汇编器as)---->
可重定位目标文件*.o----(链接器程序ld,链接多个.o文件)---->
可执行的目标文件p
-
- 静态链接器ld
- 符号解析:目标文件定义和引用符号。符号解析的目的是将每个符号引用和一个符号定义联系起来。
- 重定位:编译器和汇编器生成从地址零开始的代码和数据节。链接器通过把每个符号定义与一个存储器联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定位这些节。
- 目标文件三种形式:可重定位目标文件(.o);可执行文件;共享目标文件(举例:动态链接库)
- 【可重定位目标文件(*.o)】
- ELF头以一个16字节的序列开始,描述了字大小和生成该文件的系统的字节顺序。
- 以下需要以-g选项调用编译驱动程序才会得到这张表。
- .debug(调试符号表,包括程序中定义的局部变量和类型定义、程序中定义和引用的全局变量等等)
- .line(原始C源程序中的行号和.text节中机器指令之间的映射)
- 符号表(Symbol):包含这个可重定位目标文件m(module)所定义、引用的符号信息。在链接器的上下文中,有三种不同的符号:
- 由m定义并能被其他模块引用的全局符号。对应于非静态的C函数以及被定义为不带“C的static属性”的全局变量(断句问题,看了英文版才懂……)。
- 由其他模块定义并被m引用的全局符号。外部符号(externals),对应于定义在其他模块中的C函数和变量。
- 只被模块m定义和引用的本地符号(和局部变量不一样,本地符号local symbol,局部变量local variable)。有的对应于带static属性的C函数和全局变量。m中可见,但不能被其他模块引用。目标文件中对应于模块m的节和相应源文件的名字也能获得本地符号。
- 小问题:为什么没有提到局部变量?
- 我的回答:链接器不care局部变量。在程序运行过程中,局部变量在栈上,new出来的在堆上。未初始化的全局变量在BSS段,初始化的在数据段,常量字符串在静态存储区。对于静态存储区,其中的变量常量在程序运行期间会一直存在,不会释放,且变量常量在其中只有一份拷贝,不会出现相同的变量和常量的不同拷贝。
- 静态全局变量、非静态全局变量、静态局部变量、非静态局部变量都放在什么位置?
- 静态和全局都为静态存储方式(包括数据段和代码段),初始化的放一块儿,未初始化的放一块儿。
- 非静态局部变量在堆栈上。new出来的在堆上,直接定义的在栈上。
- 非静态全局->静态全局:改变作用域;非静态局部->静态局部:改变变量的存储位置
- bonus:JAVA是一门“基于引用”的语言,不存在没有new的构造。故类成员变量在堆上。
- 使用GNU READELF工具显示.o文件的符号表表目
-
- 符号解析
- 将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来(One definition of each local symbol per module)。(Static / Non-static) Local symbols are simple, while the global symbols are hard.能知道输入目标模块中的代码节和数据节的确切大小。
-
(强弱符号,都属于全局符号)强符号:函数和已初始化的全局变量、弱符号:未初始化的全局变量
- 三规则:不能有“多强”、“一强”“多弱”选一强、“多弱”任意选一个。
-
重要性分析:多文件编译时需要考虑这个问题。注意强弱符号都是针对于全局符号的定义。联想上文提及可以使用static属性隐藏变量和函数名字,对于这块的理解是有益的。
- 带像GCC-warn-common调用链接器(ld),解析多定义的全局符号定义时,输出一条警告信息。
- 静态库 *.a
- 链接器构造一个可执行文件时只拷贝静态库里被应用程序引用的目标模块。如果不用静态库,可能会这样提供函数:
- 让编译器辨认出对标准函数的调用,并直接生成相应代码
- Pascal采用这种方法,C标准函数太多
- 对编写编译器的程序员不友好,对应用程序员友好
- 编译器复杂性增加,增改删一个函数需要一个编译器版本
- 将所有标准C函数放在一个单独的可重定位目标模块中
- 编译器实现和标准函数的实现分离开
- 程序员有适当的便利
- 但是每个可执行文件都有集合的完全拷贝,浪费磁盘空间(在一个典型的系统上,libc.a大约是8MB,libm.a大约是1MB)
- 如果要修改某个函数,链接到旧模块的源文件不会发生改变,所以需要重新编译。
- 为每个标准函数创建一个分离的可重定位文件
- 要求应用程序员显示地链接,容易出错而且耗时
- 让编译器辨认出对标准函数的调用,并直接生成相应代码
- 链接器构造一个可执行文件时只拷贝静态库里被应用程序引用的目标模块。如果不用静态库,可能会这样提供函数:
- 【存档】一组连接起来的可重定位目标文件的集合,头部描述每个成员目标文件的大小和位置。Unix下创建可以使用AR工具:
-
unix> gcc -c addvec.c multvec.c # -c : compile only unix> ar rcs libvector.a addvec.o multvec.o
unix> gcc -O2 -c main.c unix> gcc -static -o p2 main2.o ./libvector.a # -static : complete link ?.o
- 使用静态库解析引用
- 维持一个可重定位文件集合E;未解析符号U;在前面输入文件中已定义的符号集合D。
- 对命令行上每个输入文件f,链接器判断
- f是一个目标文件,f被添加到E,并修改U和D反映f中的符号定义和引用)
- f是一个一个存档文件,尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加到E中,修改U和D。
- 扫描完文件,如果U非空,那么会得到一个“符号未定义”的链接错误(具体如何有待查证);如果D中有多个定义的全局符号,会根据强弱符号进行判断,如果有两个强符号,会报“多重定义”的链接错误(具体如何有待查证);
- 出于以上考虑,要十分注意编译时的依赖需求(从右往左为依赖增加?)。如果出现两个静态库互相依赖,比如libx.a与liby.a互相依赖,则需要在命令里写"libx.a liby.a libx.a",也即libx.a重写一遍。
- 重定位:合并输入模块,为每个符号分配运行时地址
- 重定位节和符号定义:将同类型的节合并。比如.data节合并成一个节,成为输出的可执行目标文件的.data节。
- 重定位节中的符号引用:修改代码节和数据节中的符号引用,指向正确的运行时地址。依赖于“重定位表目”。
-
【可执行目标文件(p)】
- 和可重定位文件进行对比
-
加载器(驻留在存储器中)运行可执行目标文件。代码段总是从地址0x08048000开始,数据段是接下来的下一个4KB对其的地址处(考虑4KB页大小)。
-
【共享目标文件,动态链接共享库(Unix, *.so; Microsoft, *dll)】
-
静态库的缺陷:静态库需要定期维护和更新,应用程序员需要显示将程序和库重新链接;基本上每个C程序都会使用标准I/O,会被复制到每个进程的文本段中,存储浪费。
-
共享
- 在两方面不同:
- 任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的共享这个.so文件的代码和数据。
-
存储器中一个共享库的.text节只有一个副本可以被不同的正在运行的进程共享。
- -fPIC指示生成与位置无关代码。-shared指示链接器创建一个共享的目标文件。
-
unix> gcc -o p2 main2.c ./libvector.so
- 创建可执行文件时,静态执行一些链接;运行时,动态完成链接过程
-
创建时,没有.so的代码和数据节被真的拷贝到可执行文件p2中;而是拷贝了一些重定位和符号表信息,是的运行时可以解析对.so中代码和数据的引用。
- 在两方面不同:
- 从应用程序中加载和链接共享库
- 之前的内容为,在应用程序执行之前(加载时),动态链接器加载和链接共享库的情景。
- 现在讨论运行时的场景。
- 现实例子
- 分发软件:Microsoft Windows应用开发者利用共享库分发软件更新。
- 构建高性能Web服务器:Web服务器生成动态内容。
-
- 【PIC】不做赘述
- 【GNU binutils】
- 查了一下GNU,感觉挺有意思,名字取“GNU's Not Unix”之意。
-