GOT表,PLT表,代码段重定位,数据段重定位--Linux动态连接原理

本文深入探讨了Linux系统中动态链接的原理,详细解释了GOT表和PLT表的作用及其实现机制,以及如何通过这些机制实现代码和数据段的重定位。

http://blog.sina.com.cn/s/blog_54f82cc201011oqv.html

Linux动态连接原理

注意:

以下所用的连接器是指,ld,

而加载器是指ld-linux.so;

GOT表;

GOT(GlobalOffsetTable)表中每一项都是本运行模块要引用的一个全局变量或函数的地址。可以用GOT表来间接引用全局变量、函数,也可以把GOT表的首地址作为一个基准,用相对于该基准的偏移量来引用静态变量、静态函数。由于加载器不会把运行模块加载到固定地址,在不同进程的地址空间中,各运行模块的绝对地址、相对位置都不同。这种不同反映到GOT表上,就是每个进程的每个运行模块都有独立的GOT表,所以进程间不能共享GOT表。
在x86体系结构上,本运行模块的GOT表首地址始终保存在�x寄存器中。编译器在每个函数入口处都生成一小段代码,用来初始化�x寄存器。这一步是必要的,否则,如果对该函数的调用来自另一运行模块,�x中就是调用者模块的GOT表地址;不重新初始化�x就用来引用全局变量和函数,当然出错。

 

这两段话的意思是说,GOT是一个映射表,这里的内容是此段代码里面引用到的外部符号的地址映射,比如你用到了一个printf函数,在这里就会有一项假设是1000,则就像这样的:

.Got

符号                       地址

Printf                    1000

 

………

这样的话程序在运行到printf的时候就寻找到这个地址1000从而走到其实际的代码中的地方去。

但是这里存在一个问题,因为printf是在共享库里面的,而共享库在加载的时候是没有固定地址的,所以你不知道它的地址是1000还是2000?怎么办呢?

于是引入了下面的表plt,这个表的内容是什么呢?请看下面:

2,  PLT表;

PLT(Procedure LinkageTable)表每一项都是一小段代码,对应于本运行模块要引用的一个全局函数。以对函数fun的调用为例,PLT中代码片断如下:

.PLT

fun:  jmp*fun@GOT(�x)
pushl $offset
jmp .PLT0@PC

其中引用的GOT表项被加载器初始化为下一条指令(pushl)的地址,那么该jmp指令相当于nop空指令。

用户程序中对fun的直接调用经编译连接后生成一条call [email]fun@PLT指令,这是一条相对跳转指令(满足浮动代码的要求!),跳到.PLTfun。如果这是本运行模块中第一次调用该函数,此处的jmp等于一个空指令,继续往下执行,接着就跳到PLT[email]0。该PLT项保留给编译器生成的额外代码,会把程序流程引入到加载器中去。加载器计算fun的实际入口地址,填入fun@GOT表项。图示如下:

user program
--------------
call fun@PLT
|
v
DLL           PLTtable            loader
--------------  --------------  -----------------------
fun:         <--jmp*fun@GOT  -->change GOT entry from
         $loader to$fun,
         then jumpto there
GOT table
--------------
fun@GOTloader

第一次调用以后,GOT表项已指向函数的正确入口。以后再有对该函数的调用,跳到PLT表后,不再进入加载器,直接跳进函数正确入口了。从性能上分析,只有第一次调用才要加载器作一些额外处理,这是完全可以容忍的。还可以看出,加载时不用对相对跳转的代码进行修补,所以整个代码段都能在进程间共享。

 

上面的话是什么意思呢?

拿我们上面举的例子,printf在got表里面对应的地址是1000,而这个1000到底以为着什么呢?

PLTfun:  jmp*fun@GOT(�x)
1000: pushl $offset
jmp
.PLT0@PC

