Appearance
程序的编译过程
上一章我们安装了 gcc 编译器和 CLion IDE,这一章我们要深入理解:从源代码到可执行程序,中间发生了什么?
概述
一个 C 源文件(.c)变成可执行文件(.exe 或 Linux ELF),经历四个阶段:
源代码(.c) → 预处理(.i) → 编译(.s) → 汇编(.o) → 链接 → 可执行文件第一阶段:预处理(Preprocessing)
预处理器处理以 # 开头的指令:
c
#include <stdio.h> // 把头文件内容"粘贴"进来
#define PI 3.14159 // 把 PI 全部替换为 3.14159
#ifdef DEBUG // 条件编译可以用下面的命令只看预处理结果:
bash
gcc -E main.c -o main.i打开 main.i 看看——你会发现头文件 stdio.h 的内容全部展开了,洋洋洒洒几百行。
第二阶段:编译(Compilation)
这是最核心的阶段。编译器(cc1)做三件事:
- 词法分析 — 把代码拆成 token(关键字、标识符、运算符)
- 语法分析 — 检查语法,生成语法树
- 生成汇编代码 — 把 C 语言翻译成汇编指令
bash
gcc -S main.i -o main.s看看 main.s,你会看到熟悉的 mov、push、call 等汇编指令。
这才是"编译器"真正干的事。前面预处理、后面汇编链接,都不是它的活。
第三阶段:汇编(Assembly)
汇编器(as)把汇编代码翻译成机器码,生成目标文件(Object File):
bash
gcc -c main.s -o main.o或者一步到位:
bash
gcc -c main.c -o main.o.o 文件已经是二进制了,但还不能直接运行——因为它里面的函数调用地址还是"待定"的符号。
查看目标文件里的符号:
bash
nm main.o你会看到类似这样的输出:
00000000 T main
U printfT main 表示 main 函数已定义(Text 段),U printf 表示 printf 是未定义的外部符号——等着链接器去找到它。
第四阶段:链接(Linking)
链接器(ld)把多个目标文件和库文件拼接成一个可执行文件:
bash
gcc main.o -o main.exe链接器干两件事:
| 任务 | 说明 |
|---|---|
| 符号解析 | 找到所有 U 符号的定义(比如 printf 在 libc 里) |
| 重定位 | 把所有符号引用替换为实际内存地址 |
如果只编译不链接,会看到这样的错误:
text
undefined reference to 'printf'意思就是链接器没找到 printf 的实现。
动手试试
用一个最简单的程序走一遍全流程:
c
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
int x = 3, y = 4;
printf("%d + %d = %d\n", x, y, add(x, y));
return 0;
}bash
# 一步到位
gcc test.c -o test.exe
# 或者分步查看过程
gcc -E test.c -o test.i # 预处理
gcc -S test.i -o test.s # 编译
gcc -c test.s -o test.o # 汇编
gcc test.o -o test.exe # 链接
# 查看符号
nm test.o
# 查看文件类型
file test.o # ELF 可重定位文件
file test.exe # ELF 可执行文件多文件编译
实际项目不可能只有一个文件。例如:
bash
# main.c 调用 add.c 里的函数
gcc -c main.c -o main.o
gcc -c add.c -o add.o
gcc main.o add.o -o program.exe链接器会把 main.o 和 add.o 合并,解决跨文件的符号引用。
小结
预处理 编译 汇编 链接
源文件 ───────→ 纯 C ──────→ 汇编 ──────→ 目标文件 ────→ 可执行文件
.c .i .s .o .exe/.out有了这个基础,下一章就能理解为什么要有"静态库"和"动态库"了——它们本质上就是把 .o 文件打包起来,方便链接器复用。