7.12 PIC+PLT+GOT

一、地址无关代码(Position-Independent code,PIC)

共享库的一个重要作用是允许多个正在运行的进程在内存中共享相同的库代码,从而节省宝贵的内存资源。那么,多个进程如何共享一个程序的单个副本呢?一种方法是预先将地址空间的一个专用块分配给每个共享库,然后要求加载程序始终在该地址加载共享库。这种方法虽然简单,但会产生一些严重的问题。这将是地址空间的低效使用,因为即使进程不使用库,也会分配部分空间。其次,这将很难管理。我们必须确保没有一块重叠。每次修改一个库时,我们都必须确保它仍然适合指定的块。如果没有,那么我们就必须找到一个新的块。如果我们建了一个新的库,我们就得给它找地方。随着时间的推移,考虑到一个系统中有数百个库和库的版本,很难防止地址空间碎片化为许多未使用但不可用的小空间。更糟糕的是,对于每个系统,库到内存的分配是不同的,因此会造成更多的管理问题。

更好的方法是编译库代码使得库代码可以在任何地址加载和执行,而无需链接器进行修改。这种代码被称为位置无关代码(position-independent-code,PIC)。用户可以使用gcc的-fPIC选项指示GNU编译系统生成PIC代码。

在IA32系统上,对同一目标模块中的过程的调用不需要特殊处理,因为引用是PC相关的,具有已知的偏移量,因此已经是PIC。但是,调用外部定义的过程和引用全局变量通常不是PIC,因为它们需要在链接时进行重定位。

1.1 PIC数据引用

编译器利用以下方式来生成对全局变量的PIC引用:无论我们在内存中的哪个位置加载目标模块(包括共享目标模块),数据段总是紧跟在代码段之后分配。因此,代码段中的指令和数据段中的变量之间的距离是运行时常数,与代码和数据段的绝对内存位置无关。

编译器在数据段的开头创建了一个全局偏移表(global offset table,GOT)。GOT包含目标模块引用的每个全局数据对象的条目。编译器还为GOT中的每个条目生成一个重定位记录。在加载时,动态链接器重新定位GOT中的每个条目,使其包含适当的绝对地址。引用全局数据的每个目标模块都有自己的GOT。

在运行时,每个全局变量都通过GOT进行间接引用,涉及到的代码形式如下:

		call L1
L1:		pop  %ebx					;ebx包含当前PC
		addl $VAROFF,%ebx			;ebx指向var在GOT中的偏移量
		movl (%ebx),%eax			;reference indirect through the GOT
		movl (%eax),%eax

在上面的代码中,对L1的调用将返回地址(恰好是popl指令的地址)推送到堆栈上。popl指令随后将此地址弹出到%ebx中。这两条指令的净作用是将PC的值移到寄存器%ebx中。

addl指令向%ebx添加一个常量偏移量,以便它指向GOT中的相应条目,该条目包含数据项的绝对地址。此时,可以通过%ebx中包含的GOT条目间接引用全局变量。在本例中,两条movl指令将全局变量的内容(间接通过GOT)加载到寄存器%eax中。

PIC代码在性能上有一些缺点。每一个全局变量引用现在需要五个指令,而不是一个(此时需要额外的内存引用来指向GOT)。并且,PIC代码使用额外的寄存器来保存GOT表项的地址。在拥有大量寄存器文件的机器上,这并不是问题,但是在寄存器稀缺的IA32系统上,占用仅仅一个寄存器都可能引发寄存器溢出到堆栈上。

1.2 PIC函数调用

PIC代码同样可以使用相同的方法来解析外部过程调用:

		call L1
L1: 	popl %ebx 				ebx contains the current PC
		addl $PROCOFF, %ebx 	ebx points to GOT entry for proc
		call *(%ebx) 			call indirect through the GOT

但是,这种方法需要为每个过程调用附加三条指令。ELF编译系统使用了一种称为延迟绑定(lazy binding)的技术,它将过程地址的绑定推迟到第一次调用。在第一次调用过程时会有一个不寻常的运行时开销,但此后的每次调用只需要一条指令和一个内存引用。

