关于程序运行的系列文章
ELF文件结构和实战分析
深入分析动态链接的延迟绑定
文章目录
- 为什么使用动态链接库?
- 静态链接的问题:
- 动态链接库
- 动态共享库是怎样存在于内存中的?
- 动态链接 原理简析
- 代码示例
- dynsym
- GOT
- .rela.dyn 和 .rela.plt
- PLT
- 使用 gdbserver 调试 Android Native 程序.so文件
- 查看延迟绑定后的函数地址
- Hook动态链接库
这篇文章深入分析动态链接的原理,结合示例来分析二进制文件。从文件、虚拟内存,详细记录了每一个关键步骤
在 Linux 系统中, ELF 动态链接文件被称为动态共享对象 (DSO, Dynamic Shared Objects) , 简称共享对象, 它们是以 .so 为扩展名
在 Windows 系统中, 动态链接文件被称为动态链接库 (Dynamical Linking Library) , 它们通常以 .dll 为扩展名.
为什么使用动态链接库?
静态链接的问题:
1、程序中引用到的第三方库都会被打包进可执行文件中,浪费磁盘空间。
2、在运行的时候,如果同时运行N个可执行文件,那么第三方库就会被加载N次。物理内存可能会同时存在<=N个第三方库
3、静态链接的库更新麻烦,例如,10个软件使用到了系统库libc.so, 如果libc.so有漏洞需要更新,那这10个软件需要重新编译打包。
动态链接库
动态链接库是在运行时,才去加载的,多个进程可共享一份,所以它可以减少对内存的占用。如果用的是系统动态库,例如libc.so,是不需要把libc.so 打包进 可执行文件。
动态共享库是怎样存在于内存中的?
不同进程是如何共用同一个动态链接库,来节省内存的呢?我们来思考几个问题
Q: 如果物理内存中,只有一份动态链接库,那系统是如何知道目标链接库被加载了?
The operating system tracks loaded shared libraries primarily by their absolute file paths, not just their names. Two libraries can have the same base name (e.g., libfoo.so) as long as they are located in different directories.
When a program requests a dynamic library, it typically does so by name, and the dynamic linker is responsible for finding the appropriate library file. The linker follows a specific search algorithm to locate the correct library file:
-
It first checks if the library is already loaded in memory. If it is, the linker uses that copy.
-
If the library isn’t already loaded, the linker checks in certain directories for the library file. The directories it checks are determined by several factors, including the LD_LIBRARY_PATH environment variable, the /etc/ld.so.conf file, and built-in defaults (typically /lib and /usr/lib).
-
If the linker still can’t find the library, it gives up and returns an error.
Q: 可执行文件被加载后,是加载到虚拟地址空间。不同进程,共用同一个库,该库要出现在每个进程的虚拟地址空间吗?
是的,虽然多个进程共享一个物理内存中的动态共享库,但是在各自的虚拟内存空间中,还是有各自的动态共享库
Q: 不同进程的同一个动态库的虚拟地址,是不是要相同?
这个问题,网上很多人都有问,也有文章解释,其实简单想一下就知道了,不可能相同?如果相同,加载到虚拟地址由谁来确定?如果由动态库的作者们来定,肯定会出现冲突。如果由操作系统来定,那系统需要知道所有的动态库,以预留地址,这显然不是一个好的解决方案。
Q: 不同进程的同一个动态库的虚拟地址不同,是如何映射到同一个物理内存呢?
When a shared library is loaded into memory, the operating system maps the physical memory pages containing the library into the virtual memory space of each process that uses the library. This is done by adding entries to the page tables of those processes.
网上搜了很久,没有文章详细描述这个过程,后面有时间再去研究源码。我从宏观的角度去猜测一下,操作系统会把使用了动态共享库A的所有进程进行管理,动态库在进程的虚拟地址是知道的,哪些进程使用了动态库A是知道的。动态库内部的相对位置的虚拟页时相同的,该页被加载到物理内存的地址是知道的,操作系统是有能力映射其他进程的动态库A的相同偏移位置的虚拟页到该物理内存。
动态链接 原理简析
动态链接,在编译阶段,可执行文件中保存了动态链接库的符号。延迟绑定的重定位,是在调用外部函数时确定地址的,就是把符号的地址填写到对应的位置。因为代码段不可写,所以需要用 可写的数据段(got)作为跳板。
这里以深入理解计算机,第七章的图来解释
左边是符号的地址没有被确定
- 调用函数addvec(),指向该符号对应的plt节的对应项,此处是plt[2]
- 跳转到addvec符号在GOT节中的对应项保存的地址,此处是got[4] = 0x4005c6
- 跳转到plt[0] ,去加载动态库,解析符号addvec的地址。并保存到got[4]
右边是符号的地址已经确定,调用函数addvec(),此时got[4]是函数的地址,就直接调用了
代码示例
项目地址:https://gitee.com/xuexuan/elf.git
dynsym
查看一下当前模块引用到其他模块的符号,例如,main中的dynsym中可以看到 a2、_Z7func_a1v,两个 liba中的外部变量
.dynsym(动态符号表: 表示模块中符号的导出、导入关系)
.symtab(符号表: 表示模块中的所有符号);
Ndx列是数字,表示该符号位于当前文件的哪一个段中(即:段索引); 是UND,表示这个符号没有找到,是一个外部符号(需要重定位);
GOT
全局偏移量表(global offset table) 中的数据格式是一个数组形式的,每一个需要重定位的符合对应其中一项,内容一个绝对位置。本例如下图:每一项8字节,从文件偏移的0x1F70开始,GOT[0] = 0x0000000000000000,GOT[3] = 0x00000000 000008D0
那如何知道某一项对应的是哪个符合呢?但从GOT是看不出来的,在调用那个符合的位置,那里会使用相对偏移位置来找到对应的GOT中的对应项。
本例中,GOT被加载的虚拟内存位置时0x2f70,在ELF文件的偏移是0x1f70
.rela.dyn 和 .rela.plt
.rela.dyn 需要重定位的变量,.rela.plt 需要重定位的函数
这两个段的每一项就对应GOT的一项,例如:.rela.plt 的_Z7func_a1v 的Offset是0x2f98,这个值就是ELF被加载后的虚拟内存的地址,也是在GOT[5] = 0x00000000 000008D0,也就是说调用函数_Z7func_a1v(func_a1)就会到这个地址0x00000000 000008D0
PLT
对于每一个需要延迟绑定的符号,都在PLT中有一小段代码,如下图(使用Hopper Disassembler打开main),sub_910开始处就是func_a对应的plt代码,main函数调用func_a,会跳转到0x910地址处
分析一下,sub_910这段代码:
adrp x16, #0x2000 ; 0x2f98@PAGE, CODE XREF=main+548
: 该指令将 0x2000 加载到寄存器 x16 中。 0x2f98@PAGE 注释表示该页内的最终目标地址是0x2f98。 CODE XREF=main+528 注释表示在主函数中偏移量 548 处存在对此指令的交叉引用。
ldr x17, [x16, #0xf98] ; 0x2f98,_Z7func_a1v
: 该指令将 x16 中当前值加上 0xf98 计算得出的内存地址中的值加载到 x17 寄存器中。通过上面的GOT表可知,在虚拟地址的0x2f98处值是 0x8d0,所以x17 = 0x8d0。查询.rela.plt节可知,它对应 _Z7func_a1v
add x16, x16, #0xf98 ; 0x2f98@PAGEOFF, 0x2f98
: 表示 x16 = x16 + 0xf98。 0x2f98@PAGEOFF 注释表示 0x2f98 是页内的偏移量。
br x17
: 跳转到 x17 寄存器中指定的内存位置0x8d0
于是代码执行到0x8d0,在PLT节的开始处, 最终是跳转到动态链接器去解析符合 _Z7func_a1v,加载liba.so文件,找到函数的地址,并填写到GOT节的0x2f98地址处。这样下次再执行时,就直接跳转到实际的函数地址。下面来深入分析。
使用 gdbserver 调试 Android Native 程序.so文件
执行main程序,130|x1q:/data/local/tmp $ ./main & ps
这样可保证继续 输入命令,而main程序不会退出
-
进入 $NDK/prebuilt/android-arm64/gdbserver/目录,执行
adb push main /data/local/tmp/
。 -
adb shell
,./gdbserver 127.0.0.1:1234 --attach 12606
表示 gdbserver 监听127.0.0.1: 1234 。12606 是main程序本次运行的pid。成功会显示 -
adb forward tcp:1234 tcp:1234
,表示将本地 1234 端口转发到手机的 9090 端口。 -
进入 $NDK/prebuilt/linux-x86_64/bin/目录,打开shell执行
gdb
-
连接远程的1234 端口
target remote :1234
。
查看延迟绑定后的函数地址
此时还是在gdb的调试环境中,i proc mappings
查看进程的虚拟地址
通过查看elf文件可知,GOT节会被加载到0x2f70的位置,这里是从地址0开始计算的。从上图可以得知,实际运行时,是从虚拟地址0x5ea85e0000开始加载的,那也就是说实际运行中GOT是在0x5ea85e2f70处
查看0x5ea85e2f70处的值,果然是GOT,在plt节中,调用func_a1函数 的位置是0x2f98,那在实际运行的中的虚拟内存地址就是0x5ea85e2f98,下图中红框,此时已经是动态加载完成,0x7425fbd6b8 地址就是func_a1函数的地址
验证一下,确实是func_a1函数
Hook动态链接库
掌握了动态加载原理,Hook动态链接库,就是修改got对应项的值为自己函数的地址。具体实现还是有很多细节需要处理,这个库xHook可以方便的实现
参考:
含大量图文解析及例程 | Linux下的ELF文件、链接、加载与库(中)
ELF文件详解
【图片+代码】:Linux 动态链接过程中的【重定位】底层原理
深入了解GOT,PLT和动态链接
gdb小技巧