Linux笔记---动静态库(原理篇)
1. ELF文件格式
动静态库文件的构成是什么样的呢?或者说二者的内容是什么?
实际上,可执行文件,目标文件,静态库文件,动态库文件都是使用ELF文件格式进行组织的。

ELF(Executable and Linkable Format)文件格式是Unix系统及其衍生系统中广泛使用的可执行文件、共享库和核心转储的二进制文件格式。
主要包括以下四种文件*
- 可重定位文件(Relocatable File):包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据,如xxx.o文件。
- 可执行文件(Executable File):包含适合于执行的一个程序,规定了exec()如何创建一个程序的进程映像,如a.out文件。
- 共享目标文件(Shared Object File):包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件;其次,动态链接器可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像,如xxx.so文件。
- 内核转储(core dumps):存放当前进程的执行上下文,用于dump信号触发。
结构组成
ELF头(ELF header):位于文件的开始位置,主要目的是定位文件的其他部分。
程序头表(Program header table):列举了所有有效的段(segments)和它们的属性。
节(Section):文件的最小逻辑组织单元,用于存储程序在编译、链接或执行过程中需要的特定类型数据。每个Section都有明确的用途,例如存储代码、全局变量、符号表或调试信息等。
节头表(Section header table):包含对节(sections)的描述。
总结来说,除了节以外的三个部分实际上都是帮助定位的辅助信息,程序的核心信息在节部分。
我们可以通过 readelf 工具来查看一个可执行程序的各个部分,下面我们以ls为例展示各个部分。
1.1 ELF header
位于文件的开始位置,主要目的是定位文件的其他部分。包含文件类型、目标结构、ELF文件格式的版本、程序入口地址、程序头表的文件偏移、节头表的文件偏移等重要信息。
readelf -h /usr/bin/ls
shishen@hcss-ecs-b8e6:~$ readelf -h /usr/bin/ls
ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: DYN (Position-Independent Executable file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0x6aa0Start of program headers: 64 (bytes into file)Start of section headers: 136232 (bytes into file)Flags: 0x0Size of this header: 64 (bytes)Size of program headers: 56 (bytes)Number of program headers: 13Size of section headers: 64 (bytes)Number of section headers: 31Section header string table index: 30
Magic: 标识ELF文件的魔数字节序列。
Class: 文件位数(32位或64位)。
Data: 数据存储的字节序(小端或大端)。
Version: ELF格式版本号。
OS/ABI: 目标操作系统和应用二进制接口(ABI)。
ABI Version: ABI的版本号。
Type: 文件类型(可执行、共享库等)。
Machine: 目标CPU架构。
Version: ELF版本(通常为1)。
Entry point address: 程序执行的起始地址。
Start of program headers: 程序头表在文件中的偏移量。
Start of section headers: 节头表在文件中的偏移量。
Flags: 处理器特定的标志位。
Size of this header: ELF头的大小(字节)。
Size of program headers: 单个程序头条目的大小。
Number of program headers: 程序头条目数量。
Size of section headers: 单个节头条目的大小。
Number of section headers: 节头条目数量。
Section header string table index:节名称字符串表的索引号。
1.2 Section header table
ELF文件的链接视图(Linking view),包含对节(sections)的描述。即一个ELF文件中到底有哪些具体的sections,以及这些sections的属性信息。
简单来说,这部分包含了一个个的section header,它们与section一一对应,指示各个section的属性信息,这与文件系统中的inode table的设计思路相同。
readelf -S /usr/bin/ls
shishen@hcss-ecs-b8e6:~$ readelf -S /usr/bin/ls
There are 31 section headers, starting at offset 0x21428:Section Headers:[Nr] Name Type Address OffsetSize EntSize Flags Link Info Align[ 0] NULL 0000000000000000 000000000000000000000000 0000000000000000 0 0 0[ 1] .interp PROGBITS 0000000000000318 00000318000000000000001c 0000000000000000 A 0 0 1[ 2] .note.gnu.pr[...] NOTE 0000000000000338 000003380000000000000030 0000000000000000 A 0 0 8[ 3] .note.gnu.bu[...] NOTE 0000000000000368 000003680000000000000024 0000000000000000 A 0 0 4[ 4] .note.ABI-tag NOTE 000000000000038c 0000038c0000000000000020 0000000000000000 A 0 0 4[ 5] .gnu.hash GNU_HASH 00000000000003b0 000003b0000000000000004c 0000000000000000 A 6 0 8[ 6] .dynsym DYNSYM 0000000000000400 000004000000000000000b88 0000000000000018 A 7 1 8[ 7] .dynstr STRTAB 0000000000000f88 00000f8800000000000005a6 0000000000000000 A 0 0 1[ 8] .gnu.version VERSYM 000000000000152e 0000152e00000000000000f6 0000000000000002 A 6 0 2[ 9] .gnu.version_r VERNEED 0000000000001628 0000162800000000000000c0 0000000000000000 A 7 2 8[10] .rela.dyn RELA 00000000000016e8 000016e80000000000001410 0000000000000018 A 6 0 8[11] .rela.plt RELA 0000000000002af8 00002af80000000000000960 0000000000000018 AI 6 25 8[12] .init PROGBITS 0000000000004000 00004000000000000000001b 0000000000000000 AX 0 0 4[13] .plt PROGBITS 0000000000004020 000040200000000000000650 0000000000000010 AX 0 0 16[14] .plt.got PROGBITS 0000000000004670 000046700000000000000030 0000000000000010 AX 0 0 16[15] .plt.sec PROGBITS 00000000000046a0 000046a00000000000000640 0000000000000010 AX 0 0 16[16] .text PROGBITS 0000000000004ce0 00004ce000000000000123a2 0000000000000000 AX 0 0 16[17] .fini PROGBITS 0000000000017084 00017084000000000000000d 0000000000000000 AX 0 0 4[18] .rodata PROGBITS 0000000000018000 000180000000000000004dcc 0000000000000000 A 0 0 32[19] .eh_frame_hdr PROGBITS 000000000001cdcc 0001cdcc000000000000056c 0000000000000000 A 0 0 4[20] .eh_frame PROGBITS 000000000001d338 0001d3380000000000002120 0000000000000000 A 0 0 8[21] .init_array INIT_ARRAY 0000000000020fd0 0001ffd00000000000000008 0000000000000008 WA 0 0 8[22] .fini_array FINI_ARRAY 0000000000020fd8 0001ffd80000000000000008 0000000000000008 WA 0 0 8[23] .data.rel.ro PROGBITS 0000000000020fe0 0001ffe00000000000000a78 0000000000000000 WA 0 0 32[24] .dynamic DYNAMIC 0000000000021a58 00020a580000000000000200 0000000000000010 WA 7 0 8[25] .got PROGBITS 0000000000021c58 00020c5800000000000003a0 0000000000000008 WA 0 0 8[26] .data PROGBITS 0000000000022000 000210000000000000000278 0000000000000000 WA 0 0 32[27] .bss NOBITS 0000000000022280 0002127800000000000012c0 0000000000000000 WA 0 0 32[28] .gnu_debugaltlink PROGBITS 0000000000000000 000212780000000000000049 0000000000000000 0 0 1[29] .gnu_debuglink PROGBITS 0000000000000000 000212c40000000000000034 0000000000000000 0 0 4[30] .shstrtab STRTAB 0000000000000000 000212f8000000000000012f 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)// 注:
// [Nr] 节的索引号(从 1 开始),0 表示无效节。
// Name 节的名称(如 .text、.data、.rodata)。
// Type 节的类型(如 PROGBITS 表示程序数据,SYMTAB 表示符号表)。
// Address 节加载到内存时的虚拟地址(未加载时为 0)。
// Offset 节在文件中的起始偏移量(字节)。
// Size 节的大小(字节)。
// EntSize 如果节是表格(如符号表),表示每个条目的大小;否则为 0。
// Flags 节的属性标志(如 A 可分配,X 可执行,W 可写)。
// Link 链接到其他节的索引(如符号表会链接到字符串表)。
// Info 节的附加信息(如符号表的局部符号起始索引)。
// Align 节的对齐要求(如 16 表示按 16 字节对齐)。
链接视图的含义是:目标文件以及动静态库在进行链接形成可执行程序时,是以section为单位进行的,即各个section各自进行合并。

