GOT, PLT
以防丢失,记录一下
linux 下的动态链接是通过 PLT, GOT 实现。我们考察 test.c, test.o, test 来说明
首先通过 objdump -d test.o 查看反汇编,考察 glibc 的 printf。在 .o 文件 call printf 的时候地址先用 fc ff ff ff (有符号数 -4) 代替,交由链接器处理。
.text 段的权限是 rx,在这里填写的地址运行时无法修改。所以,链接器需要生成一个跳转代码段(即 .plt 的一项),作为中介跳转到动态库(.data)?
.text
call printf_stub; // 调用printf的call指令
printf_stub:
jmp [<address stores printf>]; // 获取printf的动态库地址,并跳转
.data
printf: ; // real printf
上述伪代码是不带延迟绑定的 plt 表项,其中,[<address stores printf>] 访问了 GOT 表,GOT 表的每一项保存一个地址(不是 jmp 指令)。plt, got 填写的地址在链接时确定。
延迟绑定
在调用动态函数时进行地址解析和重定位工作,伪代码形如
// 初始化时 printf@got 处填写 lookup_printf 的地址
void printf@plt()
{
address_good:
jmp *printf@got
lookup_printf:
;// 调用重定位函数查找 printf 地址,并写到 printf@got
jmp address_good; // 返回执行 address_good
}
graph LR
A[printf@plt] --> |*printf@got| B[printf@plt]
B --> C[common@plt]
C --> D[_dl_runtime_resolve]
GOT 查询并不需要跳转 pc
实际 plt
查看 elf 文件中 .plt 节,.plt 节为每个函数生成了形如下面的代码
08048360 <setbuf@plt-0x10>: # fake symbol using the nearest one
8048360: push 0x804a004
8048366: jmp *0x804a008
804836c: add %al,(%eax)
...
08048370 <setbuf@plt>:
8048370: jmp *0x804a00c
8048376: push $0x0
804837b: jmp 8048360 <setbuf@plt-0x10>
其中,每个 plt 表项会先试图跳转到 got 表项
- got 表项初始值为 plt 表项的下一条指令,即 jmp 回来继续执行
- 继续执行后将 push 函数标识,进入公共表项跳转到
_dl_runtime_resolve - 其中,
0x0804a008保存_dl_runtime_resolve的地址,初始为 0,运行后重写为实际函数地址
一些细节
_dl_runtime_resolve 通过访问 elf 的重定位符号表 .rel.plt 来决定应该把函数地址填回哪个 got.entry
使用 readelf -r 查看重定位信息,形如
Relocation section '.rel.plt' at offset 0x2b0 contains 1 entries:
Offset Info Type Sym.Value Sym. Name
0804836c 00000107 R_386_JUMP_SLOT 00000000 printf
在 i386 架构下,除了每个函数占用一个 GOT 表项外,GOT 表前三项是 reserved,分别保存:
- got [0]: 本 ELF 动态段 (.dynamic 段) 的装载地址
- got [1]:本 ELF 的 link_map 数据结构描述符地址
- got [2]:_dl_runtime_resolve 函数的地址
动态链接器在加载完 ELF 之后,都会将这3地址写到 GOT 表的前3项