链接_链接

要点一:

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也可以执行于加载时,甚至执行于运行时。

和其他机械的发展过程类似,在早期计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器的程序自动执行的。

链接器可以使得分离编译成为可能。这样意味着程序员可以不用讲一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。

要点二:

驱动程序再假示例程序从ASCII码源文件翻译成可执行目标文件时的步骤:

先将源程序main.c翻译成一个ASCII码的中间文件main.i,再将main.i翻译成一个ASCII汇编语言文件main.s,最后将main.s翻译成一个可重定位目标文件main.o。这些在第一章里已经讲述。

在这时一切正常,如果引入书里的例子:

在main函数里引入了一个swap.c,这个就是前面提到的链接。

驱动程序经过相同的过程生成swap.o,最后它运行链接器程序ld,将main.o和swap.o以及一些必要的系统目标文件组合起来,创建一个可执行的目标文件。

具体的过程还可以用一个流程图来表示:

要点三:

静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完成链接的、可以加载和运行的可执行目标文件作为输出。

输入的可重定位目标文件有各种不同的代码和数据节组成,每一节都是一个连续的字节序列。

为了构造可执行文件,链接器必须完成两个主要任务:符号解析和重定位。

目标文件有三种形式:可重定位目标文件、可执行目标文件和共享目标文件。

要点四:

拿ELF文件举例,ELF头以一个16字节的序列开始,这个序列描述了字的大小和生成该文件的系统的字节顺序。ELF头剩下的部分包含帮助链接器解析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中的表目大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的表目。

要点五:

每个可重定位目标木块都有一个符号表,它包含m所定义的引用的符号地信息。

在链接器地上下文中,符号一共有三种:

 1)由m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的c以及被定义为budaic的static属性的全局变量。

 2)由其他模块定义被模块m引用的全局符号。这些符号成为外部符号,对应于定义在其他模块中的c函数和变量。

 3)只被模块m定义和引用的本地符号。有的本地连接器符号对应于带static属性的c函数和全局变量。这些符号在模块m中的任何地方都是可见的,但是不能被其他模块引用。目标文件中对应于模块m的节和相应的源文件的名字也能活得本地符号。

要点六:

定义为带有Cstatic属性的本地过程变量是不在栈中管理的。取而代之,编译器在.data和.bss中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地连接器符号。

在这个例子中,我就可以用x.1表示f中的定义,x.2表示g中的定义。

符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。

有一个小知识点,common与bss的区别很小。common只存放未初始化的全局变量;而bss存放未初始化的静态变量,和已初始化为0的全局或静态变量。

要点七:

在编译时,编译器向汇编器输出每个全局符号,或者是强或者是弱。而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。

在Linux链接器中,使用下面的规则来处理多重定义的符号名:

 1)不允许有多个同名的强符号

 2)在一强多弱的同名符号中,选择强符号

 3)在多弱同名中,任意选择一个

这里的”处理“大部分都是指用于代码函数中的运算、赋值等操作。

要点八:

所有编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库,它可以用做链接器的输入。

如果不适用静态库,那么有两种方法向用户提供函数。

一种方法是让编译器辨认出对标准函数的调用,并直接生成相应的代码。这个对于Pascal可能是有用的,但是对于定义了大量的标准函数的c来说,这种方法将给百年一起增加显著的复杂性。并且每次对标准函数的调整,都必须更新一次编译器版本。然而对于应用程序员而言,这种方法会是非常方便的,因为标准函数将总是可用的。

另一种方法就是将所有标准函数都放在一个单独的可重定位目标模块中。

这种方法的优点是它将编译器的实现与标准函数的实现分离开来,并且仍然对程序员保持适度的便利。然而,系统中每个可执行文件现在都包含着一份标准函数集合的完全拷贝,这对磁盘空间是很大的浪费(因为全部都拷贝了,但是有时候需要的标准函数不多,而且就算一份的目标模块很小,但是如果可执行文件数量多起来,就很恐怖了)。再者,每个正在运行的程序都将它自己的这些函数的拷贝放在存储器中,这又是极度浪费存储器。并且,对于任何标准函数的任何改变,无论大小,都要求库的开发人员重新编译整个源文件,这是一个非常耗时的操作,使得标准函数的开发和维护变得很复杂。

静态库概念就被提出来,以解决这些不同方法的缺点。相关函数可以被编译为独立的目标模块,然后封装成一个单独的静态库文件。应用程序可以通过在命令行上指定单独的文件名字来使用这些在库中定义的函数。在链接时,链接器将只拷贝被应用程序引用的目标模块,这就减少了可执行文件在磁盘和存储器中的大小。另一方面,应用程序员只需要包含较少的库文件的名字。