你可以看到所谓1000就是它下面的这个地址,也就是说在外部函数还没有实现连接的时候,got表里面的内容其实是指向下一条指令的,于是开始执行了plt表里面的内容,于是这个段里面的内容肯定包括计算当前这个函数的实际地址的内容,于是求得实际地址添入got表,假设地址为0x800989898

于是got表里面的内容就应该这样的:

Printf                      0x800989898

 

………………..

这样当下一次调用这个printf的时候就不需要再去plt表里面走一遭了。
这里需要提一下的是,查找printf的地址实际上就是递归查找当前执行的程序所依赖的库,在她们export的符号表里面寻找,如果找到就返回,否则,报错,就是我们经常看到的undefinedreferenc to XXXXX.

3,  代码段重定位前提。

代码段本身是存在于只读区域的,所以理论上它是不可能在运行的时候重新修改的,但是这就涉及一个问题,如何保证Got表的正确使用,因为每一个进程都有自己的got表,而共享库完全同时被许多个进程使用的,于是在每个函数的入口都有这样的语句:

callL1
L1:  popl �x
addl $GOT+[.-.L1], �x
.o:  R_386_GOTPC
.so: NULL

上述过程是编译、连接相合作的结果。编译器生成目标文件时,因为此时还不存在GOT表(每个运行模块有一个GOT表,一个PLT表,由连接器生成),所以暂时不能计算GOT表与当前IP间的差值,仅在第三句处设上一个R_386_GOTPC重定位标记而已。然后进行连接。连接器注意到GOTPC重定位项,于是计算GOT与此处IP的差值,作为addl指令的立即寻址方式操作数。以后再也不需要重定位了。

这样做的好处是目的是什么呢?

就是在函数内部引用外部符号的时候能够正确的转到适当的地方去。

 

4,  变量、函数引用

当引用的是静态变量、静态函数或字符串常量时,使用R_386_GOTOFF重定位方式。它与GOTPC重定位方式很相似,同样首先由编译器在目标文件中设上重定位标记,然后连接器计算GOT表与被引用元素首地址的差值,作为leal指令的变址寻址方式操作数。代码片断如下:

leal .LC1@GOTOFF(�x), �x
.o:  R_386_GOTOFF
.so: NULL

当引用的是全局变量、全局函数时,编译器会在目标文件中设上一个R_386_GOT32重定位标记。连接器会在GOT表中保留一项,注上R_386_GLOB_DAT重定位标记,用于加载器填写被引用元素的实际地址。连接器还要计算该保留项在GOT表中的偏移,作为movl指令的变址寻址方式操作数。代码片断如下:

movl x@GOT(�x), �x
.o:  R_386_GOT32
.so: R_386_GLOB_DAT

需要指出,引用全局函数时,由GOT表读出不是全局函数的实际入口地址,而是该函数在PLT表中的入口.PLTfun。这样,无论直接调用,还是先取得函数地址再间接调用,程序流程都会转入PLT表,进而把控制权转移给加载器。加载器就是利用这个机会进行动态连接的。

 

  注意:这里讨论的是变量函数的引用,不是函数的直接调用,而是函数,变量的地址的取得,如果是函数的话,取得的实际上是plt里面的地址,于是最终还是没能逃过加载器的协助。

5,  直接调用函数
如前所述,浮动代码中的函数调用语句会编译成相对跳转指令。首先编译器会在目标文件中设上一个R_386_PLT32重定位标记,然后视静态函数、全局函数不同而连接过程也有所不同。

如果是静态函数,调用一定来自同一运行模块,调用点相对于函数入口点的偏移量在连接时就可计算出来,作为call指令的相对当前IP偏移跳转操作数,由此直接进入函数入口,不用加载器操心。相关代码片断如下:

call f@PLT
.o:  R_386_PLT32
.so: NULL

