跳转到内容

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

来自 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 说明了这些组件及其相关的源文件表示形式。抽象语法树 (AST)寄存器传输语言 (RTL)目标 是主要表示形式。

GCC front end, middle end, and back end with source file representations.
GCC 前端、中间端和后端,以及源文件表示形式。

主要表示

[编辑源代码]

前端的目的是读取源文件、解析它并将其转换为标准抽象语法树 (AST) 表示形式。AST 是双类型表示形式:它是一棵树,其中一个节点可以有子节点和一个语句列表,其中节点彼此链接。每种编程语言都有一个前端。

然后使用 AST 生成寄存器传输语言 (RTL) 树。RTL 是基于硬件的表示形式,对应于具有无限数量寄存器的抽象目标架构。RTL 优化过程以 RTL 形式优化树。最后,GCC 后端使用 RTL 表示形式为目标架构生成汇编代码。后端示例包括 x86 后端和 MIPS 后端。

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

辅助数据结构

[编辑源代码]

GCC 有许多其他数据结构,它们有助于代码开发,例如向量和堆。

vec.h 中定义的宏实现了一组模板向量类型和相关接口。这些模板使用宏实现,因为我们不在 C++ 环境中。接口函数类型安全,使用静态内联函数,有时由非内联通用函数支持。这些向量旨在与 GTY 机制交互操作。

由于结构对象、标量对象和指针的行为不同,因此有三种类型,分别对应于这三种变体。指针和结构对象变体都传递指向对象的指针 - 在前一种情况下,指针被存储到向量中,在后一种情况下,指针被解引用,并且对象被复制到向量中。标量对象变体适用于 int 类型的对象,并且向量元素通过值返回。

有“索引”和“迭代”访问器。迭代器返回一个布尔迭代条件,并通过引用更新传递的迭代变量。由于迭代器将被内联,因此对地址的引用可以被优化掉。

这些向量使用尾随数组习惯用法实现,因此它们不可调整大小,除非改变向量对象本身的地址。这意味着你不能使用向量类型的变量或字段 - 始终使用指向向量的指针。唯一的例外是结构的最后一个字段,它可以是向量类型。你将不得不使用 embedded_size 和 embedded_init 调用来创建这样的对象,而且它们可能不可调整大小(所以不要使用“安全”分配变体)。使用尾随数组习惯用法(而不是指向数据数组的指针)是因为,如果我们允许 NULL 也表示空向量,则空向量在包含它们的结构中占用最少的空间。

每个增加活动元素数量的操作都有“快速”和“安全”变体。前者假设有足够的已分配空间来执行操作(如果没有则会失败)。后者会根据需要重新分配向量。重新分配会导致向量大小呈指数增长。如果你知道要添加 N 个元素,那么在使用“快速”操作添加元素之前使用 reserve 操作会更高效。这将确保至少与你要求的一样多的元素,如果剩余插槽太少,则会呈指数增长。如果你想保留特定数量的插槽,但不想呈指数增长(例如,你知道这是最后一次分配),则使用负数进行预留。你也可以从一开始就创建一个特定大小的向量。

你应该优先使用 push 和 pop 操作,因为它们在向量末尾附加和移除元素。如果你需要一次性移除多个项目,请使用 truncate 操作。insert 和 remove 操作允许你在向量中间改变元素。有两个 remove 操作,一个保留元素顺序“ordered_remove”,另一个不保留“unordered_remove”。后一个函数将末尾元素复制到已移除的插槽中,而不是调用 memmove 操作。“lower_bound”函数将确定使用 insert 在数组中放置项目的哪个位置,以保持排序顺序。

当定义向量类型时,首先创建一个非内存管理版本。然后你可以定义垃圾回收和堆分配版本中的一个或两个。分配机制在定义类型时指定,因此它是类型的一部分。如果你需要垃圾回收和堆分配版本,你仍然必须精确地定义一个公共非内存管理基本向量。

如果你需要直接操作向量,那么“地址”访问器将返回向量开头的地址。“空间”谓词将告诉你向量中是否有剩余容量。你通常不需要使用这两个函数。

向量类型使用 DEF_VEC_{O,P,I}(TYPEDEF) 宏定义,以获得非内存分配版本,然后使用 DEF_VEC_ALLOC_{O,P,I}(TYPEDEF,ALLOC) 宏获得内存管理的向量。向量类型的变量使用 VEC(TYPEDEF,ALLOC) 宏声明。ALLOC 参数指定分配策略,可以是 'gc' 或 'heap',分别表示垃圾回收和堆分配。它可以是 'none',以获得必须显式分配的向量(例如,作为另一个结构的尾部数组)。字符 O、P 和 I 指示 TYPEDEF 是指针 (P)、对象 (O) 还是整型 (I) 类型。务必选择正确的类型,因为使用错误的类型会导致笨拙且效率低下的 API。对于 P 和 I 版本,有一个检查,它会导致编译时警告,但对于 O 版本没有检查,因为在纯 C 中这是不可能的。由于 GTY 的工作方式,您必须使用 GTY(()) 标签对要插入或从向量中引用的任何结构进行注释。即使从未声明 GC 分配的变体,也需要这样做。