1.3 Program header table
ELF文件的执行视图(execution view),列举了所有有效的段(segments)和它们的属性。
同样地,这部分包含了一个个的program header(或者说segment header),它们与segment一一对应,指示各个segment的属性信息。
可执行程序在被加载到内存当中时,多个节会合并成一个段(合并原则:相同属性,比如可读,可写,可执行,需要加载时申请空间等)。所以,Program header table其实就是对中间部分的一个重新划分。

readelf -l /usr/bin/ls
shishen@hcss-ecs-b8e6:~$ readelf -l /usr/bin/lsElf file type is DYN (Position-Independent Executable file)
Entry point 0x6aa0
There are 13 program headers, starting at offset 64Program Headers:Type Offset VirtAddr PhysAddrFileSiz MemSiz Flags AlignPHDR 0x0000000000000040 0x0000000000000040 0x00000000000000400x00000000000002d8 0x00000000000002d8 R 0x8INTERP 0x0000000000000318 0x0000000000000318 0x00000000000003180x000000000000001c 0x000000000000001c R 0x1[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]LOAD 0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000003458 0x0000000000003458 R 0x1000LOAD 0x0000000000004000 0x0000000000004000 0x00000000000040000x0000000000013091 0x0000000000013091 R E 0x1000LOAD 0x0000000000018000 0x0000000000018000 0x00000000000180000x0000000000007458 0x0000000000007458 R 0x1000LOAD 0x000000000001ffd0 0x0000000000020fd0 0x0000000000020fd00x00000000000012a8 0x0000000000002570 RW 0x1000DYNAMIC 0x0000000000020a58 0x0000000000021a58 0x0000000000021a580x0000000000000200 0x0000000000000200 RW 0x8NOTE 0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000030 0x0000000000000030 R 0x8NOTE 0x0000000000000368 0x0000000000000368 0x00000000000003680x0000000000000044 0x0000000000000044 R 0x4GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000030 0x0000000000000030 R 0x8GNU_EH_FRAME 0x000000000001cdcc 0x000000000001cdcc 0x000000000001cdcc0x000000000000056c 0x000000000000056c R 0x4GNU_STACK 0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 RW 0x10GNU_RELRO 0x000000000001ffd0 0x0000000000020fd0 0x0000000000020fd00x0000000000001030 0x0000000000001030 R 0x1Section to Segment mapping:Segment Sections...00 01 .interp 02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 03 .init .plt .plt.got .plt.sec .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss 06 .dynamic 07 .note.gnu.property 08 .note.gnu.build-id .note.ABI-tag 09 .note.gnu.property 10 .eh_frame_hdr 11 12 .init_array .fini_array .data.rel.ro .dynamic .got // 注:
// Type 段的类型(如 LOAD 表示可加载段,DYNAMIC 表示动态链接信息)。
// Offset 段在文件中的起始偏移量(字节)。
// VirtAddr 段加载到内存时的虚拟地址(程序运行时访问的地址)。
// PhysAddr 段加载到内存时的物理地址(通常与 VirtAddr 相同,现代系统忽略)。
// FileSiz 段在文件中的大小(字节)。
// MemSiz 段在内存中的大小(字节,可能大于 FileSiz,如 .bss 节会填充零)。
// Flags 段的权限标志(R=读,W=写,X=执行)。
// Align 段在内存和文件中的对齐要求(如 0x1000 表示按 4KB 对齐)。
Section to Segment mapping部分就显示了各个segment包含了哪些section。
执行视图的含义是:该部分负责告诉操作系统,如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定有 program header table。
节(Section) 是链接时的逻辑划分(如 .text、.data),数量多且大小不一。
段(Segment) 是执行时的物理加载单元(如代码段、数据段),合并相同权限的节后,操作系统只需按段映射内存,减少内存碎片和系统调用次数。
加载到内存时将节合并为段的原因
- 符合操作系统的内存页管理:操作系统以 页(Page) 为单位管理内存(如4KB)。段会按页对齐,避免跨页的节导致内存浪费或权限冲突。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。
- 统一内存访问权限:每个段有明确的权限(读/写/执行),而节可能分散且权限不同。
2. 静态链接原理
2.1 地址重定位
我们知道,静态库实际上就是一系列目标文件的集合。所以,要理解静态链接,我们只需要知道目标文件在进行链接时发生了什么即可。
将各个节的数据分别合并到一起是必然的,但是除此之外呢?
以如下代码为例:
// hello.c
#include<stdio.h>void run();int main() {printf("hello world!\n");run();return 0;
} // code.c
#include<stdio.h>void run() {printf("running...\n");
}
我们将这两个原文件进行编译得到目标文件:
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ gcc -c *.c
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ ls
code.c code.o hello.c hello.o
这里,我们需要用到一个指令objdump -d:将代码段(.text)进行反汇编查看。
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ objdump -d hello.o > hello.s
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ objdump -d code.o > code.s
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ ls
code.c code.o code.s hello.c hello.o hello.s

