文章目录
-
- 库制作与原理(二):ELF格式与静态链接原理
- 一、编译链接回顾
-
- [1.1 从源码到可执行程序](#1.1 从源码到可执行程序)
- [1.2 目标文件是什么](#1.2 目标文件是什么)
- 二、ELF文件格式
-
- [2.1 什么是ELF](#2.1 什么是ELF)
- [2.2 ELF文件的四种类型](#2.2 ELF文件的四种类型)
- [2.3 ELF文件的整体结构](#2.3 ELF文件的整体结构)
- [2.4 查看ELF文件头](#2.4 查看ELF文件头)
- 三、ELF的Section详解
-
- [3.1 查看所有Section](#3.1 查看所有Section)
- [3.2 常见Section说明](#3.2 常见Section说明)
-
- [3.2.1 .text - 代码段](#3.2.1 .text - 代码段)
- [3.2.2 .data - 已初始化数据段](#3.2.2 .data - 已初始化数据段)
- [3.2.3 .bss - 未初始化数据段](#3.2.3 .bss - 未初始化数据段)
- [3.2.4 .rodata - 只读数据段](#3.2.4 .rodata - 只读数据段)
- [3.2.5 .symtab - 符号表](#3.2.5 .symtab - 符号表)
- [3.2.6 .strtab - 字符串表](#3.2.6 .strtab - 字符串表)
- [3.2.7 .rel.text / .rela.text - 重定位表](#3.2.7 .rel.text / .rela.text - 重定位表)
- 四、静态链接原理
-
- [4.1 链接的核心任务](#4.1 链接的核心任务)
- [4.2 符号表详解](#4.2 符号表详解)
-
- [4.2.1 符号的类型](#4.2.1 符号的类型)
- [4.2.2 符号解析过程](#4.2.2 符号解析过程)
- [4.3 地址重定位](#4.3 地址重定位)
-
- [4.3.1 为什么需要重定位](#4.3.1 为什么需要重定位)
- [4.3.2 重定位表的作用](#4.3.2 重定位表的作用)
- [4.3.3 实战:观察重定位过程](#4.3.3 实战:观察重定位过程)
- [4.4 Section的合并](#4.4 Section的合并)
- 五、ELF加载与执行视图
-
- [5.1 链接视图vs执行视图](#5.1 链接视图vs执行视图)
- [5.2 查看Program Header](#5.2 查看Program Header)
- [5.3 为什么要合并Section为Segment](#5.3 为什么要合并Section为Segment)
- [5.4 程序入口点](#5.4 程序入口点)
- 六、静态库的链接
-
- [6.1 静态库的本质](#6.1 静态库的本质)
- [6.2 链接静态库的过程](#6.2 链接静态库的过程)
- [6.3 验证静态链接](#6.3 验证静态链接)
- 七、链接时的符号解析规则
-
- [7.1 强符号与弱符号](#7.1 强符号与弱符号)
- [7.2 符号解析规则](#7.2 符号解析规则)
- [7.3 避免符号冲突](#7.3 避免符号冲突)
- 八、实战:分析完整的链接过程
-
- [8.1 准备测试代码](#8.1 准备测试代码)
- [8.2 编译但不链接](#8.2 编译但不链接)
- [8.3 查看符号表](#8.3 查看符号表)
- [8.4 查看重定位表](#8.4 查看重定位表)
- [8.5 链接](#8.5 链接)
- [8.6 查看链接后的符号](#8.6 查看链接后的符号)
- [8.7 反汇编对比](#8.7 反汇编对比)
- 九、工具大全
-
- [9.1 readelf - 查看ELF信息](#9.1 readelf - 查看ELF信息)
- [9.2 objdump - 反汇编工具](#9.2 objdump - 反汇编工具)
- [9.3 nm - 查看符号](#9.3 nm - 查看符号)
- [9.4 ar - 静态库工具](#9.4 ar - 静态库工具)
- [9.5 ldd - 查看动态库依赖](#9.5 ldd - 查看动态库依赖)
- [9.6 file - 识别文件类型](#9.6 file - 识别文件类型)
- 十、总结
库制作与原理(二):ELF格式与静态链接原理
💬 欢迎讨论:在上一篇中,我们学习了如何制作和使用静态库与动态库。但你是否好奇:编译器是如何将多个.o文件链接成可执行文件的?静态库中的函数是如何被找到并调用的?本篇将深入ELF文件格式,揭示静态链接的底层原理,带你理解从目标文件到可执行程序的完整过程。
👍 点赞、收藏与分享:这篇文章包含了ELF文件结构、符号表、重定位表的完整解析,配合大量实战命令,内容硬核,如果对你有帮助,请点赞、收藏并分享!
🚀 循序渐进:建议先学习第一篇的库制作基础,这样理解本篇的链接原理会更轻松。
一、编译链接回顾
1.1 从源码到可执行程序
在深入链接原理之前,我们先回顾一下完整的编译过程:
bash
源文件(.c/.cpp)
↓ 预处理(gcc -E)
预处理文件(.i)
↓ 编译(gcc -S)
汇编文件(.s)
↓ 汇编(gcc -c)
目标文件(.o)
↓ 链接(ld)
可执行文件(a.out)
详细步骤:
bash
# 1. 预处理:展开宏、处理#include
gcc -E main.c -o main.i
# 2. 编译:生成汇编代码
gcc -S main.i -o main.s
# 3. 汇编:生成机器码
gcc -c main.s -o main.o
# 4. 链接:生成可执行文件
gcc main.o -o main
一步到位:
bash
gcc main.c -o main
# 等价于上面4步

1.2 目标文件是什么
目标文件(.o):
- 包含机器码的二进制文件
- 但还不能直接运行
- 需要经过链接才能成为可执行文件
为什么需要链接?
c
// main.c
#include <stdio.h>
extern int add(int a, int b); // 声明,但未定义
int main() {
int result = add(3, 5);
printf("Result: %d\n", result);
return 0;
}
c
// add.c
int add(int a, int b) {
return a + b;
}
编译:
bash
gcc -c main.c -o main.o
gcc -c add.c -o add.o
问题:
main.o中调用了add函数,但add的代码在add.o中main.o中还调用了printf,代码在C库中- 链接器的任务:将这些目标文件组合在一起!
二、ELF文件格式
2.1 什么是ELF
ELF (Executable and Linkable Format):可执行与可链接格式
Linux下的所有二进制文件都采用ELF格式:
- 目标文件(.o)
- 可执行文件(a.out)
- 动态库(.so)
- 静态库(.a) - 从上篇文章我们就知道这实际上是.o文件的归档
查看文件类型:
bash
file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, not stripped
file libmystdio.so
libmystdio.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), not stripped
2.2 ELF文件的四种类型
| 类型 | 说明 | 文件扩展名 |
|---|---|---|
| Relocatable File (可重定位文件) | 目标文件 | .o |
| Executable File (可执行文件) | 可执行程序 | 通常无扩展名 |
| Shared Object (共享目标文件) | 动态库 | .so |
| Core Dump File (核心转储文件) | 程序崩溃时的内存快照 | core / core.* |
2.3 ELF文件的整体结构
ELF文件由四个主要部分组成:
bash
┌─────────────────────────────────┐
│ ELF Header (文件头) │ ← 描述整个文件的基本信息
├─────────────────────────────────┤
│ Program Header Table (程序头表) │ ← 描述如何加载到内存(可执行文件)
├─────────────────────────────────┤
│ │
│ Sections (节区) │ ← 实际的代码和数据
│ ├─ .text (代码段) │
│ ├─ .data (已初始化数据) │
│ ├─ .bss (未初始化数据) │
│ ├─ .rodata (只读数据) │
│ ├─ .symtab (符号表) │
│ ├─ .strtab (字符串表) │
│ ├─ .rel.text (重定位表) │
│ └─ ... │
│ │
├─────────────────────────────────┤
│ Section Header Table (节头表) │ ← 描述各个Section
└─────────────────────────────────┘

两个重要概念:
- Section(节):编译和链接时使用
- Segment(段):程序加载时使用
2.4 查看ELF文件头
使用readelf命令:
bash
readelf -h main.o
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: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1088 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
关键字段:
Type: REL- 可重定位文件(目标文件)Entry point address: 0x0- 目标文件没有入口点Start of section headers- Section Header Table的位置Number of section headers: 14- 有14个Section
查看可执行文件:
bash
readelf -h a.out
ELF Header:
Type: EXEC (Executable file)
Entry point address: 0x400430
Start of program headers: 64 (bytes into file)
Number of program headers: 9
对比:
- 可执行文件类型是
EXEC - 有入口点地址
0x400430 - 有Program Header Table
三、ELF的Section详解
3.1 查看所有Section
bash
readelf -S main.o
There are 14 section headers, starting at offset 0x440:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000002a 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000318
0000000000000048 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 0000006a
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 0000006a
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 0000006a
000000000000000b 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00000075
000000000000002e 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000a3
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000a8
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000360
0000000000000018 0000000000000018 I 11 8 8
[10] .symtab SYMTAB 0000000000000000 00000228
00000000000000d8 0000000000000018 12 8 8
[11] .strtab STRTAB 0000000000000000 00000300
0000000000000015 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 00000378
00000000000000c1 0000000000000000 0 0 1
3.2 常见Section说明
3.2.1 .text - 代码段
存储内容:编译后的机器指令
bash
readelf -x .text main.o
Hex dump of section '.text':
0x00000000 554889e5 4883ec10 c745fc03 000000c7 UH..H....E......
0x00000010 45f80500 00008b55 fc8b45f8 89d7e800 E......U..E.....
0x00000020 000000c9 c3 .....
反汇编查看:
bash
objdump -d main.o
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%rbp)
f: c7 45 f8 05 00 00 00 movl $0x5,-0x8(%rbp)
16: 8b 55 fc mov -0x4(%rbp),%edx
19: 8b 45 f8 mov -0x8(%rbp),%eax
1c: 89 d7 mov %edx,%edi
1e: e8 00 00 00 00 callq 23 <main+0x23>
23: c9 leaveq
24: c3 retq
注意 :地址0x1e处调用函数的地址是00 00 00 00,这是因为链接器还没有填入真实地址!
3.2.2 .data - 已初始化数据段
存储内容:已初始化的全局变量和静态变量
c
// test.c
int global_init = 10; // 存在.data
static int static_init = 20; // 存在.data
int main() {
return 0;
}
bash
gcc -c test.c
readelf -x .data test.o
Hex dump of section '.data':
0x00000000 0a000000 14000000 ........
↑ ↑
10 20 (小端序)
3.2.3 .bss - 未初始化数据段
存储内容:未初始化的全局变量和静态变量
c
// test.c
int global_uninit; // 存在.bss
static int static_uninit; // 存在.bss
int main() {
return 0;
}
bash
readelf -S test.o | grep bss
[ 4] .bss NOBITS 0000000000000000 0000006a
0000000000000008 0000000000000000 WA 0 0 4
↑
8字节(两个int)
注意: .bss段不占用文件空间(NOBITS),只记录大小,加载时由系统分配并清零!
3.2.4 .rodata - 只读数据段
存储内容:字符串常量、const变量
c
// test.c
#include <stdio.h>
int main() {
const char *str = "Hello World"; // 字符串存在.rodata
printf("%s\n", str);
return 0;
}
bash
readelf -x .rodata test.o
Hex dump of section '.rodata':
0x00000000 48656c6c 6f20576f 726c6400 257325 Hello World.%s%
3.2.5 .symtab - 符号表
存储内容:所有符号的信息(函数名、变量名等)
bash
readelf -s main.o
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
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 37 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND add
关键信息:
main:函数,全局符号,定义在Section 1(.text),大小37字节add:未定义符号(UND),需要链接时解析
注意:
.symtab:链接/调试更完整的符号表(常见于 .o 和未 strip 的文件)
.dynsym:运行时动态链接器需要的子集符号表(常见于可执行文件/so)
3.2.6 .strtab - 字符串表
存储内容:符号表中用到的字符串
bash
readelf -x .strtab main.o
Hex dump of section '.strtab':
0x00000000 006d6169 6e2e6300 6d61696e 00616464 .main.c.main.add
0x00000010 00 .
所有字符串以\0分隔,符号表通过偏移量引用字符串。
3.2.7 .rel.text / .rela.text - 重定位表
存储内容:需要重定位的位置信息
- REL:重定位项里没有 Addend,Addend 存在于被重定位的位置
- RELA:重定位项里带 Addend 字段(x86-64 常用 RELA)
bash
readelf -r main.o
Relocation section '.rela.text' at offset 0x318 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000001f 000900000004 R_X86_64_PLT32 0000000000000000 add - 4
000000000028 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 0
00000000002d 000a00000004 R_X86_64_PLT32 0000000000000000 printf - 4
解释:
- 偏移
0x1f处需要填入add函数的地址 - 偏移
0x28处需要填入.rodata的地址 - 偏移
0x2d处需要填入printf函数的地址
四、静态链接原理
4.1 链接的核心任务
链接器要完成两个主要任务:
- 符号解析(Symbol Resolution):找到所有未定义符号的定义
- 重定位(Relocation):修正代码和数据中的地址引用
4.2 符号表详解
4.2.1 符号的类型
全局符号(Global Symbols):
- 可以被其他目标文件引用
- 函数定义、全局变量定义
局部符号(Local Symbols):
- 只在本文件内部可见
- static函数、static变量
外部符号(External Symbols):
- 在本文件中使用,但在其他文件中定义
- extern声明的符号
示例:
c
// main.c
int global_var = 10; // 全局符号(定义)
static int static_var = 20; // 局部符号
extern int add(int, int); // 外部符号(未定义)
int main() {
return add(global_var, static_var);
}
c
// add.c
int add(int a, int b) { // 全局符号(定义)
return a + b;
}
查看符号:
bash
nm main.o
0000000000000000 D global_var
0000000000000000 T main
0000000000000004 d static_var
U add
nm add.o
0000000000000000 T add
符号类型标识:
T- 定义在.text段的全局符号D- 定义在.data段的全局符号d- 定义在.data段的局部符号U- 未定义符号
4.2.2 符号解析过程
链接器的工作:
bash
1. 扫描所有输入的.o文件,收集所有符号
main.o: 定义了 main, global_var
引用了 add
add.o: 定义了 add
2. 建立符号表
符号名 定义位置 类型
main main.o FUNC
global_var main.o OBJECT
add add.o FUNC
3. 解析符号引用
main.o中引用的add → 在add.o中找到定义 ✓
4. 如果有未定义符号 → 链接失败
链接失败示例:
c
// main.c
extern int foo();
int main() {
return foo(); // foo未定义
}
bash
gcc main.c -o main
/usr/bin/ld: /tmp/ccXXXXXX.o: undefined reference to `foo'
collect2: error: ld returned 1 exit status
4.3 地址重定位
4.3.1 为什么需要重定位
问题本质:
在编译阶段,编译器并不知道函数或变量在最终可执行文件中的真实地址。
因此,目标文件(.o)中的地址都是"临时的" ,这些地址在链接完成前是无法确定的。
示意说明:
bash
main.o 中的代码(逻辑视角):
地址0x00: push %rbp
地址0x01: mov %rsp,%rbp
...
地址0x1e: call 0x00000000 ← 这里要调用 add,但 add 的地址未知
bash
add.o 中的代码:
地址0x00: push %rbp
地址0x01: mov %rsp,%rbp
...
此时存在两个关键事实:
main.o和add.o是分别编译的main.o并不知道add最终会被放到哪里
链接完成后:
bash
最终可执行文件中:
main 从地址 0x400430 开始
add 从地址 0x400460 开始
地址 0x40044e: call 0x400460 ← 链接器填入 add 的真实地址
📌 结论:
编译阶段无法确定地址
👉 链接阶段必须"回过头来"修改指令中的地址
👉 这个过程就叫 地址重定位(Relocation)
4.3.2 重定位表的作用
重定位表(Relocation Table) 的作用是:
明确告诉链接器:
- 哪些指令位置需要修改
- 使用哪个符号的地址
- 采用什么重定位方式
查看目标文件的重定位表:
bash
readelf -r main.o
bash
Relocation section '.rela.text' at offset 0x318 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000001f 000900000004 R_X86_64_PLT32 0000000000000000 add - 4
字段解释:
-
Offset:
0x1f→ 需要修改
.text段中 偏移 0x1f 处 的指令操作数 -
Type:
R_X86_64_PLT32→ 表示 x86-64 平台的 32 位 PC 相对重定位(常用于函数调用)
-
Sym. Name:
add→ 要引用的目标符号
-
Addend:
-4→ 用于修正相对地址计算(与指令长度有关)
x86-64 下的重定位计算公式:
text
重定位值 = 符号地址 + Addend - 重定位位置
📌 对 call 指令来说,
最终编码的是 "目标地址 - 下一条指令地址"。
4.3.3 实战:观察重定位过程
下面通过一个最小示例,完整观察重定位是如何发生的。
准备代码:
c
// main.c
extern int add(int a, int b);
int main() {
return add(3, 5);
}
c
// add.c
int add(int a, int b) {
return a + b;
}
只编译,不链接:
bash
gcc -c main.c -o main.o
gcc -c add.c -o add.o
反汇编 main.o:
bash
objdump -d main.o
bash
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 03 00 00 00 mov $0x3,%edi
9: be 05 00 00 00 mov $0x5,%esi
e: e8 00 00 00 00 callq 13 <main+0x13>
↑
call 的目标地址是 0(占位符)
13: 5d pop %rbp
14: c3 retq
👉 此时 call 指令中的偏移量还没有被填充。
查看 main.o 的重定位表:
bash
readelf -r main.o
bash
Relocation section '.rela.text' at offset 0x200 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000f 000500000004 R_X86_64_PLT32 0000000000000000 add - 4
↑
需要修正 call 指令的操作数字段
执行链接:
bash
gcc main.o add.o -o main
查看链接后的反汇编:
bash
objdump -d main
bash
0000000000401106 <main>:
401106: 55 push %rbp
401107: 48 89 e5 mov %rsp,%rbp
40110a: bf 03 00 00 00 mov $0x3,%edi
40110f: be 05 00 00 00 mov $0x5,%esi
401114: e8 07 00 00 00 callq 401120 <add>
↑
链接器已完成重定位
401119: 5d pop %rbp
40111a: c3 retq
bash
0000000000401120 <add>:
401120: 55 push %rbp
401121: 48 89 e5 mov %rsp,%rbp
...
验证重定位计算是否正确:
text
call 指令地址: 0x401114
下一条指令地址: 0x401114 + 5 = 0x401119
call 操作数: 0x7
目标地址: 0x401119 + 0x7 = 0x401120
✔ 成功跳转到 add 函数
📌 本节小结:
.o文件中的地址都是未定的- 重定位表精确描述"哪里需要改、怎么改"
- 链接器根据重定位表填充真实地址
- 这是静态链接和动态链接的共同基础
4.4 Section的合并
链接时的Section合并:

五、ELF加载与执行视图
5.1 链接视图vs执行视图
ELF文件有两种视图:
链接视图(Section):
- 编译和链接时使用
- 以Section为单位
- 通过Section Header Table描述
执行视图(Segment):
- 程序加载时使用
- 以Segment为单位
- 通过Program Header Table描述
bash
链接时:关注Section的内容和符号
┌───────────────────┐
│ Section Header │
│ .text .data .bss │ ← 链接器看这个
└───────────────────┘
运行时:关注内存如何布局
┌───────────────────┐
│ Program Header │
│ LOAD LOAD DYNAMIC │ ← 加载器看这个
└───────────────────┘
5.2 查看Program Header
bash
readelf -l a.out
Elf file type is EXEC (Executable file)
Entry point 0x401040
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R 0x8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000448 0x0000000000000448 R E 0x200000
LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x0000000000000228 0x0000000000000230 RW 0x200000
DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
0x00000000000001d0 0x00000000000001d0 RW 0x8
...
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .dynamic .got .got.plt .data .bss
04 .dynamic
关键信息:
LOAD类型:表示需要加载到内存VirtAddr:虚拟地址FileSiz:文件中的大小MemSiz:内存中的大小(.bss会使MemSiz > FileSiz)Flags:R(可读) W(可写) E(可执行)
5.3 为什么要合并Section为Segment
原因:减少内存碎片,提高效率!
bash
如果每个Section单独映射:
.text → 4KB页
.rodata → 4KB页
.data → 4KB页
.bss → 4KB页
共需要4个页表项
合并为Segment:
Segment 1 (.text + .rodata) → 需要的页数
Segment 2 (.data + .bss) → 需要的页数
减少页表项数量
合并原则:相同属性的Section合并
bash
可读可执行的Section → 合并为一个Segment
.text
.init
.fini
.rodata (有些系统会独立出来)
可读可写的Section → 合并为另一个Segment
.data
.bss
5.4 程序入口点
bash
readelf -h a.out | grep Entry
Entry point address: 0x401040
这个地址是什么?
bash
objdump -d a.out | grep -A 5 "401040"
0000000000401040 <_start>:
401040: 31 ed xor %ebp,%ebp
401042: 49 89 d1 mov %rdx,%r9
401045: 5e pop %rsi
401046: 48 89 e2 mov %rsp,%rdx
401049: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
程序的真正入口是_start,不是main!
_start函数会:
- 初始化运行环境
- 调用动态链接器(如果有动态库)
- 调用
main函数 - 调用
exit退出
六、静态库的链接
6.1 静态库的本质
静态库(.a)是.o文件的归档包!
bash
ar -t libmystdio.a
my_stdio.o
my_string.o
创建静态库:
bash
ar -rc libmystdio.a my_stdio.o my_string.o
静态库 .a 本质是 ar archive,里面按成员形式存放多个 .o。
它和 tar类似都是"打包归档",但格式不同,工具链也不同(ar/ranlib)
6.2 链接静态库的过程
bash
gcc main.o -L. -lmystdio -o main
链接器的工作:
bash
1. 解析命令行
输入:main.o
库:libmystdio.a
2. 从main.o中收集未定义符号
main.o: 需要 my_strlen, mfopen, mfwrite, mfclose
3. 在libmystdio.a中查找定义
解包libmystdio.a:
my_stdio.o: 定义了 mfopen, mfwrite, mfclose
my_string.o: 定义了 my_strlen
4. 提取需要的.o文件
只提取 my_stdio.o 和 my_string.o
5. 链接所有.o文件
main.o + my_stdio.o + my_string.o → main
注意:只提取需要的.o文件!
bash
# 如果静态库中有10个.o文件
# 但你只用了其中2个.o中的函数
# 链接器只会提取这2个.o文件
6.3 验证静态链接
编译链接:
bash
gcc main.c -L. -lmystdio -o main
查看符号:
bash
nm main | grep my_
0000000000401156 T mfclose
0000000000401136 T mfflush
00000000004010c6 T mfopen
0000000000401166 T mfwrite
0000000000401202 T my_strlen
所有符号都在可执行文件中!
删除静态库:
bash
rm libmystdio.a
./main # 仍然可以运行!
证明代码已经被链接进可执行文件!
七、链接时的符号解析规则
7.1 强符号与弱符号
强符号(Strong Symbol):
- 函数定义
- 已初始化的全局变量
弱符号(Weak Symbol):
- 未初始化的全局变量
c
// strong.c
int strong_var = 10; // 强符号
int weak_var; // 弱符号
void func() { // 强符号
// ...
}
7.2 符号解析规则
规则1:不允许多个强符号
c
// file1.c
int x = 1;
// file2.c
int x = 2; // 错误:重复定义
gcc file1.c file2.c
/usr/bin/ld: multiple definition of `x'
规则2:如果有一个强符号和多个弱符号,选择强符号
c
// file1.c
int x = 1; // 强符号
// file2.c
int x; // 弱符号
gcc file1.c file2.c # 成功,使用x=1
规则3:如果有多个弱符号,任选一个
c
// file1.c
int x; // 弱符号
// file2.c
int x; // 弱符号
gcc file1.c file2.c # 成功,但行为未定义
警告:这会导致难以发现的bug!
c
// file1.c
int x; // 被分配为4字节
// file2.c
double x; // 需要8字节,但只分配了4字节!
// 访问x可能导致内存溢出
7.3 避免符号冲突
方法1:使用static限制作用域
c
// file1.c
static int x = 1; // 仅在file1.c可见
// file2.c
static int x = 2; // 仅在file2.c可见,不冲突
方法2:使用命名空间(C++)
cpp
// file1.cpp
namespace lib1 {
int x = 1;
}
// file2.cpp
namespace lib2 {
int x = 2;
}
方法3:添加前缀
c
// file1.c
int lib1_x = 1;
// file2.c
int lib2_x = 2;
八、实战:分析完整的链接过程
8.1 准备测试代码
c
// main.c
#include <stdio.h>
extern int add(int a, int b);
extern int sub(int a, int b);
int global = 100;
int main() {
int result1 = add(global, 50);
int result2 = sub(global, 30);
printf("add: %d, sub: %d\n", result1, result2);
return 0;
}
c
// math.c
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
8.2 编译但不链接
bash
gcc -c main.c -o main.o
gcc -c math.c -o math.o
8.3 查看符号表
bash
# main.o的符号
nm main.o
U add
0000000000000000 D global
0000000000000000 T main
U printf
U sub
# math.o的符号
nm math.o
0000000000000000 T add
0000000000000015 T sub
8.4 查看重定位表
bash
readelf -r main.o
Relocation section '.rela.text' at offset 0x1f8 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000010 000500000002 R_X86_64_PC32 0000000000000000 global - 4
00000000001a 000800000004 R_X86_64_PLT32 0000000000000000 add - 4
000000000024 000500000002 R_X86_64_PC32 0000000000000000 global - 4
00000000002e 000900000004 R_X86_64_PLT32 0000000000000000 sub - 4
000000000038 000a00000004 R_X86_64_PLT32 0000000000000000 printf - 4
5个位置需要重定位!
8.5 链接
bash
gcc main.o math.o -o main
8.6 查看链接后的符号
bash
nm main
0000000000401136 T add
0000000000404028 D global
0000000000401106 T main
U printf@@GLIBC_2.2.5
000000000040114b T sub
所有符号都有了地址!
8.7 反汇编对比
链接前:
bash
objdump -d main.o | grep -A 10 "<main>"
0000000000000000 <main>:
0: push %rbp
1: mov %rsp,%rbp
...
10: mov 0x0(%rip),%eax # 引用global,地址为0
1a: callq 1f <main+0x1f> # 调用add,地址未定
链接后:
bash
objdump -d main | grep -A 10 "<main>"
0000000000401106 <main>:
401106: push %rbp
401107: mov %rsp,%rbp
...
401116: mov 0x2f0c(%rip),%eax # 404028 <global>
401120: callq 401136 <add>
地址都被正确填入了!
九、工具大全
9.1 readelf - 查看ELF信息
bash
# 查看ELF头
readelf -h file.o
# 查看Section Header
readelf -S file.o
# 查看Program Header
readelf -l a.out
# 查看符号表
readelf -s file.o
# 查看重定位表
readelf -r file.o
# 查看动态段
readelf -d libxxx.so
9.2 objdump - 反汇编工具
bash
# 反汇编代码段
objdump -d file.o
# 反汇编所有段
objdump -D file.o
# 显示Section信息
objdump -h file.o
# 显示符号表
objdump -t file.o
# 显示动态符号表
objdump -T libxxx.so
# 查看Section的十六进制内容
objdump -s -j .rodata file.o
9.3 nm - 查看符号
bash
# 查看符号表
nm file.o
# 只显示未定义符号
nm -u file.o
# 显示符号大小
nm -S file.o
# 动态符号(用于动态库)
nm -D libxxx.so
# C++符号解码
nm -C file.o
9.4 ar - 静态库工具
bash
# 创建静态库
ar -rc libxxx.a file1.o file2.o
# 查看静态库内容
ar -t libxxx.a
# 详细查看
ar -tv libxxx.a
# 提取.o文件
ar -x libxxx.a
# 删除.o文件
ar -d libxxx.a file.o
9.5 ldd - 查看动态库依赖
bash
# 查看程序依赖的动态库
ldd a.out
# 查看动态库的依赖
ldd libxxx.so
9.6 file - 识别文件类型
bash
file main.o
main.o: ELF 64-bit LSB relocatable, x86-64
file a.out
a.out: ELF 64-bit LSB executable, x86-64
file libxxx.so
libxxx.so: ELF 64-bit LSB shared object, x86-64
十、总结
本文深入讲解了ELF格式和静态链接原理:
核心知识点:
-
ELF文件结构
- 四部分:ELF Header、Program Header Table、Sections、Section Header Table
- 常见Section:.text、.data、.bss、.rodata、.symtab、.strtab
- 链接视图(Section)vs执行视图(Segment)
-
符号表
- 全局符号、局部符号、外部符号
- 强符号与弱符号
- 符号解析规则
-
静态链接过程
- 符号解析:找到所有未定义符号的定义
- 重定位:修正代码和数据中的地址引用
- Section合并:相同属性的Section合并为Segment
-
重定位机制
- 重定位表记录需要修改的位置
- 链接器根据重定位表填入正确地址
- 不同的重定位类型(R_X86_64_PC32、R_X86_64_PLT32等)
-
静态库链接
- 静态库是.o文件的归档
- 只提取需要的.o文件
- 代码被完全链接进可执行文件
完整的静态链接流程图:
bash
源文件(.c)
↓ gcc -c
目标文件(.o)
↓
├→ 符号表(定义了哪些符号,引用了哪些符号)
└→ 重定位表(哪些位置需要修改)
链接器工作:
1. 收集所有符号 → 建立全局符号表
2. 解析符号引用 → 找到每个未定义符号的定义
3. 合并Section → .text合并、.data合并...
4. 重定位 → 根据重定位表修正地址
5. 生成可执行文件 → 包含Program Header
💡 思考题
- 为什么.bss段不占用文件空间?
- 如果两个.o文件定义了同名的全局变量,链接会发生什么?
- 静态库中的所有.o文件都会被链接进可执行文件吗?
- 重定位表中的Addend字段有什么作用?
下一篇我们将探索动态链接的奥秘,揭示GOT/PLT机制!