查看: 158|回复: 1

[原创图文] Linux动态链接机制详解

[复制链接]
发表于 2021-5-5 00:34:02 | 显示全部楼层 |阅读模式
什么是动态链接
随着系统中可执行文件的增加,静态链接带来的磁盘和内存空间浪费问题愈发严重。例如大部分可执行文件都需要glibc,那么在静态链接时就要把libc.a和编写的代码链接进去,单个libc.a文件的大小为5M左右,那么1000个就是5G。如图2-6左半部所示,两个静态链接的可执行文件都包含testLib.o,那么在装载入内存时,两个相同的库也会被装载进去,造成内存空间的浪费。静态链接另一个明显的缺点是,如果对标准函数做了哪怕一点很微小的改动,都需要重新编译整个源文件,使得开发和维护很艰难。如果不把系统库和自己编写的代码链接到一个可执行文件,而是分割成两个独立的模块,等到程序真正运行时,再把这两个模块进行链接,就可以节省硬盘空间,并且内存中的一个系统库可以被多个程序共同使用,还节省了物理内存空间。这种在运行或加载时,在内存中完成链接的过程叫作动态链接,这些用于动态链接的系统库称为共享库,或者共享对象,整个过程由动态链接器完成。
如图右半部所示,func1.ELF和func2.ELF中不再包含单独的testLib.o,当运行func1.ELF时,系统将func1.o和依赖的testLib.o装载入内存,然后进行动态链接。完成后系统将控制权交给程序入口点,程序开始执行。接下来,当func2.ELF想要执行时,由于内存中已经有testLib.o,因此不再重复加载,直接进行链接即可。

Linux动态链接机制详解

Linux动态链接机制详解

GCC默认使用动态链接编译,通过下面的命令我们将func.c编译为共享库,然后使用这个库编译main.c。参数-shared表示生成共享库,-fpic表示生成与位置无关的代码。这样可执行文件func.ELF2就会在加载时与func.so进行动态链接。需要注意的是,动态加载器ld-linux.so本身就是一个共享库,因此加载器会加载并运行动态加载器,并由动态加载器来完成其他共享库以及符号的重定位。

Linux动态链接机制详解

Linux动态链接机制详解

位置无关代码
可以加载而无须重定位的代码称为位置无关代码(Position-Independent Code,PIC),它是共享库必须具有的属性,通过给GCC传递-fpic参数可以生成PIC。通过PIC,一个共享库的代码可以被无限多个进程所共享,从而节约内存资源。由于一个程序(或者共享库)的数据段和代码段的相对距离总是保持不变的,因此,指令和变量之间的距离是一个运行时常量,与绝对内存地址无关。于是就有了全局偏移量表(Global Offset Table, GOT),它位于数据段的开头,用于保存全局变量和库函数的引用,每个条目占8个字节,在加载时会进行重定位并填入符号的绝对地址。实际上,为了引入RELRO保护机制,GOT被拆分为.got节和.got.plt节两个部分,不需要延迟绑定的前者用于保存全局变量引用,加载到内存后被标记为只读;需要延迟绑定的后者则用于保存函数引用,具有读写权限(详情请查看4.6节)。我们看一下func.so的情况,可以看到全局变量tmp位于GOT上,R_X86_64_GLOB_DAT表示需要动态链接器找到tmp的值并填充到0x200fd8。在func()函数需要取出tmp时,计算符号相对PC的偏移rip+0x20090f,也就是0x6c9+0x20090f=0x200fd8。

Linux动态链接机制详解

Linux动态链接机制详解


Linux动态链接机制详解

Linux动态链接机制详解


延迟绑定
由于动态链接是由动态链接器在程序加载时进行的,当需要重定位的符号(库函数)多了之后,势必会影响性能。延迟绑定(lazy binding)就是为了解决这一问题,其基本思想是当函数第一次被调用时,动态链接器才进行符号查找、重定位等操作,如果未被调用则不进行绑定。
ELF文件通过过程链接表(Procedure Linkage Table, PLT)和GOT的配合来实现延迟绑定,每个被调用的库函数都有一组对应的PLT和GOT。
位于代码段.plt节的PLT是一个数组,每个条目占16个字节。其中PLT[0]用于跳转到动态链接器,PLT[1]用于调用系统启动函数__libc_start_main(),我们熟悉的main()函数就是在这里面调用的,从PLT[2]开始就是被调用的各个函数条目。
位于数据段.got.plt节的GOT也是一个数组,每个条目占8个字节。其中GOT[0]和GOT[1]包含动态链接器在解析函数地址时所需要的两个地址(.dynamic和relor条目),GOT[2]是动态链接器ld-linux.so的入口点,从GOT[3]开始就是被调用的各个函数条目,这些条目默认指向对应PLT条目的第二条指令,完成绑定后才会被修改为函数的实际地址。
以func.ELF2调用库函数func()为例。可以看到,执行call指令会进入[email protected],第一条jmp指令找到对应的GOT条目,这时该位置保存的还是第二条指令的地址,于是执行第二条指令push,将对应的0x1(func在.rel.plt中的下标)压栈,然后进入PLT[0]。PLT[0]先将GOT[1]压栈,然后调用GOT[2],也就是动态链接器的_dl_runtime_resolve()函数,完成符号解析和重定位工作,并将func()的真实地址填入[email protected],也就是GOT[4],最后才把控制权交给func()。延迟绑定完成后,如果再调用func(),就可以由[email protected]的第一条指令直接跳转到[email protected],将控制权交给func()。

Linux动态链接机制详解

Linux动态链接机制详解


Linux动态链接机制详解

Linux动态链接机制详解

最后,我们简单介绍一下运行时链接,即程序在运行时加载和链接共享库。Linux为此提供了一个简单的接口dlopen。传统的动态链接会生成一个GOT表,记录着可能用到的所有符号,并且这些符号在链接时都是可以找到的。运行时链接则需要在运行时定位这些符号。

Linux动态链接机制详解

Linux动态链接机制详解


温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的最好奖励,还可以获得学币奖励,请尊重作者的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
论坛交流群:672619046

0

主题

3

帖子

0

精华

初级会员

Rank: 4

学币
0
荣耀
0
rank
0
违规
0

    发表于 2021-5-27 11:05:34 | 显示全部楼层
    太给力了,这么多好东西!
    学逆向论坛-免费的逆向学习论坛
    关闭

    论坛公告上一条 /1 下一条

    快速回复 返回顶部 返回列表