计算机操作系统 — 链接

😀前言
这篇文章希望带你从零开始,一步步理解编译系统的工作流程,深入掌握静态链接与动态链接的原理,让你不再只是“会用 gcc”,而是真正搞懂程序是如何被构建、加载、运行的。
🏠个人主页:尘觉主页
文章目录
- 计算机操作系统 — 链接
- 一、写在前面:那一行 `gcc hello.c` 背后的故事
- 二、从源代码到可执行文件的“旅程图”
- 三、阶段一:预处理(Preprocessing)
- 四、阶段二:编译(Compilation)
- 五、阶段三:汇编(Assembling)
- 六、阶段四:链接(Linking)
- 七、三种目标文件类型
- 八、静态链接 vs 动态链接
- 1️⃣ 静态链接(Static Linking)
- 2️⃣ 动态链接(Dynamic Linking)
- 九、运行时加载(Loader 的工作)
- 十、动手实验:探索编译的“黑箱”
- 十一、总结:从源码到执行的一条生命线
- 十二、延伸阅读
计算机操作系统 — 链接
一、写在前面:那一行 gcc hello.c 背后的故事
每个学编程的人几乎都从一句话开始:
#include <stdio.h>
int main() {printf("hello, world\n");return 0;
}
然后我们在命令行里输入:
gcc -o hello hello.c
./hello
屏幕输出:
hello, world
故事到这里似乎结束了,但对于操作系统来说,它才刚刚开始。
你有没有想过:这一行 gcc 命令,到底帮我们做了什么?
为什么写了 C 代码,最终却能变成 CPU 能执行的机器指令?
二、从源代码到可执行文件的“旅程图”
整个过程其实分为四个主要阶段:
预处理 → 编译 → 汇编 → 链接
可以理解为一个“流水线”:
hello.c ──预处理──> hello.i ──编译──> hello.s ──汇编──> hello.o ──链接──> hello
每一步都在让代码逐渐靠近“机器能懂”的世界。

三、阶段一:预处理(Preprocessing)
预处理就是在编译前“打扫战场”。
它主要做三件事:
- 展开
#include(把头文件内容插进来); - 展开
#define宏; - 删除注释、处理条件编译。
命令行可以单独查看预处理结果:
gcc -E hello.c -o hello.i
打开 hello.i,你会看到 #include <stdio.h> 被替换成一大堆标准库声明。
这一步结束后,源文件变成了纯净的 C 代码,没有宏、没有注释。
四、阶段二:编译(Compilation)
编译器真正的主角登场了。
它把人类可读的 C 语言翻译成 汇编语言(Assembly)。
汇编是“贴近机器但仍然能看懂”的语言。
执行:
gcc -S hello.i -o hello.s
此时的 hello.s 内容大概长这样:
.file "hello.c".section .rodata
.LC0:.string "hello, world".text.globl main
main:pushq %rbpmovq %rsp, %rbpleaq .LC0(%rip), %rdicall printfmovl $0, %eaxpopq %rbpret
可以看到,它已经开始接近机器操作了。
五、阶段三:汇编(Assembling)
汇编器负责把 .s 文件变成机器能执行的二进制指令。
这一阶段输出的是 .o 文件(object file,也叫“目标文件”)。
gcc -c hello.s -o hello.o
.o 文件里包含机器码和符号信息,但还不能运行,因为它还不知道 printf 在哪。
此时的代码里到处是“占位符”。
六、阶段四:链接(Linking)
链接器就像一位“总导演”,它要把所有“零散的片段”组装成完整的电影。
主要干两件事:
| 阶段 | 说明 |
|---|---|
| 符号解析(Symbol Resolution) | 找到每个函数或变量真正的定义,比如 printf 在哪里实现 |
| 重定位(Relocation) | 把所有符号引用替换成它们在内存中的真实地址 |
执行:
gcc -o hello hello.o
链接器会自动去找 libc 标准库,把 printf 解析到正确的函数地址中。
最终生成的 hello 文件就是可执行程序。
七、三种目标文件类型
在整个编译过程中,我们会遇到三种不同类型的目标文件:
| 类型 | 描述 | 示例 |
|---|---|---|
| 可重定位目标文件 (.o) | 中间产物,包含机器码和符号信息,不能直接运行 | hello.o |
| 可执行目标文件 | 链接完成后生成,可直接运行 | hello |
| 共享目标文件 (.so) | 特殊的可重定位文件,可在运行时被多个程序共享加载 | libc.so.6 |
八、静态链接 vs 动态链接
到这里,我们就得谈谈两种最重要的链接方式。
1️⃣ 静态链接(Static Linking)
静态链接器会把所有库的实现代码都复制进可执行文件中。
gcc -static -o hello_static hello.c
🔹 优点:
- 程序完全独立,不依赖外部库文件;
- 部署方便。
🔹 缺点:
- 可执行文件体积大;
- 库更新后需要重新编译;
- 内存中无法共享同一个库的副本。

