# 2016q3 Homework5 (rubi) contributed by <`fzzzz`>, <`nekoneko`>, <`abba123`> ###### tags `fzzzz` `nekoneko` `abba123` `sysprog` ## 預期目標 * 閱讀 Dynasm ,了解 bf_dynasm.c * 學習 JIT 編譯器設計,透過輔助開發工具 * 設計 benchmark suite * 延展程式語言 ## TODO - [ ]學習 Dynasm ,了解 bf_dynasm.c - [ ]研讀 rubi 的程式碼 - [ ]參照 jit-compiler ,設計 rubi 的 benchmark ,並解釋為何如此設計 - [ ]試圖最佳化並利用上面的 benchmark suite 來比對解釋 ## 資料閱讀 ### Dynasm #### Tutorial - .arch : 設定產生組語的指令集類型 - dasm_init : - allocate Dynasm state ```c |.section code dasm_init(&d, DASM_MAXSECTION); ``` - .section : `| .section name1 [, name2 [, ...]]` 允許宣告多個 code section ,宣告的 name 會是新的 directive ==.name== 。之後呼叫 `dasm_encode()` 把所有的 section 合併成單一連續的 machine code block 。 - DASM_MAXSECTION : - code section 的數量 - Dynasm preprocessor 會根據 .section directive 的描述自動產生 `#define DASM_MAXSECTION 1` - ex : bf_dynasm.c 第62行`| .section code` 對應到 第142行 `| .code` - dasm_setupglobal ```c |.globals lbl_ void* labels[lbl__MAX]; dasm_setupglobal(&d, labels, lbl__MAX); ``` - | .globals *indent* 經過 Dynasm preprocessor 產生 enum 包含 *indent_MAX* - void* labels[indent_MAX]; - 用來紀錄 global labels 的位址( address ),紀錄的位址會再呼叫 dasm_link 裡用到。 - After ==dasm_link== has been called, the address of a global label can be retreived by indexing into the array previously passed to ==dasm_setupglobal== using one of the enum members defined by ==.globals==. - global lables : 格式 : `|->name:` > 不確定有沒有理解錯誤,附上原文[name=cheng hung lin] - dasm_setup : - 初始化 Dynasm state ```c |.actionlist bf_actions dasm_setup(&d, bf_actions); ``` - | .actionlist _indent_ - 經過 preprocessor 後產生 static const unsigned char 的陣列,紀錄有用到哪些組語語言。 - The contents of the array reflects the assembly used in the DynASM source file, though in an unspecified format. - dasm_growpc : - ~~紀錄 dynamic labels~~ 設定 dynamic lables 可以使用的數字的上限 - dasm_growpc: 呼叫dasm_growpc(dynasm_state, maxpc)後,可使用命名為 0 到 (maxpc-1) 之間的lables,也就是 |=>0 到 |=>(maxpc-1)。 - dynamic labels : - 格式: `|=>imm32:` - 跟 global label 類似,差別只在於 dynamic label 只限用非零整數作為命名 - Abstractions : - .define 和 .macro 將單個或多個 instructions 和 registers 定義成 abstractions - prologue : ~~建立 stack frame~~ 儲存進入 function 會使用到的 register 的原值到 stack,複製傳近來的參數( rcx )到 aState( r12 ) - prepcall1 arg1 : 儲存 local register ( arg1 )到 stack 並傳遞 arg1 - prepcall2 arg1, arg2 : 儲存 local register ( arg1, arg2 )到並傳遞 arg1, arg2 - postcall n : pop n 個 stack 元素 - epilogue : ~~刪除stack~~ 將進入 function 前register保留的原值,從 stack pop 儲來。並回到 caller 的 address + 1 > 這邊abstractions是bf_dynasm.c裡自訂的,非內建instrucitons。[name=cheng hung lin][color=#1652f7] > > [prologue 和 epilogue 的概念](https://en.wikipedia.org/wiki/Function_prologue)[name=cheng hung lin] - .type : syntex suger,可以使用語法詳閱 [.type](http://corsix.github.io/dynasm-doc/reference.html#_type) - dasm_State\*\* Dst = &d : Dynasm preprocessor 產生dasm_put(Dst, int start, ...) ,將所有 verticla bar 的描述轉成 dasm_put 的呼叫 - Arithmetic : - `| add byte [aPtr], n` : 在對記憶體做存取與運算時,需要決定記憶體單位,例如上述存取的單位為 byte,詳細可參照 [memory](http://corsix.github.io/dynasm-doc/instructions.html#memory) - Loop: - 對`[-]`做優化,`while(*p--) ;` 等價於 `*p = 0;` - loop assembly ``` | cmp byte [aPtr], 0 | jz =>nextpc+1 ------- |=>nextpc: <--- | ... | | | cmp byte [aPtr], 0 | | | jnz =>loops[nloops] --- | |=>loops[nloops]+1: <------ ``` - case '[' : - 假如 code 為 `[-]` ,則代表等價於 `*ptr = 0;` ``` | xor eax, eax // 自己 exclusive-or 自己 為 0 | mov byte [aPtr], al // al 為 exa 裡最右側的word ``` - nextpc 等於 pc: 呼叫 dyn_growpc 增加 dynamic labels 可使用的上限。 - `loops[nloops++] = nextpc;` 用來紀錄 nextpc 的位址,用於讀到`]`才知道跳回去的address - Epilogue: ```c | epilogue link_and_encode(&d); dasm_free(&d); return (void(*)(bf_state_t*))labels[lbl_bf_main]; ``` - link_and_encode(&d) : ```c= size_t sz; void* buf; dasm_link(d, &sz); buf = mmap(0, sz, PROT_READ | PROT_WRITE, \ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); dasm_encode(d, buf); mprotect(buf, sz, PROT_READ | PROT_EXEC); return buf; ``` - int dasm_link(Dst_DECL, size_t *szp) : 計算會產生 machine code 的 size - mmap : allocate一塊 memory ,可讀可寫但還不可執行 - int dasm_encode(Dst_DECL, void *buffer) : 將 machine code 寫入到 buffer裡,其中紀錄 global labels address 的陣列,也會寫入 - mprotect : 將 buffer 的 memory protection 改為 PROT_EXEC,變為可執行的 memory block - `return (void(*)(bf_state_t*))labels[lbl_bf_main];` - labels 是用來紀錄 global labels 的位址,所以這行是還傳 bf_main 函式,讓外部做呼叫 bf_main function call 定傳入型態為 bf_state_t \* 的變數 - 參照`prologue`可以看出傳入的參數為 bf_state_t \* ``` |.macro prologue | push aPtr | push aState | push aTapeBegin | push aTapeEnd | push rax ******* | mov aState, rArg1 ****passing argument*** ``` - bf_c.c & bf_dynasm.c - ~~brainfuck 語言的實做是 Turing Machine~~ brainfxck 是一個符合 turing completeness 的程式語言,ptr所指向的記憶體在本程式中是有限的,定義在 `bf_state_t bf_state` ,記憶體size則定義在 `#define TAPE_SIZE 30000` 。另外,bf_state_t還包含兩個 pointer to function 實做對 tape 記憶體讀取和寫入。 > 這邊第一句中文怪怪的, 改成 brainfxck 是一個符合 turing completeness 的程式語言感覺會更好點(Turing completeness is the ability for a system of instructions to simulate a Turing machine.)。[name=fzzzz] ```c=4 #define TAPE_SIZE 30000 ``` ```c=7 typedef struct bf_state { unsigned char* tape; unsigned char (*get_ch)(struct bf_state*); void (*put_ch)(struct bf_state*, unsigned char); } bf_state_t; ``` - bf_interpret 裡宣告的`tape_begin 和 tape_end`是為了在`>`和`<`中對記體位置移動時,做超出邊界的判斷。 ```c=22 unsigned char* tape_begin = state->tape - 1; ``` ```c=27 case '<': for(n = 1; *program == '<'; ++n, ++program); if(!nskip) { ptr -= n; while(ptr <= tape_begin) ptr += TAPE_SIZE; } break; ``` - tape 示意圖 ![tape illustratione](http://science.slc.edu/~jmarshall/courses/2002/fall/cs30/Lectures/week08/TuringMachine.gif) ### Interpreter, Compiler, JIT * Interpreter * 直接轉譯並執行程式,不經過編譯的步驟 * 不會一次看完程式整體 * Compiler * 將高階語言編譯成組合語言 * 相較於JIT,編譯時會輸出成檔 * Just-In-Time Compiler * 每次執行時都會重新進行編譯成機器碼,但不會輸出成檔。 * 相較於 interpreter ,因其會看到程式整體,所以能做的最佳化也會比較多 ## 研讀 rubi 程式碼 #### lex - 在 rubi.h 檔有宣告一個 structure "tok" ```c=10 typedef struct { char val[32]; int nline; } Token; struct { Token *tok; int size, pos; } tok; ``` Token 用來紀錄 toke 所在的行號與內容 - tok.size 和 tok.pos 初始化在 engine.c 裡的 init() ```c=28 void init() { tok.pos = 0; tok.size = 0xfff; set_xor128(); tok.tok = calloc(sizeof(Token), tok.size); brks.addr = calloc(sizeof(uint32_t), 1); rets.addr = calloc(sizeof(uint32_t), 1); } ``` - tok 的賦值在 lex.c 裡的 lex(char \*) - lex(char \*) - 讀入 number - 讀入 ident ,指的是像變數名稱或是函式名稱等。變數命名上支援字母開頭,中間可夾雜數字和 "\_" - skip whitespace and tab - skip comment - 讀入 string ,若在兩個 " 之間出現 `\0`,則會有語意分析錯誤 - 換行符號,`\n` 或 `\r\n`,會以 `;` 的 tok val 來記錄 - 其餘 : - 支援以上除外的單元符號 - 支援部份雙元符號 : `\*=`, `++`, `--`。其中第一個`*`表示任何符號,像是 `+=`, `-=` 等等之類 - engine.c 裡的 dispose() 會將配置給tok的記憶體釋放 #### parser 如果有觀察一下程式碼,可以發現在跑完 parser 的同時就整個編譯過程就結束了,所以我們要關注的點是 parser.dasc 這份檔案 (當然還有 codegen.c , 但因為他是由執行 ./minilua dynasm.lua 所產生的,所以在這裡先不討論) Q: parser 是在幹嘛的? parsing 是對輸入做語法分析( semantic/syntax analyze ),看是否合乎程式語言規範的一項步驟 :::info 程式在編譯時會經歷以下幾個步驟 * lexical analyzer -> Syntax/Sematic Analyzer(Parser) -> Intermediate-code Generator 但在這份實作裡,因為是使用 Dynasm 做code generation 的部份,所以 parser 裡就有指示 Dynasm 怎麼去做 code generation 了 ::: 直接看程式碼 ```C int (*parser())(int *, void **) { jit_init(); tok.pos = 0; strings.addr = calloc(0xFF, sizeof(int32_t)); main_address dasm_growpc(&d, npc); |->START: | jmp =>main_address eval(0, 0); for (int i = 0; i < strings.count; ++i) replaceEscape(strings.text[i]); uint8_t* buf = (uint8_t*)jit_finalize(); for (int i = 0; i < functions.count; i++) *(int*)(buf + dasm_getpclabel(&d, functions.func[i].espBgn) - 4) = (locVar.size[i+1] + 6)*4; dasm_free(&d); return ((int (*)(int *, void **))rubilabels[L_START]); } ``` 這段程式碼位於 parser.dasc 中,其作用是 parsing 的主體(廢話XD ```C jit_init(); ``` jit 的起始 初始化 dsm_state 並設置一些全域的東西 ( labels , actions , 詳情可見上方的 dynasm 研讀部份 ) ```C jit_finalize(); ``` jit 的尾端 link 然後把要用的機械碼利用 mmap() 放入我們執行時期要用到的記憶體 然後將值寫入 mmap() 的區塊。 ```C for (int i = 0; i < functions.count; i++) *(int*)(buf + dasm_getpclabel(&d, functions.func[i].espBgn) - 4) = (locVar.size[i+1] + 6)*4; ``` 知道了 jit 的頭跟尾後,再來看看那邊是負責 parse 的地方 ```C eval(0,0); ``` 最後回傳剛剛建置完的程式的起始點 ```C return ((int (*)(int *, void **))rubilabels[L_START]); ``` ### x86 Assembly 參考資料來自[x86 Assembly Guide](http://www.cs.virginia.edu/~evans/cs216/guides/x86.html) ![](http://www.cs.virginia.edu/~evans/cs216/guides/x86-registers.png) * special purposes register — stack pointer (ESP) and base pointer (EBP). * base pointer - The base pointer is used by convention as a point of reference for finding parameters and local variables on the stack. * push — Push stack * usage : push target — push target onto stack. * pop — Pop stack * usage : pop dst. — pop the top element of the stack into dst. 因為 general-purpose register 有限( eax , ebx , ecx , edx ),所以當我們需要更多的 register 時,會把之前的 register 內的值, push 上去 stack,需要的時候再 pop 出來即可。(詳情請看參考資料的 Callee Rules ) ```=298 |=>func_addr: | push ebp | mov ebp, esp ``` 挪出空間給 local variable,並定義 func_esp 的 label ```=302 | sub esp, 0x80000000 |=>func_esp: ``` > 這裡不懂為什麼要用 func_esp ,會跳到這裡嗎? 接著將 arguments 存入我們剛剛開的空間 ```C=304 for(int i = 0; i < argsc; i++) { | mov eax, [ebp + ((argsc - i - 1) * sizeof(int32_t) + 8)] | mov [ebp - (i + 2)*4], eax } ``` ### Code Refactoring 發現TODO!! ```C=536 else if ((inc = skip("++")) || (dec = skip("--"))) { // TODOs ``` 但發現實際上執行時不會跑到這裡,所以這裡算是 dead code => 砍掉! ### benchmark ![](https://i.imgur.com/U8Rf5e3.png) ### 其他 - 在`~/.vimrc`設定 *.dasc 為 C type file ,使其支援 C 的 syntax highlight ``` " associate *.dasc with c filetype au BufRead,BufNewFile *.dasc set filetype=c ``` - 沒有做 function without end 的handling ```Ruby def func return 1 ``` > 會 segmentation fault(core dumped)[name=fzzzz] ### References  [Interpreter, Compiler, JIT](http://nickdesaulniers.github.io/blog/2015/05/25/interpreter-compiler-jit/) [林郁寧共筆](https://embedded2015.hackpad.com/-Homework4-B-h5FfVE6RTeu) [x86 Assembly Guide](http://www.cs.virginia.edu/~evans/cs216/guides/x86.html)