【Linux】库的链接与加载
目录
一、静态链接
二、ELF加载与进程地址空间
2.1 虚拟地址/逻辑地址
2.2 重新理解进程虚拟地址空间
三、动态链接与动态库加载
3.1 进程如何看到动态库
3.2 进程间如何共享库的
3.3 动态链接
3.3.1 概要
3.3.2 我们的可执行程序被编译器动了手脚
3.3.3 动态库中的相对地址
3.3.4 我们的程序,怎么和库具体映射起来的
3.3.5 我们的程序,怎么进行库函数调用
3.3.6 全局偏移量表GOT(global offset table)
3.3.7 库间依赖(简单说明)
3.4 总结
一、静态链接
- 无论是自己的 .o,还是静态库中的 .o,本质都是把 .o 文件进行链接的过程。
- 所以,研究静态链接,本质就是研究 .o 如何链接的。
查看编译后的 .o 目标文件
# objdump -d hello.o
hello.o: file format elf64-x86-64
Disassembly of section .text:0000000000000000 <func>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <func+0xf>
f: 48 89 c7 mov %rax,%rdi
12: e8 00 00 00 00 call 17 <func+0x17>
17: 90 nop
18: 5d pop %rbp
19: c3 ret
# objdump -d main.o
main.o: file format elf64-x86-64
Disassembly of section .text:0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: b8 00 00 00 00 mov $0x0,%eax
d: e8 00 00 00 00 call 12 <main+0x12>
12: b8 00 00 00 00 mov $0x0,%eax
17: 5d pop %rbp
18: c3 ret
- objdump -d 命令:将代码段(.text)进行反汇编查看
- main.o 中的 main 函数不认识 func 函数
- hello.o 中的 func 函数不认识 printf 函数
我们可以看到这里的 call 指令,它们分别对应调用之前的 printf 和 func 函数,但是我们能发现它们的跳转地址都被设置成了0,这是为什么呢?
其实就是在编译 main.c 的时候,编译器是完全不知道 func 函数的存在的,比如它位于内存的哪个区块,代码长什么样都是不知道的。因此,编译器只能将这个函数的地址暂时设置成0。
这个地址会在那个时候被修正呢?链接的时候。为了让链接器将来在链接时能够正确定位到这些被修正的地址,在代码块(.data)中还存在一个重定位表,这张表将来在链接的时候,就会根据表里记录的地址进行修正。
注意:printf 还涉及到了动态库,这里暂不说明。
// 读取hello.o的符号表
# readelf -s hello.oSymbol table '.symtab' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .rodata
4: 0000000000000000 26 FUNC GLOBAL DEFAULT 1 func
5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts// puts:就是printf的实现
// UND:undefine,表示未定义// 读取main.o的符号表
# readelf -s main.oSymbol table '.symtab' contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000000000 25 FUNC GLOBAL DEFAULT 1 main
4: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND func// func:我们自己的方法在main.o中未定义(因为定义在hello.o中)
// UND:undefine,表示未定义// 读取main.exe符号表
# readelf -s main.exeSymbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _[...]@GLIBC_2.34 (2)
2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterT[...]
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (3)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMC[...]
6: 0000000000000000 0 FUNC WEAK DEFAULT UND [...]@GLIBC_2.2.5 (3)Symbol table '.symtab' contains 38 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS Scrt1.o
2: 000000000000038c 32 OBJECT LOCAL DEFAULT 4 __abi_tag
3: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
4: 0000000000001090 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones
5: 00000000000010c0 0 FUNC LOCAL DEFAULT 16 register_tm_clones
6: 0000000000001100 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux
7: 0000000000004010 1 OBJECT LOCAL DEFAULT 26 completed.0
8: 0000000000003dc0 0 OBJECT LOCAL DEFAULT 22 __do_global_dtor[...]
9: 0000000000001140 0 FUNC LOCAL DEFAULT 16 frame_dummy
10: 0000000000003db8 0 OBJECT LOCAL DEFAULT 21 __frame_dummy_in[...]
11: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
12: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
13: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
14: 0000000000002118 0 OBJECT LOCAL DEFAULT 20 __FRAME_END__
15: 0000000000000000 0 FILE LOCAL DEFAULT ABS
16: 0000000000003dc8 0 OBJECT LOCAL DEFAULT 23 _DYNAMIC
17: 0000000000002014 0 NOTYPE LOCAL DEFAULT 19 __GNU_EH_FRAME_HDR
18: 0000000000003fb8 0 OBJECT LOCAL DEFAULT 24 _GLOBAL_OFFSET_TABLE_
19: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_mai[...]
20: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterT[...]
21: 0000000000004000 0 NOTYPE WEAK DEFAULT 25 data_start
22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5
23: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 25 _edata
24: 000000000000117c 0 FUNC GLOBAL HIDDEN 17 _fini
25: 0000000000004000 0 NOTYPE GLOBAL DEFAULT 25 __data_start
26: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
27: 0000000000004008 0 OBJECT GLOBAL HIDDEN 25 __dso_handle
28: 0000000000002000 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used
29: 0000000000001149 26 FUNC GLOBAL DEFAULT 16 func
30: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 26 _end
31: 0000000000001060 38 FUNC GLOBAL DEFAULT 16 _start
32: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 26 __bss_start
33: 0000000000001163 25 FUNC GLOBAL DEFAULT 16 main
34: 0000000000004010 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__
35: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMC[...]
36: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@G[...]
37: 0000000000001000 0 FUNC GLOBAL HIDDEN 12 _init// 两个 .o 进行合并之后,在最终的可执行程序中,就找到了 func
// 0000000000001149:其实就是地址
// FUNC:表示 func 符号类型是个函数
// 16:就是 func 函数所在的 section 被合并最终的那一个 section 中了,16就是下标
// 读取可执行程序最终的所有的section清单
# readelf -S main.exe
There are 31 section headers, starting at offset 0x36d8:Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.pr[...] NOTE 0000000000000338 00000338
0000000000000030 0000000000000000 A 0 0 8
[ 3] .note.gnu.bu[...] NOTE 0000000000000368 00000368
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000038c 0000038c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003b0 000003b0
0000000000000024 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003d8 000003d8
00000000000000a8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000480 00000480
000000000000008d 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 000000000000050e 0000050e
000000000000000e 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000520 00000520
0000000000000030 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000000550 00000550
00000000000000c0 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000000610 00000610
0000000000000018 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000001000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020
0000000000000020 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 0000000000001040 00001040
0000000000000010 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 0000000000001050 00001050
0000000000000010 0000000000000010 AX 0 0 16
[16] .text PROGBITS 0000000000001060 00001060
000000000000011c 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 000000000000117c 0000117c
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000002000 00002000
0000000000000011 0000000000000000 A 0 0 4
[19] .eh_frame_hdr PROGBITS 0000000000002014 00002014
000000000000003c 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 0000000000002050 00002050
00000000000000cc 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000003db8 00002db8
0000000000000008 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003dc0 00002dc0
0000000000000008 0000000000000008 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003dc8 00002dc8
00000000000001f0 0000000000000010 WA 7 0 8
[24] .got PROGBITS 0000000000003fb8 00002fb8
0000000000000048 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000004000 00003000
0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000004010 00003010
0000000000000008 0000000000000000 WA 0 0 1
[27] .comment PROGBITS 0000000000000000 00003010
000000000000002b 0000000000000001 MS 0 0 1
[28] .symtab SYMTAB 0000000000000000 00003040
0000000000000390 0000000000000018 29 19 8
[29] .strtab STRTAB 0000000000000000 000033d0
00000000000001e7 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 000035b7
000000000000011a 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)// main.o 和 hello.o 的 .text 被合并了,是 main.exe 的第16个section
// 怎么证明上面的说法?
// 关于 hello.o 或者 main.o call 后面的00 00 00 00有没有被修改成具体的最终函数地址呢?
// 反汇编 main.exe 只查看代码段信息,包含源代码
# objdump -d main.exe
main.exe: file format elf64-x86-64
Disassembly of section .init:0000000000001000 <_init>:
1000: f3 0f 1e fa endbr64
1004: 48 83 ec 08 sub $0x8,%rsp
1008: 48 8b 05 d9 2f 00 00 mov 0x2fd9(%rip),%rax # 3fe8 <__gmon_start__@Base>
100f: 48 85 c0 test %rax,%rax
1012: 74 02 je 1016 <_init+0x16>
1014: ff d0 call *%rax
1016: 48 83 c4 08 add $0x8,%rsp
101a: c3 retDisassembly of section .plt:
0000000000001020 <.plt>:
1020: ff 35 9a 2f 00 00 push 0x2f9a(%rip) # 3fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: ff 25 9c 2f 00 00 jmp *0x2f9c(%rip) # 3fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
102c: 0f 1f 40 00 nopl 0x0(%rax)
1030: f3 0f 1e fa endbr64
1034: 68 00 00 00 00 push $0x0
1039: e9 e2 ff ff ff jmp 1020 <_init+0x20>
103e: 66 90 xchg %ax,%axDisassembly of section .plt.got:
0000000000001040 <__cxa_finalize@plt>:
1040: f3 0f 1e fa endbr64
1044: ff 25 ae 2f 00 00 jmp *0x2fae(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
104a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)Disassembly of section .plt.sec:
0000000000001050 <puts@plt>:
1050: f3 0f 1e fa endbr64
1054: ff 25 76 2f 00 00 jmp *0x2f76(%rip) # 3fd0 <puts@GLIBC_2.2.5>
105a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)Disassembly of section .text:
0000000000001060 <_start>:
1060: f3 0f 1e fa endbr64
1064: 31 ed xor %ebp,%ebp
1066: 49 89 d1 mov %rdx,%r9
1069: 5e pop %rsi
106a: 48 89 e2 mov %rsp,%rdx
106d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
1071: 50 push %rax
1072: 54 push %rsp
1073: 45 31 c0 xor %r8d,%r8d
1076: 31 c9 xor %ecx,%ecx
1078: 48 8d 3d e4 00 00 00 lea 0xe4(%rip),%rdi # 1163 <main>
107f: ff 15 53 2f 00 00 call *0x2f53(%rip) # 3fd8 <__libc_start_main@GLIBC_2.34>
1085: f4 hlt
1086: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
108d: 00 00 000000000000001090 <deregister_tm_clones>:
1090: 48 8d 3d 79 2f 00 00 lea 0x2f79(%rip),%rdi # 4010 <__TMC_END__>
1097: 48 8d 05 72 2f 00 00 lea 0x2f72(%rip),%rax # 4010 <__TMC_END__>
109e: 48 39 f8 cmp %rdi,%rax
10a1: 74 15 je 10b8 <deregister_tm_clones+0x28>
10a3: 48 8b 05 36 2f 00 00 mov 0x2f36(%rip),%rax # 3fe0 <_ITM_deregisterTMCloneTable@Base>
10aa: 48 85 c0 test %rax,%rax
10ad: 74 09 je 10b8 <deregister_tm_clones+0x28>
10af: ff e0 jmp *%rax
10b1: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
10b8: c3 ret
10b9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)00000000000010c0 <register_tm_clones>:
10c0: 48 8d 3d 49 2f 00 00 lea 0x2f49(%rip),%rdi # 4010 <__TMC_END__>
10c7: 48 8d 35 42 2f 00 00 lea 0x2f42(%rip),%rsi # 4010 <__TMC_END__>
10ce: 48 29 fe sub %rdi,%rsi
10d1: 48 89 f0 mov %rsi,%rax
10d4: 48 c1 ee 3f shr $0x3f,%rsi
10d8: 48 c1 f8 03 sar $0x3,%rax
10dc: 48 01 c6 add %rax,%rsi
10df: 48 d1 fe sar $1,%rsi
10e2: 74 14 je 10f8 <register_tm_clones+0x38>
10e4: 48 8b 05 05 2f 00 00 mov 0x2f05(%rip),%rax # 3ff0 <_ITM_registerTMCloneTable@Base>
10eb: 48 85 c0 test %rax,%rax
10ee: 74 08 je 10f8 <register_tm_clones+0x38>
10f0: ff e0 jmp *%rax
10f2: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
10f8: c3 ret
10f9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)0000000000001100 <__do_global_dtors_aux>:
1100: f3 0f 1e fa endbr64
1104: 80 3d 05 2f 00 00 00 cmpb $0x0,0x2f05(%rip) # 4010 <__TMC_END__>
110b: 75 2b jne 1138 <__do_global_dtors_aux+0x38>
110d: 55 push %rbp
110e: 48 83 3d e2 2e 00 00 cmpq $0x0,0x2ee2(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
1115: 00
1116: 48 89 e5 mov %rsp,%rbp
1119: 74 0c je 1127 <__do_global_dtors_aux+0x27>
111b: 48 8b 3d e6 2e 00 00 mov 0x2ee6(%rip),%rdi # 4008 <__dso_handle>
1122: e8 19 ff ff ff call 1040 <__cxa_finalize@plt>
1127: e8 64 ff ff ff call 1090 <deregister_tm_clones>
112c: c6 05 dd 2e 00 00 01 movb $0x1,0x2edd(%rip) # 4010 <__TMC_END__>
1133: 5d pop %rbp
1134: c3 ret
1135: 0f 1f 00 nopl (%rax)
1138: c3 ret
1139: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)0000000000001140 <frame_dummy>:
1140: f3 0f 1e fa endbr64
1144: e9 77 ff ff ff jmp 10c0 <register_tm_clones>0000000000001149 <func>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1158: 48 89 c7 mov %rax,%rdi
115b: e8 f0 fe ff ff call 1050 <puts@plt>
1160: 90 nop
1161: 5d pop %rbp
1162: c3 ret0000000000001163 <main>:
1163: f3 0f 1e fa endbr64
1167: 55 push %rbp
1168: 48 89 e5 mov %rsp,%rbp
116b: b8 00 00 00 00 mov $0x0,%eax
1170: e8 d4 ff ff ff call 1149 <func>
1175: b8 00 00 00 00 mov $0x0,%eax
117a: 5d pop %rbp
117b: c3 retDisassembly of section .fini:
000000000000117c <_fini>:
117c: f3 0f 1e fa endbr64
1180: 48 83 ec 08 sub $0x8,%rsp
1184: 48 83 c4 08 add $0x8,%rsp
1188: c3 ret最终:
- 两个 .o 的代码段合并到一起了,并进行了统一的编址。
- 链接的时候,会修改 .o 中没有确定的函数地址,在合并完成之后,进行相关call地址,完成代码调用。
静态链接就是把库中的 .o 进行合并,和上述过程一样。
所以链接其实就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立的可执行文件。其中就包括我们只前提到的地址修正,当所有模块组合到一起之后,链接器会根据我们的 .o 文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这其实就是静态链接的过程。
所以,链接过程中会涉及到对 .o 中外部符号进行地址重定位。
二、ELF加载与进程地址空间
2.1 虚拟地址/逻辑地址
问题:
- 一个 ELF 程序,在没有被加载到内存的时候,有没有地址呢?
- 进程 mm_struct,vm_area_struct 在进程刚刚创建的时候,初始化数据从哪里来的?
答案:
- 一个 ELF 程序,在没有被加载到内存的时候,本来就有地址,当代计算机工作的时候,都采用“平坦模式”进行工作。所以也要求 ELF 对自己的代码和数据进行统一编址,下面是 objdump -S 反汇编之后的代码
最左侧就是 ELF 的虚拟地址,其实,严格意义上应该叫逻辑地址(起始地址 + 偏移量),但是我们认为起始地址是0。也就是说,其实虚拟地址在我们的程序还没加载到内存的时候,就已经把可执行程序进行统一编址了。
- 进程 mm_struct,vm_area_struct 在进程刚刚创建的时候,初始化数据从哪里来的?从 ELF 各个 segment 来,每个 segment 有自己的起始地址和长度,用来初始化内核数据结构中的 [start, end] 等范围数据,另外在用详细地址,填充页表。
所以,虚拟地址机制,不光光OS要支持,编译器也要支持。
2.2 重新理解进程虚拟地址空间
ELF 在被编译好之后,会把自己未来程序的入口地址记录在 ELF header 中的 Entry 字段中:
三、动态链接与动态库加载
3.1 进程如何看到动态库
3.2 进程间如何共享库的
3.3 动态链接
3.3.1 概要
动态链接其实远比静态链接要常用的多。比如我们查看一下 main.exe 这个可执行程序所依赖的动态库,会发现它就用到了一个C动态链接库:
ldd命令:用于打印程序或者库文件所依赖的共享库列表。
这里的 libc.so 是C语言的运行时库,里面提供了常用的标准输入输出文件字符串处理等等这些功能。
那么为什么编译器默认不使用静态链接呢?静态链接会将编译产生的所有目标文件,连同用到的各种库,合并形成一个独立的可执行文件,它不需要额外的依赖就可以运行。照理来说应该更加方便才是吧?
静态链接最大的问题在于生成的文件体积大,并且相当耗费内存资源。随着软件复杂度的提升,我们的操作系统也越来越臃肿,不同的软件就有可能包含了相同的功能呢个和代码,显然会浪费大量的硬盘空间。
这个时候,动态链接的优势就体现出来了。我们可以将需要共享的代码单独提取出来,保存成一个独立的动态链接库,等到程序运行的时候再将它们加载到内存中。这样不但可以节省空间,而且因为同一个模块在内存中只需保存一份副本,可以被不同的进程共享。
那么动态链接是如何工作的呢?
首先要交代一个结论,动态链接实际是将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将它的数据代码和连同它用到的一系列动态库先加载到内存,其中每个动态库加载的地址是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。
当动态库被加载到内存以后,一旦它的内存地址确定,我们就可以去修正那些动态库中的那些函数跳转地址了。
3.3.2 我们的可执行程序被编译器动了手脚
# ldd /usr/bin/ls
linux-vdso.so.1 (0x00007ffe5ddf0000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x000076232f616000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000076232f400000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x000076232f366000)
/lib64/ld-linux-x86-64.so.2 (0x000076232f671000)# ldd main.exe
linux-vdso.so.1 (0x00007ffec5feb000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000735b22400000)
/lib64/ld-linux-x86-64.so.2 (0x0000735b22812000)
在C/C++程序中,当程序开始执行时,他首先并不会直接跳转到main函数。实际上,程序的入口点是 _start,这是一个由C运行时库(通常是glibc)或者链接器(如ld)提供的特殊函数。
在 _start 函数中,会执行一系列初始化操作,这些操作包括:
- 设置堆栈:为程序创建一个初始的堆栈环境。
- 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。
- 动态链接:这是关键的一步,_start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确的映射到动态库中的实际位置。
动态链接器:
- 动态链接器(如ld-linux.so)负责在程序运行时加载动态库。
- 当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存。
环境变量和配置文件:
- Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf及其子配置文件)来指定动态库的搜索路径。
- 这些路径会被动态链接器在加载动态库时搜索。
缓存文件:
- 为了提高动态库的加载效率,Linux会维护一个名为/etc/ld.so.cache的缓存文件。
- 该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先搜索这个缓存文件。
- 调用 __libc_start_main:一旦动态链接完成,_start 函数会调用 __libc_start_main(这是glibc提供的一个函数)。 __libc_start_main 负责执行一些额外的初始化工作,比如设置信号处理函数,初始化线程库(如果使用了线程)等等。
- 调用 main 函数:最后, __libc_start_main 函数会调用程序的 main 函数,此时程序的执行控制权才正式交给用户编写的代码。
- 处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回值,并最终调用 _exit 函数终止程序。
上述过程描述了C/C++程序在 main 函数之前执行的一系列操作,但这些操作对于大多数程序员来说是透明的。程序员通常只要关注 main 函数中的代码,而不需要关心底层的初始化过程。然而,了解这些底层细节,有助于更好了解程序的执行流程和调试问题。
3.3.3 动态库中的相对地址
动态库为了随时进行加载,为了支持并映射到任意进程的任意位置,对动态库中的方法,统一编址,采用相对编址的方法编址的(其实可执行程序也一样,都要遵循平坦模式,只不过exe是直接加载的)。
// ubuntu下查看任意一个库的反汇编
objdump -S /lib/x86_64-linux-gnu/libc-2.31.so | less
3.3.4 我们的程序,怎么和库具体映射起来的
注意:
- 动态库已是一个文件,要访问也是要先被加载,要加载也是要被打开的。
- 让我们的进程找到动态库的本质:也是文件操作,不过我们访问库函数,通过虚拟地址进行跳转访问的,所以需要把动态库映射到进程的地址空间中。
3.3.5 我们的程序,怎么进行库函数调用
注意:
- 库已经被我们映射到当前进程的地址空间中。
- 库的虚拟起始地址我们也已经知道了。
- 库中的每一个方法的偏移量地址我们也知道。
- 所以,访问库中任意方法,只需知道库的起始虚拟地址 + 方法偏移量即可定位库中的方法。
- 而且,整个调用过程,是从代码区跳转到共享区,调用完毕再返回代码区,整个过程完全在进程地址空间中进行。
3.3.6 全局偏移量表GOT(global offset table)
注意:
- 也就是说,我们的程序运行之前,先把所有库加载并映射,所有库的起始虚拟地址都应该提前知道的。
- 然后对我们加载到内存的程序的库函数调用进行地址修正,在内存中二次完成地址设置(这个叫做加载地址重定位)。
- 这时我们好像修改的代码区?但是代码区在进程中不是只读的吗?能修改吗?怎么修改呢?
所以,动态链接采用的方式是在 .data(可执行程序或者库自己)中专门预留一个区域用来存放函数的跳转地址,它也被叫做全局偏移量表GOT,表中每一项都是本运行模块要引用的一个全局变量或者函数地址。
- 因为 .data 区域是可读写的,所以可以支持动态进行修改。
# readelf -S main.exe
. . .
[24] .got PROGBITS 0000000000003fb8 00002fb8
0000000000000048 0000000000000008 WA 0 0 8. . .
# readelf -l main.exe// .got 在加载的时候,会和 .data合并成一个 segment,然后加载在一起
. . .
05 .init_array .fini_array .dynamic .got .data .bss
. . .
- 由于代码段只读,我们不能直接修改代码段,但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不相同。反映到GOT表中,就是每个进程中的每个动态库都有独立的GOT表,所以进程间不能共享GOT表。
- 在单个 .so 下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利用CPU的相对寻址来找到GOT表。
- 在调用函数的时候会首先查表,然后根据表中的地址进行跳转,这些地址在加载的时候会被修改为真正的地址。
- 这种方式实现的动态链接被叫做 PIC 地址无关代码。换句话来说,我们的动态库不需要做任何修改,被加载到任意内存地址都能正常运行,并且能够被所有进程共享,这也是为什么之前我们给编译器指定 -fPIC 参数的原因,PIC = 相对编址 + GOT。
# objdump -S main.exe
. . .
0000000000001050 <puts@plt>:
1050: f3 0f 1e fa endbr64
1054: ff 25 76 2f 00 00 jmp *0x2f76(%rip) # 3fd0 <puts@GLIBC_2.2.5>
105a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1). . .
. . .
0000000000001149 <func>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1158: 48 89 c7 mov %rax,%rdi
115b: e8 f0 fe ff ff call 1050 <puts@plt>. . .
3.3.7 库间依赖(简单说明)
注意:
- 不仅仅有可执行程序调用库,库也会调用其他库!
- 库之间也是由依赖的,如何做到库和库之间互相调用也是与地址无关的呢?
- 库中也有GOT,和可执行一样!这也就是为什么大家都是ELF格式!
由于GOT表中的映射地址会在运行时去修改,我们可以通过gdb调试去观察GOT表的地址变化。我们这里只用知道原理即可,感兴趣的可以参考下面这篇文章:通过GDB观察GOT
由于动态链接在程序加载的时候需要对大量的函数进行重定位,这一步显然是非常耗时的。为了进一步降低开销,我们的操作系统还做了一些其他优化,比如延迟绑定,或者也叫PLT(过程连接表(Procedure Linkage Table))。与其在一开始就对所有函数进行重定位,不如将这个过程推迟到函数第一次调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。
思路:GOT的跳转地址默认会指向一段辅助代码,它也被叫做桩代码/stup。在我们第一次调用函数的时候,这段代码会负责查询真正的函数跳转地址,并且去更新GOT表。于是我们再次调用函数的时候,就会直接跳转到动态库真正的函数实现。
总而言之,动态链接实际上将链接的整个过程,比如符号查询,地址的重定位等从编译时推迟到程序的运行时,它虽然牺牲了一定性能和程序的加载时间,但绝对是物有所值的。因为动态链接能够更有效地利用磁盘空间和内存资源,以及极大方便了代码的更新和维护,更关键的是,它实现了二进制级别代码的复用。
解析依赖关系的时候,就是加载并完善互相之间的GOT表的过程。
3.4 总结
- 静态链接的出现,提高了程序的模块化水平。对于一个大的项目,不同的人可以独立的测试和开发自己的模块。通关静态链接,生成最终可执行文件。
- 我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并为一个独立的可执行文件,其中我们会去修正模块间函数的跳转地址,也叫做编译重定位(静态重定位)。
- 而动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将它的数据代码连同它用到的一系列动态库先加载到内存中,其中每个动态库的加载地址都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间中,然后通过.GOT方式进行调用(运行重定位,也叫做动态地址重定位)。