以实际的Hello World小程序为例子,作为动态库的学习材料。
开章上个图,看完明白这个图。
1.编译过程以最简单的 Hello world为例
|
预处理:
gcc -E hello.c -o hello.i
编译:
gcc -S hello.i -o hello.s
汇编:
gcc -g -c hello.s -o hello.o
链接:
gcc hello.o -o hello
2.ELF文件格式得到的hello是elf格式的。
readelf -h hello
ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: AArch64 Version: 0x1 Entry point address: 0x4004a0 Start of program headers: 64 (bytes into file) Start of section headers: 69408 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 34 Section header string table index: 33
|
这个可以写个文章,后续涉及了再展开。
3.GOT/PLT
我们知道.text segment是只读的,也就是说在编译成可以执行文件之后,不能被修改了,如何确保它能够正确的引用在加载时才能确定下来的动态链接库里的符号呢?需要GOT和PLT作为跳板来实现了。
GOT全称Global Offset Table,即全局偏移量表。它在可执行文件中是一个单独的section,位于.data section的前面。每个被目标模块引用的全局符号(函数或者变量)都对应于GOT中一个8字节的条目。编译器还为GOT中每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含正确的目标地址。
PLT全称Procedure Linkage Table,即过程链接表。它在可执行文件中也是一个单独的section,位于.text section的前面。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目实际上都是一小段可执行的代码。
GOT/PLT是链接器在执行重定向时会用到的部分。
3.1GOT
GOT, 即Global Offset Table, 全局偏移表. 是链接器在执行链接时实际上要填充的部分, 保存了所有外部符号的地址信息。除了每个函数占用一个GOT表项外。
.got.plt相当于.plt的GOT全局偏移表, 其内容有两种情况, 1)如果在之前查找过该符号,内容为外部函数的具体地址。2)如果没查找过, 则内容为跳转回.plt的代码, 并执行查找。
.got保存全局变量引用的地址,.got.plt保存函数引用地址。
.got表属于数据段, 是可写的. 表中存储的是指针.
3.2PLT
PLT, 即Procedure Linkage Table, 进程链接表, 表里包含了一些代码,
用来调用链接器来解析某个外部函数的地址, 并填充到.got.plt中, 然后跳转到该函数; 或直接在.got.plt中查找并跳转到对应外部函数(如果已经填充过)。
GOT表可写不可执行, PLT可执行不可写,他们相互作用来实现函数符号的延时绑定。ASLR并不随机化PLT部分。
我们知道通过readelf -S hello可以得到段信息。
4.Section通过readelf -S hello可以得到段信息。
有两种类型的符号表, 一种是常规的(.symtab和.strtab), 另一种是动态的(.dynsym和.dynstr)。常规的符号表通常只在调试时用到. 平时用的strip命令删除的就是该符号表。
这里要关注的是这些section的地址。
There are 30 section headers, starting at offset 0x10db8:
Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001b 0000000000000000 A 0 0 1 [ 2] .note.ABI-tag NOTE 0000000000400254 00000254 0000000000000020 0000000000000000 A 0 0 4 [ 3] .note.gnu.build-i NOTE 0000000000400274 00000274 0000000000000024 0000000000000000 A 0 0 4 [ 4] .gnu.hash GNU_HASH 0000000000400298 00000298 0000000000000030 0000000000000000 A 5 0 8 [ 5] .dynsym DYNSYM 00000000004002c8 000002c8 0000000000000078 0000000000000018 A 6 1 8 [ 6] .dynstr STRTAB 0000000000400340 00000340 0000000000000042 0000000000000000 A 0 0 1 [ 7] .gnu.version VERSYM 0000000000400382 00000382 000000000000000a 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 0000000000400390 00000390 0000000000000020 0000000000000000 A 6 1 8 [ 9] .rela.dyn RELA 00000000004003b0 000003b0 0000000000000018 0000000000000018 A 5 0 8 [10] .rela.plt RELA 00000000004003c8 000003c8 0000000000000060 0000000000000018 AI 5 23 8 [11] .init PROGBITS 0000000000400428 00000428 0000000000000014 0000000000000000 AX 0 0 4 [12] .plt PROGBITS 0000000000400440 00000440 0000000000000060 0000000000000010 AX 0 0 16 [13] .text PROGBITS 00000000004004a0 000004a0 00000000000001fc 0000000000000000 AX 0 0 8 [14] .fini PROGBITS 000000000040069c 0000069c 0000000000000010 0000000000000000 AX 0 0 4 [15] .rodata PROGBITS 00000000004006b0 000006b0 000000000000001d 0000000000000000 A 0 0 8 [16] .eh_frame_hdr PROGBITS 00000000004006d0 000006d0 0000000000000044 0000000000000000 A 0 0 4 [17] .eh_frame PROGBITS 0000000000400718 00000718 0000000000000134 0000000000000000 A 0 0 8 [18] .init_array INIT_ARRAY 000000000041fdf0 0000fdf0 0000000000000008 0000000000000008 WA 0 0 8 [19] .fini_array FINI_ARRAY 000000000041fdf8 0000fdf8 0000000000000008 0000000000000008 WA 0 0 8 [20] .jcr PROGBITS 000000000041fe00 0000fe00 0000000000000008 0000000000000000 WA 0 0 8 [21] .dynamic DYNAMIC 000000000041fe08 0000fe08 00000000000001d0 0000000000000010 WA 6 0 8 [22] .got PROGBITS 000000000041ffd8 0000ffd8 0000000000000010 0000000000000008 WA 0 0 8 [23] .got.plt PROGBITS 000000000041ffe8 0000ffe8 0000000000000038 0000000000000008 WA 0 0 8 [24] .data PROGBITS 0000000000420020 00010020 0000000000000004 0000000000000000 WA 0 0 1 [25] .bss NOBITS 0000000000420024 00010024 0000000000000004 0000000000000000 WA 0 0 1 [26] .comment PROGBITS 0000000000000000 00010024 000000000000002d 0000000000000001 MS 0 0 1 [27] .symtab SYMTAB 0000000000000000 00010058 0000000000000978 0000000000000018 28 79 8 [28] .strtab STRTAB 0000000000000000 000109d0 00000000000002de 0000000000000000 0 0 1 [29] .shstrtab STRTAB 0000000000000000 00010cae 0000000000000108 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), p (processor specific) |
4.1Section片段PLT/GOT信息
[12] .plt PROGBITS 0000000000400440 00000440 0000000000000060 0000000000000010 AX 0 0 16
[22] .got PROGBITS 000000000041ffd8 0000ffd8 0000000000000010 0000000000000008 WA 0 0 8 [23] .got.plt PROGBITS 000000000041ffe8 0000ffe8 0000000000000038 0000000000000008 WA 0 0 8 [24] .data PROGBITS 0000000000420020 00010020 0000000000000004 0000000000000000 WA 0 0 1
|
可以看到
.plt起始于0x400440,每个条目大小为0x10(16个字节),共6个条目;
.got起始于0x41ffd8,每个条目大小为0x8(8个字节),共2个条目;
.got.plt起始于41ffe8,每个条目大小为0x8(8个字节),共7个条目;
通过objdump -d hello摘取片段:
.plt的section
5.单步调试/实例演示有了这些知识有,以上面的hello为例。在调用puts函数的地方设置断点。
#gdb ./hello
在puts处设置断点,通过disassemble main显示汇编
gdb>disassemble main
=> 0x0000000000400610 <+16>: bl 0x400490 <puts@plt>
可以看到0x400490这个地址就是plt section中的puts@plt,也就是说当调用puts函数的时候,是会调到plt中的。
gdb>disassemble 0x400490
0000000000400490 <puts@plt>: 400490: 90000110 adrp x16, 420000 <__libc_start_main@GLIBC_2.17> 400494: f9400e11 ldr x17, [x16,#24] 400498: 91006210 add x16, x16, #0x18 40049c: d61f0220 br x17 40044c: 913fe210 add x16, x16, #0xff8 400450: d61f0220 br x17 400454: d503201f nop 400458: d503201f nop 40045c: d503201f nop |
其中adrp是获取4KB的页的基址。这个地址:0x420000中有包含了进程相关外部函数的地址,而__libc_start_main为首个。0x420000这个地址是属于.got.plt的。.got的地址是 000000000041ffd8, .got.plt的地址是000000000041ffe8。
先来看下:
(gdb) x /16x 0x00000000041ffe8 0x41ffe8: 0x00000000 0x00000000 0xbe801168 0x0000ffff 0x41fff8: 0xbe7e4110 0x0000ffff 0xbe621624 0x0000ffff 0x420008: 0x00400440 0x00000000 0x00400440 0x00000000 0x420018: 0x00400440 0x00000000 0x00000000 0x00000000 (gdb) disas 0x00000000041ffd8 Dump of assembler code for function _GLOBAL_OFFSET_TABLE_: 0x000000000041ffd8: .inst 0x0041fe08 ; undefined 0x000000000041ffdc: .inst 0x00000000 ; undefined 0x000000000041ffe0: .inst 0x00000000 ; undefined 0x000000000041ffe4: .inst 0x00000000 ; undefined End of assembler dump. |
在.got.plt中
0x0000ffffbe801168 即GOT[1]:设置动态库映射信息数据结构link_map地址。
0x0000ffffbe7e4110即GOT[2]:是_dl_runtime_resolve函数地址,设置动态连接器符号解析函数的地址。
在.got中,第一项是0x0041fe08也就是.dynamic section,其他都是未定义的。
0x0000ffffbe621624是__libc_start_main函数地址。
继续,接着查看地址0x420000。
(gdb) x /20x 0x420000
0x420000: 0xbe621624 0x0000ffff 0x00400440 0x00000000
0x420010: 0x00400440 0x00000000 0x00400440 0x00000000
0x420020: 0x00000000 0x00000000 0x00000000 0x00000000
0x420030: 0x00000000 0x00000000 0x00000000 0x00000000
0x420040: 0x00000000 0x00000000 0x00000000 0x00000000
其中[x16,#24]是0x00400440 ,并不是函数的地址,而是.plt的开始地址。这个不是函数的地址,是PLT中prepare resolver指令的地址。如下源码中。
(gdb) disassemble 0x00400440,0x00400490 Dump of assembler code from 0x400440 to 0x400490: 0x0000000000400440: stp x16, x30, [sp,#-16]! 0x0000000000400444: adrp x16, 0x41f000 0x0000000000400448: ldr x17, [x16,#4088] 0x000000000040044c: add x16, x16, #0xff8 0x0000000000400450: br x17 0x0000000000400454: nop 0x0000000000400458: nop 0x000000000040045c: nop |
其中0x41f000+#4088= 0x41fff8,该地址中是_dl_runtime_resolve 的函数地址。所以说_dl_runtime_resolve是常驻在.plt中的开始部分的。
也可以如下方式查看:(gdb) x /16x 0x00400440
0x400440: 0xa9bf7bf0 0xf00000f0 0xf947fe11 0x913fe210
0x400450: 0xd61f0220 0xd503201f 0xd503201f 0xd503201f
0x400460 <__libc_start_main@plt>: 0x90000110 0xf9400211 0x91000210 0xd61f0220
0x400470 <__gmon_start__@plt>: 0x90000110 0xf9400611 0x91002210 0xd61f0220
0x400480 <abort@plt>: 0x90000110 0xf9400a11 0x91004210 0xd61f0220
会调用 _dl_runtime_resolve 解析 puts 函数的地址,并将该函数真正的地址填充到 puts@got.plt,最后跳转到 puts 函数继续执行代码。
继续gdb调试执行后如下:
(gdb) s
hello, world
23 mov w0, 0
(gdb) x /20x 0x420000
0x420000: 0xbe621624 0x0000ffff 0x00400440 0x00000000
0x420010: 0x00400440 0x00000000 0xbe66c7e8 0x0000ffff
0x420020: 0x00000000 0x00000000 0x00000000 0x00000000
0x420030: 0x00000000 0x00000000 0x00000000 0x00000000
0x420040: 0x00000000 0x00000000 0x00000000 0x00000000
发现got.plt中的函数地址变化了,已经是0xbe66c7e8 0x0000ffff
(gdb) disassemble 0x0000ffffbe66c7e8
Dump of assembler code for function puts:
……
然后发现这个地址已经是puts函数所在地址了。
这个地址是如何变化的?就是_dl_runtime_resolve函数干的。
关于dll_runtime_resolve的工作机制,这个更加深入的探讨,到以后有机会再进一步深入