Linux下程序库函数调用的动态链接过程是很常见的,其实刚学编程时写的helloworld程序调用的printf就牵涉到动态链接,只是我们那时没有去注意罢了。
请看下面的helloworld程序反汇编代码
int main(int argc, char **argv) { 80483e4: 55 push %ebp 80483e5: 89 e5 mov %esp,%ebp 80483e7: 83 e4 f0 and $0xfffffff0,%esp 80483ea: 83 ec 10 sub $0x10,%esp printf("helloworld/n");
80483ed: c7 04 24 c8 84 04 08 movl $0x80484c8,(%esp) 80483f4: e8 03 ff ff ff call 80482fc return 0;
80483f9: b8 00 00 00 00 mov $0x0,%eax } 80483fe: c9 leave 80483ff: c3 ret
图1
可以看到图1第9行输出时的指令为call 80482fc
再看看80482fc处是什么东东,如下
Disassembly of section .plt: 080482cc <__gmon_start__@plt-0x10>: 80482cc: ff 35 f8 9f 04 08 pushl 0x8049ff8 80482d2: ff 25 fc 9f 04 08 jmp *0x8049ffc 80482d8: 00 00 add %al,(%eax) ... 080482dc <__gmon_start__@plt>: 80482dc: ff 25 00 a0 04 08 jmp *0x804a000 80482e2: 68 00 00 00 00 push $0x0 80482e7: e9 e0 ff ff ff jmp 80482cc <_init+0x18> 080482ec <__libc_start_main@plt>: 80482ec: ff 25 04 a0 04 08 jmp *0x804a004 80482f2: 68 08 00 00 00 push $0x8 80482f7: e9 d0 ff ff ff jmp 80482cc <_init+0x18> 080482fc : 80482fc: ff 25 08 a0 04 08 jmp *0x804a008 8048302: 68 10 00 00 00 push $0x10 8048307: e9 c0 ff ff ff jmp 80482cc <_init+0x18>
图2
可见这是.plt段的一段代码。细心的我们或许会发现.plt有一个特点,除了第一项外,其他的项都是三条指令,jmp *, push, jmp,并且第三条指令跳到了.plt的第一项。我们来分析这一项:
图2第16行的间接跳转,以GOT表的某一项间接寻址(如果不知道GOT表是什么,看看elf手册就清楚了),GOT表是一个如下的数组
extern Elf32_Addr _GLOBAL_OFFSET_TABLE_[];
我们用readelf -s helloworld命令可以看出这个数组的起始地址为08049ff4,如下
35: 08049ff40 OBJECTLOCALHIDDEN22 _GLOBAL_OFFSET_TABLE_
可以计算一下,上面的间接跳转以GOT[(0x804a008 - 0x8049ff4) / 4] = GOT[5]的值间接跳转,那么这个值是什么呢,当然静态是看不到的,我们gdb启动程序并查看0x804a008地址的值
(gdb) b main
Breakpoint 1 at 0x80483ed: file helloworld.c, line 6.
(gdb) r
Starting program: /home/lzs/programming/test/helloworld
Breakpoint 1, main (argc=1, argv=0xbfffefe4) at helloworld.c:6
6 printf("helloworld/n");
(gdb) x/x 0x804a008
0x804a008 <_GLOBAL_OFFSET_TABLE_+20>: 0x08048302
(gdb)
可见该值为0x08048302,大家发现了没有,这个值就是图2第17行指令的地址,也就是执行了第16行的指令后就跑到了第17行,然后到了.plt的第一项,再然后又间接跳转到了某个地方,大家觉得这很没有必要,其实是有其原因的,这就是动态链接的过程。试想如果有人把GOT[5]的值改成了puts的绝对地址后,那么第16行的间接跳转不就直接到了puts的函数体了吗。 到了这里,大家可能有很多疑问,试列举如下: 1。第18行跳到.plt的第一项,然后第4行间接跳转到哪儿去了,干了些什么事情? 答:这是动态链接的过程,本文后面要结合源码分析的,现在可以简单的说下,第4行的间接跳转到了ld-linux.so.2的_dl_runtime_resolve函数,这个函数解析出puts的绝对地址,回填到GOT[5],以达到前面所叙的效果。 2。为什么不一上来就把所有的库函数对应的GOT[]项全部填上函数的绝对地址? 答:这是Linux系统设计的哲学之一,称之为LAZY的策略,即真正要用到某个东西时,我才为它构建好必要的环境,如这里的地址回填,又如COW机制。这是有性能考虑的,程序中可能会引用很多的库函数,但有很多库函数并不会真正执行到,如if (error){perror("err");
exit(errno);
}的perror函数几乎不会执行,这样为这些不会执行的函数解析出绝对地址是没有必要的,也会增加时间的开销。 在puts函数执行了一次后,由于有动态链接的过程,GOT[5]就回填上了puts的绝对地址,请看下面 (gdb) n helloworld 8
return 0;
(gdb) x/x 0x804a008 0x804a008 <_GLOBAL_OFFSET_TABLE_+20>:
0xb7edda60 (gdb) x/10i 0xb7edda60 0xb7edda60 :
push%ebp 0xb7edda61 :
mov%esp,%ebp 0xb7edda63 :
sub$0x1c,%esp 0xb7edda66 :
mov%ebx,-0xc(%ebp) 0xb7edda69 :
mov0x8(%ebp),%eax 0xb7edda6c :
call0xb7e969ef 0xb7edda71 :
add$0xe3583,%ebx 0xb7edda77 :
mov%esi,-0x8(%ebp) 0xb7edda7a :
mov%edi,-0x4(%ebp) 0xb7edda7d :
mov%eax,(%esp) (gdb)我们现在知道了在上面的helloworld中puts使用了GOT[5]这一项,大家可能有疑问,那么GOT表到底有多少项?其他项是干嘛的? 对于第一个问题,答案是不确定,GOT项的项数是由可执行文件或者共享对象引用的外部对象确定的。 第二个问题,只有前3项有确定的用途,其他项可以用于外部函数,也可以用于外部数据。查阅elf手册可知 The table’s entry zero is reserved to hold the address of the dynamic structure, referenced with the symbol _DYNAMIC. This allows a program, such as the dynamic linker, to find its own dynamic structure without having yet processed its relocation entries. This is especially important for the dynamic linker, because it must initialize itself without relying on other programs to relocate its memory image. On the 32-bit Intel Architecture, entries one and two in the global offset table also are reserved. ‘‘Procedure Linkage Table’’ below describes them. 对于GOT[0],放的是_DYNAMIC的地址,这里仍然需要gdb启动程序查看,如下 lzs@Gentoo /home/lzs/programming/test $ readelf -s helloworld | grep OFFSET 35: 08049ff40 OBJECTLOCALHIDDEN22 _GLOBAL_OFFSET_TABLE_ lzs@Gentoo /home/lzs/programming/test $ readelf -x .got.plt helloworld Hex dump of section '.got.plt': 0x08049ff4 209f0408 00000000 00000000 0a830408............... 0x0804a004 1a830408 2a830408 3a830408....*...:... lzs@Gentoo /home/lzs/programming/test $ readelf -s helloworld | grep _DYNAMIC 38: 08049f200 OBJECTLOCALHIDDEN20 _DYNAMIC lzs@Gentoo /home/lzs/programming/test $ gdb helloworld warning: Can not parse XML syscalls information;
XML support was disabled at compile time. GNU gdb (Gentoo 7.0.1 p1) 7.0.1 Copyright (C) 2009 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or laterThis is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.Type "show copying" and "show warranty" for details. This GDB was configured as "i686-pc-linux-gnu". For bug reporting instructions, please see: ... Reading symbols from /home/lzs/programming/test/helloworld...done. (gdb) b main Breakpoint 1 at 0x80483ed: file helloworld.c, line 6. (gdb) r Starting program: /home/lzs/programming/test/helloworldBreakpoint 1, main (argc=1, argv=0xbffff014) at helloworld.c:6 6
printf("helloworld/n");
(gdb) x/x 0x08049ff4 0x8049ff4 <_GLOBAL_OFFSET_TABLE_>:
0x08049f20 (gdb) x/x 0x08049f20 0x8049f20 <_DYNAMIC>:
0x00000001 (gdb)由上面的结果可以看到,GOT[0]的静态值为209f0408,并不是_DYNAMIC的地址08049f20,但是gdb启动程序看到的动态地址就是_DYNAMIC的地址了。 GOT[1]放的是struct link_map的类型变量的地址,后面详述。 GOT[2]放的是函数_dl_runtime_resolve的地址,也在后面详述。 上面我们可看到,对GOT表的访问使用了绝对地址,这是由于helloworld是一个可执行文件,对于共享对象又有不同的处理。因为共享对象加载的内存位置是运行时确定的,因此对GOT的访问必须是某种偏移的方式,请看objdump -S /lib/libc.so.6 | less -N的结果 12 /lib/libc.so.6:file format elf32-i386 345 Disassembly of section .plt: 67 00016904
方便起见,还是一helloworld程序为例。我们可以看到,主程序helloworld调用puts时使用的call指令把控制转移到plt的相应项(图1第09行->图2第15行),每个库函数相应的plt项为三条指令(jmp *, push, jmp),对于puts来说,如下:
80482fc:ff 25 08 a0 04 08jmp*0x804a008
8048302:68 10 00 00 00push$0x10
8048307:e9 c0 ff ff ffjmp80482cc <_init+0x18>
前面谈到,0x804a008地址的初始内容为8048302,也就是第二条指令push的地址,因此jmp *执行完后,控制到了push指令,第三条的jmp又转移到下面:
80482cc:ff 35 f8 9f 04 08pushl0x8049ff8
80482d2:ff 25 fc 9f 04 08jmp*0x8049ffc
80482d8:00 00add%al,(%eax)
前面说到0x8049ff8也就是&GOT[1]放到的是struct link_map *l变量,l是一个链,该链上有所有已经加载的共享对象的信息。
【Linux下库函数动态链接过程分析-结合glibc-2.11源码】0x8049ffc也就是&GOT[2]放的是函数_dl_runtime_resolve的地址,容易分析出,当控制到了_dl_runtime_resolve时,栈的情况如下
图4. 控制到_dl_runtime_resolve时栈的情况
_dl_runtime_resolve程序比较简单,如图5所示
图5. _dl_runtime_resolve函数
执行了图5中蓝色高亮的call指令后,控制转移到_dl_fixup,这里采用了寄存器传参,容易分析出_dl_fixup的第一个参数eax为GOT[1],第二个参数edx为0x10,如图5所示。
图6. 执行call _dl_fixup前栈的情况
下面控制转移到_dl_fixup函数。我们已经知道这个函数的第一个参数为struct link_map *的链,那么第二个参数是0x10是什么呢?是puts函数对应的重定位项的在重定位表的偏移。如下
lzs@Gentoo /home/lzs/programming/test $ readelf -r helloworld
Relocation section '.rel.dyn' at offset 0x2e4 contains 1 entries:
OffsetInfoTypeSym.ValueSym. Name
08049ff000000106 R_386_GLOB_DAT00000000__gmon_start__
Relocation section '.rel.plt' at offset 0x2ec contains 3 entries:
OffsetInfoTypeSym.ValueSym. Name
0804a00000000107 R_386_JUMP_SLOT00000000__gmon_start__
0804a00400000307 R_386_JUMP_SLOT00000000__libc_start_main
0804a00800000407 R_386_JUMP_SLOT00000000puts
由于重定位项每项大小为8Byte,因此puts的重定位项偏移恰好为0x10。 下面逐段分析_dl_fixup函数。 图7. _dl_fixup part I 第76行80行得到puts的符号表项,用于在相应的共享对象中查找puts函数并得到其地址信息,第81行得到puts的相应重定位地址,大家可能已经想到了,该地址就为&GOT[5] (0x804a008)。 图8. _dl_fixup part II 图9. _dl_fixup part III 图10. _dl_fixup part IV 图8-图10,解析出puts函数在内存的绝对地址,并存放在value中。 图11. _dl_fixup part V 第154行把解析出的绝对地址写入重定位地址,也就是GOT[5]中。 继续回到图5的_dl_runtime_resolve函数,第43行的ret指令直接进入puts函数并清除栈中冗余项,至此重定位过程已经完成。 上面有些细节问题(例如puts函数绝对地址的查找过程)有些复杂,很值得去研究。很显然的,上面的重定位过程只是在调用了共享对象中的函数且是第一次此调用时才进行,以后的调用通过plt中的jmp *指令直接跳转到了对应的函数。
推荐阅读
- Linux|109 个实用 shell 脚本
- linux笔记|linux 常用命令汇总(面向面试)
- Linux|Linux--网络基础
- linux|apt update和apt upgrade命令 - 有什么区别()
- linux|2022年云原生趋势
- Go|Docker后端部署详解(Go+Nginx)
- 开源生态|GPL、MIT、Apache...开发者如何选择开源协议(一文讲清根本区别)
- GitHub|7 款可替代 top 命令的工具