库(Library)是一组预编译的可复用代码集合,以二进制形式存在,用于封装常用功能(如数学计算、字符串操作),避免重复开发。程序通过链接库来调用这些功能,无需从零编写代码。库的本质是目标文件(.o文件)的压缩打包,由操作系统加载执行 。
静态库(Static Library)在编译时将库代码完全复制到可执行文件中,生成独立的程序。运行时不再依赖原库文件,但会导致可执行文件体积较大 。
.a结尾(如libmylib.a),Windows下为.lib 。生成步骤分为编译源文件和打包:
编译目标文件:使用gcc -c生成位置无关的.o文件。
gcc -c mylib.c -o mylib.o # 编译mylib.c为目标文件打包为静态库:使用ar命令归档.o文件。
ar rcs libmylib.a mylib.o # rcs选项表示替换/创建/索引,生成libmylib.a ar是归档工具,rcs确保库更新和索引优化 ,r(替换文件)、c(创建库)、s(生成索引)。在编译可执行文件时链接静态库:
命令示例:
gcc main.c -o main -L. -lmylib # -L指定库路径,-l指定库名(省略lib前缀和.a后缀)-L.:指定库搜索路径(. 表示当前目录)。
-lmylib:链接 libmylib.a(-l 后跟库名,省略 lib 和 .a)。
特点:
关键点:
-L) > 环境变量LIBRARY_PATH > 系统默认路径(如/lib, /usr/lib) 。优点:运行时无需额外依赖;缺点:增大程序体积,无法共享更新 。
动态库(Dynamic Library,也称共享库)在程序运行时才加载到内存,多个程序可共享同一库副本,减少内存占用和磁盘空间。但运行时需确保库文件可用 。
.so结尾(如libmylib.so ),Windows下为.dll 。生成需位置无关代码(PIC),确保加载时地址灵活:
编译PIC目标文件:使用-fPIC选项。
gcc -c -fPIC mylib.c -o mylib.o # -fPIC生成位置无关代码 打包为动态库:使用gcc -shared链接.o文件。
gcc -shared -o libmylib.so mylib.o # -shared选项生成动态库 ln -s libmylib.so .1.0 libmylib.so ) 。编译时指定库,但运行时加载:
命令示例:
gcc main.c -o main -L. -lmylib # 编译时链接,与静态库语法相同 关键点:
error while loading shared libraries) 。优点:节省内存、支持热更新;缺点:依赖运行时环境 。
动态库运行时搜索路径按优先级确定,需配置以确保可执行文件正常加载:
默认系统路径:如/lib、/usr/lib,存放标准库 。
/etc/ld.so .conf配置的路径:可添加自定义目录,后运行ldconfig更新缓存 。
环境变量LD_LIBRARY_PATH:临时指定路径,适用于开发测试。
export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH # 添加当前目录编译时-rpath选项:嵌入绝对路径到可执行文件。
gcc main.c -o main -L. -lmylib -Wl,-rpath=/absolute/path # -Wl传递参数给链接器 其他方法:
/usr/lib) 。-rpath > LD_LIBRARY_PATH > /etc/ld.so .conf > 默认路径 。ldd main:检查程序依赖的库及路径 。nm libmylib.so :查看库中的符号 。关键区别总结
特性 | 静态库(.a/.lib) | 动态库(.so/.dll) |
|---|---|---|
链接时机 | 编译时 | 运行时 |
文件体积 | 较大(库代码被复制) | 较小(仅记录引用) |
部署难度 | 简单(单文件) | 需确保库存在于运行环境 |
更新灵活性 | 需重新编译程序 | 替换库文件即可生效 |
内存占用 | 每个程序独立占用内存 | 多个程序共享同一库实例 |
在Windows系统中,IDE将这些编译和链接步骤完美地封装起来,用户只需一键操作即可完成构建,操作非常便捷。然而,一旦出现错误,特别是链接相关的错误时,很多人往往不知所措。此前我们已在Linux环境下学习过如何使用gcc编译器来完成这些操作。

