一、前言:为什么需要读懂 GCC 源码 🧐

  GCC 是开源世界里最庞大的 C 语言项目之一,它支持多种编程语言、多种目标平台。从输入 gcc hello.c 到输出 a.out,中间发生的一切都可以在源码中找到答案。本文将以 GCC 主线源码 为脉络,带你从 main 函数出发,逐层深入词法分析、语法分析、中间表示、优化遍、代码生成,看清编译器这座精密工厂的内部构造。

  阅读本文前,建议具备以下基础:

  - 熟悉 C 语言,了解编译的基本流程(预处理、编译、汇编、链接)
  - 对编译器前端(词法、语法)有初步概念
  - 能够使用 GDB 或源码阅读工具(如 cscope、LXR)跟踪大型 C 项目

二、源码获取与总体目录结构 🗂️

(1) 获取源码

  GCC 官方仓库位于 gcc.gnu.org/git/gcc.git,可使用 git 克隆:

git clone https://gcc.gnu.org/git/gcc.git
cd gcc
git checkout releases/gcc-13  # 可选,锁定一个稳定版本

(2) 顶层目录导读

  克隆后看到的顶层目录主要分工如下:

目录 功能说明
gcc/ 编译器核心源码:前端、中端、后端均在此
libcpp/ C 预处理器独立库
libdecnumber/ 十进制浮点数运算库
libiberty/ 移植性辅助函数(如 xmalloc
include/ 公共头文件
intl/ 国际化支持
contrib/ 测试脚本、扩展工具
fixincludes/ 修复目标系统头文件的脚本

  我们的焦点在 gcc/ 目录,下面进一步拆解其子结构:

gcc/
├── c/            # C 语言前端(C 的词法、语法、语义)
├── cp/           # C++ 前端
├── lto/          # 链接时优化
├── objc/         # Objective-C 前端
├── fortran/      # Fortran 前端
├── go/           # Go 前端
├── ada/          # Ada 前端
├── config/       # 目标平台相关配置(如 i386、arm)
├── doc/          # 文档
├── testsuite/    # 测试套件
├── gimple*       # GIMPLE 中间表示的操作
├── tree*         # GENERIC/GIMPLE 树操作(tree.c/h, tree-ssa*)
├── rtl*          # RTL 相关(rtl.c/h, rtlanal.c, emit-rtl.c)
├── passes.c      # 优化遍管理器
├── cfg*          # 控制流图
├── df*           # 数据流分析
├── reg*          # 寄存器分配
├── simplify-rtx.c # RTL 简化
├── combine.c     # 指令合并遍
├── cse.c         # 公共子表达式消除
├── loop*         # 循环优化
├── gcse.c        # 全局公共子表达式消除
├── expr.c        # 表达式展开
├── emit-rtl.c    # RTL 发射
├── varasm.c      # 汇编输出
├── final.c       # 最终汇编生成
└── main.c        # 驱动程序入口(gcc 主函数,不是编译器本身)

  要点:编译器本身不是从 main.c 开始,gcc/main.c 是“驱动程序”的入口,它负责解析命令行参数、调用 cc1(C 编译器)、ascollect2(链接器包装)等。真正的编译器是 cc1,其入口在 gcc/c/c-lang.cgcc/main.c 中的 main 吗?实际上,GCC 的编译器程序 cc1maingcc/main.c 中,会根据前端语言触发相应 hook。为了清晰,我们分两条线:驱动程序编译器核心

三、驱动层:gcc 命令是如何“调兵遣将”的 ⚔️

  当用户在终端输入 gcc hello.c -o hello,执行的是 gcc/gcc.c(注意不是 main.c)中编译出的 gcc 程序。这部分代码位于 gcc/gcc.c 以及 gcc/gccspec.c,核心函数是 driver::main (C++ 风格,但整体仍是 C++)。它的工作流程如下:

  1. 解析参数:分离选项与文件名,识别语言(根据扩展名)。
  2. 构造编译流水线:按照 .c.i.s.o.exe(或 .out)的顺序,找到各自需要的工具:
    - cc1:C 编译器本体
    - as:GNU 汇编器
    - collect2:链接器包装(最终调用 ld
  3. 逐个执行子进程:通过 fork/exec(Unix)或 CreateProcess(Windows MinGW)依次调用上述工具,并传递正确的命令行参数。

  关键源文件:gcc/gcc.c 中的 execute() 函数和 process_command() 等。我们可通过 -v 选项观察到这些子调用:

$ gcc -v hello.c
.../cc1 -quiet -v hello.c -o /tmp/ccXXXX.s
.../as -o /tmp/ccYYYY.o /tmp/ccXXXX.s
.../collect2 ... /tmp/ccYYYY.o -o hello

四、编译器核心入口:cc1 的 main 函数 🔑

  cc1 的程序入口在 gcc/main.c,函数 main()。但在现代 GCC 中,它通过 toplev::main (C++ 类封装) 启动。核心逻辑如下(简化后):

/*CN:编译器主入口,位于gcc/main.c或toplev.cc--EN:Compiler main entry, located in gcc/main.c or toplev.cc*/
int main(int argc, char **argv)
{
    /*CN:初始化各种子系统(内存管理、目标平台、语言前端等)--EN:Initialize subsystems (memory, target, language frontend, etc.)*/
    toplev::init();

    /*CN:解析命令行参数,设置标志位--EN:Parse command-line arguments and set flags*/
    toplev::parse_options(argc, argv);

    /*CN:执行编译流程(前端 -> 中端 -> 后端)--EN:Execute compilation flow (front end -> middle end -> back end)*/
    toplev::main();

    return 0;
}

  toplev::main() 根据输入文件的语言类型(通过后缀判定)分发到具体的前端处理函数。对于 C 语言,会进入 c_common_parse_file() 等函数,最终调用词法、语法分析器。

五、C 前端:从字符流到语法树 🌲

1. 词法分析(Lexer)

  GCC 的 C 词法分析器位于 gcc/c/c-lex.cc(注意 GCC 现在大量采用 C++ 实现,尽管原有 C 文件仍保留 .c 后缀)。它读取预处理后的 .i 文件(或直接处理源文件内部的预处理),将字符流转换为 token 并填充到解析器。

  主要函数:
  - c_lex_with_flags()gcc/c/c-lex.cc):每次调用返回一个 token,同时设置其语义值(标识符名字、数值等)。
  - 内部调用 libcpp 库的 cpp_get_token(),完成宏替换、头文件展开后的 token 流来源。

  示例代码片段(已简化):

/*CN:获取下一个token并处理--EN:Get next token and process it*/
c_token * c_lex_with_flags(c_token *token, unsigned char *flags)
{
    /*CN:从libcpp读取下个token--EN:Read next token from libcpp*/
    const cpp_token *cpt = cpp_get_token(parse_in);
    
    /*CN:token类型(如CPP_NAME、CPP_NUMBER...)--EN:token type (e.g., CPP_NAME, CPP_NUMBER)*/
    token->type = cpt->type;
    
    if (cpt->type == CPP_NAME)
    {
        /*CN:标识符的tree节点--EN:tree node of identifier*/
        token->value = cpt->val.node.node;
    }
    
    /*CN:返回填充好的token--EN:Return filled token*/
    return token;
}

2. 语法分析(Parser)

  C 语言的语法分析器采用 递归下降 方式,代码位于 gcc/c/c-parser.cc。它根据函数、声明、表达式、语句等语法构造,逐个调用对应的子程序,最终生成 GENERIC 语法树

  关键函数:
  - c_parse_file():解析整个翻译单元(gcc/c/c-parser.cc
  - c_parser_translation_unit():最外层,循环调用 c_parser_external_declaration()
  - c_parser_external_declaration():处理函数定义或全局声明
  - c_parser_declaration_or_fndef():分流至函数定义或变量声明
  - c_parser_statement():解析各种语句(ifwhilereturn 等)
  - c_parser_expression():解析表达式,构建表达式树(生成的树节点类型为 tree

  语法树节点由 tree 类型表示(GCC 内部用联合体 tree_node),节点种类繁多:IDENTIFIER_NODEVAR_DECLFUNCTION_DECLCOMPOUND_STMTMODIFY_EXPR 等。每个节点都挂载了丰富的信息(类型、操作码等)。

  示例:解析 int a = 10; 会生成:

VAR_DECL (name: a, type: int)
  └─ INIT_EXPR
       ├─ VAR_DECL (a)
       └─ INTEGER_CST (10)

3. 语义分析

  语义分析嵌入在语法分析过程中或紧随其后。主要任务是:类型检查、符号管理、作用域维护。相关函数分布在 gcc/c/c-decl.cc(声明处理)、gcc/c/c-typeck.cc(类型检查)中。

  例如,c_parser_declaration_or_fndef() 在构建声明树的同时,调用 start_function() 注册新函数到符号表,调用 grokdeclarator() 解析类型并检查有效性。符号表通过 lang_identifierIDENTIFIER_SYMBOL_VALUE 宏关联 tree 节点。

六、中间表示(IR):从 GENERIC 到 GIMPLE 🧱

1. GENERIC:语言无关的树表示

  前端生成的语法树是一种特定于语言(如 C)的 GENERIC 表示。GENERIC 是一套由 tree 构成的中间语言,力图实现语言无关。但 GENERIC 仍过于复杂,不利于优化。

2. GIMPLE:三地址码的降级

  编译器紧接着将 GENERIC 转换为 GIMPLE 形式。GIMPLE 是一种三地址码,每个语句右侧最多一个操作。转换函数主要位于 gcc/gimplify.cc 中的 gimplify_function_tree()

  转换过程示例:

  C 代码:

int foo(int a, int b)
{
    return a + b * 2;
}

  转换为 GIMPLE 后(简化):

/*CN:函数foo实现--EN:Foo function implementation*/
int foo(int a, int b)
{
    int D_1234;
    int D_1235;

    D_1234 = b * 2;
    D_1235 = a + D_1234;

    return D_1235;
}

  GIMPLE 代码以 gimple 语句和 gimple_seq 链表表示。每个基本块(Basic Block)是一系列 GIMPLE 语句的序列。

3. 控制流图(CFG)与 SSA 形式

  生成 GIMPLE 后,编译器会构建 控制流图(CFG,gcc/cfg.cc 等),并将 GIMPLE 转换为 静态单赋值形式(SSA)。SSA 使得每次变量定义都有唯一版本号,极大简化了数据流分析。

  SSA 构建核心在 gcc/tree-into-ssa.c 中的 rewrite_into_ssa()。它插入 PHI 函数(PHI <a1, a2>)于控制流汇合处,确保单一定值。之后的优化遍大多在 SSA 形式上工作。

七、优化遍(Passes):构建精炼的代码 🔧

  GCC 使用 Pass Manager (gcc/passes.c) 组织上百个优化遍。每个遍为一个继承自 opt_pass 的类,并注册到 passes 列表中。编译时按顺序执行这些遍。

  常见的优化遍列举如下:

  • pass_build_cfg:构建控制流图
  • pass_build_ssa:转换为 SSA 形式
  • pass_ccp:条件常量传播
  • pass_forwprop:正向传播,简化语句
  • pass_dce:死代码消除
  • pass_cse:公共子表达式消除
  • pass_loop_invariant:循环不变量外提
  • pass_iv_optimize:归纳变量优化
  • pass_vectorize:自动向量化(SIMD)
  • pass_vrp:值范围传播
  • pass_reassoc:表达式重结合
  • pass_slp_vectorize:基本块向量化
  • pass_merge_phi:合并 PHI 节点
  • pass_expand:从 GIMPLE 展开为 RTL

  所有遍的执行入口在 gcc/passes.cexecute_pass_list()。调试时可通过 -fdump-tree-all 导出每个遍之后的 GIMPLE 表示。

八、后端:RTL 与代码生成 🎛️

  优化结束后,GIMPLE 被转换为 RTL(Register Transfer Language),这是一种接近硬件的低级中间表示。RTL 对应文件和操作主要包括:

  • gcc/expr.c:表达式展开,将树(tree)转为 RTL
  • gcc/cfgexpand.c:从 GIMPLE/CFG 扩展为 RTL 的 CFG
  • gcc/emit-rtl.c:生成 RTL 指令序列
  • gcc/recog.c:指令识别,匹配目标机器指令模板
  • gcc/reload.c / gcc/lra*.c:寄存器分配(重载)

  GCC 有两种寄存器分配器:旧的 reload 和较新的 LRA (Local Register Allocator)。LRA 源码位于 gcc/lra*.cc,是当前默认选项。

  寄存器分配后,gcc/final.c 中的 final() 函数遍历 RTL 指令链,调用目标模板的汇编输出钩子(定义在 gcc/config/<arch>/ 下),最终生成 .s 文件。

  例如 x86 的指令模板在 gcc/config/i386/i386.md,由工具 genoutput 生成 insn-output.cc 中的 get_insn_template() 等函数,根据 RTL 指令代码输出对应的汇编字符串。

九、目标描述与机器描述 📐

  GCC 的可移植性得益于机器描述文件(.md)和宏定义。每个平台在 gcc/config/<cpu>/ 下有 机器描述文件,它使用 RTL 模板定义指令的语义、约束和汇编格式。例如,define_insn 描述一条指令:

;; CN: 定义加法指令,操作数约束、汇编输出
;; EN: Define add instruction with operand constraints and assembly output
(define_insn "addsi3"
  [(set (match_operand:SI 0 "register_operand" "=r,r")
        (plus:SI (match_operand:SI 1 "register_operand" "0,r")
                 (match_operand:SI 2 "nonimmediate_operand" "rm,rm")))]
  ""
  "@
   add{l}\t{%2, %0|%0, %2}
   add{l}\t{%2, %0|%0, %2}"
  [(set_attr "type" "alu")])

  这些 .md 文件会被编译生成 C 函数,供 GCC 后端调用以识别和发射指令。

十、调试辅助:如何自己跟踪 GCC 源码 🐞

  1. 使用 -fdump-* 选项
      -fdump-tree-all 输出所有 GIMPLE 遍后的中间表示。
      -fdump-rtl-all 输出所有 RTL 遍后的表示。
      结合源文件名,生成一系列带编号的 dump 文件,方便观察每一步变化。

  2. 用 GDB 调试 cc1
      正常编译一个文件并保留临时文件 -save-temps,然后
      gdb --args cc1 -quiet -o test.s test.i,在关键函数设断点,如 c_parser_translation_unitgimplify_function_treeexecute_pass_list 等。

  3. 阅读源码时的导航
      使用 grepcscope 或在线工具 Elixir Bootlin 跳转调用链。关键头文件 tree.hgimple.hrtl.h 包含了最核心的数据结构注释。

十一、总结:GCC 源码全景图 🗺️

  GCC 的源码虽然浩如烟海,但其架构清晰分层。从驱动程序 gcc/gcc.c 出发,到 cc1 编译器核心,经历 前端 (Lex & Parse)GENERIC/GIMPLESSA 优化RTL 生成与优化汇编输出,每个阶段都有明确的数据结构和关键函数群。

  理解这些源码,不仅让我们能够定制编译器、添加优化遍,更让我们深入明白:编译器不是魔法,而是一系列精密的变换过程。希望这篇解读能为你打开 GCC 源码的大门,使你从此不再畏惧这个庞大的工程,而是充满探索的热情!🚀

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