如果是全局函数,连接器将生成到.PLTfun的相对跳转指令,之后就如前面所述,对全局函数的第一次调用会把程序流程转到加载器中去,然后计算函数的入口地址,填充fun@GOT表项。这称为R_386_JMP_SLOT重定位方式。相关代码片断如下:

call f@PLT
.o:  R_386_PLT32
.so: R_386_JMP_SLOT

如此一来,一个全局函数可能有多至两个重定位项。一个是必需JMP_SLOT重定位项,加载器把它指向真正的函数入口;另一个是GLOB_DAT重定位项,加载器把它指向PLT表中的代码片断。取函数地址时,取得的总是GLOB_DAT重定位项的值,也就是指向.PLTfun,而不是真正的函数入口。

进一步考虑这样一个问题:两个动态连接库,取同一个全局函数的地址,两个结果进行比较。由前面的讨论可知,两个结果都没有指向函数的真正入口,而是分别指向两个不同的PLT表。简单进行比较,会得出"不相等"的结论,显然不正确,所以要特殊处理。

 

 

注意:

一个是必需JMP_SLOT重定位项,这里指的就是直接调用函数的情况;

另一个是GLOB_DAT重定位项,这里指函数地址引用的情况;

6,  数据段的重定位

在数据段中的重定位是指对指针类型的静态变量、全局变量进行初始化。它与代码段中的重定位比较起来至少有以下明显不同:一、在用户程序获得控制权(main函数开始执行)之前就要全部完成;二、不经过GOT表间接寻址,这是因为此时�x中还没有正确的GOT表首地址;三、直接修改数据段,而代码段重定位时不能修改代码段。

如果引用的是静态变量、函数、串常量,编译器会在目标文件中设上R_386_32重定位标记,并计算被引用变量、函数相对于所在段首地址的偏移量。连接器把它改成R_386_RELATIVE重定位标记,计算它相对于动态连接库首地址(通常为零)的偏移量。加载器会把运行模块真正的首地址(不为零)与该偏移量相加,结果用来初始化指针变量。代码片断如下:

.section .rodata
.LC0: .string "Ok\n"
.data
p:    .long.LC0
.o:  R_386_32 w/ section
.so: R_386_RELATIVE

如果引用的是全局变量、函数,编译器同样设上R_386_32重定位标记,并且记录引用的符号名字。连接器不必动作。最后加载器查找被引用符号,结果用来初始化指针变量。对于全局函数,查找的结果仍然是函数在PLT表中的代码片断,而不是实际入口。这与前面引用全局函数的讨论相同。代码片断如下:

.data
p:      .long printf
.o:  R_386_32 w/ symbol
.so: R_386_32 w/ symbol

 

7,  总结:

下表给出了前面讨论得到的全部结果:
.o                       .so
--------------------------------------------------------------------------
|装载GOT表首地址       R_386_GOTPC    NULL
代码段|-----------------------------------------------------
重定位|引用变量函数地址静态  R_386_GOTOFF   NULL
           全局  R_386_GOT32    R_386_GLOB_DAT
|-----------------------------------------------------
|直接调用函数    静态  R_386_PLT32    NULL
             全局  R_386_PLT32    R_386_JMP_SLOT
------|-----------------------------------------------------
数据段|引用变量函数地址 静态  R_386_32w/sec  R_386_RELATIVE
重定位|             全局  R_386_32w/sym  R_386_32w/sym