让我们深入探讨编译和链接的完整流程,以更好地理解动静态库的使用原理。
让我们先来回顾一下什么是编译。编译是指将高级编程语言(如C、Java、Python等)编写的源代码,通过特定的编译程序(编译器)转换成计算机CPU能够直接识别和执行的机器代码的过程。
这个翻译过程通常包括以下几个主要步骤:
例如,当我们用C语言编写一个简单的"Hello World"程序并编译时:
#include <stdio.h>
int main() {
printf("Hello World");
return 0;
}编译器会将其转换为x86或ARM等架构的机器指令,这些指令可以直接被CPU执行。编译后的程序执行效率通常比解释型语言更高,因为代码已经针对目标平台进行了优化。
比如:在一个名为hello.c的C语言源文件中,我们编写了一个简单的程序,包含一个main()函数用于输出"hello world!"字符串,同时调用了另一个名为run()的函数。这个run()函数的具体实现被定义在另一个独立的源文件code.c中。
// 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");
}为了编译这个由多个源文件组成的项目,我们可以使用GCC编译器的-c选项来分别编译这两个源文件
// 编译两个源⽂件
$ gcc -c hello.c
$ gcc -c code.c
$ ls
code.c code.o hello.c hello.o首先编译hello.c文件,这会生成hello.o目标文件,其中包含main()函数的机器码,但run()函数调用会被标记为未解析的外部引用。接着编译code.c文件,这会生成code.o目标文件,其中包含run()函数的实现代码。
此时生成的两个目标文件是独立且不完整的:
hello.o 知道要调用 run() 但不知其位置
code.o 包含 run() 实现但不知被谁调用
需要注意的是,如果只修改了某个源文件,只需单独重新编译该文件即可,无需耗时重新编译整个项目。目标文件采用ELF格式的二进制文件,这种格式对二进制代码进行了封装。
说了这么多,也许你会有这样的疑问:
为什么要把.c编译为.o文件,最后再链接呢?为什么不直接将.c文件进行链接呢?
模块化开发
.c 文件,每个文件实现特定的功能模块。将每个 .c 文件单独编译为 .o 文件,可以实现模块的独立编译,每个模块的修改和编译不会相互影响。例如,一个项目包含 math.c、string.c 和 main.c 三个文件,开发人员可以在修改 math.c 后单独重新编译它为 math.o,而无需重新编译其他未修改的文件。
.o 文件便于对项目进行维护和更新。当需要修改某个功能模块时,只需重新编译对应的 .c 文件生成新的 .o 文件,然后再进行链接即可,无需对整个项目进行重新编译。这大大提高了开发效率,尤其是在项目规模较大时。
提高编译效率
.c 文件进行链接,编译器需要在每次链接时对所有相关的 .c 文件进行重新编译,这会浪费大量的时间和计算资源。而将 .c 文件预先编译为 .o 文件后,只要 .c 文件没有发生变化,其对应的 .o 文件就可以直接用于链接,避免了重复编译的过程,显著提高了编译效率。
.c 文件编译为 .o 文件,可以实现增量编译,即只重新编译那些被修改的 .c 文件,其他未修改的 .o 文件可以继续使用,从而加快了整个项目的编译速度。
链接阶段的优势
.o 文件)链接在一起,解析各个文件之间的外部引用,生成最终的可执行文件。如果直接对 .c 文件进行链接,链接器需要先将 .c 文件编译为目标代码,然后再进行链接,这增加了链接器的复杂性和工作量。而将编译和链接分开,链接器可以直接处理 .o 文件,更高效地完成链接任务。
.a 文件或动态库 .so 文件)。这些库文件本身也是由多个 .o 文件组成的。通过将项目中的 .c 文件编译为 .o 文件,可以方便地将这些 .o 文件与预编译库文件一起链接,生成最终的可执行程序。
举个例子,假设有一个项目包含两个 .c 文件:file1.c 和 file2.c,以及一个预编译的静态库 libmylib.a。如果直接对 .c 文件进行链接,需要在链接命令中指定所有的 .c 文件和库文件,并且编译器需要先编译 .c 文件为目标代码,然后再与库文件链接,命令如下:
gcc file1.c file2.c libmylib.a -o myprogram
而将 .c 文件预先编译为 .o 文件后,链接命令可以简化为:
gcc file1.o file2.o libmylib.a -o myprogram
这样,链接器可以直接处理 .o 文件和库文件,提高了链接的效率。
综上所述,将 .c 文件编译为 .o 文件然后再链接,是为了实现模块化开发、提高编译效率以及更好地利用链接阶段的优势。这种编译和链接的分离方式是软件开发过程中的重要实践,有助于提高开发效率和软件的质量。
引入ELF:
我们的.o文件和可执行文件都是ELF格式的
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ file code.o
code.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
## file命令⽤于辨识⽂件类型。关键特征解析:
属性 | 说明 |
|---|---|
relocatable | 可重定位文件(未完成链接) |
not stripped | 包含调试符号信息 |
LSB | 小端字节序 |
x86-64 | AMD64架构 |
目标文件是编译过程的中间产物,包含:
目标文件如何变成可执行文件
通过链接器解决:
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ gcc hello.o code.o -o hello
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ ./hello
hello world!
running...链接过程详解:
hello.o 引用 run(),在 code.o 中找到其定义。undefined reference to 'run')。.text 段(代码)和 .data 段(全局变量)。(后文原理部分会介绍)run() 在 code.o 中的地址写入 hello.o 的调用位置。ELF executable。目标文件与库的关系
库本质上就是目标文件的集合:
静态库 = 多个.o文件打包成.a文件
ar rcs libmylib.a file1.o file2.o动态库 = 特殊处理的目标文件集合(位置无关代码)
gcc -shared -fPIC -o libmylib.so file1.o file2.o要深入理解编译链接的细节,我们需要先了解ELF文件。实际上,以下四种文件都属于ELF文件类型:
ELF文件类型详解
类型 | 文件扩展名 | 特点 | 工具查看命令 |
|---|---|---|---|
可重定位文件 | .o (Linux) | 包含代码/数据,但地址未确定,需链接器处理 | readelf -h hello.o |
可执行文件 | 无扩展名 | 包含可直接执行的程序,有入口地址 | readelf -h a.out |
共享目标文件 | .so (Linux) | 可被动态加载的库文件 | readelf -h libmylib.so |
内核转储文件 | core | 进程崩溃时的内存快照,用于调试 | gdb -c core |
• 可重定位文件(Relocatable File): 即扩展名为.o的中间目标文件,由编译器生成但尚未经过链接处理。这类文件包含机器代码和数据,但其中的符号引用尚未解析,地址也是相对的(可重定位的),需要链接器(ld)将其与其他目标文件或库合并才能生成可执行文件或共享库。例如,在Linux下使用gcc编译但不链接时就会生成此类文件(gcc -c file.c)。
• 可执行文件(Executable File): 这是可以直接在操作系统上运行的完整程序文件。它们已经过完整的编译和链接过程,所有符号引用都已解析,具有固定的入口地址(如main函数)。在Linux中,这类文件通常没有扩展名,但可以通过chmod +x赋予可执行权限。例如,gcc编译链接后生成的a.out就是典型的可执行文件。
• 共享目标文件(Shared Object File): 即动态链接库文件,扩展名为.so(Windows下对应.dll文件)。这类文件包含可被多个程序共享的代码和数据,在程序运行时由动态链接器加载。与静态库不同,它们支持运行时加载,能有效减少内存占用。例如Linux系统的标准C库就是/lib/x86_64-linux-gnu/libc.so.6。
• 内核转储(Core Dumps): 这是当程序异常终止时(如段错误、非法指令等),由操作系统内核生成的进程内存快照文件(通常名为core或core.pid)。它完整记录了进程崩溃时的执行上下文,包括寄存器值、堆栈状态、内存映射等信息,配合调试器(如gdb)可以分析崩溃原因。在Linux中可通过ulimit -c设置core文件大小限制,默认可能被禁用。
ELF 文件的核心结构
ELF 文件由四个关键部分组成,通过 readelf 命令可查看详细信息:
readelf -h hello # 查看 ELF 头
readelf -l hello # 查看程序头表(段信息)
readelf -S hello # 查看节头表(节信息)ELF 头 (ELF Header)
• ELF头(ELF header) :描述文件的主要特性。其位于文件的开始位置,通常占用前52或64个字节(32位/64位系统),包含了ELF文件的魔数(0x7F+ELF)、字长(32/64位)、字节序等基本信息。它的主要目的是定位文件的其他部分,如程序头表和节头表的位置和大小。例如,通过e_phoff字段可以找到程序头表的起始偏移量。
程序头表 (Program Header Table)
LOAD可加载的代码/数据段R-X (代码) 或 RW (数据)DYNAMIC动态链接信息RWINTERP指定动态链接器路径R • 程序头表(Program header table) :列举了所有有效的段(segments)和他们的属性。每个表项通常包含以下关键信息:
节头表 (Section Header Table)
• 节头表(Section header table) :包含对节(sections)的描述,通常位于文件末尾。每个表项包含:
节 (Sections)
.text可执行机器指令(程序代码)是 (R-X).data已初始化的全局/静态变量是 (RW).rodata只读数据(如字符串常量)是 (R).bss未初始化的全局/静态变量(不占磁盘空间)是 (RW).symtab符号表(函数/变量名地址)否.strtab字符串表(符号名称字符串)否 • 节(Section) :ELF⽂件中的基本组成单位,包含了特定类型的数据。这些节在链接时会被组合成段(Segment),在加载时按段为单位映射到内存。
最常见的节:
• 代码节(.text):⽤于保存机器指令,是程序的主要执⾏部分。通常具有只读和可执行属性,包含函数的具体实现代码。
• 数据节(.data):保存已初始化的全局变量和局部静态变量,具有读写属性。例如:
int global_var = 42; // 存储在.data节
static int static_var = 100; // 也存储在.data节• 未初始化数据节(.bss):存储未初始化的全局和静态变量,不占用文件空间,但在加载时会分配内存空间并初始化为0。例如:
int uninit_var; // 存储在.bss节示例:
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ size hello
text data bss dec hex filename
1462 600 8 2070 816 hello字段 | 含义 | 对应节 | 本例值 |
|---|---|---|---|
text | 代码段大小(机器指令占用的字节数) | .text | 1462 字节 |
data | 已初始化数据大小(全局变量/静态变量初始值) | .data | 600 字节 |
bss | 未初始化数据大小(运行时分配零值内存,磁盘不占空间) | .bss | 8 字节 |
dec | 总内存占用量(十进制:text + data + bss) | - | 2070 字节 |
hex | 总内存占用量(十六进制) | - | 0x816 |
filename | 文件名 | - | hello |
关键说明:
bss 的特殊性:虽然输出显示 8 字节,但磁盘文件中 .bss 不占用空间,仅在程序加载到内存时分配空间并初始化为 0。dec 值。
ELF(Executable and Linkable Format)可执行文件形成过程
编译阶段(step-1):
链接阶段(step-2):