这里的call很明显就是函数调用,call指令的编码为e8。
对比源文件可以看出,无论是printf函数还是run函数,e8跳转到的地址都为0。这是因为,在完成链接之前,编译器并不知道这些外部函数的实现与定义,无法为其分配地址(逻辑地址),就以0代替其地址。
所以,在链接时,编译器还需要将代码段中这些为0的地址修改为实际为这些函数分配的地址。
要完成这项工作,编译器还需要符号表的帮助。每个目标文件都有自己的符号表,在进行链接时,大家相互对照符号表,就能找到外部函数或变量是在哪一个文件当中声明的了,进而就能为其分配地址。
readelf -s # 读取目标文件的符号表
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ readelf -s hello.oSymbol table '.symtab' contains 7 entries:Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text3: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .rodata4: 0000000000000000 40 FUNC GLOBAL DEFAULT 1 main5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts6: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND run
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ readelf -s code.oSymbol table '.symtab' contains 6 entries:Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS code.c2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text3: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .rodata4: 0000000000000000 26 FUNC GLOBAL DEFAULT 1 run5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts// 注:printf底层就是puts
其中,每一行就代表一个符号,我们主要关注Ndx列和Name列。
- ABS:该符号的值是绝对地址,不依赖于任何节(Section),通常是文件名或特殊定义的全局符号。
- UND:该符号在当前目标文件中未定义,需要在链接时从其他目标文件或库中解析(如外部函数或全局变量)。
数字:表示符号定义在对应索引号的节中。例如:Ndx=1:符号属于 .text 节(代码段)。Ndx=5:符号属于 .rodata 节(只读数据段)。
可以看到,在hello.o中,run和puts都是未定义,而code.o中,run有定义但puts依然未定义。
当我们将二者进行链接之后:
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ gcc -o main hello.o code.o
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ objdump -d main > main.s

