- 预处理
- 编译
- 汇编
- 链接
我们用一个简单的例子来贯穿整个过程。

(图片来源网络,侵删)
示例源程序
假设我们有一个名为 hello.c 的文件,内容如下:
// hello.c
#include <stdio.h>
#define MESSAGE "Hello, World!"
int main() {
printf("%s\n", MESSAGE);
return 0;
}
第一步:预处理
目标:处理源代码中的预处理指令,如 #include、#define 等,生成一个“纯”的C语言文件。
#include <stdio.h>:预处理器会找到标准输入输出库文件stdio.h的内容,并将其完整地复制粘贴到#include指令的位置。#define MESSAGE "Hello, World!":预处理器会把代码中所有出现的MESSAGE都替换成"Hello, World!"。
执行命令:
在Linux或macOS上,我们可以使用 gcc 的 -E 选项来只进行预处理。
gcc -E hello.c -o hello.i
这会生成一个名为 hello.i 的文件。hello.i 的内容看起来会是这样(省略了 stdio.h 中的大量内容):

(图片来源网络,侵删)
// hello.i (部分内容)
// ... (stdio.h 的内容被展开) ...
int main() {
printf("%s\n", "Hello, World!");
return 0;
}
关键点:
- 此时还没有进行真正的编译,只是文本替换和包含。
- 生成的
.i文件仍然是C语言源代码,但不再包含预处理指令。
第二步:编译
目标:将预处理后的C语言代码(.i 文件)翻译成汇编语言代码(.s 文件)。
- 编译器会进行语法分析、语义分析、词法分析等。
- 它会检查代码是否符合C语言的语法规则,变量是否定义,类型是否匹配等。
- 如果发现错误(比如拼写错误、缺少分号),编译过程会在此阶段终止并报告错误。
- 如果代码正确,编译器会将高级的C语言语句(如
printf,return)转换成等价的、低级的汇编语言指令。
执行命令:
使用 -S 选项来进行编译,生成汇编代码。
gcc -S hello.i -o hello.s
这会生成一个名为 hello.s 的文件。hello.s 的内容可能如下(不同平台的汇编语法不同):

(图片来源网络,侵删)
// hello.s (x86_64 架构示例)
.section __TEXT,__text,regular,pure_instructions
.globl _main
_main:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
movq %rsp, %rbp
.cfi_offset %rbp, -16
leaq L_.str(%rip), %rdi
movl $0, %eax
callq _printf
xorl %eax, %eax
popq %rbp
retq
.cfi_endproc
.section __TEXT,__cstring,cstring_literals
L_.str:
.ascii "Hello, World!\n" ;# "Hello, World!\n"
.subsections_via_symbols
关键点:
- 这是从人类可读的高级语言到机器可读的低级语言的第一次重大飞跃。
- 汇编语言是与特定CPU架构(如x86, ARM)相关的。
第三步:汇编
目标:将汇编语言代码(.s 文件)翻译成机器码,生成一个目标文件(.o 文件或 obj 文件)。
- 汇编器读取
.s文件,并将每一条汇编指令转换成对应的二进制机器码。 - 这个文件包含了机器码,但通常还无法直接运行,因为它可能引用了其他文件中的函数或变量(
printf函数)。
执行命令:
使用 -c 选项来进行汇编,生成目标文件。
gcc -c hello.s -o hello.o
这会生成一个名为 hello.o 的文件。hello.o 是一个二进制文件,用文本编辑器打开会看到乱码,我们可以用 file 命令查看它的类型:
file hello.o # 输出: hello.o: Mach-O 64-bit object x86_64
关键点:
.o文件是编译过程中的一个中间产物,也称为“目标文件”。- 它包含了机器码,但缺少了程序运行所需的其他部分(如库函数
printf的实际代码)。
第四步:链接
目标:将一个或多个目标文件(.o 文件)和所需的库文件链接在一起,生成一个完整的、可执行的最终文件。
- 在我们的例子中,
hello.o文件调用了printf函数,但它本身并不知道printf函数的具体代码在哪里。printf的代码位于C标准库中(libc.so或libc.dylib)。 - 链接器的工作就是:
- 合并代码和数据:将
hello.o和标准库中printf的代码合并到一个文件中。 - 解析符号:将代码中对
printf的“引用”替换为它在内存中的“实际地址”。 - 重定位:调整所有地址引用,确保程序加载到内存后能正确运行。
- 合并代码和数据:将
执行命令:
直接使用 gcc,默认会执行所有步骤直到生成可执行文件。
gcc hello.o -o hello
这会生成一个名为 hello 的可执行文件,在Linux/macOS上,它没有扩展名;在Windows上,通常是 .exe。
我们可以再次用 file 命令查看它:
file hello # 输出: hello: Mach-O 64-bit executable x86_64
运行程序:
./hello # 输出: Hello, World!
总结与图示
| 步骤 | 输入文件 | 输出文件 | 主要任务 | 命令示例 |
|---|---|---|---|---|
| 预处理 | hello.c |
hello.i |
展开宏、包含头文件 | gcc -E hello.c -o hello.i |
| 编译 | hello.i |
hello.s |
将C代码翻译成汇编代码 | gcc -S hello.i -o hello.s |
| 汇编 | hello.s |
hello.o |
将汇编代码翻译成机器码 | gcc -c hello.s -o hello.o |
| 链接 | hello.o + 库 |
hello (可执行) |
合并目标文件,解析外部符号 | gcc hello.o -o hello |
一个更简洁的命令(常用)
在实际开发中,我们通常不需要手动执行每一步,我们可以直接告诉编译器最终的输出文件,它会自动完成所有步骤:
# 一条命令完成所有步骤 gcc hello.c -o hello
这个命令会:
- 调用预处理器生成一个临时文件。
- 调用编译器将临时文件编译成另一个临时汇编文件。
- 调用汇编器将汇编文件汇编成临时目标文件。
- 调用链接器将目标文件与所需库链接,最终生成
hello可执行文件。
这个过程是理解C语言乃至所有编译型语言工作原理的基础。