延迟绑定是通过两个数据结构(GOT和procedure linkage table,PLT)之间的交互来实现的。如果一个目标模块调用共享库中定义的任何函数,那么它就有自己的GOT和PLT。GOT是.data部分的一部分。PLT是.text部分的一部分。

图7.17显示了图7.6中示例程序main2.o的GOT格式。前三个GOT条目是特殊的:GOT[0]包含.dynamic段的地址,该段包含动态链接器用于绑定过程地址的信息,例如符号表的位置和重定位信息。GOT[1]包含一些定义此模块的信息。GOT[2]包含动态链接器的延迟绑定代码的入口点:
在这里插入图片描述
在共享目标文件中定义并由main2.o调用的每个过程都会在GOT中获取一个条目,从条目GOT[3]开始。对于示例程序,我们显示了printf的GOT条目,它在libc.so中定义,同时addvec在libvector.so中定义.

图7.18显示了示例程序p2的PLT:
在这里插入图片描述
PLT是一个16字节的条目数组。第一个条目PLT[0]是指向动态链接器的特殊条目。每个被调用的过程在PLT中都有一个条目,从PLT[1]开始。在图中,PLT[1]对应于printf,PLT[2]对应于addvec。

最开始,在程序被动态链接并开始执行后,printf和addvec过程在它们各自的PLT条目中和第一个指令进行了绑定。例如,对addvec的调用有如下的形式:

80485bb: 	e8 a4 fe ff ff 		call 8048464 <addvec>

当addvec第一次被调用时,控制权移交给PLT[2]中的第一条指令,这条指令的作用是间接跳转到GOT[4]。最开始时,每个GOT条目都包含对应的PLT条目中的pushl条目的地址。所以PLT中的间接跳转仅仅是将控制权移交给了PLT[2]中的下一条指令。这个指令将addvec的符号ID推送到了栈顶。最后一个指令跳转到了PLT[0],这个指令将GOT[1]中一个字长的确认信息推送到了栈顶,然后间接通过GOT[2]跳转到了动态链接器中。动态链接器使用这两个堆栈条目来判断addvec的地址,然后用这个地址覆盖GOT[4],最后将控制权移交给addvec。

当下一次在程序中调用addvec时,控制权仍然先转移给PLT[2]。但是,这次通过GOT[4]的间接跳转可以直接将控制权转交给addvec。这次调用唯一的额外负担就是这次间接跳转的内存引用。

二、总结

链接过程可以通过静态链接器在编译时进行,也可以通过动态链接器在加载和运行时进行。链接器处理称为目标文件的二进制文件,这些二进制文件有可重定位、可执行以及共享三种类型。可重定位目标文件由静态链接器组合成一个可被载入内存并执行的可执行文件。共享目标文件(共享库)在运行时被动态链接器链接并加载。 either implicitly when the calling program is loaded and begins executing, or on demand, when the program calls functions from the dlopen library.

加载器的两个主要任务是符号解析和重定位。符号解析阶段目标文件中的每个全局符号都和一个独一无二的定义绑定在一起。而重定位阶段决定了每个符号最终的内存地址并且修改指向这些符号的引用。

静态链接器被编译器驱动所引用(例如GCC)。静态链接器将多个可重定位目标文件组合成一个单独的可执行目标文件。

多个目标文件还可以被连接成一个单独的静态库。链接器使用这些库来解析其他目标模块中的符号引用。

加载器将可执行文件的内容载入内存并运行程序。链接器也可以生成部分链接的可执行文件,这种可执行文件包含共享库中未解析的变量和函数引用。在载入时,加载器将部分链接的可执行文件加载入内存然后调用一个动态链接器。这个动态链接器通过载入共享库并且重定位引用来完成链接任务。

