读CSAPP之符号解析、定位和库
今年是离开校园的第六年,这六年来我一直在从事应用软件的设计、开发工作,大部分时间是在与高级编程语言、设计模式、业务逻辑打交道。它们大多流于表面,久而久之,与技术底层疏远了,诸如计算机组成原理、汇编语言、编译原理、数据结构以及算法慢慢得生疏,时至今日,向上碰到天花板,向下触到花岗岩。五年是一个契机,趁着下一个五年开始之际,我计划用三个月至半年时想间,重新学习这些知识,以期达到巩固基础,厚积薄发的目的。
本篇是我阅读《Computer System: A Programmer’s Perspective》一书的笔记,该书和与之搭配的《Professional Assembly Language》是我当下阅读计划的一部分。
符号表
.symtab 符号表由汇编器构造,使用编译器输出到汇编语言 .s 文件中的符号,存放程序中定义和引用的函数和全局变量的信息,包括了三类不同的符号:
- 由模块 m 定义并能被其他模块引用的全局符号;
- 由其他模块定义并被模块 m 引用的全局符号;
- 只被模块 m 定义和引用的本地符号。
.symtab 中的符号表不包含对应于本地非静态程序变量的任何符号,这些符号既不在 .data 节,也不在 .bss 节,是在运行时在栈中被管理,链接器对此此类符号不处理。
但是用 static 限定的内部变量却是例外,尽管其只能在该函数中使用,但是它一直占据存储器空间,并不伴随函数调用而产生和函数退出而消失。 其并不在栈中管理,而是由编译器在 .data 或 .bss 为其分配空间,并在符号表中创建一个有惟一名字的本地链接器符号。
符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来。
当编译器遇到一个不是在当前模块中定义的符号(函数或变量)时,它会假设该符号是在其他某个模块中定义的,生成一个链接器符号表,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用的符号,他就输出一条错误信息并终止。
全局符号有强(strong)弱(weak)之分,函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。Unix 链接器使用下面的规则来处理多重定义的符号:
- 不允许有多个强符号;
- 如果有一个强符号和多个弱符号,则选择强符号;
- 如果有多个弱符号,则从中任选一个。
规则2和3会造成一些不易察觉的运行时错误,建议使用 -fno-common 这样的选项调用链接器,遇到多重定义的全局符号时,其会输出一条警告信息。
重定位
链接器完成符号解析以后,就知道输入目标模块中的代码节和数据节的确切大小,接着开始重定位。重定位分为两步,首先是重定位节和符号定义,然后是重定位节中的符号引用。
在前者当中,链接器将所有相同类型的节合并为同一类型的新的聚合节,然后链接器将运行时存储器地址赋给新的聚合节、输入模块定义的每个节、以及输入模块定义的每个符号。在后者当中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
汇编器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在 .rel.text 中,已初始化数据的重定位条目放在 .rel.data 中。
ELF 定义了11种不同的重定位类型,其中常用的有 R_386_PC32 和 R_386_32,前者是重定位一个使用32位PC相对地址的引用,后者是重定位一个使用32位绝对地址的引用。
静态库
所有的编译器系统都提供一种机制,将所有相关的目标模块打包称为一个单独的文件,称为静态库(static library)。每个标准函数被创建一个对应的、独立的可重定位文件,然后封装成一个集合的静态库文件。当链接器构造一个输出的可执行文件时,它只拷贝静态库里被应用程序应用的目标模块。
在 Unix 系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置,由后缀名 .a 标识。
-static 参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到存储器并运行,在加载时无需更进一步的链接。
在符号解析的阶段,链接器从左到右按照它们在命令行上出现的顺序来扫描可重定位目标文件和存档文件。
共享库
共享库(shared library)是一个目标模块,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序来执行的。
静态库有两个弊端,其一,如果静态库更新,则应用程序员需要重新链接程序;其二,几乎每个程序都使用标准 I/O 函数,在运行时,这些函数的代码会被复制到每个运行进程的文本段中,这浪费了宝贵的存储器资源。
共享库以两种不同的方式实现“共享”,首先,在任何给定的文件系统中,对于一个库只有一个 .so 文件,所有引用该库的可执行目标文件共享这个 .so 文件中的代码和数据;其次,在存储器中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享。