1013 字
5 分钟
C语言编译过程
2025-11-08
2026-03-30

1、结论

分为下面四个步骤。

  1. 预处理:会处理所有以#开头的指令。

    • 文件包含:将 #include 指令指定的头文件插入到源代码中。

    • 宏展开:将 #define 定义的宏替换为宏值。

    • 条件编译:根据 #if#ifdef 等条件,决定是否包含某部分代码。

  2. 编译:编译器将预处理后的代码转换为汇编代码,格式为.s

  3. 汇编:汇编阶段是将汇编代码(.s)转换为机器代码(.o,一堆机器能看懂的0和1二进制文件)的过程。

  4. 链接:将不同的目标文件和库文件连接成一个可执行文件的过程。

第一次看不懂正常,我第一次也不懂。通读下面的示例就明白了。

2、示例

假设我有两个代码:main.c和add.c

main.c
#define NUM 10
int main()
{
add(5, NUM);
return 0;
}
add.c
int add(int a, int b)
{
return a+b;
}

以这俩为例讲解比较好,已经去掉非核心的那些内容,能够用最简单的情况、最少的代码,展现这个过程。

2.1、预处理

main.c里面包含#define,进行符号替换,得到下面的代码

int main() {
add(5, 10); // NUM 被展开为 10
return 0;
}

add.c里面没有,保持原样。

2.2、编译

通常是采用gcc工具来进行编译。经常linux下编程的同学应该熟悉这个shell指令。做单片机的同学可能会陌生,把他理解是一段编译的命令就可以,不必细究。

Terminal window
gcc -S main.c # 生成 main.s 汇编文件
gcc -S add.c # 生成 add.s 汇编文件

分别得到下面的代码,非常纯正的汇编。

main.s
.file "main.c"
.text
.globl main
.type main, @function
main:
push %rbp
mov $5, %eax # 5 被传递给 a
mov $10, %ebx # NUM 被替换为 10,传递给 b
call add # 调用 add 函数
pop %rbp
ret
add.s
.file "add.c"
.text
.globl add
.type add, @function
add:
push %rbp
mov %edi, %eax # 将 a 存储在 eax
add %esi, %eax # 将 b 加到 eax 中
pop %rbp
ret

2.3、汇编

执行下面

Terminal window
gcc -c main.s -o main.o # 编译 main.s 到 main.o
gcc -c add.s -o add.o # 编译 add.s 到 add.o

把上一步得到的.s汇编代码,编译成.o文件。里面的内容是一堆01机器码。下面的代码不必细究,我随意编造的,会意就可以。

main.o
00000000 00000000 00000001 00000010 00000000 00000000 00000000 00000000 # mov eax, 5
00000000 00000000 00000001 00000010 00000000 00000000 00000000 00001010 # mov ebx, 10
00000000 00000000 00000001 00000010 00000000 00000000 00000000 10000000 # call add (address)
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 # ret
...
add.o
00000000 00000000 00000001 00000010 00000000 00000000 00000000 01000000 # mov eax, [esp+4]
00000000 00000000 00000001 00000010 00000000 00000000 00000000 01100000 # add eax, [esp+8]
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 # ret
...

2.4、链接

下面是最后一步。把这些机器码链接起来。

这里为什么要把main.o和add.o链接起来呢?要是还有个abcd.o文件要不要链接?

因为main.o里面用到了add.o的函数,所以链接起来,abcd.o没有用到,就不连接。

Terminal window
gcc main.o add.o -o main # 链接生成可执行文件 main

下面是连接后的可执行文件,一个没有abcd.o干预的二人世界。

实际上链接器会做符号解析与重定位,并非简单地把文件拼接,比如这里的第三行就有地址的替换。为了简单我们就把他理解为追加就可以啦,又不是专业做这个的😋

00000000 00000000 00000001 00000010 00000000 00000000 00000000 00000000 # mov eax, 5
00000000 00000000 00000001 00000010 00000000 00000000 00000000 00001010 # mov ebx, 10
00000000 00000000 00000001 00000010 00000000 00000000 00000000 00100000 # call 0x2000 (地址替换)
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 # ret
00000000 00000000 00000001 00000010 00000000 00000000 00000000 01000000 # mov eax, [esp+4]
00000000 00000000 00000001 00000010 00000000 00000000 00000000 01100000 # add eax, [esp+8]
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 # ret
...

.o文件是二进制格式,包含代码段、数据段、符号表等结构,不是简单的纯机器指令序列。工作中用到再细致了解就可以,初学者不用纠结。

想要运行可执行文件,只需要执行下面的shell代码

Terminal window
./main
C语言编译过程
/posts/c语言编译过程/
作者
唐承乾
发布于
2025-11-08
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

Personal Site
唐承乾
Profile Image of the Author
技术笔记、长期专题与电子书草稿

嵌入式 & AI 工作流。螺旋式学习,把踩过的坑整理成以后还能复用的东西。

GitHub 知乎
CSDN