got, plt

89 阅读2分钟

GOT, PLT

www.yuque.com/hxfqg9/bin/…

www.bilibili.com/video/BV1a7…

以防丢失,记录一下

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项