<think>好的,我需要详细解释GOT重定位原理机制。首先,GOT全称是全局偏移(Global Offset Table),它在动态链接中扮演关键角色。用户可能已经了解动态链接的基本概念,但需要深入GOT的具体工作流程。 根据引用[1],PLTGOT的关系是调用函数时,PLT最终跳转到GOT,而GOT存储函数的实际地址。当函数第一次被调用时,GOT中的地址可能指向PLT中的代码,触发动态链接器进行地址解析重定位。之后,GOT会保存真实的函数地址,后续调用直接跳转,无需再次解析。 引用[4]提到动态重定位区如何修改指令的操作数,将真实地址填入指定位置。这说明GOT重定位涉及修改内存中的地址引用,确保它们指向正确的目标。需要解释加载时如何计算这些地址,并更新GOT条目。 引用[5]区分了静态函数动态函数的处理,强调动态函数需要重定位,而静态函数不需要。这可能帮助用户理解为何GOT动态链接至关重要。 用户的问题集中在GOT重定位的详细机制,因此需要分步骤说明其工作流程,包括延迟绑定、加载时重定位运行时解析。同时,结合引用中的例子,如_gp_disp符号(引用[3])加载时填充偏移量(引用[4]),可以具体说明地址计算过程。 还需要注意区分加载时重定位位置无关代码(PIC)的不同,引用[2]提到加载时重定位的性能优势,这可能与GOT的处理方式有关。但用户主要关注GOT本身,所以重点应放在GOT如何被填充更新。 最后,确保回答结构清晰,使用中文,正确应用LaTeX格式,如变量用$...$,独立公式用$$...$$。生成相关问题时要围绕GOT重定位的机制应用,帮助用户进一步探索。</think>### GOT重定位工作原理详解 GOT(Global Offset Table,全局偏移)是动态链接过程中实现符号重定位的核心数据结构。其核心机制是通过**间接寻址**解决动态库函数全局变量的地址延迟绑定问题。以下是分步解析: --- #### 一、GOT的基本结构 1. **物理存储**:GOT是进程内存中的一个数据段,存储着所有需要动态解析的符号(函数或变量)的绝对地址。 2. **初始状态**:在程序加载时,GOT项初始值为符号解析前的临时地址(通常指向动态链接器的解析逻辑)。 3. **重定位类型**: - **加载时重定位**:程序启动时由动态链接器(如`ld-linux.so`)填充GOT项[^2]。 - **延迟绑定(Lazy Binding)**:首次调用函数时通过PLT(Procedure Linkage Table)触发解析[^1]。 --- #### 二、GOT重定位流程(以函数调用为例) 1. **首次调用函数**: ```asm call printf@plt # 通过PLT跳转 ``` - PLT项内容: ```asm printf@plt: jmp *GOT[n] # 初始跳转到PLT的下一条指令 push index # 压入符号在重定位中的索引 jmp _dl_runtime_resolve # 调用动态链接器解析符号 ``` > 初始状态下,`GOT[n]`指向`push`指令的地址[^1]。 2. **动态链接器介入**: - 动态链接器通过`.rel.plt`段找到符号`printf`的真实地址。 - 将计算出的绝对地址写入`GOT[n]`,完成重定位[^4]。 3. **后续调用**: - 直接通过`GOT[n]`跳转到目标函数,无需重复解析。 --- #### 三、关键技术点 1. **地址计算与偏移量**: - 在位置无关代码(PIC)中,通过`_gp_disp`符号(全局指针)与GOT的偏移量快速定位GOT[^3]。 - 示例计算(MIPS架构): $$ \text{目标地址} = \text{\$gp} + \text{offset} $$ 其中`$gp`保存GOT的基地址,`offset`为符号在GOT中的偏移。 2. **重定位类型区分**: - **R_X86_64_GLOB_DAT**:直接填充符号的绝对地址到GOT项。 - **R_X86_64_JUMP_SLOT**:用于函数跳转,触发延迟绑定[^1]。 3. **与静态链接的对比**: - 静态函数地址在编译时确定,无需GOT(通过相对偏移寻址)[^5]。 - 动态符号必须通过GOT间接寻址,实现跨模块访问。 --- #### 四、性能与安全考量 1. **性能优化**: - 延迟绑定减少启动时的解析开销。 - 加载时重定位避免运行时计算,适用于对启动速度敏感的场景[^2]。 2. **安全性机制**: - GOT可标记为只读(RELRO保护),防止覆盖攻击。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值