跳转到内容

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_declgem_start_function 在函数或变量声明/定义开始时被调用。
  • 钩子 gem_build_function_call 允许修改函数调用的名称和参数。
  • 钩子 gem_finish_function 被插入到 finish_function() 中,该函数从语法文件调用。编译器扩展在函数体被翻译成 RTL 之前接收函数体。
  • 钩子 gem_output_asm_insngem_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_declgem_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 将汇编指令添加到函数序言和结尾。
华夏公益教科书