📌 重要说明:
链接优化:
特殊处理:
1. Section与Segment的本质区别
特性 | Section(节) | Segment(段) |
|---|---|---|
作用阶段 | 链接阶段(Linking View) | 执行阶段(Execution View) |
描述表 | 节头表(Section Header Table) | 程序头表(Program Header Table) |
目的 | 指导链接器合并代码/数据 | 指导操作系统加载内存 |
数量关系 | 一个Segment包含多个属性相同的Section | 一个Section仅属于一个Segment |
下文示例 | readelf -S 显示31个Section(如.text, .data) | readelf -l 显示13个Program Headers(后文详解) |
核心结论: Section是编译链接的逻辑单元,Segment是内存加载的物理单元。合并行为在链接时确定,通过程序头表描述 。
2. Section合并为Segment的原则
(1) 合并条件
.text需加载,.debug不加载)(2) 典型合并案例
Segment类型 | 包含的典型Section | 权限 | 下文示例中的地址范围 |
|---|---|---|---|
代码段 | .text, .rodata, .interp | R-X | 0x0000318-0x0001556 |
数据段 | .data, .dynamic, .got | RW | 0x0003db0-0x0003dc8 |
只读数据段 | .eh_frame, .gcc_except_table | R | 未显式展示,通常与代码段合并 |
注:示例中
readelf -S输出的.interp(解释器路径)与.text权限均为R-X,合并到同一代码段。
在 ELF 文件加载到内存时,系统会根据 Program Header Table 中的信息将多个 Section 合并为 Segment。这个合并过程主要依据以下特征:
.text, .rodata)会被合并到 TEXT Segment.data, .bss)会被合并到 DATA Segment.dynamic, .got)会被合并到 DYNAMIC Segment显然,合并操作在生成 ELF 文件时就已经确定,具体的合并规则被记录在 ELF 的程序头表(Program header table)中。
# 查看 ELF 头
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ readelf -h hello
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1060
Start of program headers: 64 (bytes into file)
Start of section headers: 14032 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
# 查看可执⾏程序的section
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ readelf -S hello
There are 31 section headers, starting at offset 0x36d0:
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
000000000000012b 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 000000000000118c 0000118c
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000002000 00002000
000000000000001c 0000000000000000 A 0 0 4
[19] .eh_frame_hdr PROGBITS 000000000000201c 0000201c
000000000000003c 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 0000000000002058 00002058
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
00000000000001e6 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 000035b6
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)
# 查看section合并的segment
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ readelf -l hello
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000628 0x0000000000000628 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000199 0x0000000000000199 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000124 0x0000000000000124 R 0x1000
LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000258 0x0000000000000260 RW 0x1000
DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
GNU_EH_FRAME 0x000000000000201c 0x000000000000201c 0x000000000000201c
0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000248 0x0000000000000248 R 0x1
Section 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 .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 .dynamic .got 📌 为什么要将section合并成为segment
• 减少内存碎片:Section合并的主要目的是为了减少页面碎片,提升内存使用效率。现代操作系统通常以4KB(4096字节)为单位进行内存分配和管理。当多个小的section独立分配时,会造成大量内存浪费。例如:
- 假设.text段占用4097字节(超过1个页面)
- .init段仅占用512字节
- 如果不合并,将浪费7679字节(4095 + 4096 - 512)的空间
- 合并后,.text和.init可以共享一个页面,只需2个页面而非3个
• 权限管理优化:合并后的segment可以统一设置内存访问权限,提高安全性: - 操作系统加载程序时,会将具有相同属性(如可读/可写/可执行)的section合并 - 例如:将所有可执行代码段(如.text、.init)合并为代码segment - 将所有数据段(如.data、.bss)合并为数据segment - 每个segment可设置独立的权限标志(如代码段只读可执行)
• 性能提升:合并带来额外好处: - 减少TLB(转译后备缓冲器)条目数量 - 降低内存管理单元(MMU)的地址转换开销 - 提高程序加载速度(减少页面映射操作)
• 实际应用场景: - 在Linux系统中,ELF文件通过程序头表(Program Header)描述segment - 典型的segment包括: 1. LOAD(可加载段) 2. DYNAMIC(动态链接信息) 3. INTERP(解释器路径) - 通过readelf命令可查看ELF文件的segment布局
程序头表和节头表作为ELF文件的两个核心部分,提供了不同维度的视角:
一、双视图设计原理
.text)、数据(.data)、符号表(.symtab)等。
.data中的小变量)独立加载,将浪费大量内存。
📊 示例:100个8字节全局变量独立加载需100×4KB=400KB;合并后仅需1页(4KB)。
.interp指定/lib64/ld-linux-x86-64.so.2)。.o)通常无此表。二、节头表:链接视图的基石
.text机器指令(代码)R-X合并为可执行段,入口地址由ELF头指定.data已初始化全局/静态变量RW合并为可读写数据段.rodata只读数据(字符串常量等)R合并到代码段(因权限匹配R-X).bss未初始化变量(磁盘不占空间)RW统计大小,运行时分配零填充内存.symtab符号表(函数/变量地址映射)-解析跨文件符号引用(如main调用run()).got.plt全局偏移表(动态链接跳转入口)RW运行时由动态链接器修改,绑定共享库函数.interp动态链接器路径R独立为PT_INTERP段,供加载器使用 .text节(如main.o和util.o的代码)合并为单一可执行段,减少内存页分配次数。.rodata)并入代码段(R-X),防止篡改。三、程序头表:执行视图的蓝图
PT_LOAD可加载段(代码/数据).text+.rodata(合并)R-X.data+.bss(合并)RWPT_INTERP动态链接器路径.interpRPT_DYNAMIC动态链接信息(依赖库列表).dynamicRWPT_GNU_STACK控制栈权限(如禁止执行)无直接对应节标记栈属性 e_phoff偏移读取表位置(示例中偏移64字节)。
PT_LOAD段:
0x0000318),权限设为R-X。.bss区分配零填充内存(p_memsz > p_filesz时)。PT_INTERP指定的动态链接器。PT_DYNAMIC中的依赖库(如libc.so)。0x1060)启动。
🔍 示例验证:
size hello输出中bss=8,对应数据段的p_memsz - p_filesz = 8。
四、双视图协作:从节到段的转换
链接器生成程序头表的逻辑
R-X节→代码段)。0x400000),数据段紧随其后并按页对齐。p_type, p_flags, p_offset等)。.bss独立加载浪费99.8%内存页;合并后仅占0.2%。PT_GNU_STACK段标记栈不可执行(RW),防御缓冲区溢出攻击。视图转换示意图
链接视图(节头表) → 链接器合并 → 执行视图(程序头表)
+----------------+ +----------------+
| .text (R-X) | | LOAD Segment1 |
+----------------+ 合并为代码段(R-X) | (R-X) |
| .rodata (R) | | |
+----------------+ +----------------+
| .data (RW) | | LOAD Segment2 |
+----------------+ 合并为数据段(RW) | (RW) |
| .bss (RW) | +----------------+
+----------------+五、实践验证:readelf命令分析
节头表分析(链接视图)
$ readelf -S hello
Section Headers:
[Nr] Name Type Address Offset Size Flags
[ 16] .text PROGBITS 0000000000001060 00001060 0000015f AX # 代码节(R-X)
[ 18] .rodata PROGBITS 0000000000002000 00002000 0000000d A # 只读数据(R)
[ 25] .data PROGBITS 0000000000004000 00003000 00000200 WA # 数据节(RW).rodata地址(0x2000)介于.text和.data之间,但因权限为R,实际被合并到代码段(权限R-X)。程序头表分析(执行视图)
$ readelf -l hello
Program Headers:
Type Offset VirtAddr MemSiz Flags Align
LOAD 0x000000 0x0000000000000000 0x5e8 R E 0x1000 # 代码段(含.rodata)
LOAD 0x002000 0x0000000000002000 0x210 RW 0x1000 # 数据段(含.bss)LOAD段包含.text(0x1060)和.rodata(0x2000),因权限均为只读。我们可以在 ELF头 中找到文件的基本信息,以及可以看到ELF头是如何定位程序头表和节头表的。
ELF头包含关键定位信息:
ELF Header:
Entry point address: 0x1060 # 程序入口地址
Start of program headers: 64 (bytes into file) # 程序头表位置
Start of section headers: 14032 (bytes into file) # 节头表位置
Size of program headers: 56 (bytes) # 每个程序头大小
Number of program headers: 13 # 程序头数量
Size of section headers: 64 (bytes) # 每个节头大小
Number of section headers: 31 # 节头数量加载器工作流程:
Start of program headers找到程序头表
Entry point address (0x1060)执行
魔数(Magic Number)是文件开头的一组特定字节序列,用于标识文件的格式。魔数通常位于文件的开头,不同的文件格式都有其特定的魔数。通过检查文件的魔数,系统可以快速判断文件的类型。
魔数工作原理
0x7F开头,接着是E、L、F三个字符对应的ASCII码值,也就是0x45、0x4C、0x46。当系统读取到文件开头的字节序列符合这个魔数时,就能判定这是一个ELF文件。
示例解释
例如我们上文中的ELF头中魔数的十六进制表示为:
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
7f 45 4c 46表示这是一个ELF文件。其中,7f是ELF文件的起始标志,45、4c、46分别对应ASCII字符E、L、F。
02表示ELF文件的版本号,这里是2,表明这是一个遵循ELF版本2规范的文件。
01表示文件的字节序,这里是小端字节序(little-endian)。这意味着在存储多字节数据时,最低有效字节存储在最低地址处。
01表示操作系统的架构类型,这里表示这是一个用于64位架构的操作系统。
魔数在文件识别中的应用
file命令)也利用魔数来识别文件类型。file命令会读取文件的魔数,并与已知的魔数列表进行比对,从而输出文件的类型信息
对于 ELF HEADER 这部分来说,我们只用知道其作用即可,它的主要目的是定位文件的其他部分。
ELF文件中的各个区域(segments)和节(sections)在文件中的位置和大小都通过文件偏移量来描述,这些信息存储在程序头表(Program Header Table)和节头表(Section Header Table)中。
一、ELF 文件区域与偏移量基础
ELF 文件由四大结构化区域构成,各区域的偏移量关系如下:
区域 | 定位方式 | 大小/偏移量依赖 | 作用 |
|---|---|---|---|
ELF 头 | 固定偏移 0 | 独立存在 | 定义文件类型、入口点、程序头表/节头表位置 |
程序头表 | ELF 头的 e_phoff 字段指定 | 表项数由 e_phnum 定义 | 描述段(Segment)的加载信息 |
节头表 | ELF 头的 e_shoff 字段指定 | 表项数由 e_shnum 定义 | 描述节(Section)的链接信息 |
节/段内容区 | 程序头表的 p_offset 或节头表的 sh_offset | 由 p_filesz 或 sh_size 定义 | 存储实际代码/数据 |
📌 关键验证:示例中
readelf -h hello显示:
64 字节14032 字节二、偏移量关系的三层映射体系
1. 物理文件层(磁盘存储)
文件按连续字节流存储,各区域通过偏移量精确锚定:
文件偏移示例:
0x0000 ┌──────────────┐ ELF 头 (64字节)
│Magic/类型/入口点│
0x0040 ├──────────────┤ 程序头表 (56字节×13=728字节)
│PT_LOAD/INTERP│
0x0368 ├──────────────┤ .text 节 (代码)
│ 机器指令 │
0x2000 ├──────────────┤ .data 节 (初始化数据)
│ 全局变量值 │
0x36D0 └──────────────┘ 节头表 (描述31个节)🔍 设计逻辑:
2. 链接视图(节头表管理)
节头表(Section Header Table)通过 sh_offset 定位各节:
节名 | sh_offset 作用 | 示例值 | 权限 |
|---|---|---|---|
.text | 代码起始偏移(如 0x1060) | 0x0318 → 0x1556 | R-X |
.data | 初始化数据偏移(如 0x2000) | 0x3DB0 → 0x3DC8 | RW |
.rodata | 只读数据偏移 | 未显示 | R |
.bss | 无文件偏移(内存中分配) | 大小 8 字节 | RW |
⚠️ 特殊案例:
.bss 节:文件无存储空间(sh_size=0),但需在内存分配空间 .text 包含 .rodata) 3. 执行视图(程序头表管理)
程序头表(Program Header Table)通过 p_offset 定位段:
段类型 | p_offset 作用 | 内存映射 | 包含的节 |
|---|---|---|---|
PT_LOAD | 可加载段起始偏移 | VirtAddr → VirtAddr+MemSiz | .text + .rodata (代码段) / .data + .bss (数据段) |
PT_INTERP | 动态链接器路径偏移 | 不直接加载 | .interp |
PT_DYNAMIC | 动态链接信息偏移 | 加载到数据段 | .dynamic |
🔧 映射示例(readelf -l hello 隐含逻辑):
程序头表项:
Type: PT_LOAD (代码段)
Offset: 0x00000000
VirtAddr: 0x400000
FileSiz: 0x1556
MemSiz: 0x1556
Flags: R-E → 包含 .text (0x1060-0x1556)
Type: PT_LOAD (数据段)
Offset: 0x00002000
VirtAddr: 0x402000
FileSiz: 0x018 → .data 文件内容
MemSiz: 0x020 → 含 .bss 扩展内存验证:
MemSiz - FileSiz = 8与size命令的bss=8一致 。
三、偏移量设计的工程价值
1. 空间效率最大化
.text 和 .rodata)合并为段,减少内存碎片 📏 例:100 个 8 字节变量独立加载需 400KB 内存;合并后仅 4KB
.bss 零存储:未初始化数据不占文件空间,仅运行时分配内存 2. 加载性能优化
Offset=0x2000, FileSiz=0x1000),触发操作系统批量读取 .got.plt 偏移定位) 3. 安全与隔离
p_flags 将可执行段(R-X)与数据段(RW)物理分离,阻止代码注入 VirtAddr 可偏移,但文件内 Offset 固定不变 结论:偏移量关系的本质
ELF 文件的偏移量体系是物理存储(文件)与逻辑执行(内存)的桥梁:
sh_offset 实现精准符号定位(如 .symtab 记录函数偏移) p_offset 指导按段加载,映射到虚拟地址 静态链接是将多个目标文件(.o)及静态库(本质是.o的归档)合并为单一可执行文件的过程。其核心任务是 符号解析 和 重定位:
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ ll
total 24
drwxrwxr-x 2 ltx ltx 4096 Jul 30 17:46 ./
drwxrwxr-x 11 ltx ltx 4096 Jul 29 16:35 ../
-rw-rw-r-- 1 ltx ltx 62 Jul 29 16:37 code.c
-rw-rw-r-- 1 ltx ltx 1496 Jul 29 16:38 code.o
-rw-rw-r-- 1 ltx ltx 102 Jul 29 16:36 hello.c
-rw-rw-r-- 1 ltx ltx 1560 Jul 29 16:38 hello.o
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ gcc *.o -o main.exe
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ ll
total 40
drwxrwxr-x 2 ltx ltx 4096 Jul 30 17:47 ./
drwxrwxr-x 11 ltx ltx 4096 Jul 29 16:35 ../
-rw-rw-r-- 1 ltx ltx 62 Jul 29 16:37 code.c
-rw-rw-r-- 1 ltx ltx 1496 Jul 29 16:38 code.o
-rw-rw-r-- 1 ltx ltx 102 Jul 29 16:36 hello.c
-rw-rw-r-- 1 ltx ltx 1560 Jul 29 16:38 hello.o
-rwxrwxr-x 1 ltx ltx 16016 Jul 30 17:47 main.exe*查看编译后的.o目标文件
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ objdump -d code.o
code.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <run>:
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 <run+0xf>
f: 48 89 c7 mov %rax,%rdi
12: e8 00 00 00 00 call 17 <run+0x17>
17: 90 nop
18: 5d pop %rbp
19: c3 ret
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ objdump -d hello.o
hello.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: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <main+0xf>
f: 48 89 c7 mov %rax,%rdi
12: e8 00 00 00 00 call 17 <main+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: e8 00 00 00 00 call 21 <main+0x21>
21: b8 00 00 00 00 mov $0x0,%eax
26: 5d pop %rbp
27: c3 ret objdump -d 命令:将代码段(.text)进行反汇编查看
hello.o 中的 main 函数不认识 printf和run 函数, code.o 不认识 printf 函数
// 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");
}在反汇编的输出中可以看到:
hello.o中的main函数,调用printf和run的地址都是0code.o中的run函数,调用printf的地址也是0# hello.o 中的调用
12: e8 00 00 00 00 call 17 <main+0x17> # 调用printf
1c: e8 00 00 00 00 call 21 <main+0x21> # 调用run
# code.o 中的调用
12: e8 00 00 00 00 call 17 <run+0x17> # 调用printf这是因为编译器在编译单个源文件时:
1. 编译时的占位符机制
printf 或 run)时,因无法确定其地址,生成指令时使用 0 占位 。2. 重定位表:链接器的修正指南
每个目标文件包含 重定位表(如 .rel.text),记录需要修正的位置及其类型 :
r_offset:需修正的指令在文件中的偏移量(如 call 指令的操作数位置)。r_info:符号索引 + 重定位类型(绝对地址/相对地址修正)。r_addend:附加常数(通常为0)。R_X86_64_32) :用于全局变量,直接替换为符号的绝对地址。R_X86_64_PC32) :用于函数调用,替换为符号与下条指令的地址差 。📌 关键点: 示例中的
call指令需进行 相对地址修正(R_X86_64_PC32),因为函数调用依赖与当前指令指针(PC)的偏移 。
1. 符号解析:构建全局符号表
链接器扫描所有目标文件,提取符号定义(如 run 在 code.o 中)和引用(如 main 调用 run),构建全局符号表:
libc.a),链接报错 undefined reference 。main 和 run 均在合并的 .o 文件中,故符号可解析。2. 地址分配:合并同类型段
链接器将所有目标文件的段按类型合并:
.text 段合并为代码段,.data 段合并为数据段 。run() 的入口地址)。3. 重定位修正:覆盖占位符
遍历重定位表,根据符号地址修正指令:
绝对寻址修正:
// 修正前(假设符号地址为0x400500)
movl $0x0, 0x4(%esp) // 占位符0
// 修正后
movl $0x400500, 0x4(%esp) // 替换为符号绝对地址相对寻址修正(示例中的函数调用):
// 修正前(call指令的操作数占位0)
1c: e8 00 00 00 00 call 21 <main+0x21>
// 修正后(假设run在0x401250
1c: e8 2f 01 00 00 call 401250 <run>计算:0x401250 - (0x40101c + 5) = 0x12F → 小端存储为2F 01 00 00
注:实际计算时需考虑指令长度和地址对齐 。
静态库(如 libc.a)是多个目标文件的归档(ar 打包)。链接时:
printf 的 .o)。
在链接过程中,地址重定位是一个关键步骤,主要涉及对目标文件(.o文件)中的外部符号进行地址解析和修正。具体来说:
这个过程确保了程序中的各个模块能够正确引用彼此的函数和数据,最终形成一个可以正确执行的完整程序映像。
下面是objdump -S main.exe之后的反汇编代码
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ objdump -S 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: f2 ff 25 9b 2f 00 00 bnd jmp *0x2f9b(%rip) # 3fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
102d: 0f 1f 00 nopl (%rax)
1030: f3 0f 1e fa endbr64
1034: 68 00 00 00 00 push $0x0
1039: f2 e9 e1 ff ff ff bnd jmp 1020 <_init+0x20>
103f: 90 nop
Disassembly of section .plt.got:
0000000000001040 <__cxa_finalize@plt>:
1040: f3 0f 1e fa endbr64
1044: f2 ff 25 ad 2f 00 00 bnd jmp *0x2fad(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
104b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Disassembly of section .plt.sec:
0000000000001050 <puts@plt>:
1050: f3 0f 1e fa endbr64
1054: f2 ff 25 75 2f 00 00 bnd jmp *0x2f75(%rip) # 3fd0 <puts@GLIBC_2.2.5>
105b: 0f 1f 44 00 00 nopl 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 %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 <run>:
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: 48 8d 05 9d 0e 00 00 lea 0xe9d(%rip),%rax # 200f <_IO_stdin_used+0xf>
1172: 48 89 c7 mov %rax,%rdi
1175: e8 d6 fe ff ff call 1050 <puts@plt>
117a: b8 00 00 00 00 mov $0x0,%eax
117f: e8 c5 ff ff ff call 1149 <run>
1184: b8 00 00 00 00 mov $0x0,%eax
1189: 5d pop %rbp
118a: c3 ret
Disassembly of section .fini:
000000000000118c <_fini>:
118c: f3 0f 1e fa endbr64
1190: 48 83 ec 08 sub $0x8,%rsp
1194: 48 83 c4 08 add $0x8,%rsp
1198: c3 ret 问题 1:ELF 程序未加载时是否存在地址?
答案:存在逻辑地址(虚拟地址) ELF 程序在磁盘上时已具备完整的逻辑地址布局,这是现代计算机采用 平坦模式(Flat Mode) 的必然要求。编译器在生成可执行文件时,会从 地址 0 开始对代码、数据等所有元素进行统一编址,形成连续的虚拟地址空间。
关键机制解析
编址原理
objdump -S main.exe 输出中左侧的地址列(各段都有明确的起始地址(如.init段从0000000000001000开始),各函数都有固定的偏移地址(如_start函数位于0000000000001060)),这些地址在程序加载前已确定。平坦模式的核心价值
验证示例 反汇编输出片段:
0000000000001060 <_start>:
1060: f3 0f 1e fa endbr64
1064: 31 ed xor %ebp,%ebp1060 是 _start 函数的逻辑地址,在磁盘文件中已固化。问题 2:进程内存结构初始化数据来源
答案:数据源于 ELF 文件的程序头表(Program Header Table)
进程创建时,内核通过解析 ELF 的程序头表,提取 Segment 信息 初始化 mm_struct 和 vm_area_struct。
初始化流程详解
数据结构作用
结构体 | 功能 | 初始化来源 |
|---|---|---|
mm_struct | 管理进程的整个虚拟地址空间 | ELF 的 PT_LOAD 段信息 |
vm_area_struct | 描述虚拟内存区域(代码段/数据段等)的属性(起始地址、长度、权限) | ELF Segment 的 p_vaddr, p_memsz, p_flags |
具体步骤
Step 1:读取程序头表
内核定位 ELF 头的 e_phoff 字段,获取程序头表位置(示例中偏移 64 字节)。
Step 2:映射 PT_LOAD 段
// 伪代码:基于 Segment 初始化 vma
for (每个 PT_LOAD 段) {
vma = kmalloc(sizeof(vm_area_struct));
vma->vm_start = segment.p_vaddr; // 如 0x1060(代码段)
vma->vm_end = segment.p_vaddr + segment.p_memsz;
vma->vm_flags = segment.p_flags; // 如 R-X(代码段)、RW(数据段)
insert_vma(mm, vma); // 插入 mm_struct
}注意:
set_brk() 函数设置堆的起止地址(mm->start_brk = mm->brk),初始为空。PT_INTERP 段(如 /lib64/ld-linux-x86-64.so.2),将其映射到内存映射区域。示例验证
ELF 头信息(readelf -h main.exe):
Entry point address: 0x1060 // 进程从 _start 开始执行
Start of program headers: 64 // 程序头表位置
Number of program headers: 13 // 含多个 PT_LOAD 段PT_LOAD 段的 p_vaddr=0x1060(代码段)、p_memsz=文件大小 初始化 vm_area_struct。设计意义:从磁盘到内存的协同
0x1060 → 实际虚拟地址 0x400000+1060)。.bss 段在文件中无实体(p_filesz=0),但内核根据 p_memsz 分配归零内存,节省磁盘空间。
vm_flags 不同,阻止代码注入。ELF(Executable and Linkable Format)文件在被编译完成后,会在其头部结构中记录程序的关键信息。具体来说,ELF header 中专门设置了一个 Entry 字段,用于存储程序执行时的入口地址。这个地址指向程序代码段(.text section)中 main 函数的起始位置,或者更准确地说,是程序启动后执行的第一条指令所在的内存地址(通常是 _start 而非直接是 main)。
不是直接指向 main() :而是指向 _start 函数(由 C 运行时库提供)
_start 的作用:
_start:
xor %ebp, %ebp ; 清除帧指针
mov (%rsp), %edi ; 获取 argc
lea 8(%rsp), %rsi ; 获取 argv
lea 16(%rsp,%rdi,8), %rdx ; 获取 envp
call __libc_start_main ; 调用初始化函数__libc_start_main 的工作:
main() 函数main() 的返回值exit() 结束进程下文会详细说明_start的工作机制
当操作系统加载并运行 ELF 可执行文件时,会首先解析 ELF header,读取其中的 Entry 字段值,然后将程序计数器(PC)设置为该地址值,从而开始执行程序。这个机制确保了程序能够从正确的起始位置开始运行。
在典型的 32 位 ELF 格式中,Entry 字段位于 ELF header 的第 24 字节处,占用 4 个字节;而在 64 位 ELF 格式中,它位于第 24 字节处,占用 8 个字节。开发者可以使用 readelf 或 objdump 等工具查看这个入口地址的具体数值。
例如,使用命令:
readelf -h [文件名]ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ readelf -h main.exe
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1060 #Entry字段
Start of program headers: 64 (bytes into file)
Start of section headers: 14032 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30可以在输出结果中看到类似"Entry point address: 0x1060"这样的信息,这就是程序的实际入口地址。这个地址的具体值由链接器(ld)在链接阶段确定,并受到链接脚本、代码布局等多种因素的影响。
一、ELF 文件的预编址机制与虚拟地址空间基础
ELF 的逻辑地址预定义
未加载时的地址存在性:ELF 文件在磁盘中已通过逻辑地址(或称虚拟偏移地址)进行统一编址,编译器采用 平坦模式(Flat Mode) 从地址 0 开始布局所有代码和数据。
入口地址固化:ELF 头中的 e_entry 字段(32 位占 4 字节,64 位占 8 字节)存储程序入口地址(如上面示例中的 0x1060),该地址指向 _start 而非直接指向 main。
验证工具:
readelf -h main.exe | grep "Entry point" # 输出:0x1060
objdump -d --start-address=0x1060 main.exe # 反汇编入口代码虚拟地址空间的构成要素
区域 | 作用 | ELF 来源 | 权限 | 典型地址 (x86-64) |
|---|---|---|---|---|
代码段 | 存储可执行指令 | .text 节 | R-X | 0x400000-0x401000 |
数据段 | 已初始化全局变量 | .data 节 | RW- | 0x601000-0x602000 |
BSS 段 | 未初始化全局变量(零填充) | .bss 节(磁盘无内容) | RW- | 紧邻数据段 |
堆 | 动态分配内存(malloc) | 无直接对应,运行时扩展 | RW- | 0x700000-0x800000 |
栈 | 局部变量/函数调用 | 无直接对应 | RW- | 0x7ffffffff000 |
内存映射区 | 动态库/文件映射 | PT_INTERP(如 ld.so) | R-X/RW | 0x7f0000000000 |
📌 注:权限分离(R/W/X)是安全隔离的核心机制。
二、从磁盘到虚拟地址空间的转换过程
1. 内核加载流程(基于程序头表)
解析程序头表:
内核读取 ELF 头的 e_phoff 定位程序头表(示例中偏移 64 字节),遍历 PT_LOAD 段:
// 内核源码伪代码 (fs/binfmt_elf.c)
for (i = 0; i < elf_ex->e_phnum; i++) {
if (phdr[i].p_type == PT_LOAD) {
// 计算虚拟地址:Vaddr = phdr[i].p_vaddr + 加载基址
// 映射内存:mmap(Vaddr, phdr[i].p_memsz, PROT_READ | PROT_EXEC, ...)
}
}虚拟地址空间初始化:
固定基址 + 逻辑偏移:
逻辑地址 0x1060 → 虚拟地址 0x400000 + 0x1060 = 0x401060。
ASLR(地址随机化):
若启用 ASLR,加载基址随机偏移(如 0x555555550000),但段内逻辑关系不变:
// 实际虚拟地址 = 随机基址 + p_vaddr
vaddr = random_base + phdr[i].p_vaddr; // 如 0x555555551060.bss 段优化:磁盘中不占用空间(p_filesz=0),内存分配零填充页。PT_INTERP 指定的 ld.so 被映射到内存映射区。2. 进程内存结构初始化
mm_struct 与 vm_area_struct:
内核为每个 PT_LOAD 段创建 vm_area_struct,记录虚拟地址范围、权限和文件映射关系:
struct vm_area_struct {
unsigned long vm_start; // 如 0x400000
unsigned long vm_end; // 如 0x401000
pgprot_t vm_page_prot; // 如 PROT_READ | PROT_EXEC
struct file *vm_file; // 指向 ELF 文件
};入口地址激活:
通过 start_thread(regs, elf_entry) 设置 RIP=0x401060,启动程序执行。
三、从虚拟地址到物理地址的转换机制
1. MMU 与页表的核心作用

