跳转至

06-链接器与可执行文件

重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐⭐ 学习时间:2天 必须掌握:是


为什么学这一章?

链接器是从目标文件(.o)到可执行文件的最后一步。许多开发者遇到的"未定义引用"、"重复定义"等错误都发生在链接阶段。理解链接器的原理能帮助你解决这些问题并优化程序结构。

学完这一章,你将能够: - ✅ 理解链接器的工作原理和符号解析过程 - ✅ 掌握静态链接与动态链接的区别 - ✅ 理解ELF可执行文件格式 - ✅ 解决常见的链接错误


📖 核心概念

1. 链接的整体过程

Text Only
┌─────────────────────────────────────────────────────────────┐
│                  编译与链接流程                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  main.c ─→ [编译] ─→ main.o ──┐                            │
│  utils.c ─→ [编译] ─→ utils.o ─┼→ [链接器] ─→ a.out       │
│  math.c ─→ [编译] ─→ math.o ──┤                            │
│  libm.a(静态库)──────────────┘                            │
│                                                             │
│  链接器的核心工作:                                          │
│  1. 符号解析:将引用与定义配对                               │
│  2. 重定位:调整地址引用                                     │
│  3. 段合并:将同类段合并                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2. 目标文件与符号表

C
// === 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;
}
C
// === math.c ===
int global_var = 42;            // 全局符号定义

int add(int a, int b) {         // 全局函数定义
    return a + b;
}
Bash
# 编译为目标文件
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. 符号解析规则

Text Only
┌─────────────────────────────────────────────────────────────┐
│                  符号解析规则                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  符号类型:                                                  │
│  ├── 强符号:函数定义、已初始化的全局变量                     │
│  └── 弱符号:未初始化的全局变量                              │
│                                                             │
│  解析规则:                                                  │
│  规则1:不允许有多个同名强符号(报错:重复定义)             │
│  规则2:若有强符号和弱符号,选择强符号                       │
│  规则3:若全是弱符号,选择占空间最大的那个                   │
│                                                             │
│  常见错误:                                                  │
│  ├── undefined reference → 符号只有引用没有定义              │
│  ├── multiple definition → 同名强符号在多个文件中定义        │
│  └── 类型不匹配         → 弱符号合并时大小不一致             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
C
// 常见链接错误示例

// --- 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文件格式

Text Only
┌─────────────────────────────────────────────────────────────┐
│              ELF文件结构(可执行文件)                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────┐                                      │
│  │   ELF Header     │  魔数、类型、入口地址、段表偏移       │
│  ├──────────────────┤                                      │
│  │ Program Headers  │  运行时视图:描述段(segment)        │
│  ├──────────────────┤                                      │
│  │   .text          │  代码段:机器指令                     │
│  ├──────────────────┤                                      │
│  │   .rodata        │  只读数据:字符串常量等               │
│  ├──────────────────┤                                      │
│  │   .data          │  已初始化的全局/静态变量              │
│  ├──────────────────┤                                      │
│  │   .bss           │  未初始化的全局/静态变量(不占空间)  │
│  ├──────────────────┤                                      │
│  │   .symtab        │  符号表                               │
│  ├──────────────────┤                                      │
│  │   .strtab        │  字符串表                             │
│  ├──────────────────┤                                      │
│  │   .rel.text      │  重定位表                             │
│  ├──────────────────┤                                      │
│  │ Section Headers  │  链接时视图:描述节(section)        │
│  └──────────────────┘                                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
Bash
# 分析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 动态链接

C
// === 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;
}
Bash
# ===== 静态链接 =====
# 编译目标文件
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
Text Only
┌─────────────────────────────────────────────────────────────┐
│            静态链接 vs 动态链接对比                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  特性          静态链接              动态链接                │
│  ─────────     ──────────            ──────────             │
│  文件大小      大(包含库代码)      小(仅引用)            │
│  运行时依赖    无                    需要.so文件             │
│  内存占用      每个进程一份          多进程共享              │
│  更新库        需重新编译            替换.so即可             │
│  加载速度      快(无需运行时链接)  略慢(动态加载)         │
│  部署          简单(自包含)        需同时部署库             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6. 重定位过程

C
// 重定位示例说明
// 编译阶段: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重定位(动态链接)
Bash
# 查看重定位条目
objdump -d -r main.o
# 可以看到call指令处有重定位标记

7. 链接器脚本(进阶)

Text Only
/* 简单的链接器脚本示例 simple.ld */
ENTRY(main)                  /* 入口函数 */

SECTIONS
{
    . = 0x400000;            /* 起始地址 */
    .text : { *(.text) }    /* 代码段 */
    .rodata : { *(.rodata) }/* 只读数据段 */
    .data : { *(.data) }    /* 数据段 */
    .bss : { *(.bss) }      /* BSS段 */
}
Bash
# 使用自定义链接器脚本
gcc -T simple.ld main.o math.o -o custom_program -nostdlib

💡 面试常见问题

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编译器不做修饰,直接用addextern "C"告诉C++编译器使用C的命名方式,确保C和C++代码能正确链接。


📝 本章小结

Text Only
┌─────────────────────────────────────────────┐
│              本章核心知识点                    │
├─────────────────────────────────────────────┤
│                                             │
│  1. 链接 = 符号解析 + 重定位 + 段合并       │
│  2. 强符号vs弱符号决定多定义如何解析        │
│  3. ELF格式:段(section)与段(segment)   │
│  4. 静态链接自包含,动态链接共享            │
│  5. PIC/GOT/PLT实现位置无关代码             │
│  6. nm/readelf/objdump/ldd是分析工具        │
│                                             │
└─────────────────────────────────────────────┘

回到目录README.md