puts我们暂时也不关心,因为puts使用的是动态链接。可以看到,run函数的地址为1171,该地址也标注在run函数的名称之前。
这就说明编译器在链接时,找到了run函数,并为其分配了地址。
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ readelf -s mainSymbol table '.dynsym' contains 7 entries:Num: Value Size Type Bind Vis Ndx Name0: 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 Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS Scrt1.o2: 000000000000038c 32 OBJECT LOCAL DEFAULT 4 __abi_tag3: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c4: 0000000000001090 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones5: 00000000000010c0 0 FUNC LOCAL DEFAULT 16 register_tm_clones6: 0000000000001100 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux7: 0000000000004010 1 OBJECT LOCAL DEFAULT 26 completed.08: 0000000000003dc0 0 OBJECT LOCAL DEFAULT 22 __do_global_dtor[...]9: 0000000000001140 0 FUNC LOCAL DEFAULT 16 frame_dummy10: 0000000000003db8 0 OBJECT LOCAL DEFAULT 21 __frame_dummy_in[...]11: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c12: 0000000000000000 0 FILE LOCAL DEFAULT ABS code.c13: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c14: 0000000000002120 0 OBJECT LOCAL DEFAULT 20 __FRAME_END__15: 0000000000000000 0 FILE LOCAL DEFAULT ABS 16: 0000000000003dc8 0 OBJECT LOCAL DEFAULT 23 _DYNAMIC17: 000000000000201c 0 NOTYPE LOCAL DEFAULT 19 __GNU_EH_FRAME_HDR18: 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_start22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.523: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 25 _edata24: 0000000000001171 26 FUNC GLOBAL DEFAULT 16 run25: 000000000000118c 0 FUNC GLOBAL HIDDEN 17 _fini26: 0000000000004000 0 NOTYPE GLOBAL DEFAULT 25 __data_start27: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__28: 0000000000004008 0 OBJECT GLOBAL HIDDEN 25 __dso_handle29: 0000000000002000 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used30: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 26 _end31: 0000000000001060 38 FUNC GLOBAL DEFAULT 16 _start32: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 26 __bss_start33: 0000000000001149 40 FUNC GLOBAL DEFAULT 16 main34: 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
从上面的结果中可以看到run函数对应的section的编号为16,这意味着run函数在编号为16的节中。
readelf -S main
![]()
从main.s中可以得到验证(只关注开头的描述以及main函数和run函数即可):
Disassembly of section .text:0000000000001060 <_start>:1060: f3 0f 1e fa endbr64 1064: 31 ed xor %ebp,%ebp1066: 49 89 d1 mov %rdx,%r91069: 5e pop %rsi106a: 48 89 e2 mov %rsp,%rdx106d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp1071: 50 push %rax1072: 54 push %rsp1073: 45 31 c0 xor %r8d,%r8d1076: 31 c9 xor %ecx,%ecx1078: 48 8d 3d ca 00 00 00 lea 0xca(%rip),%rdi # 1149 <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,%rax10a1: 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,%rax10ad: 74 09 je 10b8 <deregister_tm_clones+0x28>10af: ff e0 jmp *%rax10b1: 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,%rsi10d1: 48 89 f0 mov %rsi,%rax10d4: 48 c1 ee 3f shr $0x3f,%rsi10d8: 48 c1 f8 03 sar $0x3,%rax10dc: 48 01 c6 add %rax,%rsi10df: 48 d1 fe sar %rsi10e2: 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,%rax10ee: 74 08 je 10f8 <register_tm_clones+0x38>10f0: ff e0 jmp *%rax10f2: 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 %rbp110e: 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,%rbp1119: 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 %rbp1134: 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 <main>:1149: f3 0f 1e fa endbr64 114d: 55 push %rbp114e: 48 89 e5 mov %rsp,%rbp1151: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 2004 <_IO_stdin_used+0x4>1158: 48 89 c7 mov %rax,%rdi115b: e8 f0 fe ff ff call 1050 <puts@plt>1160: b8 00 00 00 00 mov $0x0,%eax1165: e8 07 00 00 00 call 1171 <run>116a: b8 00 00 00 00 mov $0x0,%eax116f: 5d pop %rbp1170: c3 ret 0000000000001171 <run>:1171: f3 0f 1e fa endbr64 1175: 55 push %rbp1176: 48 89 e5 mov %rsp,%rbp1179: 48 8d 05 91 0e 00 00 lea 0xe91(%rip),%rax # 2011 <_IO_stdin_used+0x11>1180: 48 89 c7 mov %rax,%rdi1183: e8 c8 fe ff ff call 1050 <puts@plt>1188: 90 nop1189: 5d pop %rbp118a: c3 ret Disassembly of section .fini:
目标文件又叫可重定位文件,这里的重定位就是指的为这些外部函数或变量重新分配地址的过程。
总结来说,静态链接的原理就是将各个目标文件的对应节分别合并,并对照符号表完成对外部函数或变量的重定位。
2.1 虚拟地址空间补充
我们前面在main.s中看到,run函数的地址是1171,但实际上这个说法并不准确。准确的说法是:1171是run函数在代码段的地址,也即run函数在代码段的偏移量(各个段内部从0开始编址)。
run函数最终被加载到内存当中的虚拟地址应该是代码段的地址+偏移量。
其中代码段的地址是在每次程序被加载到内存当中时随机分配的。
我们修改一下hello.c的代码:
#include<stdio.h>void run();int main() {printf("hello world!\n");run();printf("%p\n", &run);return 0;
}
重新编译链接之后,run函数的地址变为:
00000000000011af <run>:
运行./main可以看到结果:
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ ./main
hello world!
running...
0x55b1458801af
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ ./main
hello world!
running...
0x55ca012441af
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ ./main
hello world!
running...
0x56372f8ed1af
很明显,run函数的虚拟地址是极富规律且与其偏移量强相关的。
ELF文件是地址空间初始化的基础,但完整的内存布局是内核、ELF、动态链接器共同作用的结果。