要点九:

在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件(即静态库)。在这次扫描中,链接器维护一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号(即引用了但是尚未定义的符号)集合U,以及一个在前面输入文件中已定义的符号集合D。

要点十:

重定位分为两个步骤:

1)重定位节和符号定义。链接器将所有相同类型的节合并为同一类型的新的聚合节。这个节成为输出的可执行目标文件的节。

2)重定位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。链接器依赖于称为重定位标目的可重定位目标模块中的数据结构。

无论何时,汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位表目,告诉链接器在将目标文件合并成可执行文件时如何修改引用。代码的重定位表目放在.relo.text中。已初始化数据的重定位表目放在.relo.data中。

例如,在下面这个例子中:

offset就是我们需要修改的引用的节偏移,symbol标识被修改引用应该指向的符号,type告知链接器如何修改新的引用。

ELF定义了32种不同的重定位类型,有些相当隐秘。但是只需要关心其中最基本的重定位类型:

R_386_PC32(64位里就是:R_X86_64_PC32)重定位一个使用32位PC相对地址的引用。

R_386_32(64位里就是:R_X86_64_32)重定位一个使用32位就对地址的引用。

要点十一:

假设有一个可重定位目标文件有:

 

 

可以得知,swap是一个相对地址重定位的一个引用。那么怎么得出在可执行目标文件中call的格式呢?

(这里需要理清一个逻辑,可执行目标文件中调用了一个内容,这个内容在可重定位目标文件里,所以才需要重定位。)

根据举例的内容分析,call指令开始于节偏移0x6处,由一个字节的操作码0xe8和随后的32位引用0xfffffffc(-4)组成。这是小端法的显示。

结合上一个重点的例子中的要素,我们可以知道:

r.offset=0x7

r.symbol=swap

r.type=R_386_PC32

这些域告诉链接器修改开始于偏移量位0x7处的32位PC相关引用,使得运行时它指向swap程序。现在,假设链接器已经判定(就是链接器已知了。可以当作题目的条件,不需要死磕在这里。):

ADDR (s) = ADDR(.text) = 0x80483b4

ADDR (r.symbol) = ADDR(swap) = 0x80483c8

首先需要计算出引用的运行时的地址:

refaddr = ADDR (s) + r.offset = 0x80483b4+0x7 = 0x80483bb

之后,它将引用从当前值(-4)修改为0x9,使得它在运行时指向swap程序

*refptr = (unsigned) (ADDR (r.symbol) + *refptr -refaddr)

  = (unsigned) (0x80483c8+(-4)-0x80483bb) = 0x9

因此,就可以得到可执行目标文件中,call指令的重定位形式:

 

也就是在引用中,有以下步骤:

1)pc入栈

2)跳转:PC ← PC+0x9

在本例就是 0x80483bf + 0x9= 0x80483c8

绝对地址中,就只需要将最后*refptr = (unsigned) (ADDR (r.symbol) + *refptr -refaddr)

改为:*refptr = (unsigned) (ADDR (r.symbol) + *refptr)

即可。

要点十二:

以下是ELF中可执行文件中的各类信息:

 

要点十三:

静态库和所有软件一样,需要定期维护和更新。如果应用程序员想要使用最新版本,就必须以某种方式了解到库的更新情况,然后显式地将程序与新的库重新连接。并且,几乎每个c程序都是用标准I/O函数,这些函数的代码都会被复制到每个运行进程的文本段中。就好像之前要点八提供函数的一个方法缺点一样,一个程序运行代表一个进程,复制到运行进程的文本段中显然没什么,但是如果在一个运行几十个乃至上百上千个进程的计算机系统中,这就是对稀少的存储器系统资源的极大浪费了。

于是就出现了共享库这一个致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并在存储器中和一个程序链接起来。这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的,共享库也称为共享目标,在Linux系统中常用.so后缀来表示。微软的操作系统大量地使用乐共享库,它们称为dll(动态链接库)。

以下是共享库链接过程:

 

这里可以看出,动态链接器本身就是一个共享目标。加载器不再像它通常那样将控制传递给应用,取而代之的是加载和运行这个动态链接器。之后,动态链接器通过执行下面的重定位完成链接任务:

 1)重定位libc.so的文本和数据到某个内存段

 2)重定位libvector.so的文本和数据到另一个内存段

 3)重定位可执行文件p2中所有对由libc.so 和 libvector.so 定义的符号的引用。

最后,动态链接器将控制传递给应用程序。从这个时刻开始,共享库的位置就固定了,并且在程序执行的过程中都不会改变。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值