第7章:链接(Linking)
一、导读
链接(Linking)是将多个目标文件和库文件组合成一个可执行文件的过程。它是程序从源代码到最终可运行程序的关键步骤之一,也是许多程序员容易忽视的底层机制。
学习目标
通过本章的学习,你将掌握以下核心能力:
理解链接的基本原理:了解编译器如何将多个源文件合并为一个可执行文件
掌握静态链接的完整过程:包括符号解析、重定位等关键步骤
理解目标文件的格式:熟悉ELF(Executable and Linkable Format)文件格式
掌握符号的概念:区分强符号与弱符号、全局符号与局部符号
理解动态链接的机制:了解共享库的工作原理和位置无关代码
能够分析和解决链接错误:如未定义引用、多重定义等常见问题
核心问题
- 为什么我们需要链接?为什么不把所有代码写在一个文件中?
- 链接器是如何将多个模块的函数调用正确关联的?
- 静态链接和动态链接有什么本质区别?
- 为什么有时候会出现"未定义引用"的链接错误?
- 共享库是如何在运行时被加载的?
重要性
链接是软件工程中不可或缺的一环。理解链接机制有助于:
- 编写大型项目时正确组织代码模块
- 诊断和修复链接阶段的错误
- 优化程序的加载时间和内存使用
- 理解库的版本管理和依赖关系
- 开发可复用、可维护的软件系统
二、核心概念详解
2.1 编译器驱动程序
实际的编译过程是由一系列工具协作完成的,这些工具统称为编译器驱动程序(Compiler Driver):
// 一个简单的程序 hello.c
#include <stdio.h>
int main() {
printf("hello, world\n");
return 0;
}当我们执行 gcc -o hello hello.c 时,实际上触发了以下四个阶段:
预处理阶段(cpp):处理 #include、#define 等预处理指令,生成 hello.c 的预处理文件
编译阶段(cc1):将预处理后的C代码翻译为汇编代码 hello.s
汇编阶段(as):将汇编代码翻译为机器语言指令,打包成目标文件 hello.o
链接阶段(ld):将目标文件与必要的库文件链接,生成最终的可执行文件 hello
源代码 hello.c
│
▼ [预处理器 cpp]
预处理文件 hello.cpp.c(文本)
│
▼ [编译器 cc1]
汇编文件 hello.s(文本)
│
▼ [汇编器 as]
可重定位目标文件 hello.o(二进制)
│
▼ [链接器 ld]
可执行目标文件 hello(二进制)2.2 静态链接详解
静态链接是最基本的链接方式。在静态链接中,链接器在编译阶段将所有需要的库代码复制到最终的可执行文件中。
静态链接的工作过程
考虑一个包含两个源文件的程序:
/* sum.c - 定义 sum 函数 */
int sum(int a[], int n) {
int i, s = 0;
for (i = 0; i < n; i++)
s += a[i];
return s;
}/* main.c - 主程序 */
int sum(int a[], int n);
int array[2] = {1, 2};
int main() {
int val = sum(array, 2);
return val;
}静态链接器处理这两个文件时的完整流程如下:
第一步:符号解析(Symbol Resolution)
链接器扫描所有目标文件,收集每个文件中定义的符号和引用的符号。对于上面的例子:
| 符号 | 定义文件 | 引用文件 | 类型 |
|---|---|---|---|
main | main.o | - | 全局 |
sum | sum.o | main.o | 全局 |
array | main.o | - | 全局 |
第二步:重定位(Relocation)
链接器确定了所有符号的位置后,需要修改代码和数据中的引用地址。例如,main 函数中调用 sum 的指令,最初引用的是一个占位地址,链接器需要将其修改为 sum 函数在最终可执行文件中的实际地址。
2.3 目标文件格式
现代Linux系统使用ELF(Executable and Linkable Format)格式作为标准的目標文件格式。ELF文件有多种类型:
- 可重定位目标文件(.o 文件):包含二进制代码和数据,但某些地址尚未确定
- 可执行目标文件:包含可以直接加载到内存中执行的二进制代码
- 共享目标文件(.so 文件):特殊的可重定位目标文件,可以在加载时或运行时被动态链接
ELF文件格式结构
一个典型的ELF文件由以下部分组成:
ELF头(ELF Header)
├── 魔数:0x7F 'E' 'L' 'F'
├── 文件类别(可重定位/可执行/共享)
├── 机器类型(x86/x86-64/ARM等)
├── 入口点地址
└── 段头表偏移
节头表(Section Header Table)
├── .text - 代码段
├── .rodata - 只读数据段
├── .data - 已初始化数据段
├── .bss - 未初始化数据段
├── .symtab - 符号表
├── .rel.text - 重定位信息
├── .strtab - 字符串表
└── ...使用 readelf 或 objdump 工具可以查看ELF文件的详细内容:
# 查看所有节的信息
readelf -S hello.o
# 查看符号表
readelf -s hello.o
# 查看重定位信息
readelf -r hello.o
# 反汇编代码段
objdump -d hello.o2.4 符号(Symbols)
符号是链接过程中最重要的概念之一。每个目标文件都有一个符号表,记录了所有全局符号的信息。
符号的分类
在C语言中,链接器可以看到以下类型的符号:
全局符号:由本模块定义,可供其他模块引用的函数和非全局变量。例如:
int global_var = 10; // 全局符号,其他文件可用 extern 引用
void public_func(void) {} // 全局符号外部符号:由其他模块定义,本模块需要引用的函数和变量。例如:
extern int external_var; // 外部符号,在其他文件中定义
extern void external_func(void); // 外部符号局部符号:在本模块中定义,但仅在本模块内可见的符号。使用 static 声明的函数和全局变量属于此类:
static int local_var = 5; // 局部符号
static void local_func(void) {} // 局部符号重要区别:局部链接符号(static变量/函数)与局部程序变量(栈上的自动变量)是完全不同的概念。前者在编译后保留在目标文件的符号表中,后者仅在运行时存在于栈上。
强符号与弱符号
ELF符号分为强符号和弱符号:
- 强符号:函数和已初始化的全局变量
- 弱符号:未初始化的全局变量
链接器处理多重定义符号的规则:
不允许有多个强符号:如果两个文件都定义了同名的已初始化全局变量或函数,链接器报错
一个强符号 + 多个弱符号:选择强符号
多个弱符号:选择占用空间最大的那个
/* 文件1 */
int x = 10; // 强符号
/* 文件2 */
int x = 20; // 强符号 → 链接错误:multiple definition of 'x'/* 文件1 */
int x; // 弱符号(未初始化)
/* 文件2 */
int x = 10; // 强符号 → 选择文件2的定义,x = 10/* 文件1 */
int x; // 弱符号
/* 文件2 */
int x; // 弱符号 → 选择占用空间最大的,这里都是4字节,取第一个2.5 静态库
当项目变大时,将每个模块编译为独立的目标文件会导致管理困难。静态库(Static Library)提供了一种将多个目标文件打包在一起的方式。
创建和使用静态库
# 创建 addvec.c 和 multvec.c 的目标文件
gcc -c addvec.c multvec.c
# 将目标文件打包为静态库
ar rcs libvector.a addvec.o multvec.o
# 使用静态库链接程序
gcc -o main main.c ./libvector.a
# 或者使用 -L 和 -l 选项
gcc -o main main.c -L. -lvectorar 命令的参数含义:
r:将文件插入归档中(替换已有的同名成员)c:创建归档文件s:为归档文件建立索引(等同于运行ranlib)
链接器解析静态库的算法
链接器在解析静态库时采用单遍扫描算法:
维护一个目标文件集合 S(初始为空)、未解析符号集合 U 和已定义符号集合 D
对于命令行上的每个输入文件:
- 如果是目标文件,直接加入 S,更新 U 和 D
- 如果是静态库,扫描库中的成员,将那些能解析 U 中未解析符号或定义 D 中需要的符号的成员加入 S
如果扫描结束后 U 非空,报告未定义符号错误
这个算法意味着库文件的顺序很重要:被依赖的库应该放在依赖它的文件之后。
# 正确:libm 在依赖它的文件之后
gcc -o prog main.c -L. -lmylib -lm
# 错误:libm 在依赖它的文件之前,可能导致未解析的数学函数
gcc -o prog main.c -lm -L. -lmylib2.6 重定位
重定位是链接器的核心工作之一。它的目的是将符号引用与符号定义关联起来,修改代码和数据中的地址引用。
重定位条目
每个重定位条目在 .rel.text 或 .rel.data 节中描述,包含以下信息:
typedef struct {
long offset; // 需要修改的位置的偏移
long type:32; // 重定位类型
long symbol:32; // 相关的符号表索引
long addend; // 常数加数
} Elf64_Rela;重定位过程
以 x86-64 为例,考虑以下代码:
/* main.c */
extern int sum(int[], int);
int array[2] = {1, 2};
int main() {
int val = sum(array, 2);
return val;
}编译后 main.o 中的调用指令可能如下:
call sum # e8 00 00 00 00(地址尚未确定,使用占位值0)链接器在处理这个调用时:
在符号表中找到 sum 的定义地址(假设在 sum.o 的 .text 节偏移 0x0 处)
确定 sum.o 的 .text 节在最终可执行文件中的运行时地址(假设为 0x400500)
计算相对偏移并修改 call 指令中的地址字段
2.7 可执行目标文件
可执行目标文件是链接过程的最终产物,它包含了将程序加载到内存并运行所需的所有信息。
可执行文件的内存映像
当Linux系统将一个可执行文件加载到内存时,它按照以下方式组织内存映像:
0x0000000000400000 ┌─────────────────────┐
│ ELF头 │
├─────────────────────┤
│ .text 段(代码) │
├─────────────────────┤
│ .rodata 段(只读) │
├─────────────────────┤
│ .data 段(已初始化) │
├─────────────────────┤
│ .bss 段(未初始化) │
├─────────────────────┤
│ 堆(向上增长) │
│ ↑ │
│ │ │
│ ↓ │
│ 栈(向下增长) │
├─────────────────────┤
│ 内核虚拟内存映射 │
├─────────────────────┤
0xFFFFFFFFFFFFFFFF └─────────────────────┘程序入口点
ELF头中的 e_entry 字段指定了程序的入口点地址。在Linux中,这通常是 _start 函数的地址,而不是 main 函数。_start 函数是由C运行时库(CRT)提供的,它负责初始化C运行时环境,然后调用 main 函数:
/* 简化的 _start 函数伪代码 */
void _start(void) {
__libc_start_main(main, argc, argv, ...);
}2.8 动态链接
动态链接是现代系统中最重要的链接方式。与静态链接不同,动态链接将库的链接过程推迟到程序加载时甚至运行时。
动态链接的优势
代码共享:多个进程可以共享同一份库代码的内存副本,节省内存
更新方便:更新库文件后,使用它的程序无需重新编译即可使用新版本
节省磁盘空间:可执行文件不包含库代码,体积更小
延迟加载:库只有在被实际使用时才加载
共享库的创建和使用
# 创建位置无关代码的目标文件
gcc -c -fPIC addvec.c multvec.c
# 创建共享库
gcc -shared -o libvector.so addvec.o multvec.o
# 使用共享库链接程序
gcc -o main main.c -L. -lvector
# 运行时指定共享库搜索路径
LD_LIBRARY_PATH=. ./main动态链接器的加载过程
当Linux加载一个使用共享库的程序时,执行以下步骤:
加载可执行文件:内核读取ELF头,将代码段和数据段映射到内存
加载共享库:动态链接器(/lib64/ld-linux-x86-64.so.2)被加载,它递归地加载程序依赖的所有共享库
重定位:动态链接器修改可执行文件和库中的代码和数据,使引用指向正确的运行时地址
控制权转移:将控制权传递给程序的入口点
2.9 位置无关代码(PIC)
共享库的一个关键要求是位置无关代码(Position-Independent Code, PIC)。因为共享库可能被加载到内存中的任意位置,所以库中的代码不能包含绝对地址引用。
PIC的实现原理
PIC通过全局偏移表(GOT, Global Offset Table)来实现位置无关的数据访问:
; 不使用PIC(绝对地址引用)
mov eax, [global_var] ; 直接引用绝对地址
; 使用PIC(通过GOT间接引用)
mov rbx, [rbx + global_var@GOTPCREL] ; 通过GOT间接引用
mov eax, [rbx]对于函数调用,PIC使用过程链接表(PLT, Procedure Linkage Table)实现延迟绑定(Lazy Binding):
; PLT条目
plt_func:
jmp *GOT_entry ; 第一次调用时跳转到解析器
push index ; 推送符号索引
jmp plt_resolver ; 跳转到动态链接器的解析函数2.10 延迟绑定(Lazy Binding)
延迟绑定是动态链接中的一项重要优化技术。它的核心思想是:只有在函数第一次被调用时,才进行地址解析和绑定。
延迟绑定的工作流程
程序调用一个外部函数时,实际跳转到PLT中的对应条目
PLT条目首先跳转到GOT中存储的地址
第一次调用时,GOT中存储的是PLT中下一条指令的地址
这导致控制权返回到PLT,PLT调用动态链接器的解析函数
解析函数查找函数的实际地址,更新GOT条目
函数实际执行
后续调用直接通过GOT跳转到函数实际地址,无需再次解析
这种机制的优点是:如果某个函数从未被调用,就无需花费时间解析它的地址。
三、重要知识点
3.1 符号解析的详细规则
符号解析是链接器将符号引用与符号定义匹配的过程。理解其规则对于诊断链接错误至关重要。
编译器如何分类符号
对于C语言源文件中的每个符号,编译器根据以下规则将其分类:
| 声明 | C语言中的位置 | 符号类型 |
|---|---|---|
int foo; | 在所有函数之外 | 弱符号 |
int foo = 1; | 在所有函数之外 | 强符号 |
extern int foo; | 在所有函数之外 | 外部符号(未定义) |
int foo; | 在函数内部 | 局部变量(非链接符号) |
static int foo; | 在所有函数之外 | 局部链接符号 |
void foo() {} | 顶层函数定义 | 强符号 |
extern void foo(); | 在所有函数之外 | 外部符号 |
实际案例分析
案例1:未定义引用错误
/* main.c */
#include <stdio.h>
void helper(void);
int main() {
helper();
return 0;
}$ gcc -o main main.c
/tmp/ccXXXXXX.o: In function `main':
main.c:(.text+0x12): undefined reference to `helper'
collect2: error: ld returned 1 exit status原因:helper 函数被引用但未定义。
案例2:多重定义错误
/* a.c */
int x = 10;
/* b.c */
int x = 20;$ gcc a.c b.c
/tmp/ccXXXXXX.o:(.data+0x0): multiple definition of `x'案例3:静态库顺序问题
/* main.c */
extern void foo(void);
extern void bar(void);
int main() { foo(); bar(); return 0; }
/* foo.c */
extern void bar(void);
void foo(void) { bar(); }
/* bar.c */
void bar(void) { printf("bar\n"); }# 错误:foo依赖bar,但libfoo在libbar之后扫描
$ gcc main.c -L. -lbar -lfoo
# foo中的bar引用未被解析
# 正确:被依赖的库放在后面
$ gcc main.c -L. -lfoo -lbar3.2 ELF节详解
使用 objdump 和 readelf 工具可以深入理解ELF文件的内部结构。
关键节的含义
$ objdump -h main.o输出中各节的含义:
| 节名 | 含义 | 内容 |
|---|---|---|
.text | 代码节 | 编译后的机器指令 |
.rodata | 只读数据节 | 常量字符串、const全局变量 |
.data | 已初始化数据节 | 有初始值的全局变量 |
.bss | 未初始化数据节 | 未初始化或初始化为0的全局变量 |
.symtab | 符号表 | 所有全局符号的信息 |
.rel.text | 代码重定位节 | 代码中需要修改的地址引用 |
.rel.data | 数据重定位节 | 数据中需要修改的地址引用 |
.strtab | 字符串表 | 符号名称的字符串存储 |
.shstrtab | 节名字符串表 | 节名称的字符串存储 |
符号表分析
$ readelf -s main.o符号表条目包含:
- Num:符号编号
- Value:符号的地址偏移
- Size:符号的大小
- Type:符号类型(FUNC/OBJECT/NOTYPE等)
- Bind:绑定类型(GLOBAL/LOCAL/WEAK)
- Vis:可见性
- Ndx:所在的节索引
- Name:符号名称
3.3 链接与静态库的高级话题
归档文件索引
静态库(归档文件)维护一个索引,记录每个成员文件中定义的符号。这个索引使得链接器可以快速定位包含特定符号定义的文件,而不需要扫描每个成员。
# 查看归档文件的索引
ar t libvector.a
# 输出:
# addvec.o
# multvec.o
# 重建索引
ranlib libvector.a链接顺序的实践建议
被依赖的库放在后面:如果A依赖B,则命令行中A在B之前
使用 `--start-group` 和 `--end-group`:处理循环依赖
```bash
gcc -o prog main.c -Wl,--start-group -la -lb -la -Wl,--end-group
```
使用 `pkg-config`:自动获取库的编译和链接参数
```bash
gcc -o prog main.c $(pkg-config --cflags --libs gtk+-3.0)
```
3.4 动态链接的深入分析
共享库的搜索路径
动态链接器按以下顺序搜索共享库:
DT_RPATH:可执行文件中的硬编码路径(已废弃)
LD_LIBRARY_PATH:环境变量指定的路径
DT_RUNPATH:可执行文件中的运行时路径
/etc/ld.so.cache:ldconfig维护的缓存文件
默认路径:/lib 和 /usr/lib
# 查看程序依赖的共享库
ldd ./main
# 更新共享库缓存
sudo ldconfig
# 查看共享库的详细信息
readelf -d libvector.so共享库的版本管理
Linux使用命名约定管理共享库的版本:
libvector.so → 链接时使用的名称(符号链接)
libvector.so.1 → 主版本号(符号链接)
libvector.so.1.0 → 实际的库文件- 主版本号变化表示不兼容的API变更
- 次版本号变化表示向后兼容的功能增加
3.5 链接与内存布局
地址空间布局
在x86-64 Linux系统中,进程的虚拟地址空间布局如下:
高地址
┌──────────────────────────┐ 0xFFFFFFFFFFFFFFFF
│ 内核空间 │
├──────────────────────────┤
│ 栈 │ ← 向下增长
│ ↓ │
│ │
│ ↑ │
│ 堆 │ ← 向上增长
├──────────────────────────┤
│ 共享库映射区 │
├──────────────────────────┤
│ .bss(未初始化数据) │
├──────────────────────────┤
│ .data(已初始化数据) │
├──────────────────────────┤
│ .rodata(只读数据) │
├──────────────────────────┤
│ .text(代码) │
├──────────────────────────┤ 0x00400000(默认加载地址)
│ 保留区域 │
└──────────────────────────┘ 0x0000000000000000
低地址地址随机化(ASLR)
现代Linux系统默认启用地址空间布局随机化(ASLR),每次运行程序时栈、堆和共享库的基地址都会随机化,以增加安全性。
# 查看ASLR状态
cat /proc/sys/kernel/randomize_va_space
# 临时关闭ASLR(调试用)
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space四、常见误区与难点
4.1 误区一:混淆链接时地址与运行时地址
错误理解:认为目标文件中的地址就是程序运行时的地址。
正确理解:
- 目标文件(.o)中的地址是相对于各节的偏移,不是最终地址
- 可执行文件中的地址是链接器确定的虚拟地址
- 使用共享库时,实际运行时地址可能因ASLR而不同
4.2 误区二:认为static只影响可见性
错误理解:认为 static 关键字只是让变量不可见。
正确理解:
- 对于全局变量/函数,
static确实限制了链接可见性(变为局部链接符号) - 但对于局部变量(函数内),
static改变了存储持续期(从自动变为静态) - 这两个含义完全不同,需要根据上下文区分
4.3 误区三:弱符号可以随意定义
错误理解:认为未初始化的全局变量可以随意在多个文件中定义。
正确理解:
- 虽然链接器允许这样做(选择一个定义),但这是危险的做法
- 不同文件中类型不一致的弱符号定义会导致难以调试的错误
- 最佳实践:使用
extern声明,只在一个文件中定义
/* 正确做法 */
/* header.h */
extern int global_count;
/* main.c */
int global_count = 0; // 唯一定义
/* other.c */
#include "header.h" // 使用extern声明引用4.4 难点:理解重定位的计算过程
重定位的计算涉及多个地址的加减运算,是链接中最复杂的部分。
关键公式(以x86-64的PC相对寻址为例):
重定位后的值 = S + A - P其中:
- S:符号的运行时地址
- A:重定位条目中的加数(addend)
- P:被重定位的存储单元的运行时地址
示例计算:
假设 main.o 中有一条调用 sum 的指令:
sum在sum.o的.text节中偏移为 0sum.o的.text节运行时地址为 0x400500- 调用指令位于
main.o的.text节偏移 0x12 处 main.o的.text节运行时地址为 0x400400
则:
- S = 0x400500(sum的运行时地址)
- A = -4(addend,x86-64 call指令的addend为-4)
- P = 0x400400 + 0x12 = 0x400412(call指令下一条指令的地址)
重定位后的值 = 0x400500 + (-4) - 0x400412 = 0x4000EA
4.5 难点:动态链接的延迟绑定机制
延迟绑定涉及PLT、GOT和动态链接器的协作,理解起来较为复杂。
简化理解:
PLT是函数调用的"中转站"
GOT是存储函数实际地址的"通讯录"
第一次调用时"通讯录"是空的,需要"秘书"(动态链接器)帮忙查找
查找后填入"通讯录",后续直接查找
五、实践应用
5.1 使用工具分析目标文件
objdump 实用技巧
# 查看目标文件的所有信息
objdump -a main.o
# 反汇编代码段
objdump -d main.o
# 显示所有节头
objdump -h main.o
# 显示完整的符号表
objdump -t main.o
# 显示重定位信息
objdump -r main.o
# 混合显示源代码和汇编
objdump -d -S main.oreadelf 实用技巧
# 显示ELF头
readelf -h main.o
# 显示所有节头
readelf -S main.o
# 显示符号表
readelf -s main.o
# 显示重定位信息
readelf -r main.o
# 显示动态段信息
readelf -d ./main
# 显示程序头(段信息)
readelf -l ./main5.2 诊断链接错误
常见链接错误及解决方法
1. 未定义引用(undefined reference)
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x12): undefined reference to `calculate'解决方法:
- 确认函数是否正确定义
- 检查是否遗漏了包含该函数定义的目标文件或库
- 检查库的链接顺序
2. 多重定义(multiple definition)
/usr/bin/ld: a.o:(.data+0x0): multiple definition of `x'; b.o:(.data+0x0): first defined here解决方法:
- 检查是否在多个文件中定义了同名的全局变量
- 使用
static限制变量的链接范围 - 使用
extern声明,只在一个文件中定义
3. 找不到库(cannot find -l)
/usr/bin/ld: cannot find -lmylib解决方法:
- 确认库文件存在
- 检查
-L路径是否正确 - 确认库文件名格式正确(
libmylib.a或libmylib.so)
5.3 创建和使用静态库的实践
# 1. 创建多个源文件的目标文件
gcc -c math_ops.c string_ops.c io_ops.c
# 2. 打包为静态库
ar rcs libmyutils.a math_ops.o string_ops.o io_ops.o
# 3. 查看库内容
ar t libmyutils.a
# 4. 使用库
gcc -o myapp main.c -L. -lmyutils
# 5. 查看库的符号
nm libmyutils.a5.4 创建和使用共享库的实践
# 1. 创建位置无关代码
gcc -c -fPIC -Wall math_ops.c string_ops.c
# 2. 创建共享库
gcc -shared -o libmyutils.so math_ops.o string_ops.o
# 3. 查看共享库信息
readelf -d libmyutils.so
objdump -p libmyutils.so
# 4. 链接程序
gcc -o myapp main.c -L. -lmyutils
# 5. 运行程序(指定库搜索路径)
LD_LIBRARY_PATH=. ./myapp
# 6. 或者将库安装到系统路径
sudo cp libmyutils.so /usr/local/lib/
sudo ldconfig5.5 链接在大型项目中的实践
在大型项目中,良好的链接实践至关重要:
模块化设计:将功能相关的代码组织为独立的模块
最小化依赖:减少模块间的耦合
使用头文件保护:防止头文件被多次包含
明确接口:通过头文件清晰定义模块接口
使用构建系统:利用CMake、Make等工具管理链接依赖
# CMakeLists.txt 示例
cmake_minimum_required(VERSION 3.10)
project(MyProject)
# 创建库
add_library(myutils STATIC
src/math_ops.c
src/string_ops.c
)
# 链接库
add_executable(myapp src/main.c)
target_link_libraries(myapp myutils)
# 设置包含目录
target_include_directories(myutils PUBLIC include/)六、本章小结
核心要点回顾
链接是将多个目标文件组合成可执行文件的过程,由链接器完成
静态链接在编译时完成,将所有需要的代码复制到可执行文件中
动态链接在运行时完成,多个进程共享同一份库代码
ELF格式是Linux标准的目標文件格式,包含代码、数据、符号表等信息
符号解析将引用与定义匹配,遵循强符号优先的规则
重定位修改代码和数据中的地址引用,使其指向正确的运行时地址
位置无关代码(PIC)使共享库可以被加载到任意地址
延迟绑定通过PLT和GOT实现,只在函数首次调用时才解析地址
关键工具
| 工具 | 用途 |
|---|---|
gcc | 编译器驱动程序 |
ar | 创建和管理静态库 |
objdump | 查看目标文件信息 |
readelf | 查看ELF文件详细信息 |
nm | 列出符号表 |
ldd | 查看共享库依赖 |
ldconfig | 配置共享库缓存 |
学习建议
- 动手使用
objdump和readelf分析自己编译的目标文件 - 尝试制造各种链接错误,理解错误信息
- 练习创建静态库和共享库
- 阅读CSAPP配套的CS:APP3e链接实验(Link Lab)