看到静态链接和动态链接这里,静态链接和windows上感觉一样,就不写了,感觉这里有个位置无关代码和延迟绑定挺有意思,记录一下

位置无关代码

​ 在windows平台上,对于DLL文件的全局变量,在DLL加载到内存中后,要是没有加载到对应的位置,需要重定位,这个时候就需要重定位表, 而这里提到了一个位置无关代码的概念,可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code, PIC),通过GCC传递 -fpic 参数就可以生成PIC。

1个ELF文件本身,数据段和代码段的相对距离是不变的,因此指令和变量之间的举例就是一个运行时常量,与绝对地址无关,这就是PIC的核心。

全局偏移量表GOT(Global Offset Table)被拆分为.got节和.got.plt节,可以理解为是个数组,单位是8字节

  1. .got 不需要延迟绑定,用于保存全局变量引用,加载到内存标记为只读
  2. .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
//main.c
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;
}
//func.c
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 # 这里与书上不一样,查到-fno-pie -no-pie 去除地址随机化,要不影响实验
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] # 200fd8 <tmp-0x50>
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表格来表示下整体的流程