一、库的认识
库(Library) 是一组预先编写好的代码、函数、类或模块的目标代码(.o)集合,提供其他程序在编译链接或运行时链接使用,而不需要从头编写这些功能。
一个源文件编译成可执行文件的过程:
源文件形成可执行文件的过程: 源码 (.c) --> 编译 --> 目标文件 (.o) --> 链接 --> 可执行文件
目标代码文件是:
.o文件里的东西——机器能看懂的指令,其中.o文件不能独立运行,因为里面可能有未解析的符号(比如调用了别的函数,地址还没填上)。
一个 库将多个相关的 .o 文件 进行打包:
一个库不是只有一个.o文件,而是把多个相关的目标文件打包在一起形成一个集合。 libmymath.a ├── mymath_add.o (加法) ├── mymath_sub.o (减法) ├── mymath_mul.o (乘法) └── mymath_div.o (除法)
库的制作与使用流程图如下所示:
库的制作与使用流程图:
别人写好源码 --> 编译成 .o --> 打包成库(.a 或 .so)
|
v
你的程序在 "编译时或运行时",去"借用"这些现成的机器码
在 Linux 环境下,库分为两种:
| 类型 | 文件后缀 | 链接方式 |
|---|---|---|
| 静态库 | .a(archive) | 编译时链接,代码拷贝进可执行文件 |
| 动态库 | .so(shared object) | 运行时链接,通过地址映射调用 |
在windows系统中,其中 静态库的文件后缀为 .lib ,动态库的文件后缀为 .so 。
二、静态库
2.1 什么是静态库
静态库的特征 : 在 Linux 下以 .a为文件后缀,在windows下以 .lib 为文件后缀。
静态库本质:在程序编译链接阶段,库中的目标代码会被完整拷贝到最终的可执行文件中,程序运行时不再需要该库文件。
静态库使用的本质就是将 .o 打包的文件原封不动的进行拷贝到需要链接的可执行文件中。
静态库命名格式:
静态库遵循固定的命名格式: lib<库名>.a - 开头为: lib - 中间为: 自定义的库名 - 末尾为: .a 例如:libcalc.a、libmyutil.a
2.2 制作静态库
以创建一个简单的计算器库,包含加法、减法、乘法、除法四个功能,演示制作静态库。
mymath/
|
├── include/ # .h 头文件目录
│ └── calc.h
|
├── lib/ # .c 源文件目录
│ ├── add.c
│ ├── sub.c
│ ├── mul.c
│ └── div.c
|
└── bin/ # 将 .o 输出到此目录.
├── add.o
├── div.o
├── mul.o
└── sub.o
2.2.1 头文件包含
文件:include/calc.h:头文件声明了所有对外暴露的函数接口,使用者只需包含这个头文件即可。
#ifndef __CALC.H__ #define __CALC .H__ int add(int a, int b); int sub(int a, int b); int mul(int a, int b); int div(int a, int b); #endif
2.2.2 源文件编写
文件:lib/add.c
#include "calc.h"
int add(int a, int b)
{
return a + b;
}
文件:lib/sub.c
#include "calc.h"
int sub(int a, int b)
{
return a - b;
}
文件:lib/mul.c
#include "calc.h"
int mul(int a, int b)
{
return a * b;
}
文件:lib/div.c
#include "calc.h"
int div(int a, int b)
{
if (b == 0)
{
perror("dev fail\n");
return 0;
}
return a / b;
}
2.2.3 输出目标文件
将源文件编译为目标文件(.o)
gcc -c lib/add.c -I include -o bin/add.o gcc -c lib/sub.c -I include -o bin/sub.o gcc -c lib/mul.c -I include -o bin/mul.o gcc -c lib/div.c -I include -o bin/div.o
-c:只编译生成目标文件(.o),不进行链接。
-I:指定头文件的搜索路径
-o bin/xxx.o:指定输出文件的路径和名称
2.2.4 目标文件打包
使用 ar 命令打包为静态库:
ar命令: ar rcs libcalc.a bin/*.o
| 参数 | 含义 |
|---|---|
r | replace,将目标文件插入归档文件中。如果归档中已存在同名文件,则替换它 |
c | create,如果归档文件不存在,则创建它 |
s | 为归档文件创建索引,加速链接时的符号查找 |
2.3 使用静态库
目录结构如下所示:
.
├── main.c
└── mymath
├── bin
│ ├── add.o
│ ├── div.o
│ ├── mul.o
│ └── sub.o
├── include
│ └── calc.h
├── lib
│ ├── add.c
│ ├── div.c
│ ├── mul.c
│ └── sub.c
└── libcalc.a
编译指令:
gcc main.c -o main -L mymath -l calc -I mymath/include
gcc 编译可执行文件的参数如下:
| 参数 | 含义 |
|---|---|
-I | 指定 头文件搜索路径 |
-L | 指定 库文件的搜索路径 |
-l | 指定 要链接的库名,编译器会自动查找 libcalc.a 或 libcalc.so |
-o | 指定 输出的可执行文件名 |
在C/C++中,如果要使用自己编写的库 或者 第三方外部库,都需要指明 -L 和 -l 参数。
2.4 静态库的优缺点
静态库的优点如下:
优点: - 编译后的程序可以独立运行,无需额外依赖 - 部署简单,适合嵌入式或分发场景 - 链接时编译器可以进行更好的优化
静态库的缺点如下:
静态库的缺点: - 可执行文件体积较大(所有用到的库代码都被拷贝进去) - 库更新后,所有使用该库的程序都需要重新编译 - 多个程序使用同一个库时,会浪费内存和磁盘空间(每个程序都有一份拷贝)
三、动态库 (共享库)
3.1 什么是动态库
假设你的系统上有 100 个程序都调用了 printf,如果用静态库:内存中同时存在 100 份完全相同的代码,浪费 400KB。
程序A (含一份 printf 机器码) 占用 4KB 程序B (含一份 printf 机器码) 占用 4KB 程序C (含一份 printf 机器码) 占用 4KB ... 程序100 (含一份 printf 机器码) 占用 4KB
动态库的思路:内存中只放一份 printf 的代码,100 个进程共享它。
程序A ──┐ 程序B ──┼──> 内存中唯一一份 libc.so (含 printf) 程序C ──┤ ... ──┘
动态库的本质:是一个编译好的、可被多个进程在运行时共同加载的代码文件。
- 文件后缀:Linux 下为 .so ,Windows 下为 .dll - 命名规则:lib<名字>.so,例如 libc.so、libm.so、libpthread.so - 运行逻辑:它不能独立运行,必须被某个可执行文件在运行时加载
3.2 制作动态库
以创建一个简单的demo_lib库,演示制作动态库。
代码结构如下所示:
.
└── demo_lib
|
├── include
│ └── mymath.h
|
└── src
│ └── mymath.c
├── bin
|
└── main.c
3.2.1 头文件包含
mymath.h
#ifndef MYMATH_H #define MYMATH_H int my_add(int a, int b); int my_sub(int a, int b); int my_mul(int a, int b); #endif
3.2.2 源文件实现
mymath.c
#include "mymath.h"
int my_add(int a, int b)
{
return a + b;
}
int my_sub(int a, int b)
{
return a - b;
}
int my_mul(int a, int b)
{
return a * b;
}
3.2.3 测试代码实现
main.c
#include <stdio.h>
#include "mymath.h"
int main()
{
printf("10 + 3 = %d\n", my_add(10, 3));
printf("10 - 3 = %d\n", my_sub(10, 3));
printf("10 * 3 = %d\n", my_mul(10, 3));
return 0;
}
3.2.4 输出目标文件
gcc -fPIC -c src/mymath.c -o bin/mymath.o -I include
| 参数 | 作用 |
|---|---|
-fPIC | 生成位置无关代码,动态库被加载到内存的地址不固定,代码必须用相对寻址,不能写死绝对地址 |
-c | 只编译,不链接,输出 .o 文件 |
-I | 指定头文件的搜索路径 |
注意:如果不加
-fPIC:这个.o只能用于静态库。若强行用它做动态库,在 x86_64 上会报错。
3.2.5 打包动态库
打包命令: gcc -shared -o libmymath.so bin/mymath.o
| 参数 | 作用 |
|---|---|
-shared | 输出共享库,而非可执行文件 |
-o libmymath.so | 指定输出文件名,必须以 lib 开头、.so 结尾 |
文件目录如下所示:
.
├── bin
│ └── mymath.o
|
├── include
│ └── mymath.h
|
├── libmymath.so
|
├── main.c
|
└── src
└── mymath.c
hamber@VM-0-14-ubuntu:~/code/lesson23/demo_lib$ gcc main.c -o main -L. -lmymath -I include/
3.3 使用动态库
运行main出现错误:
运行可执行程序:./main 预期报错: ./main: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory 原因分析: 1. 编译时,链接器通过 -L. 找到了库,确认符号存在,在 main 中记录了"我需要 libmymath.so"。 2. 运行时,动态链接器 ld-linux-x86-64.so.2 按自己的规则搜索寻找动态库,但当前目录不在默认搜索路径中,故而找不到动态库。
用 ldd 查看动态库依赖:
输入指令:ldd main 运行结果如下: linux-vdso.so.1 (0x00007ffd2f8b9000) /$LIB/libonion.so => /lib/x86_64-linux-gnu/libonion.so (0x00007f0b321c3000) libmymath.so => not found <-- 找不到 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0b31fc8000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0b31fc2000) /lib64/ld-linux-x86-64.so.2 (0x00007f0b322d0000)
3.4 解决动态库运行时搜索路径
3.4.1 将自定义动态库拷贝到系统目录
执行cp指令: sudo cp libmymath.so /lib64 ldd 验证: linux-vdso.so.1 (0x00007ffd50754000) /$LIB/libonion.so => /lib/x86_64-linux-gnu/libonion.so (0x00007ff479465000) libmymath.so (0x00007ff479460000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff479265000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ff47925f000) /lib64/ld-linux-x86-64.so.2 (0x00007ff479572000)
3.4.2 环境变量 LD_LIBRARY_PATH
添加临时环境变量:
export LD_LIBRARY_PATH=${$LD_LIBRARY_PATH}:/home/user/demo_lib
ldd 验证:
linux-vdso.so.1 (0x00007ffd50754000)
/$LIB/libonion.so => /lib/x86_64-linux-gnu/libonion.so (0x00007ff479465000)
libmymath.so (0x00007ff479460000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff479265000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ff47925f000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff479572000)
3.4.3 编译时写入 rpath
编译时写入 rpath gcc main.c -o main -L. -lmymath -I include/ -Wl,-rpath,'$ORIGIN' #通过环境变量指定路径 $ORIGIN gcc main.c -o main -L. -lmymath -I. -Wl,-rpath,/home/user/demo_lib #指定绝对路径 ldd 验证: linux-vdso.so.1 (0x00007ffd50754000) /$LIB/libonion.so => /lib/x86_64-linux-gnu/libonion.so (0x00007ff479465000) libmymath.so (0x00007ff479460000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff479265000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ff47925f000) /lib64/ld-linux-x86-64.so.2 (0x00007ff479572000)
$ORIGIN 的含义: 可执行文件自身所在的目录。
demo_lib/ ├── main <-- $ORIGIN 就是 demo_lib/ └── libmymath.so <-- 所以能找到同目录下的 .so
| 项目 | 说明 |
|---|---|
| 生效范围 | 仅该可执行文件 |
| 持久性 | 永久(嵌入二进制中) |
| 适用场景 | 发布软件包、不想依赖系统配置 |
| 是否需要 root | 不需要 |
3.4.4 编写 .conf 配置文件
1. 在 /etc/ld.so.conf.d/ 目录下新建一个 .conf 文件 2. 在新建的.conf文件中填写,动态库的绝对路径 3. 执行命令 ldconfig
四、ELF文件
4.1 什么是ELF文件
ELF(Executable and Linkable Format)是一种用于二进制文件的标准格式,它是 Linux 等类 Unix 系统中最常见的目标文件格式,用于表示以下四类文件:
| 类型 | 说明 |
|---|---|
| 可执行文件(Executable) | 可直接运行的程序,如 ls |
| 可重定位文件(Relocatable) | 编译产生但未链接的目标文件,如 .o 文件 |
| 共享目标文件(Shared Object) | 动态链接库,如 .so 文件 |
| 核心转储文件(Core Dump) | 程序崩溃时的内存快照,由dump信号触发 |
4.2 ELF 文件的整体结构
+-------------------+ | ELF Header | <-- 文件最开头,描述文件元信息(属性) +-------------------+ | Program Header | <-- 描述"段"(Segment),运行时视图 | Table | +-------------------+ | | | .text | <-- 代码段 | .rodata | <-- 只读数据 | .data | <-- 已初始化全局变量 | .bss | <-- 未初始化全局变量 | .symtab | <-- 符号表 | .strtab | <-- 字符串表 | ... | | | +-------------------+ | Section Header | <-- 描述"节"(Section),链接时视图 | Table | +-------------------+
核心要点:ELF 文件同时包含两种视图
| 视角 | 表 | 基本单位 | 谁在用 |
|---|---|---|---|
| 链接视图 | Section Header Table | 节(.text、.data、.symtab、.rela.text…) | 链接器 |
| 执行视图 | Program Header Table | 段(segment) | 操作系统加载器 |
4.3 ELF Header ---ELF头
ELF Header:位于文件最开头,固定大小(64 位系统下为 64 字节),包含以下关键字段。
typedef struct
{
unsigned char e_ident[16]; // 魔数及标识信息
uint16_t e_type; // 文件类型(ET_EXEC, ET_DYN, ET_REL...)
uint16_t e_machine; // 目标架构(EM_X86_64, EM_ARM...)
uint32_t e_version; // ELF 版本
uint64_t e_entry; // 程序入口地址
uint64_t e_phoff; // ★ Program Header Table 的文件偏移
uint64_t e_shoff; // ★ Section Header Table 的文件偏移
uint32_t e_flags; // 处理器相关标志
uint16_t e_ehsize; // ELF Header 大小
uint16_t e_phentsize; // 每个 Program Header 条目大小
uint16_t e_phnum; // Program Header 条目数量
uint16_t e_shentsize; // 每个 Section Header 条目大小
uint16_t e_shnum; // Section Header 条目数量
uint16_t e_shstrndx; // 节名字符串表的索引
} Elf64_Ehdr;
4.4 Program Header Table ---程序头表
描述如何将文件映射到内存,是运行时的关键结构。
typedef struct
{
uint32_t p_type; // 段类型(PT_LOAD, PT_DYNAMIC, PT_INTERP...)
uint32_t p_flags; // 权限标志(PF_R, PF_W, PF_X)
uint64_t p_offset; // 段在文件中的偏移
uint64_t p_vaddr; // 段在内存中的虚拟地址
uint64_t p_paddr; // 物理地址(通常忽略)
uint64_t p_filesz; // 段在文件中的大小
uint64_t p_memsz; // 段在内存中的大小
uint64_t p_align; // 对齐要求
} Elf64_Phdr;
常见的段类型:PT_LOAD、PT_INTERP 和 PT_DYNAMIC
| 类型 | 含义 |
|---|---|
| PT_LOAD | 映射进内存的段,携带地址、长度、权限、文件位置四要素。 |
| PT_INTERP | 指定解释器路径,如 /lib64/ld-linux-x86-64.so.2(动态链接器) |
| PT_DYNAMIC | 动态链接信息(.dynamic 节) |
4.5 Section Header Table ---节头表
描述文件中各个"节"的元信息,是链接时的关键结构:
typedef struct
{
uint32_t sh_name; // 节名(在 .shstrtab 中的偏移)
uint32_t sh_type; // 节类型(SHT_PROGBITS, SHT_SYMTAB...)
uint64_t sh_flags; // 标志(SHF_WRITE, SHF_ALLOC, SHF_EXECINSTR)
uint64_t sh_addr; // 内存地址(若可加载)
uint64_t sh_offset; // 文件偏移
uint64_t sh_size; // 节大小
uint32_t sh_link; // 关联节索引
uint32_t sh_info; // 附加信息
uint64_t sh_addralign; // 对齐
uint64_t sh_entsize; // 条目大小(若为表结构)
} Elf64_Shdr;
常见的节(section):
-
.text:机器指令(可执行) -
.rodata:只读数据(如字符串常量) -
.data:已初始化的全局/静态变量 -
.bss:未初始化的全局/静态变量(不占文件空间) -
.symtab:符号表 -
.strtab:字符串表 -
.dynamic:动态链接信息 -
.got/.plt:全局偏移表 / 过程链接表(用于动态链接)
4.6 符号表 .symtab
符号表(.symtab) 记录函数名、变量名等符号信息:
typedef struct
{
uint32_t st_name; // 符号名(在 .strtab 中的偏移)
unsigned char st_info; // 绑定类型 + 符号类型
unsigned char st_other; // 可见性
uint16_t st_shndx; // 所在节索引
uint64_t st_value; // 符号值(地址)
uint64_t st_size; // 符号大小
} Elf64_Sym;
五、分析ELF文件的命令
| 工具 | 用途 |
|---|---|
readelf -a file | 全面显示 ELF 结构 |
readelf -h file | 仅显示 ELF Header |
readelf -l file | 显示 Program Headers |
readelf -S file | 显示 Section Headers |
readelf -s file | 显示ELF文件的符号表(symbol table) |
objdump -d file | 反汇编 |
objdump -t file | 显示符号表 |
nm file | 列出符号 |
ldd file | 查看动态库依赖 |
六、静态库链接
6.1 实例演示静态链接原理
6.1.1 hello.c 文件
#include <stdio.h>
void run();
int main()
{
printf("hello Linux\n");
run();
return 0;
}
6.1.2 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 3d 00 00 00 00 lea 0x0(%rip),%rdi # f <main+0xf> f: e8 00 00 00 00 callq 14 <main+0x14> 14: b8 00 00 00 00 mov $0x0,%eax 19: e8 00 00 00 00 callq 1e <main+0x1e> 1e: b8 00 00 00 00 mov $0x0,%eax 23: 5d pop %rbp 24: c3 retq
6.1.3 hello.o 的符号表
hamber@VM-0-14-ubuntu:~/code/lesson23/static_link$ readelf -s hello.o
Symbol table '.symtab' contains 14 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
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
10: 0000000000000000 37 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND run
6.1.4 code.c 文件
#include <stdio.h>
void run()
{
printf("running ...\n");
}
6.1.5 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 3d 00 00 00 00 lea 0x0(%rip),%rdi # f <run+0xf> f: e8 00 00 00 00 callq 14 <run+0x14> 14: 90 nop 15: 5d pop %rbp 16: c3 retq
6.1.6 code.o 的符号表
hamber@VM-0-14-ubuntu:~/code/lesson23/static_link$ readelf -s code.o
Symbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS code.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
10: 0000000000000000 23 FUNC GLOBAL DEFAULT 1 run
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
6.1.7 main.exe 的符号表
hamber@VM-0-14-ubuntu:~/code/lesson23/static_link$ 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 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 67 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000318 0 SECTION LOCAL DEFAULT 1
2: 0000000000000338 0 SECTION LOCAL DEFAULT 2
3: 0000000000000358 0 SECTION LOCAL DEFAULT 3
4: 000000000000037c 0 SECTION LOCAL DEFAULT 4
5: 00000000000003a0 0 SECTION LOCAL DEFAULT 5
6: 00000000000003c8 0 SECTION LOCAL DEFAULT 6
7: 0000000000000470 0 SECTION LOCAL DEFAULT 7
8: 00000000000004f2 0 SECTION LOCAL DEFAULT 8
9: 0000000000000500 0 SECTION LOCAL DEFAULT 9
10: 0000000000000520 0 SECTION LOCAL DEFAULT 10
11: 00000000000005e0 0 SECTION LOCAL DEFAULT 11
12: 0000000000001000 0 SECTION LOCAL DEFAULT 12
13: 0000000000001020 0 SECTION LOCAL DEFAULT 13
14: 0000000000001040 0 SECTION LOCAL DEFAULT 14
15: 0000000000001050 0 SECTION LOCAL DEFAULT 15
16: 0000000000001060 0 SECTION LOCAL DEFAULT 16
17: 0000000000001208 0 SECTION LOCAL DEFAULT 17
18: 0000000000002000 0 SECTION LOCAL DEFAULT 18
19: 000000000000201c 0 SECTION LOCAL DEFAULT 19
20: 0000000000002068 0 SECTION LOCAL DEFAULT 20
21: 0000000000003db8 0 SECTION LOCAL DEFAULT 21
22: 0000000000003dc0 0 SECTION LOCAL DEFAULT 22
23: 0000000000003dc8 0 SECTION LOCAL DEFAULT 23
24: 0000000000003fb8 0 SECTION LOCAL DEFAULT 24
25: 0000000000004000 0 SECTION LOCAL DEFAULT 25
26: 0000000000004010 0 SECTION LOCAL DEFAULT 26
27: 0000000000000000 0 SECTION LOCAL DEFAULT 27
28: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
29: 0000000000001090 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones
30: 00000000000010c0 0 FUNC LOCAL DEFAULT 16 register_tm_clones
31: 0000000000001100 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux
32: 0000000000004010 1 OBJECT LOCAL DEFAULT 26 completed.8061
33: 0000000000003dc0 0 OBJECT LOCAL DEFAULT 22 __do_global_dtors_aux_fin
34: 0000000000001140 0 FUNC LOCAL DEFAULT 16 frame_dummy
35: 0000000000003db8 0 OBJECT LOCAL DEFAULT 21 __frame_dummy_init_array_
36: 0000000000000000 0 FILE LOCAL DEFAULT ABS code.c
37: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
38: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
39: 000000000000218c 0 OBJECT LOCAL DEFAULT 20 __FRAME_END__
40: 0000000000000000 0 FILE LOCAL DEFAULT ABS
41: 0000000000003dc0 0 NOTYPE LOCAL DEFAULT 21 __init_array_end
42: 0000000000003dc8 0 OBJECT LOCAL DEFAULT 23 _DYNAMIC
43: 0000000000003db8 0 NOTYPE LOCAL DEFAULT 21 __init_array_start
44: 000000000000201c 0 NOTYPE LOCAL DEFAULT 19 __GNU_EH_FRAME_HDR
45: 0000000000003fb8 0 OBJECT LOCAL DEFAULT 24 _GLOBAL_OFFSET_TABLE_
46: 0000000000001000 0 FUNC LOCAL DEFAULT 12 _init
47: 0000000000001200 5 FUNC GLOBAL DEFAULT 16 __libc_csu_fini
48: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
49: 0000000000004000 0 NOTYPE WEAK DEFAULT 25 data_start
50: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
51: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 25 _edata
52: 0000000000001149 23 FUNC GLOBAL DEFAULT 16 run
53: 0000000000001208 0 FUNC GLOBAL HIDDEN 17 _fini
54: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
55: 0000000000004000 0 NOTYPE GLOBAL DEFAULT 25 __data_start
56: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
57: 0000000000004008 0 OBJECT GLOBAL HIDDEN 25 __dso_handle
58: 0000000000002000 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used
59: 0000000000001190 101 FUNC GLOBAL DEFAULT 16 __libc_csu_init
60: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 26 _end
61: 0000000000001060 47 FUNC GLOBAL DEFAULT 16 _start
62: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 26 __bss_start
63: 0000000000001160 37 FUNC GLOBAL DEFAULT 16 main
64: 0000000000004010 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__
65: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
66: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2
6.1.8 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__>
100f: 48 85 c0 test %rax,%rax
1012: 74 02 je 1016 <_init+0x16>
1014: ff d0 callq *%rax
1016: 48 83 c4 08 add $0x8,%rsp
101a: c3 retq
Disassembly of section .plt:
0000000000001020 <.plt>:
1020: ff 35 9a 2f 00 00 pushq 0x2f9a(%rip) # 3fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: f2 ff 25 9b 2f 00 00 bnd jmpq *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 pushq $0x0
1039: f2 e9 e1 ff ff ff bnd jmpq 1020 <.plt>
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 jmpq *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 jmpq *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: 4c 8d 05 86 01 00 00 lea 0x186(%rip),%r8 # 1200 <__libc_csu_fini>
107a: 48 8d 0d 0f 01 00 00 lea 0x10f(%rip),%rcx # 1190 <__libc_csu_init>
1081: 48 8d 3d d8 00 00 00 lea 0xd8(%rip),%rdi # 1160 <main>
1088: ff 15 52 2f 00 00 callq *0x2f52(%rip) # 3fe0 <__libc_start_main@GLIBC_2.2.5>
108e: f4 hlt
108f: 90 nop
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 2e 2f 00 00 mov 0x2f2e(%rip),%rax # 3fd8 <_ITM_deregisterTMCloneTable>
10aa: 48 85 c0 test %rax,%rax
10ad: 74 09 je 10b8 <deregister_tm_clones+0x28>
10af: ff e0 jmpq *%rax
10b1: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
10b8: c3 retq
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>
10eb: 48 85 c0 test %rax,%rax
10ee: 74 08 je 10f8 <register_tm_clones+0x38>
10f0: ff e0 jmpq *%rax
10f2: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
10f8: c3 retq
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 callq 1040 <__cxa_finalize@plt>
1127: e8 64 ff ff ff callq 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 retq
1135: 0f 1f 00 nopl (%rax)
1138: c3 retq
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 jmpq 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 3d ac 0e 00 00 lea 0xeac(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
1158: e8 f3 fe ff ff callq 1050 <puts@plt>
115d: 90 nop
115e: 5d pop %rbp
115f: c3 retq
0000000000001160 <main>:
1160: f3 0f 1e fa endbr64
1164: 55 push %rbp
1165: 48 89 e5 mov %rsp,%rbp
1168: 48 8d 3d a1 0e 00 00 lea 0xea1(%rip),%rdi # 2010 <_IO_stdin_used+0x10>
116f: e8 dc fe ff ff callq 1050 <puts@plt>
1174: b8 00 00 00 00 mov $0x0,%eax
1179: e8 cb ff ff ff callq 1149 <run>
117e: b8 00 00 00 00 mov $0x0,%eax
1183: 5d pop %rbp
1184: c3 retq
1185: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
118c: 00 00 00
118f: 90 nop
0000000000001190 <__libc_csu_init>:
1190: f3 0f 1e fa endbr64
1194: 41 57 push %r15
1196: 4c 8d 3d 1b 2c 00 00 lea 0x2c1b(%rip),%r15 # 3db8 <__frame_dummy_init_array_entry>
119d: 41 56 push %r14
119f: 49 89 d6 mov %rdx,%r14
11a2: 41 55 push %r13
11a4: 49 89 f5 mov %rsi,%r13
11a7: 41 54 push %r12
11a9: 41 89 fc mov %edi,%r12d
11ac: 55 push %rbp
11ad: 48 8d 2d 0c 2c 00 00 lea 0x2c0c(%rip),%rbp # 3dc0 <__do_global_dtors_aux_fini_array_entry>
11b4: 53 push %rbx
11b5: 4c 29 fd sub %r15,%rbp
11b8: 48 83 ec 08 sub $0x8,%rsp
11bc: e8 3f fe ff ff callq 1000 <_init>
11c1: 48 c1 fd 03 sar $0x3,%rbp
11c5: 74 1f je 11e6 <__libc_csu_init+0x56>
11c7: 31 db xor %ebx,%ebx
11c9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
11d0: 4c 89 f2 mov %r14,%rdx
11d3: 4c 89 ee mov %r13,%rsi
11d6: 44 89 e7 mov %r12d,%edi
11d9: 41 ff 14 df callq *(%r15,%rbx,8)
11dd: 48 83 c3 01 add $0x1,%rbx
11e1: 48 39 dd cmp %rbx,%rbp
11e4: 75 ea jne 11d0 <__libc_csu_init+0x40>
11e6: 48 83 c4 08 add $0x8,%rsp
11ea: 5b pop %rbx
11eb: 5d pop %rbp
11ec: 41 5c pop %r12
11ee: 41 5d pop %r13
11f0: 41 5e pop %r14
11f2: 41 5f pop %r15
11f4: c3 retq
11f5: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)
11fc: 00 00 00 00
0000000000001200 <__libc_csu_fini>:
1200: f3 0f 1e fa endbr64
1204: c3 retq
Disassembly of section .fini:
0000000000001208 <_fini>:
1208: f3 0f 1e fa endbr64
120c: 48 83 ec 08 sub $0x8,%rsp
1210: 48 83 c4 08 add $0x8,%rsp
1214: c3 retq
6.2 静态链接原理
6.2.1 链接前的状态
A. 编译器填写占位符
编译器将 hello.c 翻译为 hello.o 时,面临一个根本问题:它不知道其他目标文件中的函数和变量最终会被放在内存的哪个位置。
由此可知:
编译器只能在指令中留下"占位符",等待链接器来填补。
编译器进行对每个文件的编译是孤立得。
链接器核心问题:把所有 UND(未定义)符号与某个目标文件中已定义的符号匹配起来。
0000000000000000 <main>: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 空洞1:字符串地址 f: e8 00 00 00 00 callq 14 # 空洞2:调用 puts 14: b8 00 00 00 00 mov $0x0,%eax 19: e8 00 00 00 00 callq 1e # 空洞3:调用 run() 1e: b8 00 00 00 00 mov $0x0,%eax 23: 5d pop %rbp 24: c3 retq
三个"空洞"分别是:
| 位置 | 指令 | 含义 | 为什么是 0 |
|---|---|---|---|
偏移 0x08 | lea 0x0(%rip),%rdi | 加载字符串 "hello Linux\n" 的地址 | 字符串在 .rodata 节中,链接前不知道最终地址 |
偏移 0x0F | callq 14 | 调用 printf(被优化为 puts) | puts 在 libc 中,链接前不知道地址 |
偏移 0x19 | callq 1e | 调用 run() | run 定义在 code.o 中,链接前不知道地址 |
B. 符号表记录依赖
hello.o 的符号表清晰地表达了这种依赖关系:
Num: Value Size Type Bind Vis Ndx Name 10: 0000000000000000 37 FUNC GLOBAL DEFAULT 1 main ← 我定义了 main 12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts ← 我需要 puts(未定义) 13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND run ← 我需要 run(未定义)
Ndx = 1:main定义在本文件的第 1 个节(.text)中。
Ndx = UND:puts和run在本文件中未定义,必须由链接器从其他目标文件或库中找到。
6.2.2 链接器的工作流程
执行链接命令 gcc -o main.exe hello.o code.o,链接器依次完成以下步骤:
hello.o ──┐
├──→ [1. 符号解析] ──→ [2. 节合并] ──→ [3. 地址分配] ──→ [4. 重定位] ──→ main.exe
code.o ──┘
A.步骤 1:符号解析
链接器扫描所有输入文件的符号表,建立一张全局符号映射表:
符号名 定义位置 状态 ───────────────────────────────────────── main hello.o (.text) 已定义 run code.o (.text) 已定义 puts libc.so / libc.a 动态链接时解析(本例中为动态) _GLOBAL_OFFSET_TABLE_ 链接器自行创建 __libc_start_main libc 动态链接时解析
关键规则:
强符号(函数、已初始化全局变量)在多个目标文件中只能定义一次,否则报
multiple definition错误。弱符号(未初始化全局变量)允许多处定义,链接器选一个。
如果某个 UND 符号在所有输入中都找不到定义,链接器报
undefined reference错误。
在本例中: hello.o 需要 run → code.o 提供了 run,匹配成功。 hello.o 和 code.o 都需要 puts → 本例中 puts 并未被静态链接进 main.exe,而是标记为动态符号(UND),运行时由动态链接器解析。
B. 步骤 2:节合并
链接器把所有输入文件的同名节拼接在一起:
合并前: 合并后(main.exe):
┌─────────────────┐ ┌─────────────────────────────┐
│ hello.o: .text │──┐ │ .text │
│ │ │ │ ┌───────────────────────┐ │
└─────────────────┘ │ │ │ _start (crt1.o) │ │
┌─────────────────┐ ├────────→ │ │ ...... │ │
│ code.o: .text │ │ │ │ run (code.o) │ │ ← 0x1149
│ │──┘ │ │ main (hello.o) │ │ ← 0x1160
└─────────────────┘ │ │ __libc_csu_init │ │
│ │ ... │ │
│ └───────────────────────┘ │
┌─────────────────┐ ┌─────────────────────────────┐
│ hello.o: .rodata│──┐ │ .rodata │
│ "hello Linux\n" │ ├────────→ │ "hello Linux\n" │
└─────────────────┘ │ │ "running ...\n" │
┌─────────────────┐ │ │ ... │
│ code.o: .rodata │──┘ └─────────────────────────────┘
│ "running ...\n" │
└─────────────────┘
C. 步骤 3:地址分配
将相应的 .o 文件链接合并后,链接器为每个节分配最终的虚拟地址,在 main.exe 符号表可以读出:
节号 地址 内容 ────────────────────────────── 12 0x1000 .init 13 0x1020 .plt 14 0x1040 .plt.got / .plt.sec 15 0x1050 (可能是 .plt.sec 的延续) 16 0x1060 .text ← 代码段起始 17 0x1208 .fini 18 0x2000 .rodata ← 只读数据 19 0x201c .eh_frame_hdr 20 0x2068 .eh_frame 21 0x3db8 .init_array 22 0x3dc0 .fini_array 23 0x3dc8 .dynamic 24 0x3fb8 .got 25 0x4000 .data 26 0x4010 .bss
由 mian.exe 读出关键符号的地址
_start = 0x1060 (程序入口) run = 0x1149 (code.o 的 run 函数) main = 0x1160 (hello.o 的 main 函数) puts = UND (动态链接,运行时才确定)
D. 步骤 4:重定位
这是链接器最本质的工作:修改指令中的占位符,填入正确的地址或偏移。
编译器在生成 .o 文件时,不仅留下了空洞,还在 重定位表(.rela.text) 中记录了"哪个位置需要修补、修补什么符号、用什么方式修补"。
具体例子:在 hello.o 中 callq run
编译后(hello.o):
callq 00 00 00 00 ← 要调用 run(),但不知道地址,先填 0
重定位表记录:位置=这里,符号=run,方式=相对偏移
链接时:
链接器查重定位表 → run 最终在 0x1149
算出相对偏移 = -0x35
把 00 00 00 00 替换为 cb ff ff ff
链接后(main.exe):
callq cb ff ff ff ← 现在能正确跳到 run() 了
七、 ELF文件加载与可执行程序加载运行
可程序程序加载到内存并运行的流程图:

task_struct 结构体的流程图:

7.1 程序加载过程中各个角色
1. 可执行程序(磁盘):ELF 文件,代码段内容按页对齐存放。 2. 物理内存:代码页最终落脚的地方。图中把物理地址也画成 0x1060,这是教学简化;实际上物理地址与虚拟地址不同,靠页表翻译。 3. task_struct:进程描述符,其中 mm 指针指向该进程的内存描述结构。 4. mm_struct:描述整个地址空间,持有页表基址(mm->pgd)和 VMA 链表头(mm->mmap)。 5. 页表:虚拟地址 → 物理地址的翻译表。 6. CPU 的 CR3:存放当前进程页表的物理基址。
7.2 从全局理解可执行程序到 CPU 执行
当 shell 执行 ./main.exe 时,实际发生的是 fork() + execve() 系统调用。
内核中处理 ELF 的函数是 load_elf_binary(),它的核心工作可以概括为:
execve("./main.exe")
│
▼
[1] 读取并校验 ELF Header(魔数、e_type、e_machine)
│
▼
[2] task_struct 创建新的 mm_struct + 页表
│
▼
[3] 遍历 Program Header Table,对每个 PT_LOAD 段
创建 vm_area_struct(VMA),建立"虚拟地址 → 文件偏移"的映射
│
▼
[4] 处理 BSS(p_memsz > p_filesz 的部分清零)
│
▼
[5] 建立用户栈,压入 argc / argv / envp / 辅助向量
│
▼
[6] 设置 EIP = e_entry(0x1060),CR3 = 新页表基址
│
▼
[7] 返回用户态,CPU 从 _start 开始执行
7.3 入口点与"加载"
ELF Header 中的 e_entry = 0x1060,正是可执行文件中符号表中 _start 的地址。
内核在最后一步把它写入 RIP(EIP 是 32 位时代的名字,64 位下即 RIP)。
7.3.1 程序入口位置
回顾之前的main.exe中的符号表:
_start = 0x1060 ← e_entry,进程第一条指令 main = 0x1160 __libc_csu_init = 0x1190 __libc_csu_fini = 0x1200 _GLOBAL_OFFSET_TABLE_ = 0x3fb8
7.3.2 PT_LOAD 映射到 进程地址空间
进程地址空间与 VMA:
task_struct ──mm──→ mm_struct ──mmap──→ vm_area_struct 链表
│ (mm->pgd → 页表) │
│ ├─ vm_start / vm_end:区域范围
│ ├─ vm_flags:权限位
│ ├─ vm_mm:指回 mm_struct
│ ├─ vm_next / vm_prev:链表
│ └─ vm_file / vm_pgoff:文件映射来源
└←── 每个 VMA 的 vm_mm 指回mm_struct
task_struct:进程描述符(含 pid、files、mm 等)。
mm_struct:整个用户地址空间的"总账本",mmap 字段指向 VMA 链表头。
vm_area_struct(VMA):描述一段连续的、属性相同的虚拟内存区域(栈、堆、代码段等)
PT_LOAD 是 ELF 程序头表 中的一种段类型,其作用是:"将这个段中的内容在进程启动加载(映射)到进程的虚拟内存中,并进行填写VMA的相关字段"。
PT_LOAD 字段 内核结构 [p_vaddr, p_vaddr+p_memsz] → VMA 的 [vm_start, vm_end](页对齐后) p_flags → vm_flags(VM_READ/VM_WRITE/VM_EXEC)→ 将来 PTE 的权限位 p_offset → vm_pgoff(缺页时知道去文件哪个位置读页) p_memsz - p_filesz → 匿名零页映射(BSS 的 0 从这来)
注意两点:
1. 此时还没有把文件内容真正读进内存, 只是建立了 VMA(虚拟内存区域)和页表映射关系,真正读磁盘是在第一次访问某页触发缺页异常时才发生的(按需加载)。
2. 这一步结束后,
main.exe的代码和数据在虚拟地址空间里"各就各位"了,但还不能执行——因为如果有动态库依赖,那些符号的地址还没解析。
7.3.2 CPU执行第一条指令
CPU 取指 EIP = 0x1060 → MMU 用 CR3 找到页表,逐级翻译页表 → 发现页面还没读入 → 触发缺页异常 #PF → 内核 do_page_fault → 找到 0x1060 所属的 VMA(代码段,vm_file = main.exe) → 从磁盘读取对应页 → 分配物理页 → 填写页面 → 返回用户态,重新执行 0x1060 处的指令
7.4 总结完整流程
可执行程序编译与运行的完整流程:
1. 编译 hello.c/code.c → .o 留下占位符 + 重定位表 2. 链接 .o → main.exe 链接器填空:分配虚拟地址(main=0x1160 等) 3. 加载 main.exe → 进程 内核按 PT_LOAD 建 VMA,EIP = e_entry = 0x1060 4. 运行 _start → __libc_start_main → main 缺页异常按需把代码/数据页从磁盘搬进物理内存
ELF文件中的section 和 segment形成并加载的完整流程:
[编译时] gcc -c hello.c -o hello.o
产物:.o 文件
状态:只有节(.text/.data/...),地址未定,没有段,没有程序头表
[链接时] ld
工作: 1. 给所有节分配最终虚拟地址
2. 按"权限分组 + 页对齐"把节打包成段
3. 把打包结果写进程序头表(PT_LOAD 等表目)
产物:main.exe ← 段在这一刻诞生,并固化在文件里
[加载时] execve → load_elf_binary()
工作:读程序头表,为每个 PT_LOAD 建 VMA
[运行时] CPU 首次访问某页
工作:缺页异常按 VMA 填页表
八、动态库加载与链接
8.1 动态链接的流程
[编译期] gcc -c hello.c → hello.o (引用 printf,地址未定)
[链接期] ld hello.o -lc → hello (只记依赖,不复制 libc)
↓
记录 DT_NEEDED=libc.so.6
为 printf 建 PLT/GOT 桩
PT_INTERP=/lib64/ld-linux-x86-64.so.2
[加载期] execve → 内核发现 PT_INTERP → 先映射 ld.so,把控制权交给它
[运行期] ld.so:
1. 读 hello 的 .dynamic,发现需要 libc.so.6
2. mmap libc 的所有 PT_LOAD 段到进程地址空间
3. 解析符号:在 libc 的 .dynsym 里找到 printf 的地址
4. 把地址填进 main 的 GOT
5. 跳转到 main 的 _star → __libc_start_main → main
[首次调用] call printf → PLT → GOT → 若尚未解析则走 resolver 现场解析
知识点补充:
"ld.so 是文件" 说的是它的物理存在形式;"ld.so 是动态链接器"说的是它被加载执行后扮演的角色。
ld.so 和普通 .so 有一个本质区别:它有真正的入口点,可以被直接"执行",普通的.so如 libc.so.6是被动的:等着ld.so来加载它、解析它的符号。
ld.so 文件(磁盘上) │ │ 内核通过 PT_INTERP 发现它 │ 内核 mmap 它的 PT_LOAD 段 │ 内核把 %rip 跳到它的入口点 │ ▼ ld.so 代码(内存中,正在执行)= 动态链接器 │ │ ld.so 开始工作... │ ▼ 跳转到 main 的 _start → 你的程序开始运行
8.2 动态库链接的细节分析 --printf 的运转
8.2.1 代码"引用"printf
假设你在 hello.c 中写了:
// hello.c
#include <stdio.h>
int main()
{
printf("hello %d\n", 42);
return 0;
}
编译 hello.c 为可执行文件 hello:
gcc -o hello hello.c
编译器看到 printf,发现它没有在当前编译单元中定义(而是定义在动态库libc 中):
-
在
.dynsym(动态符号表)中记录:printf是一个未定义符号(UND) -
在
.rela.plt(重定位表)中记录:在 GOT 里有个槽位,需要填printf的地址" -
在代码中生成一条 PLT 跳转指令,而不是直接调用
printf
8.2.2 引入GOT(全局偏移表)
GOT的本质:是一张指针数组,存放在可写的.data数据段(.got 或 .got.plt)中,每个槽位(4字节/ 8 字节)存放一个地址。
GOT的作用:
| 用途 | 说明 |
|---|---|
| 存放外部函数的真实地址 | 如 GOT[n] = printf 的地址 |
| 存放外部全局变量的地址 | 如 GOT[m] = errno 的地址 |
| 存放动态链接器的辅助信息 | 如 GOT[0] 指向 _DYNAMIC 段 |
实例说明:
.got 段 ┌──────────┬────────────────────────────────────────┐ │ 地址 │ 内容 │ ├──────────┼────────────────────────────────────────┤ │ 0x3fb8 │ [0] _DYNAMIC │ │ 0x3fc0 │ [1] │ │ 0x3fc8 │ [2] │ │ 0x3fd0 │ [3] printf 的地址 ← 关键槽位 │ │ ... │ ... │ └──────────┴────────────────────────────────────────┘
8.2.3 引入PLT (过程链接表)
PLT 的本质:PLT 是一组跳转桩(stub),存放在只读的代码段(.plt)中。每个外部函数对应一个桩。
PLT 的作用:PLT 桩是"跳板",你的代码不直接调用 printf,而是调用 printf 的 PLT 桩,桩再从 GOT 中读出 printf 的真实地址并跳过去。
实例说明:
0000000000001050 <printf@plt>:
1050: endbr64
1054: bnd jmpq *0x2f75(%rip) # 3fd0 <printf@GLIBC_2.2.5>
105b: nopl 0x0(%rax,%rax,1)
从 GOT[0x3fd0] 读出地址,跳过去
为什么需要PLT表呢?
因为第一次调用时 GOT 还是空的(或存的是初始值)。 PLT 桩除了跳转之外,还承担了"如果 GOT 没填好,就去调用解析函数"的后备逻辑(在延迟绑定模式下)。
8.2.4 延迟绑定模式
第一次调用
main PLT 桩 GOT ld.so
──── ────── ─── ─────
call printf@plt ──▶ jmp *GOT[0x3fd0]
│
│ GOT[0x3fd0] 里存的
│ 不是 printf 地址,
│ 而是下一条指令的地址
▼
push 符号索引
jmp _dl_runtime_resolve ──────────────────▶
查找 libc.so
找到 printf 地址
写入 GOT[0x3fd0]
跳转到 printf
◀─────────────────────────────────────────
执行 printf
第二次调用
main PLT 桩 GOT
──── ────── ───
call printf@plt ──▶ jmp *GOT[0x3fd0]
│
│ GOT[0x3fd0] 已经是
│ printf 的真实地址
▼
直接跳到 printf(不再经过解析)
8.2.5 验证一:.dynsym 中 printf 是未定义符号(UND)
验证:在 .dynsym(动态符号表)中记录:printf 是一个未定义符号(UND)
hamber@VM-0-14-ubuntu:~/code/lesson23/demo_printf$ readelf -s hello
Symbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2) # printf(UND)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
逐字段解读 printf 那一行(Num = 2)
| 字段 | 值 | 含义 |
|---|---|---|
| Num | 2 | 符号表中的第 2 项(第 0 项是空哨兵) |
| Value | 0000000000000000 | 地址为 0——编译器不知道 printf 在哪,留空 |
| Size | 0 | 函数体大小未知(定义不在这里) |
| Type | FUNC | 它是一个函数 |
| Bind | GLOBAL | 全局绑定,必须被解析,找不到就报错 |
| Vis | DEFAULT | 默认可见性 |
Ndx | UND | Undefined——未定义。这是核心证据 |
| Name | printf@GLIBC_2.2.5 (2) | 符号名 + 版本约束(要求 glibc >= 2.2.5) |
8.2.6 验证二:.rela.plt 中有一条重定位项指向 GOT 槽位
在 .rela.plt(重定位表)中记录:在GOT 里有个槽位,需要填 printf 的地址"
hamber@VM-0-14-ubuntu:~/code/lesson23/demo_printf$ readelf -r hello Relocation section '.rela.plt' at offset 0x5e8 contains 1 entry: Offset Info Type Sym. Value Sym. Name + Addend 000000003fd0 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0 在地址 0x3fd0 处(GOT 表的一个槽位),请 ld.so 在运行时把 printf 的真实地址填进去
逐字段解读
| 字段 | 值 | 含义 |
|---|---|---|
| Offset | 0x3fd0 | 要回填的内存地址——即 GOT 中 printf 槽位的位置 |
| Info | 0x000200000007 | 编码了两层信息(见下方拆解) |
| Type | R_X86_64_JUMP_SLOT | 重定位类型,语义是"把函数地址写入此槽位" |
Sym. Value | 0x0 | 符号当前值为 0(未定义,所以是 0) |
Sym. Name | printf@GLIBC_2.2.5 | 要解析的符号 |
| Addend | 0 | 无额外偏移 |
8.2.7 验证三:代码中生成的是 PLT 跳转,不是直接调用 printf
在代码中生成一条 PLT 跳转指令,而不是直接调用 printf
hamber@VM-0-14-ubuntu:~/code/lesson23/demo_printf$ objdump -d hello | grep -A 20 '<main>'
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: be 2a 00 00 00 mov $0x2a,%esi
1156: 48 8d 3d a7 0e 00 00 lea 0xea7(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
115d: b8 00 00 00 00 mov $0x0,%eax
1162: e8 e9 fe ff ff callq 1050 <printf@plt>
1167: b8 00 00 00 00 mov $0x0,%eax
116c: 5d pop %rbp
116d: c3 retq
116e: 66 90 xchg %ax,%ax
关键行是
callq 1050 <printf@plt>:
目标是
0x1050,这是 PLT 桩的地址,不是 printf 函数体编译器没有(也不可能)直接写
call printf,因为编译时不知道printf的地址
查看PLT桩
hamber@VM-0-14-ubuntu:~/code/lesson23/demo_printf$ objdump -d hello | grep -A 10 '<printf@plt>'
0000000000001050 <printf@plt>:
1050: f3 0f 1e fa endbr64
1054: f2 ff 25 75 2f 00 00 bnd jmpq *0x2f75(%rip) # 3fd0 <printf@GLIBC_2.2.5>
105b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
关键行是
bnd jmpq *0x2f75(%rip):
*表示间接跳转:读取内存中的值作为跳转目标
0x2f75(%rip)=0x1054 + 0x2f75 + 7(指令长度)=0x3fd0
objdump已经标注了:目标就是# 3fd0 <printf@GLIBC_2.2.5>
8.2.8 完整流程总结
时间线(从上到下)
【用户态】你在终端敲下 ./hello 回车
│
▼
【内核态】shell 调用 execve("./hello", ...)
│
├─ 1. 内核打开 ./hello,读取 ELF 头
├─ 2. 内核发现 PT_INTERP 段:
│ ".interp: /lib64/ld-linux-x86-64.so.2"
│ → 说明这是一个动态链接程序
│
├─ 3. 内核 mmap 加载 hello 的 PT_LOAD 段到虚拟地址空间
│ (.text, .rodata, .data 等)
│
├─ 4. 内核 mmap 加载 ld.so 本身到虚拟地址空间
│ (ld.so 也是 ELF,也要被加载)
│
├─ 5. 内核把控制权交给 ld.so 的入口点
│ (不是交给 hello 的 main!)
│
▼
【ld.so 开始工作】(此时 main 还没执行)
│
├─ 6. 读取 hello 的 .dynamic 段
│ → 发现 DT_NEEDED: libc.so.6
│ → 知道需要加载 libc.so
│
├─ 7. 打开 libc.so.6 文件
│ open("/lib/x86_64-linux-gnu/libc.so.6")
│
├─ 8. 读取 libc.so.6 的 ELF 头,了解其 LOAD 段
│
├─ 9. mmap 把 libc.so.6 的各段映射到虚拟地址空间
│ ├─ 代码段:mmap(..., PROT_READ|PROT_EXEC, ...)
│ └─ 数据段:mmap(..., PROT_READ|PROT_WRITE, ...)
│ 此时 libc.so 在虚拟地址空间中"存在"了, 但大部分页面还没读入物理内存(按需加载)
│
│
├─ 10. 符号解析 + 重定位
│ ├─ 读取 libc.so 的 .dynsym、.gnu.hash
│ ├─ 查找 "printf" → 得到偏移 0x4f4e0
│ ├─ 计算真实地址 = libc基址 + 0x4f4e0
│ └─ 写入 GOT[0x3fd0]
│ (BIND_NOW 模式下,这一步在启动时全部完成)
│
├─ 11. 调用所有 .so 的初始化函数(.init / .init_array)
│ libc 的初始化会设置 stdout、stderr 等
│
├─ 12. 跳转到 hello 的入口点 _start
│
▼
【hello 的 _start】
│
├─ 13. _start → __libc_start_main → main()
│
▼
【main() 终于开始执行】
│
├─ call printf@plt → PLT → GOT[0x3fd0] → printf
│ (此时 GOT 早已填好,直接跳转)
│
▼
输出:hello 42

7603

被折叠的 条评论
为什么被折叠?