被编译为位置无关代码的共享库可以在程序的任何部分被加载,并且被多个程序在运行时共享。应用程序也可以在运行时时候动态链接器进行加载、链接并且访问共享库中的函数和数据。

练习:


7.6

以图7.1的两个函数为例,下面的swap.c函数有了部分修改,现在这个程序会记录自己被调用的次数:
在这里插入图片描述
对swap.o中每一个定义和引用的符号,指出它是否在swap.o模块中的.symtab节中存在一个符号表表项。如果存在表项,指明定义这个符号的模块(swap.o或main.o),符号类型(local、global或者extern),以及它在模块中占据的节(.text、.data或者.bss)。

在这里插入图片描述

答案:

在这里插入图片描述


7.7

在不改变任何变量名的基础上,修改图7.4中的bar5.c,使得foo5.c可以正确输出x和y的值(十六进制表示下分别为15213和15212)

答案:
在这里插入图片描述


7.8

在这个问题中,让REF(x.i)–>DEF(x.k)表示链接器会将一个在模块i中引用的符号x指向模块k中的x的定义。对于下面的每个例子,都利用这样的表示方法来表示链接器如何在每个模块中解析多重定义的符号。如果存在违反规则一的错误,输出“ERROR”,如果链接器从弱引用中随机选择了一个(规则三),那么输出“UNKNOWN”:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
答案:

A
main.1
main.2
模块2用static限定了main,这使得main成为一个局部符号,因此不存在多重定义的全局符号。每个模块的引用都指向自己定义的main。这是一个重要的观念,我们要理解static的用法,它能够限定函数和变量的作用域。

B
UNKNOWN

C
ERROR


7.9

如下的程序包含了两个目标模块:
在这里插入图片描述
当程序在Linux系统下编译执行时,会打印出"0x55\n"并正常终止,尽管p2没有初始化main变量。解释下这个问题

答案:

由于规则二,在foo6.o中定义的强符号main会覆盖在bar6.o中定义的弱符号main。因此,对于bar6.o中main的引用会指向强符号main的定义,因此是取了函数main的地址的首个字节。这个字节包含了地址0x55,就是二进制编码的汇编指令pushl %ebp,在过程main中的第一指令。


7.11

图7.12的段首部指示了数据段在内存中占据了0x104大小的字节。然而,这些字节仅有前0xe8来自于可执行文件中的节。什么导致了这个不同?

答案:

运行时数据段的第一部分是用目标文件中的.data节来进行初始化的。而运行时数据段的最后一部分是用.bss节进行初始化的,这一部分总是被初始化为零,并且在可执行文件中不占用任何的实际空间。因此可执行文件与运行时数据段的区别就是bss节是否占据空间。


7.12(这题需要再好好看看)

图7.10中的swap例程包含5个重定位引用。对于每个重定位引用,写出它在图7.10中的行号,它的运行时地址和它的值。swap.o模块的初始代码以及重定位实体在图7.19中展示了出来:
在这里插入图片描述
答案:

解决问题的方式是模拟链接器的行为:要么使用重定位记录来辨别引用的地址,接下来使用图7.9所示的算法来计算重定位绝对地址,要么直接从图7.10中的重定位后的指令中提取出这些地址。在图7.19中有关重定位目标文件有几件事需要注意:

  • 第8行的movl指令包含需要两个需要进行重定位的引用
  • 在第5行和第8行中包含buf[1]的引用,初值为0x4。重定位后的地址计算为ADDR(buf)+4

在这里插入图片描述


7.13 (这题也需要认真看看)

考虑图7.20中所示的C代码和对应的重定位目标模块。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
图 7.20

A 判断在模块重定位时.text节中的哪些指令需要被链接器修改。对于每个这样的指令,列举出它存储在重定位实体中的信息:段偏移量、重定位类型、符号名。

B 判断在模块重定位时.data节中的哪个目标数据需要被链接器修改。对于每个这样的指令,列举出它存储在重定位实体中的信息:段偏移量、重定位类型、符号名。

答案:

在这里插入图片描述

待补充(730)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值