跳转到内容

GNU C 编译器内部/GNU C 编译器架构

来自 Wikibooks,开放世界中的开放书籍

GCC 架构概述

[编辑 | 编辑源代码]

本节基于 Red Hat 杂志文章 [1]

GNU 编译器集合 (GCC) 包含多个用于不同编程语言的编译器。主要的 GCC 可执行文件 gcc 处理用 C、C++、Objective-C、Objective-C++、Java、Fortran 或 Ada 编写的源文件,并为每个源文件生成汇编文件。它是一个驱动程序,根据源文件的语言调用相应的编译程序。对于 C 源文件,它们是预处理器和编译器 cc1、汇编器 as 和链接器 collect2。第一个和第三个程序附带 GCC 发行版,汇编器是 GNU binutils 包的一部分。本书描述了预处理器和编译器 cc1 的内部工作原理。

每个编译器都包含以下三个组件:前端、中间端和后端。GCC 一次编译一个文件。源文件依次通过所有三个组件。图 1 说明了与每个组件相关的组件和源文件表示。

GCC 前端、中间端和后端,以及源文件表示。

前端的目的是读取源文件,解析它,并将其转换为标准的抽象语法树 (AST) 表示。每种编程语言都有一个前端。由于语言的差异,生成的 AST 的格式对于每种语言略有不同。AST 生成后的下一步是统一步骤,其中 AST 树被转换为称为泛型的统一形式。之后,编译器的中间端部分开始接管。首先,树被转换为另一种称为GIMPLE 的表示。在此形式中,每个表达式最多包含三个操作数,所有控制流结构都表示为条件语句和 goto 运算符的组合,函数调用的参数只能是变量,等等。图 2 说明了泛型形式的树和 GIMPLE 形式的树之间的差异。GIMPLE 是优化源代码的方便表示。


在 GIMPLE 之后,源代码被转换为静态单赋值 (SSA) 表示。这种形式的核心思想是,每个变量只被赋值一次,但可以在表达式的右侧被多次使用。每次在 GIMPLE 形式的树中重新赋值同一个变量时,编译器都会创建一个该变量的新版本,并将新值存储到其中。当在条件表达式的两个分支中都将同一个变量赋值时,需要将变量的两个可能值合并成一个变量。此操作在 SSA 形式中表示为 PHI 函数。


SSA 形式也用于优化。GCC 对 SSA 树执行 20 多种不同的优化。在 SSA 优化传递之后,树被转换回 GIMPLE 形式,然后用于生成树的寄存器传输语言 (RTL) 形式。RTL 是基于硬件的表示,它对应于具有无限多个寄存器的抽象目标架构。RTL 优化传递优化 RTL 形式的树。最后,GCC 后端使用 RTL 表示为目标架构生成汇编代码。后端的例子包括 x86 后端、mips 后端等等。

在接下来的部分,我们将描述 C 前端和 x86 后端的内部工作原理。编译器从其初始化和命令行选项处理开始。之后,C 前端预处理源文件,解析它并执行一些优化。然后,后端为目标平台生成汇编代码并将其保存到文件。

要点: GCC 是一个编译器集合,它包含每个编程语言的前端、一个中间端以及每个架构的后端。每个源文件依次通过的 主要表示形式是前端的 AST、中间端的 RTL 以及后端的汇编表示。GCC 一次编译一个文件。

GCC 初始化

[编辑 | 编辑源代码]

C 前端包括 C/C++ 预处理器和 C 编译器。程序 cc1 包含预处理器和 C 编译器。它编译 C 源文件并生成汇编 (.S) 文件。

编译器前端和后端通过称为语言钩子的回调函数相互交互。所有钩子都被包含在文件 langhooks.h 中定义的全局变量 struct lang_hooks lang_hooks 中。有以下类型的钩子:用于树内联的钩子、用于调用图的钩子、用于函数的钩子、用于树转储的钩子、用于类型的钩子、用于声明的钩子以及特定于语言的钩子。钩子的默认值在文件 langhooks-def.h 中定义。

GCC 初始化包括命令行选项解析、初始化后端、创建全局范围以及初始化内置数据类型和函数。

每个声明都与一个范围相关联。例如,局部变量与其函数的范围相关联。全局声明与全局范围相关联。

函数 toplev_main() 是处理命令行选项、初始化编译器、编译文件以及释放分配资源的函数。函数 decode_options() 处理命令行选项并在编译器中设置相应的变量。

