Let be a pro
Fabrice Bellard 是目前當代我最推崇的程式設計師。他是一個很天才也很多產的工程師, 他著名的作品包含了: FFmpeg 、[QEMU][2] 以及 JsLinux 。其中 JsLinux 是他開始學習 JavaScript 之後覺得很有趣,開發出來在 Browser 可以運行的虛擬機。
TinyCC 是他在 1996 年寫出來的一個 C Compiler,是目前號稱編譯速度最快的 C Compiler,也是 GNU/Linux 下最小的 ANSI C Compiler。目前可以從 Github 下載到目前最新版本的 source code。
Layout
TinyCC 主要是由 tcc.c 開始,連接 libtcc.a 這個函式庫所組成的。libtcc.a 是由 libtcc.c、tccpp.c、tccgen.c、tccelf.c、tccasm.c、tccrun.c、X86_64.c 與 i368-asm.c 所構成的。從 tcc.c 中的 main 開始看起,可以了解開頭的流程用在處理 compile 階段送進來的參數,直到 tcc_add_file 開始才進入到 compile 的階段。
compile 流程包含了:
- 讀取檔案,並且記錄目前的讀取狀態。
- 將檔案內容切成若干的 Token。
- 根據每個 Token 來產生不同的 binary。
在第一個步驟,tcc 會利用 tcc_open 將檔案開啟並且將 fd 存在 tcc 內部的狀態機 (TCCState) 當中。在這個步驟,會一起設定狀態機的初始狀態。
ST_FUNC int tcc_open(TCCState *s1, const char *filename)
{
int fd;
if (strcmp(filename, "-") == 0)
fd = 0, filename = "stdin";
else
fd = open(filename, O_RDONLY | O_BINARY);
if ((s1->verbose == 2 && fd >= 0) || s1->verbose == 3)
printf("%s %*s%s\n", fd < 0 ? "nf":"->",
(int)(s1->include_stack_ptr - s1->include_stack), "", filename);
if (fd < 0)
return -1;
tcc_open_bf(s1, filename, 0);
file->fd = fd;
return fd;
}
進入第二個步驟前,會根據是否需要處理 Preprocess 而判斷是否要進入 tcc_preprocess。正常來說會進入到 tcc_compile,並且送入狀態 s1 當作參數。 在進入 tokenize 階段前,會先塞入所需要的 elf symbols (put_elf_sym): 這裡塞入為所編譯檔案的檔案名稱。接著使用 setjmp 來標記一個 Label ,用來當 compile 失敗的時候,利用 tcc_error 中的 longjmp 來達到 try-catch 的功能。最後呼叫 next 來拿到下一個 Token。
if (setjmp(s1->error_jmp_buf) == 0) {
s1->nb_errors = 0;
s1->error_set_jmp_enabled = 1;
ch = file->buf_ptr[0];
tok_flags = TOK_FLAG_BOL | TOK_FLAG_BOF;
parse_flags = PARSE_FLAG_PREPROCESS | PARSE_FLAG_TOK_NUM;
pvtop = vtop;
next();
decl(VT_CONST);
if (tok != TOK_EOF)
expect("declaration");
if (pvtop != vtop)
tcc_warning("internal compiler error: vstack leak? (%d)", vtop - pvtop);
/* end of translation unit info */
if (s1->do_debug) {
put_stabs_r(NULL, N_SO, 0, 0,
text_section->data_offset, text_section, section_sym);
}
}
在 next 的開頭會跟去 flag 是否有設定 PARSE_FLAG_SPACES,來決定是呼叫 next_nomacro_spc 或者 next_nomacro,後者是前者的包裝: 持續呼叫 next_nomacro_spc 直到讀到空白字元為止。在裡面的實作使用 next_nomacro1 來真正的得到下一個 Token 並且分類這個 token 屬於哪一種型態。
/* return next token with macro substitution */
ST_FUNC void next(void)
{
Sym *nested_list, *s;
TokenString str;
struct macro_level *ml;
redo:
if (parse_flags & PARSE_FLAG_SPACES)
next_nomacro_spc();
else
next_nomacro();
if (!macro_ptr) {
/* ... ignore ... */
} else {
/* ... ignore ... */
}
/* convert preprocessor tokens into C tokens */
if (tok == TOK_PPNUM &&
(parse_flags & PARSE_FLAG_TOK_NUM)) {
parse_number((char *)tokc.cstr->data);
}
}
接著第三個步驟,呼叫 decl / decl0 來產生接下來的 binary code,藉由 type_decl 來判斷進來的 token 是哪一種資料型態,接著根據型態會有處理 function、 inline asm 、scope 或者一般資料四種方式。在一般資料狀況中,會使用 sym_push 的方式將資料存到 symbol stack 當中。 在 function 的情況中,則會呼叫 func_decl_list 來處裡 function 裡面的內容: 一開始先處理所有送進來的變數,包含將送進來的參數作 轉換 :將 array 轉換成 pointer 以及將 function 轉換成 function pointer。接著呼叫 gen_function 在 symbol table 定義一個 symbol 並且產生 binary code。
gfunc_prolog(&sym->type);
rsym = 0;
block(NULL, NULL, NULL, NULL, 0, 0);
gsym(rsym);
gfunc_epilog();
gen_function 的會執行以下幾個流程: 處理 symbol table 會使用到的 symbol,這時候填入 dummy 值。 呼叫 gfunc_prolog。這個函數會根據不同平台有不同的實作,已 x86_64 ELF 為例, 首先會判斷是否函數帶有 … 的 可變參數函數 ,接著再判斷函數是否回傳 struct, 最後將送進來的參數用 ASM / binary 的方式定義。 接著呼叫 block 來處理 scope 內的程式碼 (舉下面三個為例):
- 如果遇到 if 邏輯判斷式:使用 gexpr 處理 () 內的條件,然後呼叫 block 處理 scope 內的邏輯。
- 如果遇到 while 一樣使用 gexpr 處理 () 內的條件、呼叫 block 處理 scope 的邏輯,並用 gjmp_addr 來做到重複的邏輯。
- 遇到回傳 return 就使用 gexpr 來拿到表達式,呼叫 gv 來處理 vtop 內的值。最後使用 gjmp 回到 caller。
其中 gexpr 會一再的處理目前的 token,值到一個表達式結束為止。
ST_FUNC void gexpr(void)
{
while (1) {
expr_eq();
if (tok != ',')
break;
vpop();
next();
}
}
然後呼叫 gfunc_epilog 來處理 function 最後的部分:填入 0xC9 確定會離開函數。 最後重新填入第一步驟中 symbol table 得值。
Conclusion
Fabrice Bellard 的程式碼真的是很難 trace 啊~ 例如在產生 jump 的 ASM code 時, 他使用 gjmp_addr 來產生,其中又會呼叫到 g 跟 oad 兩個內部使用的函數, 其中又會呼叫到一個叫作 o 的函數。光看函數名稱或者註解完全無法裡解他想要做什麼事情。
/* generate a jump to a fixed address */
void gjmp_addr(int a)
{
int r;
r = a - ind - 2;
if (r == (char)r) {
g(0xeb);
g(r);
} else {
oad(0xe9, a - ind - 5);
}
}
/* XXX: make it faster ? */
void g(int c)
{
int ind1;
ind1 = ind + 1;
if (ind1 > cur_text_section->data_allocated)
section_realloc(cur_text_section, ind1);
cur_text_section->data[ind] = c;
ind = ind1;
}
/* instruction + 4 bytes data. Return the address of the data */
ST_FUNC int oad(int c, int s)
{
int ind1;
o(c);
ind1 = ind + 4;
if (ind1 > cur_text_section->data_allocated)
section_realloc(cur_text_section, ind1);
*(int *)(cur_text_section->data + ind) = s;
s = ind;
ind = ind1;
return s;
}
void o(unsigned int c)
{
while (c) {
g(c);
c = c >> 8;
}
}