下面是一个使用它们的例子:

 DEF_VEC_P(tree);   // non-managed tree vector.
 DEF_VEC_ALLOC_P(tree,gc);    // gc'd vector of tree pointers.  This must
                              // appear at file scope.
 
 struct my_struct {
   VEC(tree,gc) *v;      // A (pointer to) a vector of tree pointers.
 };
 
 struct my_struct *s;
 
 if (VEC_length(tree,s->v)) { we have some contents }
 VEC_safe_push(tree,gc,s->v,decl); // append some decl onto the end
 for (ix = 0; VEC_iterate(tree,s->v,ix,elt); ix++)
   { do something with elt }


其他表示

[edit source]
Additional representations of GCC 4.1.
GCC 4.1 的其他表示。

图 2 显示了 GCC 4.1 的其他表示。

由于语言的差异,生成的 AST 的格式对于每种语言略有不同。AST 生成后的下一步是统一步骤,其中 AST 树被转换为称为通用形式的统一形式。之后,编译器的中间端部分接管。首先,树被转换为另一种称为 GIMPLE 的表示形式。在这种形式中,每个表达式最多包含三个操作数,所有控制流结构都被表示为条件语句和 goto 运算符的组合,函数调用的参数只能是变量,等等。 图 2 说明了通用形式的树和 GIMPLE 形式的树之间的区别。GIMPLE 是一个方便的表示,用于优化源代码。


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


SSA 形式也用于优化。GCC 对 SSA 树执行 20 多种不同的优化。在 SSA 优化阶段之后,树被转换回 GIMPLE 形式。

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

GCC 初始化

[edit source]
GCC initialization.
GCC 初始化。

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

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

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

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

文件 toplev.c 包含主 cc1 函数 toplev_main() 以及定义编译器状态的全局变量。变量 current_function_decl 是正在编译的函数的声明,或者如果在函数之间则为 NULL

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

在命令行选项解析函数 do_compile() 被调用之后。它通过调用函数 backend_init() 来执行后端初始化。

后端初始化包括许多步骤。函数 init_emit_once() 为许多寄存器生成 RTL 表达式:程序计数器的 pc_rtx、条件的 cc0、堆栈指针的 stack_pointer_rtx、帧指针的 frame_pointer_rtx 等等。它们保存在数组 global_rtl 中。

之后,函数 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 初始化包括命令行选项解析、初始化后端、创建全局范围以及初始化内置数据类型和函数。

解析器和预处理器

[edit source]

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

解析器

[edit source]

解析器在文件 c_parser.c 中手动实现。与 GCC 的早期版本相比,新的解析器生成更低级的 AST。例如,循环存在特殊的树代码,FOR_STMT 表示 for() 循环。在这个版本中,循环被表示为条件语句和 goto,即代码 COND_EXPRLABEL_EXPRGOTO_EXPR 的树。这可能意味着无法将 AST 表示提升回原始源代码。

预处理器

[edit source]

预处理器实现为库。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。

从源代码到 AST

[edit source]

运行预处理器后,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

当解析到一个新标识符时,它会被添加到 GCC维护的字符串池中。标识符的树代码是IDENTIFIER_NODE。当再次解析到相同的标识符时,会返回相同的树节点。函数get_identifier()返回标识符的树节点。

Parsing variable declaration.
解析变量声明。

新的变量声明是在一系列函数调用中处理的。首先,函数start_decl()会用声明的名称、词法分析器返回的类型及其属性进行调用。该函数会调用grokdeclarator(),检查类型和参数节点,并返回一个代码适合于声明的树:VAR_DECL表示变量,TYPE_DECL表示类型,等等。然后声明会被添加到scope中。作用域包含在一个函数中创建的所有声明,但不包含全局声明。还有一个全局作用域,包含全局声明。当解析一个函数时,它的声明会作为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()会将声明的attributes参数作为它们的其中一个参数。属性在 GCC 手册中进行了描述。属性是 GNU C 实现的扩展。下表列出了一些属性,并解释了它们的用途。

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

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

