预处理器
编译器
汇编程序
目标程序
链接器
可执行程序
链接之前,所有的源文件都是单独编译的
C++ 语言支持”分别编译”(separatecompilation)。也就是说,一个程序所有的内容,可以分成不同的部分分别放在不同的 .cpp 文件里。.cpp 文件里的东西都是相对独立的,在编译(compile)时不需要与其他文件互通,只需要在编译成目标文件后再与其他的目标文件做一次链接(link)就行了。比如,在文件 a.cpp 中定义了一个全局函数 “void a(){}”,而在文件 b.cpp 中需要调用这个函数。即使这样,文件 a.cpp 和文件 b.cpp 并不需要相互知道对方的存在,而是可以分别地对它们进行编译,编译成目标文件之后再链接,整个程序就可以运行了。
这是怎么实现的呢?从写程序的角度来讲,很简单。在文件 b.cpp 中,在调用 “void a()” 函数之前,先声明一下这个函数 “voida();”,就可以了。这是因为编译器在编译 b.cpp 的时候会生成一个符号表(symbol table),像 “void a()” 这样的看不到定义的符号,就会被存放在这个表中。再进行链接的时候,编译器就会在别的目标文件中去寻找这个符号的定义。一旦找到了,程序也就可以顺利地生成了。
注意这里提到了两个概念,一个是”定义”,一个是”声明”。简单地说,”定义”就是把一个符号完完整整地描述出来:它是变量还是函数,返回什么类型,需要什么参数等等。而”声明”则只是声明这个符号的存在,即告诉编译器,这个符号是在其他文件中定义的,我这里先用着,你链接的时候再到别的地方去找找看它到底是什么吧。定义的时候要按 C++ 语法完整地定义一个符号(变量或者函数),而声明的时候就只需要写出这个符号的原型了。需要注意的是,一个符号,在整个程序中可以被声明多次,但却要且仅要被定义一次。试想,如果一个符号出现了两种不同的定义,编译器该听谁的?
这种机制给 C++ 程序员们带来了很多好处,同时也引出了一种编写程序的方法。考虑一下,如果有一个很常用的函数 “void f() {}”,在整个程序中的许多 .cpp 文件中都会被调用,那么,我们就只需要在一个文件中定义这个函数,而在其他的文件中声明这个函数就可以了。一个函数还好对付,声明起来也就一句话。但是,如果函数多了,比如是一大堆的数学函数,有好几百个,那怎么办?能保证每个程序员都可以完完全全地把所有函数的形式都准确地记下来并写出来吗?
目标文件
目标文件中的基本元素有,符号(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)记录长文本(可能被省略)。
原文链接:https://blog.csdn.net/liigo/article/details/4858535