1. 示例代码
// main
extern int g_nShared;
extern void func(int *a, int *b) ;
void main(void) {
int n = 2;
func(&n, &g_nShared);
}
// func.c
#include<stdio.h>
int g_nShared = 1;
int g_nTmp = 0;
void func(int *a, int *b) {
if(!a || !b) return;
g_nTmp = *a;
*a = *b;
*b = g_nTmp;
}
2. 静态链接
按时间分类:
- compile time linking
- load time linking
- run time linking
多个模块合成一个可执行文件,方法有两种:
- 按顺序叠加,显然浪费空间(因为要按页对齐);
- 相似节合并,比如两个模块的text合成一个。这是现在的linker所采纳的。
链接过程
编译:
gcc -static -fno-stack-protector main.c func.c -save-temps --verbose -o func.out
-save-temps可以保留中间的编译文件。
看下main.o的section header,VMA(虚拟内存地址)和LMA(加载内存地址)都是0,因为还没有链接。
$ objdump -h main.o
main.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000025 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000065 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000065 2**0
ALLOC
3 .comment 0000002a 0000000000000000 0000000000000000 00000065 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000008f 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 00000090 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
而链接后的func.out,相似节已经被合并,VMA和LMA都被填充:
$ objdump -h func.out
func.out: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
...
5 .text 0008f490 00000000004004d0 00000000004004d0 000004d0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
...
18 .got 000000f8 00000000006b8ef8 00000000006b8ef8 000b8ef8 2**3
CONTENTS, ALLOC, LOAD, DATA
19 .got.plt 000000d0 00000000006b9000 00000000006b9000 000b9000 2**3
CONTENTS, ALLOC, LOAD, DATA
20 .data 00001af0 00000000006b90e0 00000000006b90e0 000b90e0 2**5
CONTENTS, ALLOC, LOAD, DATA
...
25 .bss 000016f8 00000000006bb2e0 00000000006bb2e0 000bb2d8 2**5
ALLOC
反汇编看下main.o, func地址偏移是0:
$ objdump -d -j .text main.o
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
13: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 1a <main+0x1a>
1a: 48 89 c7 mov %rax,%rdi
1d: e8 00 00 00 00 callq 22 <main+0x22>
22: 90 nop
23: c9 leaveq
24: c3 retq
相比,func.out已经获取了func函数的偏移:
$ objdump -d -j .text func.out | grep -A 20 "<main>"
0000000000400b6d <main>:
400b6d: 55 push %rbp
400b6e: 48 89 e5 mov %rsp,%rbp
400b71: 48 83 ec 10 sub $0x10,%rsp
400b75: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%rbp)
400b7c: 48 8d 45 fc lea -0x4(%rbp),%rax
400b80: 48 8d 35 69 85 2b 00 lea 0x2b8569(%rip),%rsi # 6b90f0 <g_nShared>
400b87: 48 89 c7 mov %rax,%rdi
400b8a: e8 03 00 00 00 callq 400b92 <func> # 400b8f+3==func
400b8f: 90 nop
400b90: c9 leaveq
400b91: c3 retq
0000000000400b92 <func>:
400b92: 55 push %rbp
400b93: 48 89 e5 mov %rsp,%rbp
400b96: 48 89 7d f8 mov %rdi,-0x8(%rbp)
另外对于main.o重定位文件,最重要的是重定位表:
$ objdump -r main.o
main.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000016 R_X86_64_PC32 g_nShared-0x0000000000000004
000000000000001e R_X86_64_PLT32 func-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
0x16,是指objdump -d -j .text main.o
那里,偏移0x16的地方需要重定位。
R_X86_64_PC32,是相对偏移类型。
-0x4是Elf32_Rela.r_addend的值。
静态链接库
后缀.a就是静态链接库,根据上面静态编译的func.out也能想到,.a是一组目标文件经过了压缩打包,比如printf.o, scanf.o, malloc.o。 压缩打包的工具是ar,可以看下libc.a的内容:
$ ar -t /usr/lib32/libc.a
init-first.o
libc-start.o
sysdep.o
version.o
check_fds.o
libc-tls.o
elf-init.o
...
3. 动态链接
目的:多个程序使用内存中的一个库,节省内存。
编译示例代码func.c为动态库:
gcc -shared -fpic func.c -m32 -o func.so
pic放到后面解释。
主程序修改一下,调用两次动态库的func函数, 并按注释的gcc命令编译:
// gcc main.c ./func.so -no-pie -m32 -o main.out
// -no-pie: Don't produce a position independent executable.
// 注意要带上这个参数,不然没有用.got.plt section
extern int g_nShared;
extern void func(int *a, int *b) ;
void main(void) {
int n = 2;
func(&n, &g_nShared);
func(&n, &g_nShared);
}
PIC
PIC: Position Independent Code, 位置无关代码,代码和数据的引用与地址无关,程序可以被加载到地址空间的任意位置。它是共享库必有的属性,这样才能被多个进程使用。
PIC 使用 GOT 来引用变量和函数的绝对地址,把位置独立的引用重定向到绝对位置。
- 对于模块外部引用的全局变量和全局函数,用 GOT 表的表项内容作为地址来间接寻址;
- 对于本模块内的静态变量和静态函数,用相对于GOT 表的首地址的偏移量来引用。
$ objdump -h func.so
func.so: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .note.gnu.build-id 00000024 00000000000001c8 00000000000001c8 000001c8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
...
15 .got 00000028 0000000000200fd8 0000000000200fd8 00000fd8 2**3
CONTENTS, ALLOC, LOAD, DATA
16 .got.plt 00000018 0000000000201000 0000000000201000 00001000 2**3
CONTENTS, ALLOC, LOAD, DATA
...
$ readelf -r func.so
Relocation section '.rela.dyn' at offset 0x400 contains 8 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000200e78 000000000008 R_X86_64_RELATIVE 5d0
...
000000200ff0 000600000006 R_X86_64_GLOB_DAT 0000000000201028 g_nTmp + 0
...
可以看到这个全局变量g_nTmp位于.got section内。
.got可以保存全局变量引用,那么反汇编看下func():
$ objdump -M intel -d func.so | grep -A 20 "<func>"
00000000000005da <func>:
5da: 55 push rbp
5db: 48 89 e5 mov rbp,rsp
5de: 48 89 7d f8 mov QWORD PTR [rbp-0x8],rdi
5e2: 48 89 75 f0 mov QWORD PTR [rbp-0x10],rsi
5e6: 48 83 7d f8 00 cmp QWORD PTR [rbp-0x8],0x0
5eb: 74 33 je 620 <func+0x46>
5ed: 48 83 7d f0 00 cmp QWORD PTR [rbp-0x10],0x0
5f2: 74 2c je 620 <func+0x46>
5f4: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
5f8: 8b 10 mov edx,DWORD PTR [rax]
5fa: 48 8b 05 ef 09 20 00 mov rax,QWORD PTR [rip+0x2009ef] # 200ff0 <g_nTmp-0x38>
601: 89 10 mov DWORD PTR [rax],edx
603: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10]
607: 8b 10 mov edx,DWORD PTR [rax]
609: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
60d: 89 10 mov DWORD PTR [rax],edx
60f: 48 8b 05 da 09 20 00 mov rax,QWORD PTR [rip+0x2009da] # 200ff0 <g_nTmp-0x38>
616: 8b 10 mov edx,DWORD PTR [rax]
618: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10]
61c: 89 10 mov DWORD PTR [rax],edx
rip+0x2009ef == 0x601+0x2009ef == 0x200ff0, 这就是重定位表里的offset。
GOT与PLT
一个程序的数据段和代码段的相对距离是不变的常量,GOT全局偏移表可以保存全局变量和库函数的引用。
每个条目占8字节,加载时会进行重定位并填入符号的绝对地址。
为了引入RELRO保护机制,GOT被拆分为两部分:
- .got,不需要要延迟绑定,用于保存全局变量引用,运行时只读;
- .got.plt,需要延迟绑定,用来保存函数引用,运行时可读可写;
还有一个.plt.got,存放 __cxa_finalize 函数对应的 PLT 条目,可先忽略。
.got.plt section包含一个GOT数组:
- GOT[0]存放.dynamic section地址;
- GOT[1]存放reloc entries;
- GOT[2]存放ld-linux.so的_dl_runtime_resolve动态解析函数地址;
- GOT[3]存放
__stack_chk_fail
, 用来检查canary安全防护; - GOT[4]存放
__libc_start_main
, 它会调用main(); - GOT[5]开始是其它延迟加载的函数地址,本例则是func().
可以用010editor看一下main.out section header,里面有内存偏移,条目大小(0x4,数量0x18/0x46):
另外是.plt section,Procedure Linkage Table,虽说翻译过来是表,但它是可执行的(不是函数指针):
- PLT[0]:跳转到动态链接器ld.so;
- PLT[1]:
__libc_start_main
; - PLT[2]开始则是被调用的各个函数条目。
延迟绑定Lazy Binding
静态链接要比动态链接的程序稍微快点,两个原因:
- 动态链接程序对于全局和静态的数据访问、都要进行GOT定位,,然后间接寻址;对于模块间的调用,也要先定位GOT,然后再进行间接跳转。
- 链接器会寻找并装载所需要的共享对象,然后进行符号査找地址重定位等工作。
延迟绑定(Lazy Binding)的基本思想就是,当函数第一次被用到时才进行绑定(符号査找、重定位等),如果没有用到则不进行绑定。
和windows的延迟加载模块不一样
ELF通过PLT和GOT的配合来实现延迟加载。每个库函数都有一组PLT和GOT。
看下编译好的main.out:
$ readelf -S main.out
...
[ 9] .rel.dyn REL 08048390 000390 000010 08 A 5 0 4
[10] .rel.plt REL 080483a0 0003a0 000018 08 AI 5 23 4
[11] .init PROGBITS 080483b8 0003b8 000023 00 AX 0 0 4
[12] .plt PROGBITS 080483e0 0003e0 000040 04 AX 0 0 16
[13] .plt.got PROGBITS 08048420 000420 000008 08 AX 0 0 8
...
[21] .dynamic DYNAMIC 08049f08 000f08 0000f0 08 WA 6 0 4
[22] .got PROGBITS 08049ff8 000ff8 000008 04 WA 0 0 4
[23] .got.plt PROGBITS 0804a000 001000 000018 04 WA 0 0 4
...
.rel.dyn
记录了加载时需要重定位的变量,.rel.plt
记录的是需要重定位的函数。
gdb调试,第一次调用func:
→ 0x8048583 <main+61> call 0x8048410 <func@plt>
↳ 0x8048410 <func@plt+0> jmp DWORD PTR ds:0x804a014
0x8048416 <func@plt+6> push 0x10
0x804841b <func@plt+11> jmp 0x80483e0
0x8048420 <__gmon_start__@plt+0> jmp DWORD PTR ds:0x8049ff8
0x8048426 <__gmon_start__@plt+6> xchg ax, ax
0x8048428 add BYTE PTR [eax], al
gef➤ x/4x 0x804a014
0x804a014: 0x08048416 0x00000000 0x00000000 0x00000000
gef➤ si
...
0x8048400 <__libc_start_main@plt+0> jmp DWORD PTR ds:0x804a010
0x8048406 <__libc_start_main@plt+6> push 0x8
0x804840b <__libc_start_main@plt+11> jmp 0x80483e0
→ 0x8048410 <func@plt+0> jmp DWORD PTR ds:0x804a014
0x8048416 <func@plt+6> push 0x10
0x804841b <func@plt+11> jmp 0x80483e0
gef➤ si
...
0x8048406 <__libc_start_main@plt+6> push 0x8
0x804840b <__libc_start_main@plt+11> jmp 0x80483e0
0x8048410 <func@plt+0> jmp DWORD PTR ds:0x804a014
→ 0x8048416 <func@plt+6> push 0x10
0x804841b <func@plt+11> jmp 0x80483e0
0x8048420 <__gmon_start__@plt+0> jmp DWORD PTR ds:0x8049ff8
0x8048426 <__gmon_start__@plt+6> xchg ax, ax
0x804a014, 正位于.got.plt。也就是说func@plt
要进入.got.plt去寻找func的地址。现在func还没有加载,jmp里保存的就是jmp前的下一条指令(0x8048416),
→ 0x8048416 <func@plt+6> push 0x10
0x804841b <func@plt+11> jmp 0x80483e0
这个0x10,是指func函数在.rel.plt中的偏移,在010editor里也可以看一下,下图中有3个函数数据项,依次要填入GOT[3:6]
,本次延迟加载实验关注点是,执行一次func后0x804a014要填入func的地址:
继续si跟进,进入了.plt:
→ 0x80483e0 push DWORD PTR ds:0x804a004
0x80483e6 jmp DWORD PTR ds:0x804a008
...
gef➤ x /10wx 0x804a000
0x804a000: 0x08049f08 0xf7ffd940 0xf7fead40 0x080483f6
0x804a010: 0xf7df6eb0 0x08048416 0x00000000 0x00000000
...
gef➤ x /10x 0x804a008
0x804a008: 0xf7fead40 ...
上面输出了.got.plt的内容:
- GOT[0] – 0x08049f08 – .dynamic
- GOT[1] – 0xf7ffd940 – reloc entries
- GOT[2] – 0xf7fead40 –
_dl_runtime_resolve
- GOT[3] – 0x080483f6–
__stack_chk_fail
- GOT[4] – 0xf7df6eb0 –
__libc_start_main
- GOT[5] – 待填充真实的func地址GOT[1]
也就是说,PLT[0]这里先压栈func函数在.rel.plt中的偏移0x10,再压栈GOT[1]。
跟进这个jmp _dl_runtime_resolve
,就调用了链接器:
→ 0xf7fead40 push eax
0xf7fead41 push ecx
0xf7fead42 push edx
...
0xf7fead40 in ?? () from /lib/ld-linux.so.2
多单步几次,就进入了func:
→ 0xf7fcd45d <func+0> push ebp
0xf7fcd45e <func+1> mov ebp, esp
0xf7fcd460 <func+3> call 0xf7fcd49f <__x86.get_pc_thunk.ax>
0xf7fcd465 <func+8> add eax, 0x1b9b
回到main,第二次调用func:
→ 0x8048599 <main+83> call 0x8048410 <func@plt>
↳ 0x8048410 <func@plt+0> jmp DWORD PTR ds:0x804a014
0x8048416 <func@plt+6> push 0x10
0x804841b <func@plt+11> jmp 0x80483e0
gef➤ x /10x 0x804a014
0x804a014: 0xf7fcd45d
可以看到,0x804a014,也就是GOT[5]里已经填充了func的函数地址。