C 语言源文件从编写完成到最终生成可执行文件的完整、详细过程

这个过程核心分为预处理、编译、汇编、链接 四个阶段,即使我们平时用gcc test.c -o test一键编译,编译器也会在背后依次执行这四步。下面我来详细讲讲每一个环节。

一、完整流程总览

C 文件 → 预处理(.i 文件) → 编译(.s 文件) → 汇编(.o 文件) → 链接(可执行文件)

下面以 Linux 下的 GCC 编译器为例(Windows 下的 MinGW、Clang 逻辑一致),逐个阶段拆解。

为了方便讲解,我们先创建一个简单的test.c文件,后续所有步骤都基于这个文件:

cpp 复制代码
// test.c
#include <stdio.h>  // 头文件包含
#define PI 3.14     // 宏定义

int main() 
{
    // 条件编译 + 注释
#ifdef PI
    printf("PI = %f\n", PI);
#endif
    return 0;
}

环节1:预处理(Preprocessing)

核心作用

处理源代码中 "编译前需要提前处理" 的内容,不做语法检查,只做文本替换 / 清理,最终生成纯 C 代码的预处理文件(.i)

具体操作

  1. 展开所有#define宏(比如把PI替换成3.14);
  2. 处理#include头文件(把<stdio.h>的内容直接插入到当前文件中);
  3. 处理条件编译指令(#ifdef/#if/#endif等,保留满足条件的代码);
  4. 删除所有注释(// 和 /* */);
  5. 添加行号和文件标识(方便后续编译报错时定位)。
bash 复制代码
# -E:只执行预处理阶段,-o 指定输出文件
gcc -E test.c -o test.i

此时输出

生成test.i文件(文本文件,可直接打开查看),核心内容如下(简化版):

bash 复制代码
// 省略<stdio.h>展开的上千行代码...
int main() 
{
    // 注释已被删除,PI被替换成3.14,#ifdef PI条件满足保留代码
    printf("PI = %f\n", 3.14);
    return 0;
}

环节 2:编译(Compilation)

核心作用

将预处理后的.i文件(纯 C 代码)翻译成汇编语言代码(.s),这是 "高级语言→低级语言" 的核心步骤,会做严格的语法 / 语义检查。

具体操作

  1. 词法分析 :把代码拆分成一个个 "单词"(比如intmain=3.14),检查拼写错误(如把printf写成printff会在此阶段报错);
  2. 语法分析:验证代码语法是否符合 C 语言规则(比如少分号、括号不匹配会报错),生成抽象语法树(AST);
  3. 语义分析:检查代码逻辑是否合理(比如变量未定义就使用、类型不匹配);
  4. 优化:对代码进行优化(比如常量折叠、循环优化);
  5. 生成汇编:将优化后的 AST 翻译成对应 CPU 架构的汇编语言。

执行命令(GCC)

bash 复制代码
# -S:只执行到编译阶段,生成汇编文件
gcc -S test.i -o test.s

此时输出

生成test.s文件(汇编代码,文本文件),以 x86_64 架构为例,核心内容如下:

bash 复制代码
    .file   "test.c"
    .text
    .section    .rodata
.LC0:
    .string "PI = %f\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movsd   .LC1(%rip), %xmm0  # 把3.14加载到寄存器
    leaq    .LC0(%rip), %rdi   # 加载字符串地址
    movl    $1, %eax
    call    printf@PLT         # 调用printf函数
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .section    .rodata
.LC1:
    .long   1374389535
    .long   1074339512
    .ident  "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
    .section    .note.GNU-stack,"",@progbits

环节3:汇编(Assembly)

核心作用

将汇编语言的.s文件翻译成机器码(二进制),生成 "重定位目标文件(.o)",也叫目标文件。

具体操作

  1. 汇编器(GCC 中是as工具)逐行解析汇编指令;
  2. 将每个汇编指令转换成对应 CPU 能识别的二进制机器码(比如movq对应0x48 0x89);
  3. 生成目标文件:包含机器码、符号表(比如mainprintf的引用)、重定位表(记录需要链接时修正的地址),但此时还不能直接执行 (因为外部函数如printf还未解析)。

执行命令(GCC)

bash 复制代码
# -c:只执行到汇编阶段,生成目标文件
gcc -c test.s -o test.o

# 等价于直接调用汇编器:
# as test.s -o test.o

此时输出

生成test.o文件(二进制文件,无法直接打开查看,可通过objdump查看内容):

bash 复制代码
# 查看目标文件的反汇编内容
objdump -d test.o

输出片段(可见机器码和对应的汇编):

bash 复制代码
test.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   f2 0f 10 05 00 00 00    movsd  0x0(%rip),%xmm0        # c <main+0xc>
   b:   00
   c:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 13 <main+0x13>
  13:   b8 01 00 00 00          mov    $0x1,%eax
  18:   e8 00 00 00 00          callq  1d <main+0x1d>  # printf的地址暂未解析
  1d:   b8 00 00 00 00          mov    $0x0,%eax
  22:   5d                      pop    %rbp
  23:   c3                      retq

环节 4:链接(Linking)

核心作用

将目标文件(.o)、系统库(比如libc.so,包含printf的实现)、其他依赖的目标文件合并,解析所有未定义的符号,最终生成可执行文件

具体操作

链接是新手最容易忽略但最关键的阶段,分为两步:

1. 符号解析

找到所有未定义的符号(比如printf)的实际实现位置(在系统 C 标准库libc中)。

2. 重定位

修正目标文件中的地址(比如把printf的调用地址从 "占位符" 改成实际的内存地址),合并所有目标文件的代码段、数据段,生成完整的可执行文件。

链接的两种类型(重点)

  • 动态链接(默认) :可执行文件不包含库函数的代码,运行时才加载系统中的libc.so(体积小,节省内存,但依赖系统库);
  • 静态链接 :将库函数的代码直接嵌入可执行文件(体积大,但不依赖系统库),命令:gcc test.o -o test_static -static

执行命令(GCC)

bash 复制代码
# 链接目标文件,生成可执行文件
gcc test.o -o test

关键输出

生成test可执行文件(二进制文件),可直接运行:

bash 复制代码
./test
# 输出:PI = 3.140000

补充:一键编译 vs 分步编译

平时我们用的gcc test.c -o test是 "一键编译",GCC 会自动依次执行:预处理→编译→汇编→链接,等价于:

复制代码
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

总结

  1. C 文件生成可执行文件分为 4 个核心阶段:预处理(展开文本)→ 编译(转汇编)→ 汇编(转机器码)→ 链接(合并解析)
  2. 预处理输出.i(文本)、编译输出.s(汇编文本)、汇编输出.o(二进制目标文件)、链接输出可执行文件;
  3. 链接是最终生成可执行文件的关键,核心是解析外部符号(如库函数)并修正内存地址,分为动态链接(默认)和静态链接两种方式。
相关推荐
李艺为3 小时前
根据apk包名动态修改Android品牌与型号
android·开发语言
黄河滴滴3 小时前
java系统变卡变慢的原因是什么?从oom的角度分析
java·开发语言
老华带你飞3 小时前
农产品销售管理|基于java + vue农产品销售管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
superman超哥4 小时前
Rust Workspace 多项目管理:单体仓库的优雅组织
开发语言·rust·多项目管理·rust workspace·单体仓库
kylezhao20194 小时前
C#通过HSLCommunication库操作PLC用法
开发语言·c#
lengjingzju5 小时前
一网打尽Linux IPC(三):System V IPC
linux·服务器·c语言
JIngJaneIL5 小时前
基于springboot + vue房屋租赁管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
期待のcode5 小时前
Java的抽象类和接口
java·开发语言
wadesir5 小时前
Go语言中高效读取数据(详解io包的ReadAll函数用法)
开发语言·后端·golang