页表层级结构(x86-64 四级页表):
层级 | 字段位范围 | 作用 |
|---|---|---|
PML4 | 47-39 | 顶级页目录 |
PDP | 38-30 | 页目录指针 |
PD | 29-21 | 页目录 |
PT | 20-12 | 页表 |
Offset | 11-0 | 页内偏移(4KB 页) |
2. 缺页中断的详细处理
中断触发条件:
.bss 段)。内核响应流程:
do_page_fault(vaddr) {
if (vaddr 在 vm_area 范围内) {
分配物理页;
若为文件映射(如代码段),从磁盘读取内容;
若为匿名映射(如堆),填充零;
更新页表项;
} else {
发送 SIGSEGV 信号; // 段错误
}
}流程图示例:

四、全流程实例验证
阶段 | 地址类型 | 值 | 工具验证 |
|---|---|---|---|
磁盘文件 | 逻辑地址 | 0x1060 (_start) | objdump -d main.exe |
加载后 | 虚拟地址 | 0x401060 (ASLR 关闭) | gdb -p $pid : info proc mappings |
首次执行指令 | CPU 访问虚拟地址 | 0x401060 | gdb : info reg rip |
MMU 转换 | 物理地址 | 0x89ab000 | sudo cat /proc/$pid/pagemap |
从磁盘到内存的完整旅程
一张图总结:


