06-链接器与可执行文件¶
重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐⭐ 学习时间:2天 必须掌握:是
为什么学这一章?¶
链接器是从目标文件(.o)到可执行文件的最后一步。许多开发者遇到的"未定义引用"、"重复定义"等错误都发生在链接阶段。理解链接器的原理能帮助你解决这些问题并优化程序结构。
学完这一章,你将能够: - ✅ 理解链接器的工作原理和符号解析过程 - ✅ 掌握静态链接与动态链接的区别 - ✅ 理解ELF可执行文件格式 - ✅ 解决常见的链接错误
📖 核心概念¶
1. 链接的整体过程¶
┌─────────────────────────────────────────────────────────────┐
│ 编译与链接流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ main.c ─→ [编译] ─→ main.o ──┐ │
│ utils.c ─→ [编译] ─→ utils.o ─┼→ [链接器] ─→ a.out │
│ math.c ─→ [编译] ─→ math.o ──┤ │
│ libm.a(静态库)──────────────┘ │
│ │
│ 链接器的核心工作: │
│ 1. 符号解析:将引用与定义配对 │
│ 2. 重定位:调整地址引用 │
│ 3. 段合并:将同类段合并 │
│ │
└─────────────────────────────────────────────────────────────┘
2. 目标文件与符号表¶
// === main.c ===
#include <stdio.h>
extern int global_var; // 外部符号引用
extern int add(int, int); // 外部函数引用
static int local_var = 10; // 局部符号(其他文件不可见)
int main() {
int result = add(global_var, local_var);
printf("result = %d\n", result);
return 0;
}
# 编译为目标文件
gcc -c main.c -o main.o
gcc -c math.c -o math.o
# 查看符号表
nm main.o
# 输出示例:
# U add ← U=未定义(需要链接器解析)
# U global_var ← U=未定义
# 0000000000000000 T main ← T=text段中的全局符号
# 0000000000000000 d local_var ← d=data段中的局部符号
nm math.o
# 输出示例:
# 0000000000000000 T add ← T=全局函数定义
# 0000000000000000 D global_var ← D=全局数据定义
# 链接
gcc main.o math.o -o program
./program
3. 符号解析规则¶
┌─────────────────────────────────────────────────────────────┐
│ 符号解析规则 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 符号类型: │
│ ├── 强符号:函数定义、已初始化的全局变量 │
│ └── 弱符号:未初始化的全局变量 │
│ │
│ 解析规则: │
│ 规则1:不允许有多个同名强符号(报错:重复定义) │
│ 规则2:若有强符号和弱符号,选择强符号 │
│ 规则3:若全是弱符号,选择占空间最大的那个 │
│ │
│ 常见错误: │
│ ├── undefined reference → 符号只有引用没有定义 │
│ ├── multiple definition → 同名强符号在多个文件中定义 │
│ └── 类型不匹配 → 弱符号合并时大小不一致 │
│ │
└─────────────────────────────────────────────────────────────┘
// 常见链接错误示例
// --- file1.c ---
int x = 100; // 强符号
// --- file2.c ---
int x = 200; // 强符号 → 链接错误:multiple definition of 'x'
// --- file3.c ---
int x; // 弱符号(未初始化)
// 如果file1和file3一起链接,x使用file1的定义(值为100)
4. ELF文件格式¶
┌─────────────────────────────────────────────────────────────┐
│ ELF文件结构(可执行文件) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ ELF Header │ 魔数、类型、入口地址、段表偏移 │
│ ├──────────────────┤ │
│ │ Program Headers │ 运行时视图:描述段(segment) │
│ ├──────────────────┤ │
│ │ .text │ 代码段:机器指令 │
│ ├──────────────────┤ │
│ │ .rodata │ 只读数据:字符串常量等 │
│ ├──────────────────┤ │
│ │ .data │ 已初始化的全局/静态变量 │
│ ├──────────────────┤ │
│ │ .bss │ 未初始化的全局/静态变量(不占空间) │
│ ├──────────────────┤ │
│ │ .symtab │ 符号表 │
│ ├──────────────────┤ │
│ │ .strtab │ 字符串表 │
│ ├──────────────────┤ │
│ │ .rel.text │ 重定位表 │
│ ├──────────────────┤ │
│ │ Section Headers │ 链接时视图:描述节(section) │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
# 分析ELF文件
# 查看ELF头
readelf -h program
# 查看段表(section headers)
readelf -S program
# 查看程序头(program headers)
readelf -l program
# 查看符号表
readelf -s program
# 查看重定位条目
readelf -r main.o
# 查看段大小
size program
5. 静态链接 vs 动态链接¶
// === mylib.h ===
#ifndef MYLIB_H
#define MYLIB_H
int my_add(int a, int b);
int my_mul(int a, int b);
#endif
// === mylib.c ===
#include "mylib.h" // 引入头文件
int my_add(int a, int b) { return a + b; }
int my_mul(int a, int b) { return a * b; }
// === app.c ===
#include <stdio.h>
#include "mylib.h"
int main() {
printf("add: %d\n", my_add(3, 4));
printf("mul: %d\n", my_mul(3, 4));
return 0;
}
# ===== 静态链接 =====
# 编译目标文件
gcc -c mylib.c -o mylib.o
# 创建静态库
ar rcs libmylib.a mylib.o
# 静态链接
gcc app.c -L. -lmylib -static -o app_static
ls -la app_static # 文件较大,包含库代码
# ===== 动态链接 =====
# 创建共享库
gcc -fPIC -shared mylib.c -o libmylib.so
# 动态链接
gcc app.c -L. -lmylib -o app_dynamic
ls -la app_dynamic # 文件较小,运行时加载库
# 查看动态依赖
ldd app_dynamic
# 运行时需指定库路径
LD_LIBRARY_PATH=. ./app_dynamic
┌─────────────────────────────────────────────────────────────┐
│ 静态链接 vs 动态链接对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 特性 静态链接 动态链接 │
│ ───────── ────────── ────────── │
│ 文件大小 大(包含库代码) 小(仅引用) │
│ 运行时依赖 无 需要.so文件 │
│ 内存占用 每个进程一份 多进程共享 │
│ 更新库 需重新编译 替换.so即可 │
│ 加载速度 快(无需运行时链接) 略慢(动态加载) │
│ 部署 简单(自包含) 需同时部署库 │
│ │
└─────────────────────────────────────────────────────────────┘
6. 重定位过程¶
// 重定位示例说明
// 编译阶段:main.o中调用add()的地址是0(占位符)
// 0x15: e8 00 00 00 00 call 0 ← 地址待填充
//
// 链接阶段:链接器确定add()在最终可执行文件中的地址
// 0x401015: e8 16 00 00 00 call 0x401030 ← 填入实际地址
//
// 重定位类型:
// R_X86_64_PC32 - 相对地址重定位(常用于函数调用)
// R_X86_64_32 - 绝对地址重定位
// R_X86_64_PLT32 - PLT重定位(动态链接)
7. 链接器脚本(进阶)¶
/* 简单的链接器脚本示例 simple.ld */
ENTRY(main) /* 入口函数 */
SECTIONS
{
. = 0x400000; /* 起始地址 */
.text : { *(.text) } /* 代码段 */
.rodata : { *(.rodata) }/* 只读数据段 */
.data : { *(.data) } /* 数据段 */
.bss : { *(.bss) } /* BSS段 */
}
💡 面试常见问题¶
Q1:静态库和动态库的区别?各自的优缺点?¶
答:静态库(.a)在链接时被整合到可执行文件中,优点是独立部署、加载快,缺点是文件大、更新需重编译。动态库(.so/.dll)运行时加载,优点是文件小、多进程共享内存、可独立更新,缺点是运行时依赖、加载略慢。
Q2:什么是PIC(位置无关代码)?为什么动态库需要它?¶
答:PIC使代码无论加载到内存什么位置都能正确执行,通过GOT(全局偏移表)间接访问全局数据,通过PLT(过程链接表)间接调用外部函数。动态库加载地址不固定,必须使用PIC;否则每次加载都需要修改代码段的重定位,无法多进程共享。
Q3:解释 undefined reference 和 multiple definition 错误。¶
答:undefined reference表示有符号引用(如调用函数)但找不到定义,通常因为忘记链接包含定义的.o或.a文件、拼写错误、或C++名字修饰(需extern "C")。multiple definition表示同一强符号在多个编译单元中定义,需用static限制作用域或使用头文件中的inline。
Q4:什么是GOT和PLT?动态链接的延迟绑定是什么?¶
答:GOT(Global Offset Table)存放全局变量的实际地址;PLT(Procedure Linkage Table)用于函数调用的跳转桩。延迟绑定(Lazy Binding)指动态链接器不在程序启动时解析所有函数地址,而是在首次调用时才解析。通过LD_BIND_NOW=1可关闭延迟绑定。
Q5:C++中为什么会出现符号不匹配问题?extern "C"的作用?¶
答:C++支持函数重载,编译器对函数名做修饰(name mangling),如add(int,int)变成_Z3addii。而C编译器不做修饰,直接用add。extern "C"告诉C++编译器使用C的命名方式,确保C和C++代码能正确链接。
📝 本章小结¶
┌─────────────────────────────────────────────┐
│ 本章核心知识点 │
├─────────────────────────────────────────────┤
│ │
│ 1. 链接 = 符号解析 + 重定位 + 段合并 │
│ 2. 强符号vs弱符号决定多定义如何解析 │
│ 3. ELF格式:段(section)与段(segment) │
│ 4. 静态链接自包含,动态链接共享 │
│ 5. PIC/GOT/PLT实现位置无关代码 │
│ 6. nm/readelf/objdump/ldd是分析工具 │
│ │
└─────────────────────────────────────────────┘
回到目录:README.md