Skip to content

程序的编译过程

上一章我们安装了 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)做三件事:

  1. 词法分析 — 把代码拆成 token(关键字、标识符、运算符)
  2. 语法分析 — 检查语法,生成语法树
  3. 生成汇编代码 — 把 C 语言翻译成汇编指令
bash
gcc -S main.i -o main.s

看看 main.s,你会看到熟悉的 movpushcall 等汇编指令。

这才是"编译器"真正干的事。前面预处理、后面汇编链接,都不是它的活。

第三阶段:汇编(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 printf

T main 表示 main 函数已定义(Text 段),U printf 表示 printf 是未定义的外部符号——等着链接器去找到它。

第四阶段:链接(Linking)

链接器(ld)把多个目标文件和库文件拼接成一个可执行文件:

bash
gcc main.o -o main.exe

链接器干两件事:

任务说明
符号解析找到所有 U 符号的定义(比如 printflibc 里)
重定位把所有符号引用替换为实际内存地址

如果只编译不链接,会看到这样的错误:

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.oadd.o 合并,解决跨文件的符号引用。

小结

          预处理          编译          汇编          链接
源文件 ───────→ 纯 C ──────→ 汇编 ──────→ 目标文件 ────→ 可执行文件
  .c          .i           .s           .o           .exe/.out

有了这个基础,下一章就能理解为什么要有"静态库"和"动态库"了——它们本质上就是把 .o 文件打包起来,方便链接器复用。

陕 ICP 备 19000424 号 -2 | 陕公网安备 61010302001034 号