3. 动态链接的原理
进程是如何跳转到动态库并共享动态库的代码的呢?
概括来说很简单,将动态库函数的逻辑地址映射到物理地址空间中动态库代码所在位置即可。
即库的起始虚拟地址 + 方法偏移量 ---> 库的起始物理地址 + 方法偏移量。

但实际上其中的细节与机制并不简单。
3.1 链接的时机
将动态库函数的逻辑地址映射到物理地址空间中共享库代码所在位置,这一过程显然是在程序被加载到内存时完成的。也就是说动态链接的时机就是程序被加载到内存时。
对于这一点,我们还可以更加详细一点。
上文当中我们说到,目标文件完成链接之后,程序当中多了许多库当中的函数。
除了main和run以外的函数。来自于库 /lib64/ld-linux-x86-64.so.2 ,用于程序初始化:
在C/C++程序中,当程序开始执行时,它首先并不会直接跳转到 main 函数。实际上,程序的入口点是 _start ,这是一个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。
从main.s中可以看到,_start的地址位1060,而ELF header中指明的程序入口地址就是1060:
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ readelf -h main
ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: DYN (Position-Independent Executable file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0x1060 # 程序入口地址Start of program headers: 64 (bytes into file)Start of section headers: 14032 (bytes into file)Flags: 0x0Size of this header: 64 (bytes)Size of program headers: 56 (bytes)Number of program headers: 13Size of section headers: 64 (bytes)Number of section headers: 31Section header string table index: 30
在 _start 函数中,会执行一系列初始化操作,这些操作包括:
- 设置堆栈:为程序创建一个初始的堆栈环境。
- 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。
- 动态链接:这是关键的一步, _start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。
- 调用 __libc_start_main :一旦动态链接完成, _start 函数会调用__libc_start_main (这是glibc提供的⼀个函数)。 __libc_start_main 函数负责执行一些额外的初始化工作,比如设置信号处理函数、初始化线程库(如果使用了线程)等。
- 调用 main 函数:最后, __libc_start_main 函数会调用程序的 main 函数,此时程序的执行控制权才正式交给用户编写的代码。
- 处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回值,并最终调用 _exit 函数来终止程序。
动态链接器:
- 动态链接器(如ld-linux.so)负责在程序运行时加载动态库。
- 当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。环境变量和配置文件:
- Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf及其子配置文件)来指定动态库的搜索路径。
- 这些路径会被动态链接器在加载动态库时搜索。
- 缓存文件:为了提高动态库的加载效率,Linux系统会维护一个名为/etc/ld.so.cache的缓存文件。该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先搜索这个缓存文件。
上述过程描述了C/C++程序在 main 函数之前执行的一系列操作,但这些操作对于大多数程序员来说是透明的。程序员通常只需要关注 main 函数中的代码,而不需要关心底层的初始化过程。然而,了解这些底层细节有助于更好地理解程序的执行流程和调试问题。
3.2 全局偏移量表GOT
- 动态库的代码可能是会变化的,所以在编译链接时可执行程序当中是无法完成动态库的重定位的。
- 于是,我们只能在程序被加载到内存之后,找到被加载到内存中的动态库,再进行重定位。即,加载地址重定位。
- 但代码段的权限为只读,我们无法对代码段进行修改。为了实现这一点,动态链接需要一种机制来在运行时查找和绑定符号的地址,这就是GOT的作用。
全局偏移量表(GOT,Global Offset Table)是Linux系统下ELF格式可执行文件中用于定位全局变量和函数的表,主要用于动态链接。
其实际上就是在专门预留的一片用来存放函数的跳转地址的区域:.got节。
[24] .got PROGBITS 0000000000003fb0 00002fb00000000000000050 0000000000000008 WA 0 0 8
由于.got所在的段是可读可写的,所以就可以实现在运行当中动态地完成重定位。

