【图片+代码】:Linux动态链接过程中的【重定位】底层原理

道哥分享
关注

当然,在加载liba.so时,又会发现它依赖libb.so,于是又把在虚拟地址空间中找一块能放得下libb.so的空闲空间,把libb.so中的代码段、数据段等加载到内存中,示意图如下所示:

动态链接器自身也是一个动态库,而且是一个特殊的动态库:它不依赖于其他的任何动态库,因为当它被加载的时候,没有人帮它去加载依赖的动态库,否则就形成鸡生蛋、蛋生鸡的问题了。

动态库的加载地址

一个进程在运行时的实际加载地址(或者说虚拟内存区域),可以通过指令:$ cat /proc/[进程的 pid]/maps 读取出来。

例如:我的虚拟机中执行main程序时,看到的地址信息是:

黄色部分分别是:main, liba.so, libb.so 这3个模块的加载信息。

另外,还可以看到c库(libc-2.23.so)、动态链接器(ld-2.23.so)以及动态加载库libdl-2.23.so的虚拟地址区域,布局如下:

可以看出出来:main可执行程序是位于低地址,所有的动态库都位于4G内存空间的最后1G空间中。

还有另外一个指令也很好用 $ pmap [进程的 pid],也可以打印出每个模块的内存地址:

符号重定位

全局符号表

在之前的静态链接中学习过,链接器在扫描每一个目标文件(.o文件)的时候,会把每个目标文件中的符号提取出来,构成一个全局符号表。

然后在第二遍扫描的时候,查看每个目标文件中需要重定位的符号,然后在全局符号表中查找该符号被安排在什么地址,然后把这个地址填写到引用的地方,这就是静态链接时的重定位。

但是动态链接过程中的重定位,与静态链接的处理方式差别就大很多了,因为每个符号的地址只有在运行的时候才能知道它们的地址。

例如:liba.so引用了libb.so中的变量和函数,而libb.so中的这两个符号被加载到什么位置,直到main程序准备执行的时候,才能被链接器加载到内存中的某个随机的位置。

也就是说:动态链接器知道每个动态库中的代码段、数据段被加载的内存地址,因此动态链接器也会维护一个全局符号表,其中存放着每一个动态库中导出的符号以及它们的内存地址信息。

在示例代码main.c函数中,我们通过dlopen返回的句柄来打印进程中的一些全局符号的地址信息,输出内容如下:

上文已经纠错过:本来是想打印变量的地址信息,但是 printf 语句中不小心加上了型号,变成了打印变量值。

可以看到:在全局符号表中,没有找到liba.so中的变量a1和函数func_a2这两个符号,因为它俩都是static类型的,在编译成动态库的时候,没有导出到符号表中。

既然提到了符号表,就来看看这 3 个ELF文件中的动态符号表信息:

1. 动态链接库中保护两个符号表:.dynsym(动态符号表: 表示模块中符号的导出、导入关系) 和 .symtab(符号表: 表示模块中的所有符号);

.symtab 中包含了 .dynsym;

2. 由于图片太大,这里只贴出 .dynsym 动态符号表。

绿色矩形框前面的Ndx列是数字,表示该符号位于当前文件的哪一个段中(即:段索引);

红色矩形框前面的Ndx列是UND,表示这个符号没有找到,是一个外部符号(需要重定位);

全局偏移表GOT

在我们的示例代码中,liba.so是比较特殊的,它既被main可执行程序所依赖,又依赖于libb.so。

而且,在liba.so中,定义了静态、动态的全局变量和函数,可以很好的概况很多种情况,因此这部分内容就主要来分析liba.so这个动态库。

前文说过:代码重定位需要修改代码段中的符号引用,而代码段被加载到内存中又没有可写的权限,动态链接解决这个矛盾的方案是:增加一层间接性。

例如:liba.so的代码中引用了libb.so中的变量b,在liba.so的代码段,并不是在引用的地方直接指向libb.so数据段中变量b的地址,而是指向了liba.so自己的数据段中的某个位置,在重定位阶段,链接器再把libb.so中变量b的地址填写到这个位置。

因为liba.so自己的代码段和数据段位置是相对固定的,这样的话,liba.so的代码段被加载到内存之后,就再也不用修改了。

而数据段中这个间接跳转的位置,就称作:全局偏移表(GOT: Global Offset Table)。

划重点:

liba.so的代码段中引用了libb.so中的符号b,既然b的地址需要在重定位时才能确定,那么就在数据段中开辟一块空间(称作:GOT表),重定位时把b的地址填写到GOT表中。

而liba.so的代码段中,把GOT表的地址填写到引用b的地方,因为GOT表在编译阶段是可以确定的,使用的是相对地址。

这样,就可以在不修改liba.so代码段的前提下,动态的对符号b进行了重定位!

其实,在一个动态库中存在 2 个GOT表,分别用于重定位变量符号(section名称:.got)和函数符号( section 名称:.got.plt)。

也就是说:所有变量类型的符号重定位信息都位于.got中,所有函数类型的符号重定位信息都位于.got.plt中。

并且,在一个动态库文件中,有两个特殊的段(.rel.dyn和.rel.plt)来告诉链接器:.got和.got.plt这两个表中,有哪些符号需要进行重定位,这个问题下面会深入讨论。

liba.so动态库文件的布局

为了更深刻的理解.got和.got.plt这两个表,有必要来拆解一下liba.so动态库文件的内部结构。

通过readelf -S liba.so指令来看一下这个ELF文件中都有哪些section:

可以看到:一共有28个section,其中的21、22就是两个GOT表。

