GNU C 编译器内部/GNU C 编译器架构 3 4
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) 和目标文件 是主要表示形式。
主要表示
[编辑源代码]前端的目的是读取源文件、解析它并将其转换为标准抽象语法树 (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 保持排序顺序的情况下将项目放在数组中的位置。
定义向量类型时,首先创建一个非内存管理版本。然后你可以定义垃圾收集和堆分配版本中的一个或两个。分配机制是在定义类型时指定的,因此是类型的一部分。如果你需要垃圾收集和堆分配版本,你仍然必须精确地定义一个公共非内存管理基本向量。
如果你需要直接操作向量,则“address”访问器将返回向量开始处的地址。“space”谓词将告诉你在向量中是否有剩余容量。你通常不需要使用这两个函数。
向量类型使用 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 }
附加表示
[编辑源代码]GCC 3.4 有附加表示吗?
带回家: | GCC 是一个编译器集合,它包含每个编程语言的前端、中间端和每个体系结构的后端。每个源文件经过的主要表示形式是前端的 AST、中间端的 RTL 以及后端的汇编表示。GCC 一次编译一个文件。 |
GCC 初始化
[编辑源代码]C 前端包括 C/C++ 预处理器和 C 编译器。程序 cc1 包含预处理器和 C 编译器。它编译 C 源文件并生成汇编 (.S) 文件。
编译器前端和后端通过称为语言钩子的回调函数相互交互。所有钩子都包含在一个全局变量结构 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 类型在初始化时创建。下表列出了一些类型
变量名 | 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 内置函数的示例
内置函数名 | 解释 |
builtin_constant_p | 如果参数是常量,则返回 true |
builtin_memcpy | 等效于 memcpy() |
builtin_strlen | 等效于 strlen() |
带回家: | GCC 初始化包括命令行选项解析、初始化后端、创建全局范围以及初始化内置数据类型和函数。 |
解析器和预处理器
[编辑源代码]在初始化之后,函数 do_compile() 调用函数 compile_file()。此函数调用 parse_file() 前端语言钩子,该钩子对于 C 语言设置为函数 c_common_parse_file()。后一个函数调用函数 finish_options(),该函数初始化预处理器并处理 -D、-U 和 -A 命令行选项(分别等效于 #define、#undef 和 #assert)。C 预处理器处理源代码中的预处理器指令,例如 #define、#include。
解析器
[编辑源代码]在预处理器初始化之后,调用 {{{4}}} 函数。此函数使用标准 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。 |
从源代码到 AST
[编辑源代码]运行预处理器后,GCC 为源文件的每个函数构建抽象语法树 (AST)。AST 是多个连接的 struct tree 类型的节点。每个节点都有一个 树代码,用于定义树的类型。宏 TREE_CODE() 用于引用代码。树代码在文件 tree.def 和 c-common.def 中定义。具有不同树代码的树被分组到 树类 中。以下树类在 GCC 中定义,包括:
树类 | 解释 |
'd' | 声明 |
'<' | 比较 |
'2' | 二元算术 |
有两种类型的树:语句 和 表达式。语句对应于 C 结构,例如表达式后跟一个 ';'、条件语句、循环语句等。表达式是语句的构建基础。表达式的示例包括赋值表达式、算术表达式、函数调用等。树代码的示例在下表中给出
树代码 | 树类 | 解释 | 操作数 |
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_EXPR 和 CONVERT_EXPR 树用于类型转换。
树 NULL_TREE 等效于 NULL。函数 debug_tree() 将树打印到 stderr。
当一个新的标识符被词法分析时,它会被添加到 GCC 维持的字符串池中。标识符的树代码是 IDENTIFIER_NODE。当再次词法分析相同的标识符时,会返回相同的树节点。函数 get_identifier() 返回标识符的树节点。
新的变量声明将在多个函数调用中处理。首先,调用函数 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 允许遍历语句列表并对其进行操作。
函数 | 用途 |
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 表示基于 SIMPLE,如 [2] 中所述。
根据本文,目标是将树表示为基本语句。
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 中定义。
类别 | 解释 |
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]每个目标架构都有其描述,表示为结构体 gcc_target targetm。文件 targhooks.c 中提供了默认的初始值设定项。
后端为指定的目標平台生成汇编代码。函数output_asm_insn() 用于写入汇编文件的每条指令。函数final_start_function() 在函数保存到汇编文件之前生成函数的序言。
降低传递
[edit source]函数的处理包括其降低,此时会对它应用许多优化传递,如函数 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 语句
[edit source]让我们考虑一下 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() 中被简化。其思想是将主体与决策部分分离,并生成 switch 标签,以便在验证条件后可以将执行重定向到相应的 case。我们将考虑存在默认标签的情况。此函数有两个指向语句列表的指针:pre_p(这是副作用列表)和 expr_p(这是语句本身)。
switch 语句的主体在 `gimplify_to_stmt_list()` 函数中被简化。case 标签被保存在变量 `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));
带回家: | 后端为指定的目标平台生成汇编代码。 |
- ^ https://web.archive.org/web/20160410185222/https://www.redhat.com/magazine/002dec04/features/gcc/
- ^ L. Hendren、C. Donawa、M. Emami、G. Gao、Justiani 和 B. Sridharan。基于结构化中间表示家族的 McCAT 编译器设计。在第五届并行计算语言和编译器研讨会论文集中,1992 年。