3.3 过程链接表PLT
我们在main.s中可以看到,动态链接库函数的函数名之后都会跟着PLT:
PLT(Procedure Linkage Table,过程链接表)是程序动态链接中的关键机制,主要用于延迟绑定(Lazy Binding)动态库中的函数地址。
延迟绑定:程序启动时不会立即解析所有动态库函数的地址,而是在首次调用时才通过PLT解析并缓存地址,减少启动时间。
3.3.1 GOT与PLT的作用
存储全局变量和函数地址:GOT存储了程序中使用的外部函数和全局变量的实际地址,使得程序在运行时能够正确地访问这些外部符号。
支持动态链接:在动态链接过程中,GOT允许程序在运行时解析和绑定外部符号,而不需要在编译时就确定所有符号的地址。
实现延迟绑定:通过GOT和PLT(过程链接表)的配合,实现了函数的延迟绑定,即函数在第一次被调用时才进行地址绑定,提高了程序的启动速度。
3.3.2 工作原理
第一次调用:当程序第一次调用某个外部函数时,会通过PLT跳转到GOT,由于GOT中此时没有该函数的地址,会再次跳转回PLT,PLT会将函数的ID压入栈中,然后调用_dl_runtime_resolve函数进行符号查找和重定位,找到函数地址后,将其填充到GOT中,之后再跳转到该函数地址执行。
后续调用:当再次调用该函数时,PLT会直接跳转到GOT中存储的函数地址,无需再次进行符号查找和重定位。
3.4 地址无关代码PIC
动态库被加载到内存当中之后,其内部函数的虚拟地址就都是确定了的。当我们使用这些共享代码时,在我们进程的虚拟地址空间当中也应当为其分配对应的虚拟地址,否则代码与其地址就对应不起来了(汇编代码中,每条代码都有自己的地址)。
这就会导致一个问题:两个动态库要求的地址发生冲突。
为了解决这个问题,我们希望动态库中的代码被加载到任意位置都能运行,这就是地址无关代码。
PIC(Position Independent Code)地址无关代码是一种编程技术,它使得代码不依赖于特定的内存地址。
所以我们在编译动态库对应的目标文件时,需要加上-fPIC选项:
gcc -fPIC -c
3.4.1 原理
相对寻址:PIC代码通过使用相对寻址方式来访问数据和代码,而不是使用绝对地址。这意味着代码可以在内存中的任何位置加载和执行,而不需要进行重定位。
全局偏移表(GOT):在PIC中,全局变量和函数的地址是通过全局偏移表(GOT)来访问的。GOT是一个数据结构,用于存储全局变量和函数的实际地址。当代码需要访问这些全局符号时,它会通过GOT中的相应项来间接引用。
相关文章:
Linux笔记---动静态库(原理篇)
1. ELF文件格式 动静态库文件的构成是什么样的呢?或者说二者的内容是什么? 实际上,可执行文件,目标文件,静态库文件,动态库文件都是使用ELF文件格式进行组织的。 ELF(Executable and Linkable…...
string的模拟实现 (6)
目录 1.string.h 2.string.cpp 3.test.cpp 4.一些注意点 本篇博客就学习下如何模拟实现简易版的string类,学好string类后面学习其他容器也会更轻松些。 代码实现如下: 1.string.h #define _CRT_SECURE_NO_WARNINGS 1 #pragma once #include <…...
【野火模型】利用深度神经网络替代 ELMv1 野火参数化:机制、实现与性能评估
目录 一、ELMv1 野火过程表示法(BASE-Fire)关键机制野火模拟的核心过程 二、采用神经网络模拟野火过程三、总结参考 一、ELMv1 野火过程表示法(BASE-Fire) ELMv1 中的野火模型(称为 BASE-Fire)源自 Commun…...
红宝书第四十七讲:Node.js服务器框架解析:Express vs Koa 完全指南
红宝书第四十七讲:Node.js服务器框架解析:Express vs Koa 完全指南 资料取自《JavaScript高级程序设计(第5版)》。 查看总目录:红宝书学习大纲 一、框架定位:HTTP服务器的工具箱 共同功能: 快…...
嵌入式Linux设备使用Go语言快速构建Web服务,实现设备参数配置管理方案探究
本文探讨,利用Go语言及gin框架在嵌入式Linux设备上高效搭建Web服务器,以实现设备参数的网页配置。通过gin框架,我们可以在几分钟内创建一个功能完善的管理界面,方便对诸如集中器,集线器等没有界面的嵌入式设备的管理。…...
【NLP 59、大模型应用 —— 字节对编码 bpe 算法】
目录 一、词表的构造问题 二、bpe(byte pair encoding) 压缩算法 算法步骤 示例: 步骤 1:初始化符号表和频率统计 步骤 2:统计相邻符号对的频率 步骤 3:合并最高频的符号对 步骤 4:重复合并直至终止条件 三、bpe在NLP中…...
Python对ppt进行文本替换、插入图片、生成表格
目录 1. 安装pptx2. 文本替换和插入图片3. 生成表格 1. 安装pptx pip install python-pptx2. 文本替换和插入图片 文本通过占位符例如{{$xxx}}进行标记,然后进行替换;图片通过ppt中的图形和图片中的占位符进行标记ppt如下 具体实现 from pptx import …...
AI(学习笔记第一课) 在vscode中配置continue
文章目录 AI(学习笔记第一课) 在vscode中配置continue学习内容:1. 使用背景2. 在vscode中配置continue2.1 vscode版本2.2 在vscode中下载continue插件2.2.1 直接进行安装2.2.2 在左下角就会有continue的按钮2.2.3 可以移动到右上角2.2.3 使用的时候需要login 2.3 配…...
C++ (初始面向对象之继承,实现继承,组合,修饰权限)
初始面向对象之继承 根据面向对象的编程思路,我们可以把共性抽象出来封装成类,然后让不同的角色去继承这些类,从而避免大量重复代码的编写 实现继承 继承机制是面向对象程序设计中使代码可以复用的最重要的手段,它允许程序员在保…...
vmcore分析锁问题实例(x86-64)
问题描述:系统出现panic,dmesg有如下打印: [122061.197311] task:irq/181-ice-enp state:D stack:0 pid:3134 ppid:2 flags:0x00004000 [122061.197315] Call Trace: [122061.197317] <TASK> [122061.197318] __schedule0…...
21、c#中“?”的用途
在C#中,? 是一个多用途的符号,具有多种不同的用途,具体取决于上下文。以下是一些常见的用法: 1、可空类型(Nullable Types) ? 可以用于将值类型(如 int、bool 等)变为可空类型。…...
每日搜索--12月
12.1 1. urlencode是一种编码方式,用于将字符串以URL编码的形式进行转换。 urlencode也称为百分号编码(Percent-encoding),是特定上下文的统一资源定位符(URL)的编码机制。它适用于统一资源标识符(URI)的编码,也用于为application/x-www-form-urlencoded MIME准备数…...
一天一个java知识点----Tomcat与Servlet
认识BS架构 静态资源:服务器上存储的不会改变的数据,通常不会根据用户的请求而变化。比如:HTML、CSS、JS、图片、视频等(负责页面展示) 动态资源:服务器端根据用户请求和其他数据动态生成的,内容可能会在每次请求时都…...
游戏报错?MFC140.dll怎么安装才能解决问题?提供多种MFC140.dll丢失修复方案
MFC140.dll 是 Microsoft Visual C 2015 运行库的重要组成部分,许多软件和游戏依赖它才能正常运行。如果你的电脑提示 "MFC140.dll 丢失" 或 "MFC140.dll 未找到",说明系统缺少该文件,导致程序无法启动。本文将详细介绍 …...
TDengine 3.3.6.3 虚拟表简单验证
涛思新出的版本提供虚拟表功能,完美解决了多值窄表查询时需要写程序把窄表变成宽表的处理过程,更加优雅。 超级表定义如下: CREATE STABLE st01 (ts TIMESTAMP,v0 INT,v1 BIGINT,v2 FLOAT,v3 BOOL) TAGS (device VARCHAR(32),vtype VARCHAR(…...
小白如何从0学习php
学习 PHP 可以从零开始逐步深入,以下是针对小白的系统学习路径和建议: 1. 了解 PHP 是什么 定义:PHP 是一种开源的服务器端脚本语言,主要用于 Web 开发(如动态网页、API、后台系统)。 用途:构建…...
常见的 14 个 HTTP 状态码详解
文章目录 一、2xx 成功1、200 OK2、204 No Content3、206 Partial Content 二、3xx 重定向1、301 Moved Permanently2、302 Found3、303 See Other注意4、Not Modified5、307 Temporary Redirect 三、4xx 客户端错误1、400 Bad Request2、401 Unauthorized3、403 Forbidden4、4…...
【Java学习笔记】DOS基本指令
DOS 基本指令 基本原理 接受指令 解析指令 执行指令 常用命令 查看当前目录有什么:dir 使用绝对路径查看特定目录下文件:dir 绝对路径 切换到其他盘:直接输入C: 或 D:直接切换到根目录 返回上一级目录:cd.. 切换到根目录…...
Linux Kernel 8
可编程中断控制器(Programmable Interrupt Controller,PIC) 支持中断(interrupt)的设备通常会有一个专门用于发出中断请求Interrupt ReQuest,IRQ的输出引脚(IRQ pin)。这些IRQ引脚连…...
原子操作CAS(Compare-And-Swap)和锁
目录 原子操作 优缺点 锁 互斥锁(Mutex) 自旋锁(Spin Lock) 原子性 单核单CPU 多核多CPU 存储体系结构 缓存一致性 写传播(Write Propagation) 事务串行化(Transaction Serialization&#…...
MySQL安装实战分享
一、在 Windows 上安装 MySQL 1. 下载 MySQL 安装包 访问 MySQL 官方下载页面。选择适合你操作系统的版本。一般推荐下载 MySQL Installer。 2. 运行安装程序 双击下载的安装文件(例如 mysql-installer-community-<version>.msi)。如果出现安全…...
C++ 编程指南35 - 为保持ABI稳定,应避免模板接口
一:概述 模板在 C 中是编译期展开的,不同模板参数会生成不同的代码,这使得模板类/函数天然不具备 ABI 稳定性。为了保持ABI稳定,接口不要直接用模板,先用普通类打个底,模板只是“外壳”,这样 AB…...
【WPF】 在WebView2使用echart显示数据
文章目录 前言一、NuGet安装WebView2二、代码部分1.xaml中引入webview22.编写html3.在WebView2中加载html4.调用js方法为Echarts赋值 总结 前言 为了实现数据的三维效果,所以需要使用Echarts,但如何在WPF中使用Echarts呢? 一、NuGet安装WebV…...
OpenCV 图像拼接
一、图像拼接的介绍 图像拼接是一种将多幅具有部分重叠内容的图像合并成一幅完整、无缝且具有更广阔视野或更高分辨率图像的技术。其目的是通过整合多个局部图像来获取更全面、更具信息价值的图像内容。 二、图像拼接的原理 图像拼接的核心目标是将多幅有重叠区域的图像进行准…...
数学建模AI智能体(4.16大更新)
别的不说就说下面这几点,年初内卷到现在,就现阶段AI水平,卷出了我比较满意的作品,这里分享给各位同学,让你们少走弯路: 1.轻松辅导学生 2.帮助学习 3.突破知识壁垒,缩短与大佬的差距 4.打破…...
音视频小白系统入门笔记-1
本系列笔记为博主学习李超老师课程的课堂笔记,仅供参阅 课程传送门:音视频小白系统入门课 音视频基础ffmpeg原理 往期课程笔记传送门:音视频小白系统入门笔记-0 课程实践代码仓库:传送门 音频采集 命令行采集 Android端音频…...
Flutter 强制横屏
在 Flutter 中,可以通过设置 SystemChrome 来强制应用横屏显示。以下是实现这一功能的详细步骤和代码示例: 步骤 1:导入必要的包 确保在文件顶部导入了 services.dart 包,因为 SystemChrome 类位于该包中。 import package:flut…...
量子安全邮件系统 —— NTRU算法邮件加密核心
目录 量子安全邮件系统 —— NTRU算法邮件加密核心一、项目背景与简介二、NTRU算法理论基础三、系统架构设计3.1 模块划分3.2 系统架构图(Mermaid示意图)四、邮件加密核心流程与关键技术4.1 密钥生成与公钥计算4.2 邮件加密4.3 邮件解密4.4 关键技术要点五、GUI设计与系统扩展…...
Go:方法
方法声明 type point struct { X, Y float64 }// 普通函数 func Distance(p, q Point) float64 {return math.Hypot(q.x - p.x, q.y - p.Y) }// Point类型的方法 func (p Point) Distance(q Point) float64 {return math.Hypot(q.x - p.x, q.y - p.Y) }方法声明与普通函数声…...
深入剖析 Axios 的 POST 请求:何时使用 qs 处理数据
在前端开发中,Axios 是一个广泛使用的用于发送 HTTP 请求的库,特别是在处理 POST 请求时,数据的处理方式会直接影响到请求能否正确被后端接收和处理。其中,使用 qs 库对数据进行处理是一个常见的操作点,本文将深入探讨…...