核心结论: 从磁盘到内存的地址转换是逻辑地址→虚拟地址→物理地址的三级映射过程:

注意:磁盘上的内容不会直接加载到物理内存上,而是通过操作系统的虚拟内存管理机制间接完成,详细过程如上文再谈进程虚拟地址空间。上图和下图中简化了中间的过程,但我们要知道,心里有数。

动态链接在现代操作系统中远比静态链接要常用得多,这已经成为软件开发中的标准实践。让我们通过一个具体的例子来深入了解:当我们查看main.exe这个可执行程序的依赖关系时,会发现它依赖几个关键的动态库:
ltx@hcss-ecs-d90d:~/Linux_system/lesson9$ ldd main.exe
linux-vdso.so.1 (0x00007ffe12ff9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff61129d000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff6114d3000)这里,ldd命令是一个非常有用的工具,它能够打印出程序或库文件所依赖的共享库列表。在上面的输出中:
linux-vdso.so.1是内核提供的一个虚拟动态共享对象,用于加速系统调用libc.so.6是C语言的标准运行时库,提供了诸如printf、malloc、strcpy等常用函数ld-linux-x86-64.so.2是动态链接器/加载器本身那么为什么现代编译器通常默认使用动态链接而不是静态链接呢?静态链接确实有一个明显的优势:它会将所有目标文件和所需的库合并成一个独立的可执行文件,不需要额外的依赖就能运行,这在某些特定场景下(如嵌入式系统)很有用。
然而,静态链接存在几个严重的问题:
动态链接的工作原理可以总结为"延迟绑定"——它将链接的整个过程推迟到了程序加载的时候。具体工作流程如下:
ld-linux.so)读取程序的动态段(.dynamic section),获取需要加载的共享库列表这个过程的关键创新在于:
现代操作系统如Linux、Windows和macOS都广泛采用这种动态链接机制,它是支撑现代软件生态的基础技术之一。
关键机制:
程序启动流程
在C/C++程序中,当程序开始执行时,它首先并不会直接跳转到main函数。实际上,程序的入口点是_start,这是由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。这个过程是操作系统和C运行时环境密切配合的结果。
_start函数执行流程
在_start函数中,会执行一系列关键的初始化操作:
_start函数负责建立初始的栈指针(ESP/RSP),设置栈帧.data段(已初始化的全局变量和静态变量).bss段(未初始化的全局变量和静态变量),将其内存区域清零.rodata段(只读数据)的映射_start函数会调用动态链接器的代码.dynamic段中的DT_NEEDED条目,获取依赖库列表printf()时,动态链接器会确保调用能正确跳转到libc中的实现__libc_start_main最终调用用户编写的main函数__libc_start_main_exit系统调用终止程序主要功能
动态链接器(如Linux上的ld-linux.so)是程序运行时加载的核心组件,负责:
加载机制
/lib, /usr/libLD_LIBRARY_PATH环境变量指定的路径/etc/ld.so.conf中配置的路径(通常包含/etc/ld.so.conf.d/目录下的文件)RPATH或RUNPATH/etc/ld.so.cache是ldconfig工具生成的二进制缓存ldconfig -p可以查看缓存内容性能优化
prelink工具预先计算库的加载地址,减少运行时重定位开销环境配置
重要环境变量
LD_LIBRARY_PATH:
export LD_LIBRARY_PATH=/opt/mylibs:$LD_LIBRARY_PATHLD_PRELOAD:
LD_DEBUG:
files(显示库加载)、symbols(显示符号解析)、bindings(显示绑定信息)配置文件
/etc/ld.so.conf:
include /etc/ld.so.conf.d/*.conf包含其他配置/etc/ld.so.preload:
开发者视角
虽然这些底层细节对大多数开发者是透明的,但了解它们有助于:
error while loading shared libraries)-Wl,-rpath)理解这些底层机制可以让开发者更好地掌控程序的整个生命周期,从启动到终止的每个环节。
动态库(Dynamic Link Library, DLL)为了实现灵活的加载和内存映射功能,采用了相对地址的编址方案。这种设计使得动态库能够在不同的进程地址空间中正确运行,无论被加载到内存的哪个位置。
工作原理:
实际应用示例:
技术实现细节:
这种设计使得:
文件加载与内存映射
文件操作底层:
动态库作为磁盘文件,需通过open()打开 → 获取文件描述符(fd)。
使用mmap()将文件映射到虚拟地址空间:
// 伪代码:动态链接器映射库文件
void* addr = mmap(
NULL, // 由内核选择映射地址
lib_size, // 库文件大小
PROT_READ|PROT_EXEC, // 代码段权限
MAP_SHARED, // 多进程共享
fd, // 库文件描述符
0 // 文件偏移
);虚拟地址空间映射机制
1. 共享区(Shared Region)的核心地位
区域 | 起始地址(x86-64) | 内容 | 权限 | 物理内存共享 |
|---|---|---|---|---|
栈区 | ~0x7FFFFFFFFFFF | 局部变量/调用栈 | RW- | 否 |
共享区 | 0x7F0000000000 | 动态库映射 | R-X/RW | 是 |
堆区 | 0x00600000 | malloc内存 | RW- | 否 |
数据段 | 0x00601000 | 全局变量 | RW- | 否 |
代码段 | 0x00400000 | 程序指令 | R-X | 否 |
验证工具:
cat /proc/1234/maps # 查看进程内存映射
7f3a5a200000-7f3a5a3e0000 r-xp 00000000 08:01 /lib/libc.so.6 # 代码段(共享)
7f3a5a5e0000-7f3a5a5e4000 r--p 001e0000 08:01 /lib/libc.so.6 # 只读数据(共享)
7f3a5a5e4000-7f3a5a5e6000 rw-p 001e4000 08:01 /lib/libc.so.6 # 可写数据(COW)符号表合并
动态链接器构建全局符号表:
struct Symbol {
const char *name;
Elf64_Addr value;
Elf64_Addr size;
};符号解析优先级:
函数调用机制
结合图示:

📌 核心要点: • 在程序运行之前,需要先把所有依赖的动态链接库加载并映射到内存中。此时,所有库的起始虚拟地址都应该被确定下来。 • 然后对已加载到内存中的程序进行库函数调用的地址修正,这个过程称为"加载地址重定位"(load-time relocation)。 • 这里存在一个关键问题:代码区(.text)在进程中是只读的,我们无法直接修改其中的跳转地址。那么如何实现这种地址修正呢? 解决方案:动态链接采用在.data段(可执行程序或库自身)中专门预留一块区域来存放函数的跳转地址,这块区域被称为全局偏移表GOT(Global Offset Table)。表中的每一项都记录着本运行模块需要引用的全局变量或函数的真实地址。
一、GOT 的设计背景与核心问题
代码段只读的约束
.text)在进程内存中为只读(R-X),禁止运行时修改(安全机制防御代码注入)。解决方案:数据段动态重定位
动态链接将地址修正转移到 可读写的数据段(.data) ,通过 GOT 表间接跳转:
// 伪代码:函数调用流程
call puts@PLT // 1. 跳转到PLT条目
→ PLT: jmp *GOT[n] // 2. 间接跳转到GOT存储的地址
→ GOT[n] = 0x7f8a3b251100 // 3. 动态库函数真实地址 GOT 表本质:位于 .data 段的函数指针数组,存储外部函数/变量的绝对地址。
二、GOT 表的工作机制
1. 地址生成与修正流程
步骤 | 操作 | 关键参与者 |
|---|---|---|
1. 库加载 | 动态链接器(ld.so)映射库到共享区 | 内核mmap |
2. 地址计算 | 真实地址 = 共享区基址 + 库内偏移 | 动态链接器 |
3. GOT更新 | 将真实地址写入进程的GOT表项 | ld.so |
4. 函数调用 | 通过PLT→GOT跳转到真实地址 | CPU/MMU |
寻址机制:
示例:
# libc.so 加载基址 (ASLR随机化)
0x7f8a3b200000
# printf 库内偏移 (编译固定)
0x51100
# GOT 存储的最终地址
0x7f8a3b200000 + 0x51100 = 0x7f8a3b2511002. PIC(地址无关代码)的实现
这种通过GOT表实现的动态链接机制被称为PIC(Position Independent Code,地址无关代码)。它具有以下特点:
-fPIC参数的原因编译要求:gcc -fPIC -shared
技术核心:
代码段:仅包含相对跳转指令(不依赖绝对地址)。
数据段:GOT 表存储绝对地址,通过 固定偏移 访问:
; 访问GOT表示例(x86)
lea GOT(%rip), %rax // 加载GOT地址
mov (%rax+index), %rbx // 读取函数指针
jmp *%rbx // 跳转PIC的实现机制 = 相对编址(PC-relative addressing) + GOT表
应用场景:
三、GOT 表的进程隔离与共享机制
1. 为何进程不能共享 GOT 表?
内存区域 | 共享性 | 原因 |
|---|---|---|
代码段(.text) | 多进程共享 | 只读属性+相同物理页 |
GOT 表(.data) | 进程私有 | 1. 不同进程库加载基址不同(ASLR) 2. 需存储进程专属的绝对地址 |
2. 物理内存优化
代码段共享:所有进程映射到同一物理页(只读属性)。
数据段隔离:GOT 表及库数据段使用 写时复制(COW):
// 伪代码:写时复制触发
if (进程修改GOT表) {
复制物理页;
更新页表指向新物理页;
}关键结论:GOT 表的核心价值
.data 段,绕过代码段不可修改的限制。
动态链接在程序加载时需要对大量外部函数进行重定位,这一过程会显著增加程序的启动时间。为了优化这一性能问题,现代操作系统采用了延迟绑定(Lazy Binding)技术,也称为PLT(Procedure Linkage Table)机制。
延迟绑定的核心思想是:与其在程序启动时就解析和绑定所有可能用到的动态库函数,不如将这个绑定过程推迟到函数第一次被实际调用时。这种设计基于一个重要的观察:在典型的程序运行过程中,动态库中的许多函数可能永远不会被调用到。例如,一个图像处理程序可能加载了数学库,但只使用了其中的部分数学函数;或者一个程序可能加载了错误处理函数库,但在正常运行时根本不会触发错误处理流程。
具体实现上,延迟绑定通过以下机制工作:
这种机制带来了显著的性能优势:
以Linux系统为例,当调用一个动态库函数printf()时:
_dl_runtime_resolve进行符号解析printf的条目会被更新为libc中的实际地址printf实现这种优化技术在现代操作系统中被广泛采用,如Linux的glibc、macOS的dyld等都实现了类似的延迟绑定机制,大幅提升了包含大量动态库的程序的启动速度。

总而言之,动态链接实际上将链接的整个过程从传统的编译时推迟到了程序的运行时。具体来说,这个过程包括:
这种机制虽然会带来一定的性能开销(大约增加10-15%的函数调用时间)和程序启动延迟(首次加载需要解析符号),但其优势非常显著:
在Linux系统中,典型的动态链接过程是:当执行一个动态链接的可执行文件时,内核首先加载程序解释器(如/lib64/ld-linux-x86-64.so.2),然后由解释器负责加载所有依赖的共享库,解析未定义符号,最后才将控制权转交给程序入口点。