在命令行选项解析之后,调用函数 do_compile()。它通过调用函数 backend_init() 执行后端初始化。之后,函数 lang_dependent_init() 执行特定于语言的初始化,包括前端和后端的初始化。C 初始化函数 c_objc_common_init() 创建内置数据类型,初始化全局范围并执行其他初始化任务。函数 c_common_nodes_and_builtins() 创建文件 builtin-types.def 中描述的预定义类型。

标准 C 类型在初始化时创建。下表列出了几种类型

GCC 内置类型
变量名称 C 类型
char_type_node char
integer_type_node int
unsigned_type_node unsigned int
void_type_node void
ptr_type_node void*

GCC 内置函数是在编译时求值的函数。例如,如果 strcpy() 函数的大小参数是一个常量,那么 GCC 会用所需的赋值数量替换函数调用。编译器用内置函数替换标准库调用,并在构建函数的 AST 后对其进行求值。在 strcpy() 的情况下,编译器会检查大小参数,如果参数是常量,则使用 strcpy() 的优化内置版本。内置 builtin_constant_p() 允许确定其参数的值是否在编译时已知。GCC 内置函数超出了 GCC 的使用范围。例如,Linux 内核的字符串处理库使用 builtin_constant_p() 在编译时已知字符串大小的情况下调用字符串处理函数的优化版本。

GCC 使用相应的 expand_builtin() 函数来评估每个内置函数。例如,builtin_strcmp() 使用 expand_builtin_strcmp() 求值。下表列出了 GCC 内置函数的例子

GCC 内置函数
内置名称 解释
builtin_constant_p 如果参数是常量,则返回 true
builtin_memcpy 等效于 memcpy()
builtin_strlen 等效于 strlen()


要点: GCC 初始化包括命令行选项解析、初始化后端、创建全局范围以及初始化内置数据类型和函数。

C 预处理器

[编辑 | 编辑源代码]