另外,从装载的角度来看,装载器并不是把这些sections分开来处理,而是根据不同的读写属性,把多个section看做一个segment。


再次通过指令 readelf -l liba.so ,来查看一下segment信息:

也就是说:

这28个section中(关注绿色线条):

1. section 0 ~ 16 都是可读、可执行权限,被当做一个 segment;

2. section 17 ~ 24 都是可读、可写的权限,被动作另一个 segment;

再来重点看一下.got和.got.plt这两个section(关注黄色矩形框):

可见:.got和.got.plt与数据段一样,都是可读、可写的,所以被当做同一个 segment被加载到内存中。

通过以上这2张图(红色矩形框),可以得到liba.so动态库文件的内部结构如下:

liba.so动态库的虚拟地址

来继续观察liba.so文件segment信息中的AirtAddr列,它表示的是被加载到虚拟内存中的地址,重新贴图如下:

因为编译动态库时,使用了代码位置无关参数(-fPIC),这里的虚拟地址从0x0000_0000开始。

当liba.so的代码段、数据段被加载到内存中时,动态链接器找到一块空闲空间,这个空间的开始地址,就相当于一个基地址。

liba.so中的代码段和数据段中所有的虚拟地址信息,只要加上这个基地址,就得到了实际虚拟地址。

我们还是把上图中的输出信息,画出详细的内存模型图,如下所示:

GOT表的内部结构

现在,我们已经知道了liba.so库的文件布局,也知道了它的虚拟地址,此时就可以来进一步的看一下.got和.got.plt这两个表的内部结构了。

从刚才的图片中看出:

.got 表的长度是 0x1c,说明有 7 个表项(每个表项占 4 个字节);

.got.plt 表的长度是 0x18,说明有 6 个表项;

上文已经说过,这两个表是用来重定位所有的变量和函数等符号的。

那么:liba.so通过什么方式来告诉动态链接器:需要对.got和.got.plt这两个表中的表项进行地址重定位呢?

在静态链接的时候,目标文件是通过两个重定位表.rel.text和.rel.data这两个段信息来告诉链接器的。

对于动态链接来说,也是通过两个重定位表来传递需要重定位的符号信息的,只不过名字有些不同:.rel.dyn和.rel.plt。

通过指令 readelf -r liba.so来查看重定位信息:

从黄色和绿色的矩形框中可以看出:

1. liba.so 引用了外部符号 b,类型是 R_386_GLOB_DAT,这个符号的重定位描述信息在 .rel.dyn 段中;

2. liba.so 引用了外部符号 func_b, 类型是 R_386_JUMP_SLOT,这个符号的重定位描述信息在 .rel.plt 段中;

从左侧红色的矩形框可以看出:每一个需要重定位的表项所对应的虚拟地址,画成内存模型图就是下面这样:

暂时只专注表项中的红色部分:.got表中的b, .got.plt表中的func_b,这两个符号都是libb.so中导出的。

也就是说:

liba.so的代码中在操作变量b的时候,就到.got表中的0x0000_1fe8这个地址处来获取变量b的真正地址;

liba.so的代码中在调用func_b函数的时候,就到.got.plt表中的0x0000_200c这个地址处来获取函数的真正地址;

反汇编liba.so代码

下面就来反汇编一下liba.so,看一下指令码中是如何对这两个表项进行寻址的。

执行反汇编指令:$ objdump -d liba.so,这里只贴出func_a1函数的反汇编代码:

第一个绿色矩形框(call   490 <__x86.get_pc_thunk.bx>)的功能是:把下一条指令(add)的地址存储到%ebx中,也就是:

%ebx = 0x622

然后执行: add    $0x19de,%ebx,让%ebx加上0x19de,结果就是:%ebx = 0x2000。

0x2000正是.got.plt表的开始地址!

看一下第2个绿色矩形框:

mov    -0x18(%ebx),%eax: 先用%ebx减去0x18的结果,存储到%eax中,结果是:%eax = 0x1fe8,这个地址正是变量b在.got表中的虚拟地址。

movl   $0x1f,(%eax):在把0x1f(十进制就是31),存储到0x1fe8表项中存储的地址所对应的内存单元中(libb.so的数据段中的某个位置)。

因此,当链接器进行重定位之后,0x1fe8表项中存储的就是变量b的真正地址,而上面这两步操作,就把数值31赋值给变量b了。

第3个绿色矩形框,是调用函数func_b,稍微复杂一些,跳转到符号 func_b@plt的地方,看一下反汇编代码:

jmp指令调用了%ebx + 0xc处的那个函数指针,从上面的.got.plt布局图中可以看出,重定位之后这个表项中存储的正是func_b函数的地址(libb.so中代码段的某个位置),所以就正确的跳转到该函数中了。

------ End ------

以上就是我在学习动态链接时,所整理的理解过程。如果文中有理解或表述错误,恳请指正,不胜感激!

写完这篇文章,此时的感觉可以用八个字来形容:如释重负,惶恐不安!

如果此文对您有帮助,请支持一下道哥,把文章分享给更多的嵌入式小伙伴,谢谢!

       原文标题 : 【图片+代码】:Linux动态链接过程中的【重定位】底层原理

声明: 本文由入驻OFweek维科号的作者撰写,观点仅代表作者本人,不代表OFweek立场。如有侵权或其他问题,请联系举报。
侵权投诉

下载OFweek,一手掌握高科技全行业资讯

还不是OFweek会员,马上注册
打开app,查看更多精彩资讯 >
  • 长按识别二维码
  • 进入OFweek阅读全文
长按图片进行保存