加载中…
个人资料
老徐
老徐
  • 博客等级:
  • 博客积分:0
  • 博客访问:789,035
  • 关注人气:156
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
相关博文
推荐博文
谁看过这篇博文
加载中…
正文 字体大小:

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

(2012-07-26 16:31:35)
标签:

动态编译

重定位

-fpic

got表

plt表

it

分类: NSN_BspDriver
Linux动态连接原理

注意:

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

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

GOT表;

GOT(Global Offset Table)表中每一项都是本运行模块要引用的一个全局变量或函数的地址。可以用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 Linkage Table)表每一项都是一小段代码,对应于本运行模块要引用的一个全局函数。以对函数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             PLT table                loader
--------------   --------------   -----------------------
fun:           <-- jmp*fun@GOT  --> change GOT entry from
           $loader to $fun,
           then jump to 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的符号表里面寻找,如果找到就返回,否则,报错,就是我们经常看到的undefined referenc to XXXXX.

3,  代码段重定位前提。

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

call L1
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_32 w/sec  R_386_RELATIVE
重定位|                 全局  R_386_32 w/sym  R_386_32 w/sym

0

阅读 评论 收藏 转载 喜欢 打印举报/Report
  • 评论加载中,请稍候...
发评论

    发评论

    以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

      

    新浪BLOG意见反馈留言板 电话:4000520066 提示音后按1键(按当地市话标准计费) 欢迎批评指正

    新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

    新浪公司 版权所有