在初始化之后,函数 do_compile() 调用函数 compile_file()。此函数调用 parse_file() 前端语言钩子,该钩子对于 C 语言被设置为函数 c_common_parse_file()。后面的函数调用函数 finish_options(),该函数初始化预处理器并处理 -D、-U 和 -A 命令行选项(分别等效于 #define、#undef 和 #assert)。C 预处理器处理源代码中的预处理器指令,例如 #define、#include。

在预处理器初始化完成后,调用了 c_parse_file() 函数。该函数使用标准的 lex/bison 工具来解析文件。预处理器作为词法分析器的一部分实现。C 语言词法分析器函数 c_lex() 调用 libcpp 函数 cpp_get_token(),它处理预处理器关键字。预处理器的状态由变量 cpp_reader *parse_in 定义。类型 struct cpp_reader 最重要的是包含正在处理的文本缓冲区列表。每个缓冲区对应一个源文件(.c 或 .h)。函数 cpp_get_token() 为合法的预处理器关键字调用相应的函数。例如,当遇到 #include 时,调用函数 do_include_common()。它分配一个新的缓冲区并将其放置在缓冲区栈的顶部,使其成为当前缓冲区。当编译一个新文件时,缓冲区从栈中弹出,旧文件的编译继续进行。

当使用 #define 关键字定义新的宏时,调用函数 do_define()

要点: 预处理器负责处理预处理器指令,例如 #include 和 #ifdef。

源代码解析

[编辑 | 编辑源代码]

在运行预处理器后,GCC 为源文件的每个函数构建一个抽象语法树 (AST)。AST 是若干个连接的节点,类型为 struct tree。每个节点都有一个树码,它定义了树的类型。宏 TREE_CODE() 用于引用代码。树码在文件 tree.defc-common.def 中定义。具有不同树码的树被分组到树类中。以下树类在 GCC 中定义,除此之外还有其他一些树类。

GCC 树类
树类 解释
'd' 声明
'<' 比较
'2' 二元算术


树有两种类型:语句表达式。语句对应于 C 结构,例如表达式后跟一个 ';',条件语句,循环语句等等。表达式是语句的组成部分。表达式的示例包括赋值表达式,算术表达式,函数调用等等。树码的示例如下表所示。

GCC 树码
树码 树类 解释 操作数
MODIFY_EXPR 'e' 赋值表达式 TREE_OPERAND(t,0) - 左侧;TREE_OPERAND(t,1) - 右侧;
CALL_EXPR 'e' 函数调用 TREE_OPERAND(t,0) - 函数定义;TREE_OPERAND(t,1) - 函数参数;
FUNCTION_DECL 'd' 变量声明 DECL_SOURCE_FILE(t) - 源文件;DECL_NAME(t) - 变量名;
ARRAY_TYPE 't' 数组类型 TREE_TYPE(t) - 数组元素的类型;TYPE_DOMAIN(t) - 索引的类型;
DECL_STMT 'e' 变量声明 TREE_OPERAND(t,0) - 变量;DECL_INITIAL(TREE_OPERAND(t,1)) - 初始值;

除了定义树类型的树码之外,还有许多与每种树类型不同的操作数可用。例如,赋值表达式有两个操作数,对应于表达式的左侧和右侧。宏 TREE_OPERAND 用于引用操作数。宏 IDENTIFIER_POINTER 用于查找 IDENTIFIER_NODE 树表示的标识符的名称。下表展示了一些树节点,它们的目的以及它们的操作数。

每棵树都有一个类型,它对应于它表示的 C 表达式的类型。例如,MODIFY_EXPR 节点的类型是左侧操作数的类型。 NOP_EXPRCONVERT_EXPR 树用于类型转换。

树 NULL_TREE 等效于 NULL。函数 debug_tree() 将树打印到 stderr。

解析器作为 bison 语法实现。语法调用构建 AST 的 GCC 函数。当一个新的标识符被词法分析时,它会被添加到 GCC 维持的字符串池中。标识符的树码为 IDENTIFIER_NODE。当同一个标识符再次被词法分析时,返回同一个树节点。函数 get_identifier() 返回标识符的树节点。

新的变量声明在多个函数调用中被处理。首先,函数 start_decl() 被调用,带有声明的名称,词法分析器返回的类型,以及它的属性。该函数调用 grokdeclarator() 检查类型和参数节点,并返回一个代码适合声明的树:VAR_DECL 用于变量,TYPE_DECL 用于类型等等。然后,声明被添加到作用域中。作用域包含在一个函数内创建的所有声明,但不包含全局声明。还有全局作用域,它包含全局声明。当解析一个函数时,它的声明作为 BLOCK 节点附加到函数体。当创建声明时,使用 IDENTIFIER_SYMBOL_VALUE 将标识符节点与声明节点关联。函数 lookup_name() 返回给定标识符的声明。当声明离开作用域时,树属性 C_DECL_INVISIBLE 被断言。

GCC 中没有维护符号表。相反,编译器使用标识符池和 C_DECL_INVISIBLE 属性。语言钩子 lang_hooks.decls.getdecls() 返回作用域中的变量,这些变量链接在一起。

函数 start_init()finish_init() 被用于初始化声明。函数 finish_decl() 完成声明。对于数组声明,它计算初始化数组的大小。然后调用函数 layout_decl()。它计算声明的大小和对齐方式。

解析函数取决于函数体是否存在。函数声明使用与变量声明相同的函数解析。对于函数定义,调用函数 start_function()。然后,编译器解析函数体。当函数结束时,调用函数 finish_function()

函数 start_decl()start_function() 将声明的属性参数作为其参数之一。属性在 GCC 手册 中描述。属性是 GNU 实现 C 的扩展。下表展示了一些属性并解释了它们的目的。

函数属性
属性 解释
constructor 在 main() 之前自动调用函数
destructor 在 main() 之后自动调用函数
alias 另一个函数的别名

对于每种类型的 C 语句,都有一个函数构建对应类型的树节点。例如,函数 build_function_call() 为函数调用构建 CALL_EXPR 节点。它以函数名的标识符和参数作为参数。该函数使用 lookup_name() 查找函数声明,并使用 default_conversion() 对参数进行类型转换。

然后,AST 被转换为 SSA,最终转换为 RTL 表示。是否在解析每个函数之后或解析整个文件时进行转换由编译器选项 -funit-at-a-time 控制。默认情况下为假。

要点: GCC 解析器作为 bison 语法编写。它创建源文件的 AST 表示。AST 由树节点构成。每个节点都有一个代码。树节点对应于 C 的语句和表达式。函数 debug_tree() 打印出树。

汇编代码生成

[编辑 | 编辑源代码]

后端为指定的目标平台生成汇编代码。每个写入汇编文件的指令都会调用函数 output_asm_insn()。函数 final_start_function() 在函数保存到汇编文件之前生成函数的序言。

要点: 后端为指定的目标平台生成汇编代码。
上一页: 目录 GNU C 编译器架构 下一页: GEM 框架
  1. ^ https://web.archive.org/web/20160410185222/https://www.redhat.com/magazine/002dec04/features/gcc/
华夏公益教科书