2️⃣ 动态链接(Dynamic Linking)
动态链接不会把库代码复制进程序,而是让程序在运行时去共享加载。
例如:
gcc -o hello hello.c
ldd hello
输出:
linux-vdso.so.1 => (0x00007ffd26dff000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff51b700000)
表示程序运行时会加载系统里的 libc.so.6。
🔹 优点:
- 节省磁盘和内存(多个进程共享同一库);
- 库升级无需重新编译。
🔹 缺点:
- 程序依赖外部库文件;
- 库版本不兼容可能导致运行错误。

九、运行时加载(Loader 的工作)
当你输入 ./hello 时,发生的事比你想象的多:
- 操作系统把可执行文件加载进内存;
- 动态链接器找到所需的
.so库; - 完成符号解析(绑定函数地址);
- 跳转到
main(),开始执行。
动态链接还有两种策略:
| 类型 | 说明 |
|---|---|
| 立即绑定(Eager Binding) | 程序启动时就加载所有库函数地址 |
| 延迟绑定(Lazy Binding) | 只在第一次调用函数时才加载,提高启动速度 |
十、动手实验:探索编译的“黑箱”
| 命令 | 作用 |
|---|---|
gcc -E hello.c -o hello.i | 只做预处理 |
gcc -S hello.c -o hello.s | 生成汇编代码 |
gcc -c hello.c -o hello.o | 生成目标文件 |
nm hello.o | 查看符号表 |
ldd hello | 查看动态库依赖 |
objdump -d hello | 反汇编可执行文件 |
readelf -h hello | 查看 ELF 文件头 |
👉 小练习:
gcc -o hello hello.c
gcc -static -o hello_static hello.c
ls -lh
ldd hello
ldd hello_static
对比文件大小和依赖库的不同,你会更直观地理解静态与动态链接的差异。
十一、总结:从源码到执行的一条生命线
| 阶段 | 输入 | 输出 | 主要任务 |
|---|---|---|---|
| 预处理 | hello.c | hello.i | 展开宏与头文件 |
| 编译 | hello.i | hello.s | 生成汇编 |
| 汇编 | hello.s | hello.o | 转成机器码 |
| 链接 | hello.o | hello | 符号解析与重定位 |
🧭 一句话记忆:
链接 = 符号解析 + 重定位
静态 = 打包所有库
动态 = 运行时加载共享库
十二、延伸阅读
-
《深入理解计算机系统》(CS:APP)第七章
-
Linux 动态链接器
/lib64/ld-linux-x86-64.so.2 -
LD_PRELOAD与延迟绑定机制 -
自定义共享库实践:
gcc -fPIC -shared -o libmylib.so mylib.c gcc -o test test.c -L. -lmylib ldd ./test
😁热门专栏推荐
想学习vue的可以看看这个
java基础合集
数据库合集
redis合集
nginx合集
linux合集
手写机制
微服务组件
spring_尘觉
springMVC
mybits
等等等还有许多优秀的合集在主页等着大家的光顾感谢大家的支持
🤔欢迎大家加入我的社区 尘觉社区
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😁
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🍻
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🤞

