chapt1:链接和加载
链接与加载
链接器和加载器完成几个相关但概念上不同的动作。
?
程序加载:在某些情况下,加载仅仅是将数据从磁盘拷入内存;在其他情况下,还
包括分配存储空间,设置保护位或通过虚拟内存将虚拟地址映射到磁盘内存页上。
?
重定位:编译器和汇编器通常为每个文件创建程序地址从 0 开始的目标代码,但是
几乎没有计算机会允许从地址 0
加载你的程序。如果一个程序是由多个子程序组成
的,那么所有的子程序必须被加载到互不重叠的地址上。重定位就是为程序不同部
分分配加载地址,调整程序中的数据和代码以反映所分配地址的过程。在很多系统
中,重定位不止进行一次。对于链接器的一种普遍情景是由多个子程序来构建一个
程序,并生成一个链接好的起始地址为
0
的输出程序,各个子程序通过重定位在大
程序中确定位置。当这个程序被加载时,系统会选择一个加载地址,而链接好的程
序会作为整体被重定位到加载地址。
?
符号解析:当通过多个子程序来构建一个程序时,子程序间的相互引用是通过符号
进行的;主程序可能会调用一个名为
sqrt 的计算平方根例程,并且数学库中定义了
sqrt 例程。链接器通过标明分配给 sqrt 的地址在库中来解析这个符号,并通过修
改目标代码使得
call 指令引用该地址。
链接器和加载器共有的一个重要特性就是他们都会修改目标代码
两遍链接
链接器将一系列的目标文件、库、及可能的命令文件作为它的输入,然后将输出的
目标文件作为产品结果,此外也可能有诸如加载映射信息或调试器符号文件的副产品。
每个输入文件都包含一系列的段(segments),即会被连续存放在输出文件中的代码
或数据块。每一个输入文件至少还包含一个符号表(symbol
table)。有一些符号会作为导
出符号,他们在当前文件中定义并在其他文件中使用,通常都是可以在其它地方被调用的当
前文件内例程的名字。其它符号会作为导入符号,在当前文件中使用但不在当前文件中定义,
通常都是在该文件中调用但不存在于该文件中的例程的名字。
当链接器运行时,会首先对输入文件进行扫描,得到各个段的大小,并收集对所有符
号的定义和引用。它会创建一个列出输入文件中定义的所有段的段表,和包含所有导出、导
入符号的符号表。
利用第一遍扫描得到的数据,链接器可以为符号分配数字地址,决定各个段在输出地
址空间中的大小和位置,并确定每一部分在输出文件中的布局。
第二遍扫描会利用第一遍扫描中收集的信息来控制实际的链接过程。它会读取并重定
位目标代码,为符号引用替换数字地址,调整代码和数据的内存地址以反映重定位的段地址,
并将重定位后的代码写入到输出文件中。通常还会再向输出文件中写入文件头部信息,重定
位的段和符号表信息。如果程序使用了动态链接,那么符号表中还要包含运行时链接器解析
动态符号时所需的信息。在很多情况下,链接器自己将会在输出文件中生成少量代码或数据,
例如用来调用覆盖中或动态链接库中的例程的“胶水代码”,或在程序启动时需要被调用的
指向各初始化例程的函数指针数组。
有些目标代码的格式是可以重链接的,也就是一次链接器运行的输出文件可以作为下
次链接器运行的输入。这要求输出文件要包含一个像输入文件中那样的符号表,以及其它会
出现在输入文件中的辅助信息。
几乎所有的目标代码格式都预备有调试符号,这样当程序在调试器控制下运行时,调
试器可以使用这些符号让程序员通过源代码中的行号或名字来控制程序。根据目标代码格式
细节的不同,调试符号可能会与链接器需要的符号混合在一个符号表中,也可能独立于链接
器需要的符号表为链接器建立单独建立一个有些许冗余的符号表。
当链接器处理完所有规则的输入文件后,如果还存在未解析的导入名称
(imported
name),它就会查找一个或多个库,然后将输出这些未解析名字的库中的任何
文件链接进来。
由于链接器将部分工作从链接时推迟到了加载时,使这项任务稍微复杂了一些。在链
接器运行时,链接器会识别出解析未定义符号所需的共享库,但是链接器会在输出文件中标
明用来解析这些符号的库名称,而不是在链接时将他们链入程序,这样可以在程序被加载时
进行共享库绑定。
重定位和代码修改
链接器和加载器的核心动作是重定位和代码修改。当编译器或汇编器产生一个目标代
码文件时,它使用文件中定义的未重定位代码地址和数据地址来生成代码,对于其它地方定
义的数据或代码通常就是
0。作为链接过程的一部分,链接器会修改目标代码以反映实际分
配的地址。
有些系统需要无论加载到什么位置都可以正常工作的位置无关代码。链接器需要提供
额外的技巧来支持位置无关代码,与程序中无法做到位置无关的部分隔离开来,并设法使这
两部分可以互相通讯
编译器驱动
很多情况下,链接器所进行的操作对程序员是几乎或完全不可见的,因为它会做为编
译过程的一部分自动进行。多数编译系统都有一个可以按需自动执行编译器各个阶段的编译
器驱动。例如,若一个程序员有两个
C 源程序文件(简称 A,B),那么在 UNIX 系统上编译
器驱动将会运行如下一系列的程序:
? C 语言预处理器处理 A,生成预处理的
A
? C 语言编译预处理的 A,生成汇编文件 A
? 汇编器处理汇编文件 A,生成目标文件 A
? C 语言预处理器处理 B,生成预处理的
B
? C 语言编译预处理的 B,生成汇编文件 B
? 汇编器处理汇编文件 B,生成目标文件 B
? 链接器将目标文件 A、B 和系统 C
库链接在一起
也就是说,编译器驱动首先会将每个源文件编译为汇编语言,然后转换为目标代码,
接着链接器会将目标代码链接器一起,并包含任何需要的系统
C
库例程。
链接器命令语言
每个链接器都有某种形式的命令语言来控制链接过程。
有四种常见技术向链接器传送指令:
?
命令行
? 与目标文件混在一起
? 嵌入在目标文件中:
有一些目标代码格式,特别是微软的,允许将链接器命令嵌入
到目标文件中。这就允许编译器将链接一个目标文件时所需要的任何选项通过文件
自身来传递。
?
单独的配置语言: 极少有链接器拥有完备的配置语言来控制链接过程。可以处理众
多目标文件类型、机器体系架构和地址空间规定的 GNU
链接器,拥有可以让程序员
指定段链接顺序、合并相近段规则、段地址和大量其它选项的一套复杂的控制语言。
其它链接器一般拥有诸如支持程序员可定义的重叠技术等特性的稍简单一些的配置
语言。
第
2 章
体系结构的问题
硬件体系结构的两个方面影响到链接器:程序寻址和指令格式。链接器需要做的事情
之一就是对数据和指令中的地址及偏移量都要进行修改。两种情况下链接器都必须确保所做
的修改符合计算机使用的寻址方式;当修改指令时还需要进一步确保修改结果不能是无效指
令。
应用程序二进制接口
每个操作系统都会为运行在该系统下的应用程序提供应用程序二进制接口(Applicatio
n
Binary Interface)。ABI
包含了应用程序在这个系统下运行时必须遵守的编程约定。ABI
总是包含一系列的系统调用和使用这些系统调用的方法,以及关于程序可以使用的内存地址
和使用机器寄存器的规定。
寄存器的容量几乎都是和程序地址的大小相同,就是说在一个 32 位地址的
系统中寄存器是 32 位的,而在具有 64 位地址的系统上,寄存器就是 64
位的了。