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 將彙編指令新增到函式序言和結尾。 |