认真梳理一下re2dl_runtime_resolve
原理
写个c程序自己测试一下:
1 |
|
第一次调用strlen
函数会先跳到 strlen plt
表中去,接着执行jmp
指令到0x804a010去,这里是 got 表
可以看到strlen@got
中存放的是strlen@plt
的第二条指令:0x8048336 <strlen@plt+6> push 8
对比一下__libc_start_main
(已经调用过一次的函数)
放着的则是__libc_start_main
的真实地址,而由于strlen函数第一次调用,所以got表上还没有绑定上真实地址,一律放着的都是 fun@plt+6
这个地址,看下面未调用的write也是一样
而执行完 push 指令后,执行jmp 0x8048310
,再 push 0x804a004
的内容后跳到_dl_runtime_resolve
而0x804a004
正是GOT[1],也就是push GOT[1],后jmp GOT[2],而GOT[2]放着的正是_dl_runtime_resolve
的真实地址。
1 | GOT表内容 |
所以实际上,就是执行了_dl_runtime_resolve(link_map, reloc_arg)
来得到函数的真实地址,并写到got表中去,之后 call fun@plt
的第一次jmp的时候,就可以直接跳到真实地址上去了。
用一张图直观地显示函数第一次调用和第二次调用的流程:
接着我们往下看看link_map里面有什么东西
可以看到有个.dynamic地址,这里简单介绍一下程序中的各种段
.dynamic,动态节一般保存了ELF文件如下信息:
- 依赖于哪些动态库
- 动态符号节信息
- 动态字符串信息
动态节的结构是这样的:
1 | typedef struct { |
用readelf -d test32
可以打印出程序的动态节内容
1 | Dynamic section at offset 0xf14 contains 24 entries: |
这里主要关注以下东西:
1 | 0x00000005 (STRTAB) 0x804823c |
STRTAB ,SYMTAB ,JMPREL分别指向.dynstr,.dynsym,.rel.plt节段
- 动态符号表(.dynsym)用来保存与动态链接相关的导入导出符号,不包括模块内部的符号。而.symtab则保存所有符号,包括.dynsym中的符号,因此一般来说,.symtab的内容多一点
.dynsym
是运行时所需的,ELF文件中 export/import 的符号信息全在这里,而.symtab
节中存储的信息是编译时的符号信息,用strip
工具会被删除,或者编译里加入-s
参数也会删除。
我们主要关注动态符号.dynsym中的两个成员
- st_name,该成员保存着动态符号在.dynstr表(动态字符串表)中的偏移
- st_value,如果这个符号被导出,这个符号保存着对应的虚拟地址。
.rel.plt包含了需要重定位的函数信息,使用如下的结构 ,需要区别:.rel.plt
节是用于函数重定位,rel.dyn
节是用于变量重定位
1 | typedef struct { |
r_offset: 指向对应got表的指针
r_info: r_info >> 8后得到一个下标,对应此导入符号在.dynsym中的下标
现在我们回到_dl_runtime_resolve(link_map, reloc_arg)
这里的link_map就是GOT[1],reloc_arg就是函数在.rel.plt中的偏移,就是之前的push 8
我们继续 跟进_dl_runtime_resolve函数到call _dl_fixup,这个函数就是绑定真实地址到got的核心了
1 | _dl_fixup(struct link_map *l, ElfW(Word) reloc_arg) |
综上,整个过程是这样的
1 | 1、第一次执行函数,到plt表,接下去got表,由于没有真实地址,又返回plt表的第一项,压入reloc_arg和link_map后调用_dl_runtime_resolve(link_map,reloc_arg) |