说说编译链接系统中的符号(symbol)、重定位(relocation)、字串表(string-table)和节(section)

作者:liigo
日期:2009/11
链接: http://blog.csdn.net/liigo/archive/2009/11/23/4858535.aspx
转载请注明出处: http://blog.csdn.net/liigo


  编译(compile)和链接(link),是计算机编程语言的通用处理系统。编译,是把程序源代码转换为目标文件;链接,是把目标文件转换为可执行文件。把编译和链接分成两个相对独立的子系统,是为了简化,是为了分而治之,也有基于通用性的考虑。编译器(compiler)的任务是把程序源代码编译为目标文件。通常每一种编程语言,都会有它自己的编译器,各种编译器的输出都是目标文件(.obj, .lib, .o, .a, ...)。链接器(linker)的任务是把编译器生成的目标文件经过一系列处理,最终生成可执行文件(.exe, .dll, .so, ...)。



  如果目标文件的规范是唯一的,所有编译器都输出特定格式的目标文件,链接器也都接收这种格式的目标文件,大概是一种比较理想的状态,编译器和链接器实现起来也会相对容易,而且通用性好,基至链接器只需有一个就行了,这也许是当初设计编译链接系统的初衷之一吧。但现实是很残酷的,目前来说,目标文件的格式不仅没有统一,反而分化的很厉害,粗略统计竟有逾十种之多,且互不兼容。编译器和链接器的开发者,往往只能选择支持一种或少数几种目标文件格式。通用链接器成为一种奢求,从来不曾出现过,这几乎完全背离了其设计初衷。

  好在小范围的应用通用链接器,还是可行的。例如,把D语言源代码编译为.obj目标文件,就能和C语言编译生成的其它.obj目标文件一起,用C语言的链接器链接生成EXE。又如一款新诞生的编程语言(就说易语言吧),不想重复开发专用链接器,可以考虑编译生成C语言格式的目标文件,进而得以使用现有的C语言链接器链接生成可执行文件,如此一来可大幅减少开发工作量,降低研发成本的同时还提高了系统的开放性。

  目标文件是沟通编译器和链接器的桥梁,它既是编译器的输出,又是链接器的输入;而“符号(symbol)”是目标文件中的核心元素,是编译系统和链接系统中最重要的操作对象。通俗的说,编译器的任务是“创建符号”(以及与符号相关向辅助设施),链接器的任务是“使用符号”(以及与符号相关向辅助设施)。

  

  下面结合我(liigo)的个人理解,说说目标文件的基本元素,及其内部结构。就以我目前接触最多的COFF格式的目标文件为例。

  目标文件中的基本元素有,符号(symbol)、重定位(relocation)、字串表(string-table)和节(section)。这里说的是逻辑概念。

  符号(symbol)是很什么?说不清楚,因为不好理解(对读者而言),也不好表达(对作者而言)。举例吧,假设程序源代码中有变量有常量有函数,那么编译之后那些变量常量函数都会各自成为一个符号,供它处引用。是不是可以把符号理解为“比变量常量函数更高层次上的抽象”呢?大概可以吧。正是因为符号是更高层次上的抽象,脱离了编程语言概念上的变量常量和函数,因而链接器才有可以做到与具体的编程语言无关。符号的主要属性有:名称(符号匹配完全基于名称文本),所属节(section)的序号,(符号实体)在节中的偏移,作用域(OBJ内部私有,或全局公开)。符号主要有两大类:一类是定义性质的(如变量定义、函数定义),其内容(如变量的值、函数体等)存储于指定的节中某个偏移处;另一类是声明性质的(如变量声明、函数声明),没有内容(因而不需要所属节、偏移等属性),链接器会根据名称在其它obj文件或其它lib文件中找到这个符号的定义。这里体现了链接器中“链接”二字的含义:一方声明(依赖、使用)一个符号,另一方定义这个符号,双方通过符号名称链接到一起。声明符号可以在定义符号之前,甚至在符号还没有定义的情况下。声明一个符号是编译器的行为,只是表示对该符号的依赖,相应的符号定义可以由他人(或编译器)在其他时间完成,只要链接器工作时能够(在其他目标文件中)找到定义就OK。从逻辑上说,符号通常指的是变量(变量的地址)和函数(函数可执行体首地址)。在OBJ中存储时,符号对应某个节(section)中的某处偏移;而在链接时(或链接的后期),符号则对应某个确定的内存地址(此地址由链接器指派,有了地址后才能执行后续的重定位操作)。符号在OBJ文件中是顺序存储的,所有符号的结构体组成一个数组,称为符号表。在OBJ文件内部,通常通过符号表中的索引(>=0)指代某个符号。如果指代其它OBJ中的符号呢?先在本OBJ内定义一个相同名称的“声明性质”的符号,然后通过符号索引指代本OBJ内的这个同名符号,将来链接器工作时,所有同名称的符号都被视为同一个实体并分派唯一的地址。

  节(section)是数据的容器,是存储数据的地方。节内存储的数据通常有:变量的值,常量的值,函数体,等。节的基本属性有:数据长度,数据在文件中的偏移,是否可读可写可执行,重定位表。在链接时,节总是作为一个整体参予链接的,它是不可分的。编译时节划分的比较小比较多,有利于链接时按需提取,有利于优化编译后的EXE或DLL的尺寸。分析VC6编译器生成的OBJ文件可知,一般一个函数会单独使用一个节(section)存储。如果看看C语言标准库的源代码,会发现它往往把一个函数写到一个单独的源文件中,这样编译时一个函数就会生成一个OBJ文件,尽量做到了细化。在OBJ中,所有节的节头(section-header)顺序存储形成一个数组,称为节头表或节表。通常通过OBJ文件内节表中的序号(>=1)指代某个节。

  重定位表(relocation)是从属于节(section的重要元素,用于修正节数据中的地址部分。分析编译器编译生成的函数代码的话,会发现它生成的不是完整的真正可执行的代码,而只是代码模板,其中涉及地址之处,往往简单的使用0x00000000占位,同时在此处绑定一个符号(symbol)用于修正此地址。为什么会这样呢?因为在编译器工作时,它并不知道符号(变量、函数等)地址,可能该符号来自另一个OBJ(或另一个LIB),甚至连它有没有定义都无法知晓。编译器只能先留下空白给链接器。通俗的说,编译器出了一个完形填空的题目,要链接器解答。重定位表可以理解为编译器给链接器提供的信息,它是由多个重定位项组成的数组,其中每一个重定位的基本属性有:被修正地址在节数据中的偏移,用于提供地址的符号索引,重定位类型(绝对定位、相对定位等)。链接器工作时,根据重定位项中的符号索引得到符号名称,进而查询得到符号地址(链接器负责指派符号地址),根据被修正地址在节中的偏移以及节的地址(链接器负责指派节的地址)得到被修正地址的地址,再根据重定位类型,将符号的地址填过去。举个例子,C语言代码 int a = 1;,对变量赋值,编译结果(不考虑编译优化)可能是 mov dword ptr [0x00000000], 0x12345678,相应的X86指令序列为 C7 05 00 00 00 00 78 56 34 12,中间的四字节的0就是占位符,将来需要链接器把变量a的地址覆盖上去,这是绝对定位;再如C代码 f();,编译结果(不考虑编译优化)可能是 call dword ptr [0x00000000],相应的X86指令序列为 FF 15 00 00 00 00,中间的四字节的0就是占位符,将来需要链接器把“函数f的地址与下一指令地址的差值”覆盖上去,这是相对定位的例子。具体是采用绝对定位还是相对定位还是其它定位方式,是由编译器生成的重定位表指定的,取决于编译器选择生成的指令代码。地址占位符也不见得一定是零,可以是任意数值(可正可负),表示相对目标地址的前后偏移量,链接器重定位时填写的地址其实是在此数值基础上与目标地址相加而得到的。以上说的是链接生成EXE或DLL时由链接器执行的重定位,将来DLL或EXE被载入时PE加载器还会执行一次重定位(重定位表由链接器生成,EXE中通常可省略),这两个阶段的重定位虽然细节上不同,但原理是一致的。

  字串表(string-table)是OBJ文件或LIB文件中的辅助设施,用于集中存储一些名称文本,如长度大于8字节的符号名称、段名称,以及长度大于15字节的链接成员(link member, 见于LIB中)的名称。字串表存在的目的主要是用于优化OBJ或LIB文件的尺寸。以符号名称为例,在OBJ中,一个符号所对应的结构体大小是固定的,共18字节,其中留出8个字节用于存储符号名称。如果符号名称比较短,小于等于8个字节,则直接存到这个结构体中(不存储C文本结尾字符'/0');如果符号名称长度大于8字节,则把名称存到字串表(string-table)中,然后把这个名称在字串表中的偏移记录到前面提到的8个字节区域处(在第一个字符前加'/'作为区分名称和偏移的标记)。

  

  至于LIB文件,相比OBJ就简单多了,它仅是OBJ文件的打包整理和索引,完整地包含了库中所有OBJ文件的内容,并提供了库中公开符号的名称索引表(根据一个符号名称可以快速查询到它是否在本库中定义,以及在哪个OBJ中定义)。在物理上,LIB文件的前面部分由三个固定的链接成员(linker member)组成,后面是顺序存储各OBJ文件内容(也称为linker member),每个链接成员均有一个数据头(header)。第一个固定链接成员(1st linker member),仅因兼容原因而保留,已被第二个固定链接成员(2nd linker member)取代,后者记录了符号名称索引信息和后面各OBJ成员的基本信息,第三个固定成员(3rd linker member)记录长文本(可能被省略)。

写的不是很条理,有点乱,请多包涵。liigo, 2009。

参考资料:
<>
http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx
http://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/pecoff_v8.docx

你可能感兴趣的:(C/C++,重复发明轮子,编译和链接)