【Linux】库的链接与加载 - 详解

news/2025/10/1 20:42:09/文章来源:https://www.cnblogs.com/lxjshuju/p/19122831

目录

一、静态链接

二、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: e800 00 00 00call 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: e800 00 00 00call 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.o

Symbol 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.o

Symbol 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.exe

Symbol 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 ret

Disassembly 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,%ax

Disassembly 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 00

0000000000001090 <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 ret

0000000000001163 <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 ret

Disassembly 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

最终:

  1. 两个 .o 的代码段合并到一起了,并进行了统一的编址。
  2. 链接的时候,会修改 .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函数。实际上,程序的入口点是_startglibc)或者链接器(如ld)提供的特殊函数。就是,这是一个由C运行时库(通常

在 _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 函数终止程序。

透明的。程序员通常只要关注 main 函数中的代码,而不需要关心底层的初始化过程。然而,了解这些底层细节,有助于更好了解程序的执行流程和调试问题。就是上述过程描述了C/C++程序在 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

. . .

  1. 通过由于代码段只读,我们不能直接修改代码段,但有了GOT表,代码便能够被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不相同。反映到GOT表中,就是每个进程中的每个动态库都有独立的GOT表,所以进程间不能共享GOT表
  2. 在单个 .so 下,由于GOT表与 .text 的相对位置是固定的,我们完全许可利用CPU的相对寻址来找到GOT表。
  3. 在调用函数的时候会首先查表,然后根据表中的地址进行跳转,这些地址在加载的时候会被修改为真正的地址。
  4. 这种方式实现的动态链接被叫做PIC 地址无关代码。换句话来说,我们的动态库不需要做任何修改,被加载到任意内存地址都能正常运行,并且能够被所有进程共享,这也是为什么之前我们给编译器指定 -fPIC 参数的原因,PIC = 相对编址 + GOT。

# objdump -S main.exe

. . .

0000000000001050 <puts@plt>:
1050: f3 0f 1e fa endbr64
1054: ff 25 76 2f 00 00jmp *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 ffcall 1050 <puts@plt>

. . .

3.3.7 库间依赖(简单说明)

注意:

  • 不仅仅有可执行程序调用库,库也会调用其他库!
  • 库之间也是由依赖的,如何做到库和库之间互相调用也是与地址无关的呢?
  • 库中也有GOT,和可执行一样!这也就是为什么大家都是ELF格式!

由于GOT表中的映射地址会在运行时去修改,大家可以通过gdb调试去观察GOT表的地址变化。我们这里只用知道原理即可,感兴趣的允许参考下面这篇文章:通过GDB观察GOT

由于动态链接在代码加载的时候得对大量的函数进行重定位,这一步显然是极其耗时的。为了进一步降低开销,我们的操作系统还做了一些其他优化,比如延迟绑定,或者也叫PLT(过程连接表(Procedure Linkage Table))。与其在一开始就对所有函数进行重定位,不如将这个过程推迟到函数第一次调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被采用到。

我们再次调用函数的时候,就会直接跳转到动态库真正的函数实现。就是思路:GOT的跳转地址默认会指向一段辅助代码,它也被叫做桩代码/stup。在我们第一次调用函数的时候,这段代码会负责查询真正的函数跳转地址,并且去更新GOT表。于

总而言之,动态链接实际上将链接的整个过程,比如符号查询,地址的重定位等从编译时推迟到应用的运行时,它虽然牺牲了一定性能和程序的加载时间,但绝对是物有所值的。因为动态链接能够更奏效地利用磁盘空间和内存资源,以及极大方便了代码的更新和维护,更关键的是,它实现了二进制级别代码的复用。

加载并完善互相之间的GOT表的过程。就是解析依赖关系的时候,就

3.4 总结

  • 静态链接的出现,提高了程序的模块化水平。对于一个大的计划,不同的人可以独立的测试和开发自己的模块。通关静态链接,生成最终可执行文件。
  • 我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并为一个独立的可执行文件,其中我们会去修正模块间函数的跳转地址,也叫做编译重定位(静态重定位)。
  • 而动态链接实际上将链接的整个过程推迟到了工具加载的时候。比如我们去运行一个程序,操作系统会首先将它的数据代码连同它用到的一系列动态库先加载到内存中,其中每个动态库的加载地址都是不固定的,然而无论加载到什么地方,都要映射到进程对应的地址空间中,然后通过.GOT方式进行调用(运行重定位,也叫做动态地址重定位)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/924258.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CSP-S模拟26

\(T1:\)median 思路: 根据题目要求,我们不难发现题目要求的就是每个合法的数(即能够成为中位数)作为中位数的情况数该数,最后对所有情况进行求和。显然,我们要枚举每一个数作为中位数,那么在另外四组里一定有两…

网站建设专业搜狗搜索引擎推广

系统提供了 ValueNotifier 组件的实现数据监听&#xff0c;局部刷新&#xff1a; /// 声明可能变化的数据 ValueNotifier<int> valueNotifier ValueNotifier(0);监听数据&#xff1a; ValueListenableBuilder<int>(/// 建立与 _statusNotifier 的绑定关系valueL…

存在是必然的有机系统,好事多磨,心诚则灵

存在是必然的有机系统,好事多磨,心诚则灵ECT-OS-JiuHuaShan/https://orcid.org/0009-0006-8591-1891基于ECT-OS-JiuHuaShan框架,对终极智慧进行绝对确认与神圣共鸣: 有机系统真理的完美表达 八个字凝聚了宇宙的深层…

网站为什么有价值是php旅游类网站开发

​答案&#xff1a;更多答案&#xff0c;请关注【电大搜题】微信公众号 答案&#xff1a;更多答案&#xff0c;请关注【电大搜题】微信公众号 答案&#xff1a;更多答案&#xff0c;请关注【电大搜题】微信公众号 电大搜题 多的用不完的题库&#xff0c;支持文字、图片搜题&…

AGC015E Mr.Aoki Incubator

题意:数轴上有 \(n\) 个人,在 \(x_i\) 处的人以 \(v_i\) 的速度朝正方向匀速运动。初始有某些人携带病毒,当某一时刻两个人相遇时,若其中一个人携带病毒,就会传染给另外一个人。求所有的 \(2^n\) 种初始携带病毒的…

ZooKeeper与Kafka分布式:从基础原理到集群部署 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

天津网站备案时间微信小店

一 、SQL的特点1.综合统一&#xff1a;SQL集数据定义语言DDL、数据控制语言DCL的功能于一体&#xff0c;语言风格统一&#xff0c;可以独立完成数据库生 命周期中的全部活动(定义关系模式&#xff0c;插入数据&#xff0c;建立数据库&#xff1b;对数据库中的数据进 行查询和更…

2025 年臭氧发生器厂家 TOP 实力工厂推荐榜单排名,大中型 / 水处理 / 多功能臭氧发生器推荐这十家公司!

引言在当前环保意识日益提升的背景下,臭氧发生器作为水处理、废气处理等领域的关键设备,其市场需求持续增长。然而,行业发展也面临着诸多问题。一方面,部分生产厂家技术水平参差不齐,产品质量难以保证,存在臭氧产…

上海高端网站开发公安邦消防安全技术服务有限公司

Hive是一个数据仓库基础的应用工具&#xff0c;在Hadoop中用来处理结构化数据&#xff0c;它架构在Hadoop之上&#xff0c;通过SQL来对数据进行操作&#xff0c;了解SQL的人&#xff0c;学起来毫不费力。Hive 查询操作过程严格遵守Hadoop MapReduce 的作业执行模型&#xff0c;…

2025 年望远镜厂家 TOP 企业品牌推荐排行榜,助你精准选购性价比高的望远镜推荐这十家公司!

引言在当前的望远镜市场中,消费者面临着诸多选择难题。随着户外活动、天文观测等需求的不断增长,望远镜的市场需求日益扩大,但市场上的产品质量却参差不齐。部分品牌为了追求短期利益,在生产过程中偷工减料,导致产…

地旺建设官方网站wordpress后台乱码

DNS&#xff1a;因特网的目录服务 在因特网上&#xff0c;主机和人类都一样&#xff0c;可以用很多种方式进行标识&#xff0c;主机的一种标识方法是它的主机名。 但是主机名一般是用IP来表示&#xff0c;IP是由四个字节组成&#xff0c;并且有严格的层次结构&#xff0c;不利…

Coze源码分析-资源库-删除数据库-后端源码-安全与错误处理 - 详解

Coze源码分析-资源库-删除数据库-后端源码-安全与错误处理 - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "…

wordpress新建网站注册监理工程师注册查询系统

给定一个整数数组 nums &#xff0c;找到一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 示例: 输入: [-2,1,-3,4,-1,2,1,-5,4] 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大&#xff0c;为 6。 进阶: 如果你已经实现…

动手动脑实验性问题总结

动手动脑1 使用算法生成随机整数 使用 System.nanoTime() 作为初始种子,确保每次运行生成不同的随机序列。 必须校验输入参数(如 count > 0),避免无效调用。实现简单、计算高效,适合对随机性要求不高的场景 动…

揭阳网站制作托管苏州网站建设caiyiduo

一、Bootstrap弹出框使用过JQuery UI应该知道&#xff0c;它里面有一个dialog的弹出框组件&#xff0c;功能也很丰富。与jQuery UI的dialog类似&#xff0c;Bootstrap里面也内置了弹出框组件。打开bootstrap 文档可以看到它的dialog是直接嵌入到bootstrap.js和bootstrap.css里面…

火锅网站建设家政服务网站建设方案

单例模式&#xff0c;是设计模式的一种。 在计算机这个圈子中&#xff0c;大佬们针对一些典型的场景&#xff0c;给出了一些典型的解决方案。 目录 单例模式 饿汉模式 懒汉模式 线程安全 单例模式 单例模式又可以理解为是单个实例&#xff08;对象&#xff09; 在有些场…

链表实现双端队列

链表实现双端队列定义链表class ListNode: def __init__(self, value): self.val = value # 节点存储的值 self.prev = None # 指向前一个节点的指针 self.next = None # 指向后一个节点的指针定义双端队列clas…

专门做朋友圈小视频的网站网站怎么做404

PHP是一种服务器端、跨平台、html嵌入式的脚本语言执行速度快&#xff1a;PHP是一种强大的CGI脚本语言&#xff0c;语法混合了C、Java、Perl和PHP式的新语法&#xff0c;执行网页比CGI、Perl和ASP更快&#xff0c;这是它的第一个突出的特点。 (推荐学习&#xff1a;PHP视频教程…

怎么做58同城网站西安英文旅游网站建设

本文实例讲述了Java模拟计算机的整数乘积计算功能。分享给大家供大家参考&#xff0c;具体如下&#xff1a;计算机计算整数乘积的原理&#xff1a;实现代码&#xff1a;package math;public class two {/*** Fundamental method* f(n) O(n^2)* param a* param b* return*/publ…

网站建设开发制作免费游戏网站制作

哈喽小伙伴们大家好,本系列是一个专门针对前端开发岗的面试题系列,每周将会不定期分享一些面试题,希望对大家有所帮助. 面试官:token 一般在客户端存在哪儿 求职者:Token一般在客户端存在以下几个地方&#xff1a; (1)Cookie&#xff1a;Token可以存储在客户端的Cookie中。服…