看到静态链接和动态链接这里,静态链接和windows上感觉一样,就不写了,感觉这里有个位置无关代码和延迟绑定挺有意思,记录一下
位置无关代码
在windows平台上,对于DLL文件的全局变量,在DLL加载到内存中后,要是没有加载到对应的位置,需要重定位,这个时候就需要重定位表, 而这里提到了一个位置无关代码的概念,可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code, PIC),通过GCC传递 -fpic 参数就可以生成PIC。
1个ELF文件本身,数据段和代码段的相对距离是不变的,因此指令和变量之间的举例就是一个运行时常量,与绝对地址无关,这就是PIC的核心。
全局偏移量表GOT(Global Offset Table)被拆分为.got节和.got.plt节,可以理解为是个数组,单位是8字节
- .got 不需要延迟绑定,用于保存全局变量引用,加载到内存标记为只读
- .got.plt 需要延迟绑定,保存函数引用,具有读写权限
写代码测试下 (比书上多加了一个func2,为了测试延迟绑定调用那个_dl_runtime_resolve前的push ,是不是push的func的索引)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| extern int shared; extern void func(int *a, int *b); extern void func2(int *a, int *b); int main() { int a= 100; func(&a, &shared); func2(&a, &shared); return 0; }
int shared = 1; int tmp = 0; void func(int * a, int *b) { tmp = *a; *a = *b; *b = tmp; } void func2(int * a, int *b) { *a = *b; }
|
1 2
| gcc -shared -fpic -o func.so func.c gcc -fno-stack-protector -fno-pie -no-pie -o func.ELF2 main.c ./func.so
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| ╭─ ~/Desktop/testC/dyn ✔ ╰─ objdump -d -M intel --section=.text func.so | grep -A 10 "<func>" 00000000000005ea <func>: 5ea: 55 push rbp 5eb: 48 89 e5 mov rbp,rsp 5ee: 48 89 7d f8 mov QWORD PTR [rbp-0x8],rdi 5f2: 48 89 75 f0 mov QWORD PTR [rbp-0x10],rsi 5f6: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 5fa: 8b 10 mov edx,DWORD PTR [rax] 5fc: 48 8b 05 d5 09 20 00 mov rax,QWORD PTR [rip+0x2009d5] 603: 89 10 mov DWORD PTR [rax],edx 605: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] 609: 8b 10 mov edx,DWORD PTR [rax]
|
因为这个地方写的是RIP +0x2009d5, 是一个偏移,而不是一个写死的地址,所以是PIC, 因为RIP其实就是基址+ 固定的偏移,所以不管基址怎么变化,这个地方的RIP + 0X2009E5指向的总是tmp变量, 如下如,这里是IDA动态调试起来的样子。
延迟绑定
对于调用的别的so中的函数,只有当这个函数真正调用的时候,才往对应地址处写入这个函数的地址,因为如果导入的函数比较多,都在一开始加载地址的话,会影响性能。
ELF文件通过过程链接表(Procedure Linkage Table, PLT)和GOT表配合来实现延迟绑定。,PLT也是个数组,单位是16字节,PLT[0]是用于跳转到动态链接器,IDA远程动态调试观, 在call func处下断点,观察PLT(指的是.plt节处的数据) 和 GOT(这里指的是.got.plt节处的数据),整理下来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| # 下断点位置 .text:0000000000400652 E8 E9 FE FF FF call sub_400540 ; func函数 # PLT[0] .plt:0000000000400520 push cs:qword_601008 .plt:0000000000400526 jmp cs:qword_601010 # PLT[1] .plt:0000000000400530 jmp cs:off_601018 .plt:0000000000400536 push 0 .plt:000000000040053B jmp sub_400520 # PLT[2] .plt:0000000000400540 jmp cs:off_601020 .plt:0000000000400546 push 1 .plt:000000000040054B jmp sub_400520 # GOT[0] .got.plt:0000000000601000 _GLOBAL_OFFSET_TABLE_ dq offset _DYNAMIC # GOT[1] .got.plt:0000000000601008 qword_601008 dq 7F53FD96D170h ; DATA XREF: sub_400520↑r ;reloc entries # GOT[2] .got.plt:0000000000601010 qword_601010 dq 7F53FD7598F0h ; DATA XREF: sub_400520+6↑r ; _dl_runtime_resolve # GOT[3] .got.plt:0000000000601018 off_601018 dq offset sub_400536 ; DATA XREF: .plt:loc_400530↑r ; func2 # GOT[4] .got.plt:0000000000601020 off_601020 dq offset sub_400546 ; DATA XREF: .plt:loc_400540↑r ; func
|
然后再在call func2那里下断点,再次观察PLT表和GOT表
1 2 3 4 5
| .text:0000000000400663 E8 C8 FE FF FF call loc_400530 ; func2函数 # GOT[3] .got.plt:0000000000601018 off_601018 dq offset sub_400536 ; DATA XREF: .plt:loc_400530↑r ; func2 # GOT[4] got.plt:0000000000601020 off_601020 dq offset func ; DATA XREF: .plt:loc_400540↑r ; func
|
可以发现GOT[4]这里已经填入了真正的函数地址,因为前面在调用func的时候,压入了1,而func在GOT表中,在函数的索引这里确实是1(除去其他固定的DYNAMIC, reloc entries, 和dl_runtime_resolve)
1 2
| .plt:0000000000400546 push 1 .plt:000000000040054B jmp sub_400520
|
在调用func2的时候是push 0 ,我们这里把0改为1,patch下,测试下我们的猜想
执行,可以发现确实是调用了func函数
再观察下GOT表,看看有没有变化
显然,GOT表没有变化,所以猜测成功。
这里用EXCLE表格来表示下整体的流程