GNU C 编译器内部/GEM 框架 4.1
钩子
[编辑源代码]GEM 框架旨在促进编译器扩展的开发。GEM 的理念类似于 Linux 安全模块 (LSM) 的理念,该项目定义了整个 Linux 内核的钩子,允许一个来执行安全策略。
GEM 在整个 GCC 的源代码中定义了许多钩子。它被实现为对 GCC 的补丁。使用 GEM,编译器扩展被开发为一个独立的程序。它被编译成一个动态链接的模块,该模块在调用 GCC 时被指定为命令行参数。GCC 加载模块并调用其初始化函数。然后,该模块注册其钩子,这些钩子是 GCC 中的回调函数。
除了编译器钩子之外,GEM 还提供宏和函数来简化扩展开发。在本章中,我们将首先介绍 GEM 框架添加到 GCC 的钩子。然后我们描述扩展编程中的典型问题。
项目主页位于 http://research.alexeysmirnov.name/gem
GEM 在整个 GCC 源代码中添加了几个钩子。根据需要向 GEM 添加新的钩子。
- 钩子 gem_handle_option 到函数 handle_option(),该函数处理每个命令行选项。钩子将当前选项作为其参数。如果钩子返回值 GEM_RETURN,则 GCC 将忽略该选项。
- 钩子 gem_c_common_nodes_and_builtins 在创建所有标准类型之后被调用。GCC 扩展可以创建额外的类型。
- 钩子 gem_macro_name 允许保存正在定义的宏的名称。另一个 GEM 钩子 gem_macro_def 在解析宏定义时被调用。使用新宏定义的宏名称,可以重新定义宏。此钩子被添加到函数 create_iso_definition() 中。
- 钩子 gem_start_decl 和 gem_start_function 在函数或变量声明/定义开始时被调用。
- 钩子 gem_build_function_call 允许修改函数调用的名称和参数。
- 钩子 gem_finish_function 被插入到 finish_function() 中,该函数从语法文件调用。编译器扩展在函数体被翻译成 RTL 之前接收函数体。
- 钩子 gem_output_asm_insn 和 gem_final_start_function 被分别添加到函数 output_asm_insn() 中,该函数针对汇编代码的每个指令被调用,以及函数 final_start_function() 中,该函数在汇编代码被写入文件时被调用。前一个钩子接收写入文件的文本,允许它修改输出。后一个钩子可以修改函数的序言。
要点: | GEM 钩子主要在 AST 级别定义。一些钩子在汇编级别定义。根据需要添加新的钩子。 |
遍历 AST
[编辑源代码]当函数的 AST 被构建时,可以对其进行检测。GEM 的 gem_finish_function 钩子接收函数的 AST。其思路是遍历 AST 并根据需要检测 AST 节点。函数 walk_tree() 获取 AST、回调函数、可选数据(默认情况下为 NULL)和 walk_subtrees 参数(默认情况下为 NULL)。回调函数在遍历操作数之前针对 AST 的每个节点被调用。如果回调函数修改了 walk_subtree() 变量,则操作数不会被处理。
以下代码演示了这个想法
static tree walk_tree_callback(tree *tp, int *walk_subtrees, void *data) { tree t=*tp; enum tree_code code = TREE_CODE(t); switch (code) { case CALL_EXPR: instrument_call_expr(t); break; case MODIFY_EXPR: instrument_modify_expr(t); break; } } walk_tree(&t_body, walk_tree_callback, NULL, NULL);
要点: | 函数 walk_tree() 遍历 AST,将用户定义的回调函数应用于每个树节点。 |
检测 AST
[编辑源代码]在本节中,我们将描述创建新树节点的函数以及如何将新节点添加到 AST 中。
在符号表中查找声明
[编辑源代码]void gem_find_symtab(tree *t_var, char *name) { tree t_ident = get_identifier(name); if (t_ident) *t_var = lookup_name(t_ident); else *t_var=NULL_TREE; }
构建树节点
[编辑源代码]walk_tree 回调函数可以检测 AST。函数 build1() 和 build() 构建新的树节点。前一个函数获取一个操作数,后一个函数获取多个操作数。以下代码计算操作数的地址,与 '&' C 运算符相同
t = build1(ADDR_EXPR, TREE_TYPE(t), t);
以下示例指的是数组元素 arr[0]
t = build(ARRAY_REF, integer_type_node, arr, integer_zero_node);
以下示例构建一个整数常量
t = build_int_cst(NULL_TREE, 123);
构建字符串常量更加困难。以下示例演示了这个想法
tree gem_build_string_literal(int len, const char *str) { tree t, elem, index, type; t = build_string (len, str); elem = build_type_variant (char_type_node, 1, 0); index = build_index_type (build_int_2(len-1, 0)); type = build_array_type (elem, index); T_T(t) = type; TREE_READONLY(t)=1; TREE_STATIC(t)=1; TREE_CONSTANT(t)=1; type=build_pointer_type (type); t = build1 (ADDR_EXPR, type, t); t = build1 (NOP_EXPR, build_pointer_type(char_type_node), t); return t; }
要构建函数调用,需要找到函数的声明并构建参数列表。然后构建 CALL_EXPR
gem_find_symtab(&t_func_decl, "func"); t_arg1 = build_tree_list(NULL_TREE, arg1); t_arg2 = build_tree_list(NULL_TREE, arg2); ... TREE_CHAIN(t_arg1)=t_arg2; ... TREE_CHAIN(t_argn)=NULL_TREE; t_call = build_function_call(t_func_decl, t_arg1);
如果想要构建语句列表 { stmt1; stmt2; ... },则需要使用函数 append_to_statement_list()
tree list=NULL_TREE; for (i=0; i<num_stmt; i++) { BUILD_FUNC_CALL1(t_call, t_send, t_arr[i], NULL_TREE); append_to_statement_list(t_call, &list); }
将节点添加到树
[编辑源代码]GCC 4.1 具有一个接口,允许将一个节点链添加到另一个节点链中,该接口在文件 tree-iterator.c 中实现。函数 tsi_start() 和 tsi_last() 创建一个树语句迭代器,并分别将其分配给列表中的第一个或最后一个树。函数 tsi_link_before() 和 tsi_link_after() 使用迭代器将语句链接到当前语句之前或之后。还有一个函数 append_to_statement_list(),它将节点添加到列表中。如果指定的列表参数为 NULL_TREE,则会分配一个新的语句列表。
构建函数和变量声明
[编辑源代码]全局声明是在钩子 gem_c_common_nodes_and_builtins() 中添加的。在以下示例中,我们构建了一个结构类型并创建了该类型的全局变量。该结构具有一个无符号整数类型字段和一个函数指针字段。
t_log = make_node(RECORD_TYPE); decl_chain = NULL_TREE; field_decl = build_decl(FIELD_DECL, get_identifier("addr"), unsigned_type_node); TREE_CHAIN(field_decl)=decl_chain; decl_chain=field_decl; DECL_FIELD_CONTEXT(decl_chain) = t_log; ... t_func_type = build_function_type_list(void_type_node, unsigned_type_node, NULL_TREE); field_decl = build_decl(FIELD_DECL, get_identifier("add_addr"), build_pointer_type(t_func_type); TREE_CHAIN(field_decl)=decl_chain; decl_chain=field_decl; DECL_FIELD_CONTEXT(decl_chain) = t_log; ... TYPE_FIELDS(t_log) = nreverse(decl_chain); layout_type(t_log); pushdecl(build_decl(TYPE_DECL, get_identifier("log_t"), t_log)); decl = build_decl(VAR_DECL, get_identifier("log"), build_pointer_type(t_log)); DECL_EXTERNAL(decl)=1; pushdecl(decl);
何时检测
[编辑源代码]在本节中,我们将描述每个 GEM 钩子何时使用。
- 在钩子 gem_c_common_nodes_and_builtins 中添加新的函数和类型声明。
- 在钩子 gem_finish_function 中解析 AST 后对其进行检测。
- 在钩子 gem_start_decl 和 gem_finish_decl 中修改声明的属性。假设我们要用堆数组 char *arr=(char*)malloc(10); 替换局部数组声明 char arr[10]。
void l2h_start_decl(void *p_decl, void *p_declspecs, init initialized, void *p_attr) { struct c_declarator *decl = *((struct c_declarator**)p_decl); if (current_function_decl == NULL_TREE) return; if (decl->kind == cdk_array) { decl->kind = cdk_pointer; decl->u.pointer_quals = 0; } }
void l2h_finish_decl(tree decl, tree *init, tree spec) { ... gem_find_symtab(&t_malloc, "malloc"); BUILD_FUNC_CALL1(t_call, t_malloc, build_int_cst(NULL_TREE, size), NULL_TREE); *init = build1(NOP_EXPR, build_pointer_type(char_type_node), t_call); DECL(decl) = build_int_cst(NULL_TREE, 0); // if this field is NULL the init is ignored }
- 用代理函数替换函数调用
函数 Prolog/Epilog
[编辑源代码]汇编指令被写入汇编文件
#define OUTPUT_ASM_INST(inst) \ p=inst; \ putc('\t', asm_out_file); \ while (*p++) putc(p, asm_out_file); \ putc('\n', asm_out_file); OUTPUT_ASM_INST("pushl %%eax"); OUTPUT_ASM_INST("popl %%eax");
要点: | 使用钩子 gem_output_asm_insn 和 gem_final_start_function 将汇编指令添加到函数序言和结尾。 |