跳轉到內容

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 將彙編指令新增到函式序言和結尾。
華夏公益教科書