解析完函数后,使用宏DECL_SAVED_TREE访问它的函数体。它会使用一个BIND_EXPR树来表示,该树将局部变量绑定到语句。BIND_EXPR_VARS会给出声明变量的链。BIND_EXPR_BODY会返回一个STATEMENT_LIST类型的树。

以下 API 允许遍历语句列表并对其进行操作

树构造 API
函数 用途
tsi_start(stmt_list) 获取一个指向列表头的迭代器
tsi_last(stmt_list) 获取一个指向列表尾部的迭代器
tsi_end_p(iter) 是否为列表末尾?
tsi_stmt(iter) 获取当前元素
tsi_split_statement_list_before(&iter) 在 iter 处拆分元素
tsi_link_after(&iter, stmt, mode) 在 iter 后链接链
tsi_next(&iter) 列表的下一个元素
append_to_statement_list(tree, &stmt_list) 将树追加到 stmt_list

可以在此级别上对函数序言/尾声进行插桩,如gimplify_function_tree()中所示。要向函数尾声添加语句,请使用TRY_FINALLY_EXPR树。它的第一个操作数是旧语句,第二个参数是尾声语句。这种类型的树会指示后续的传递在创建函数的公共出口基本块时执行这些语句。

要对函数序言进行插桩,请在树之前添加所需的语句。因此,BIND_EXPR_BODY将包含序言和TRY_FINALLY_EXPR树。

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

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

从 AST 到 GIMPLE

[edit source]

当最终从 finish_function() 调用 gimplify_function_tree() 时,AST 会被简化为 GIMPLE。

GIMPLE 表示基于 [2] 中描述的 SIMPLE。

根据这篇论文,目标是将树表示为基本语句。

GIMPLE 树
x=a binop b x=a x=cast b f(args)
*p=a binop b *p=a *p=cast b -
x=unop a x=*q x=&y x=f(args)
*p=unop a *p=*q *p=&y *p=f(args)

临时变量在需要时会在函数 create_tmp_var() 和 declare_tmp_vars() 中创建。

在此阶段,GCC 会优化复杂的条件表达式,即

 if (a || b) stmt;

会被转换为

 if (a) goto L1;
 if (b) goto L1; else goto L2;
 L1:
 stmt;
 L2:

此外,条件表达式的每个分支都会被包装到 STATEMENT_LIST 树中。

从 GIMPLE 到 RTL

[edit source]

寄存器传输语言表示具有无限数量寄存器的抽象机器。类型rtx描述一条指令。每个 RTL 表达式都具有代码和机器模式。

与 AST 类似,代码被分组到多个类中。它们在 mode-classes.def 中定义。

RTL 表达式的类
说明
RTX_CONST_OBJ 表示常量对象(例如,CONST_INT)
RTX_OBJ 表示对象(例如,REG,MEM)
RTX_COMPARE 比较(例如,LT,GT)
RTX_COMM_COMPARE 可交换比较(例如,EQ,NE,ORDERED)
RTX_UNARY 一元算术表达式(例如,NEG,NOT)
RTX_COMM_ARITH 可交换二元运算(例如,PLUS,MULT)
RTX_TERNARY 非位域三输入运算(IF_THEN_ELSE)
RTX_BIN_ARITH 不可交换二元运算(例如,MINUS,DIV)
RTX_BITFIELD_OPS 位域运算(ZERO_EXTRACT,SIGN_EXTRACT)
RTX_INSN 机器指令(INSN,JUMP_INSN,CALL_INSN)
RTX_MATCH 在指令中匹配的内容(例如,MATCH_DUP)
RTX_AUTOINC 自动递增寻址模式(例如,POST_DEC)
RTX_EXTRA 所有其他内容

文件 machmode.def 中列出的机器模式指定了机器级数据的大小和格式。在语法树级,每个..._TYPE 和每个..._DECL 节点都有一个机器模式,描述该类型的數據或声明的变量的数据。

编译函数时会构建一个指令列表。函数 emit_insn() 会将一条指令添加到列表中。变量声明 AST 已经生成了它的 RTL。使用 DECL_RTL 访问它。函数 emit_cmp_and_jump_insns() 会输出条件语句。emit_label() 会打印一个标签。这些函数会将指令一个接一个地链接起来。宏 PREV_INSN 和 NEXT_INSN 用于遍历列表。

可以使用 first_insn 和 last_insn 访问第一条和最后一条指令。get_insns() 会提供当前序列或当前函数的第一条指令。

使用 debug_rtx() 在屏幕上打印 RTL 指令,使用函数 print_rtl() 打印 RTL 表达式列表。

一些函数会创建节点。例如,gen_label_rtx() 会构建一个标签。最通用的函数位于特定于目标的目录中。例如,x86 架构 RTL 生成文件 genrtl.c 和 genrtl.h 位于 ./host-i686-pc-linux-gnu 中。

从 RTL 到目标

[edit source]

每个目标架构都有自己的描述,用 struct gcc_target targetm 表示。默认初始化器位于文件 targhooks.c 中。

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

降低传递

[编辑源代码]

函数的处理包括它的降低,当一系列优化阶段在函数 tree_lowering_passes() 中应用于它时。作为降低函数的结果,它的控制流图被生成。随后对函数 cgraph_create_edges() 的调用使用基本块信息来增加调用图的边,这些边带有当前函数执行的调用。对尚未定义的函数的引用保存在函数 record_references() 中。

所有降低阶段
名称 意义
remove_useless_stmts N/A
mudflap_1 通过树重写进行窄指针边界检查
lower_cf 将 GIMPLE 降低为非结构化形式
pass_lower_eh 树的异常处理语义和分解
pass_build_cfg 创建基本块
pass_lower_complex_O0 无优化情况下的复杂操作降低
pass_lower_vector 将向量运算降低为标量运算
pass_warn_function_return 发出返回警告
pass_early_tree_profile 为基于树的分析设置钩子

switch 语句

[编辑源代码]

让我们考虑如何将 switch 语句从源代码转换为 GIMPLE 到 RTL。

当在源代码中遇到语句时,会调用函数 c_parser_switch_statement()。一个典型的 switch 语句包含多个 case 语句,这些语句可能具有 break 语句。因此,解析器具有 c_break_label 树,它标记 switch 结束的位置。该函数解析语句的主体,并在至少发现一个 break 语句时为 break 标签生成 LABEL_EXPR 树。函数 c_finish_case() 将主体附加到 SWITCH_EXPR 树作为其操作数之一。此外,这棵树还有另外两个操作数:switch 条件和 switch 标签。使用宏 SWITCH_COND()、SWITCH_BODY() 和 SWITCH_LABELS() 访问操作数。标签在解析时不会被填充。

switch 语句在函数 gimplify_switch_expr() 中被 gimplify 化。其思想是将主体与决策部分分离,并生成 switch 标签,以便在验证条件后可以将执行重定向到相应的 case。我们将考虑存在默认标签的情况。此函数有两个指向语句列表的指针:pre_p,它代表副作用列表,以及 expr_p,它代表语句本身。

switch 的主体在 gimplify_to_stmt_list() 中被 gimplify 化。case 标签保存在变量 struct gimplify_ctx gimplify_ctxp 的 case_labels 字段中。然后,该函数创建一个 TREE_VEC 的标签并用相应的 case 标签初始化它们。TREE_VEC 被分配给 switch 语句的 SWITCH_LABELS 操作数,然后附加到 pre_p 列表。然后,原始语句使用 expr_p 指针被 SWITCH_BODY 覆盖。最后,switch 语句中副作用列表的 SWITCH_BODY 操作数被清除,以便它仅包含标签。

从这一点开始,很明显编译器试图使用一个跳转表来表示原始语句,该跳转表将每个可能的索引值映射到相应 case 的地址。函数 expand_case() 实现了这个想法。它生成一个 table_label,在该标签上为每个可能的索引值生成跳转指令。然后调用函数 try_tablejump(),它将索引树扩展为索引 rtl 并调用 do_tablejump()。此函数生成一个绝对索引 rtl,它组合了基地址 table_label 和索引偏移量。之后,它发出跳转指令到跳转表的正确条目。执行继续在函数 expand_case() 中进行。跳转表使用 SWITCH_LABELS 生成。

labelvec[i] = gen_rtx_LABEL_REF (Pmode, label_rtx (n->code_label));

最后,发出许多跳转指令。

if (CASE_VECTOR_PC_RELATIVE || flag_pic)
  emit_jump_insn (gen_rtx_ADDR_DIFF_VEC (CASE_VECTOR_MODE,
                                         gen_rtx_LABEL_REF (Pmode, table_label),
                                         gen_rtvec_v (ncases, labelvec),
                                         const0_rtx, const0_rtx));


要点: 后端为指定的目标平台生成汇编代码。
上一页: 目录 GNU C 编译器架构 下一页: 堆栈保护
  1. ^ https://web.archive.org/web/20160410185222/https://www.redhat.com/magazine/002dec04/features/gcc/
  2. ^ L. Hendren、C. Donawa、M. Emami、G. Gao、Justiani 和 B. Sridharan。基于结构化中间表示族的 McCAT 编译器设计。在第五届并行计算语言和编译器研讨会论文集中,1992 年。
华夏公益教科书