# 2017q3 Homework3 (simulator) contributed by <`tina0405`> >記得固定格式喔! >[name=課程助教][color=red] ### 背景知識 - [x] 閱讀 [現代處理器設計](http://hackfoldr.org/cpu) 教材和觀看裡頭所有錄影,記錄你的心得和疑惑 * 影片 [淺談虛擬機器設計與實作](https://www.youtube.com/watch?v=izy3TUBAnio&feature=youtu.be) * 資料來源 : [Wikipedia](https://en.wikipedia.org/wiki/Cross_compiler) :::danger 是 Wikipedia,而非 wiki,兩者不同 :notes: jserv ::: * cross compiler: is a compiler capable of creating executable code for a platform other than the one on which the compiler is running * 例子 : a compiler that runs on a Windows 7 PC but generates code that runs on Android smartphone is a cross compiler. * 所需背景知識 * turing complete * turing machine * compiler * linker * LLVM 現在已不可以稱作 Low Level Virtual Machine * 具有完整編譯器架構 * 一開始在 Frontend 資訊會比較不明確(如投影片中例子是 int),也許還會有註解,在 IR (Intermediate Representation) 就會更清楚說明 32-bits,到了 Backend 甚至會直接告訴你偏移量 ~~~ Frontend LLVM Backend C/C++ -> IR -> X86 組語 ~~~ * 補:為什麼我們需要 IR?([你所不知道的 C 語言:編譯器和最佳化原理篇](https://hackmd.io/s/Hy72937Me) 提到) * 因為如果今天有很多種 Frontend (比如:C/C++) 要轉成各個 Backend 的 target (比如:ARM/X86) 會有很多不同的組合,如果我們中間有一個 IR ,就大家可以專心在一件事身上 * 利用 Brainfuck instruction 來得到 C 的執行檔: * 產生 C 的執行檔或 ELF ,可以透過 Brainfuck instruction (不同類型的指令)來產生 ~~~ Brainfuck instruction | Brainfuck Compiler / \ ELF C 的執行檔 ~~~ * Brainfuck 看似只有 8 種符號,(後半段提及)但若是實現 Nested Loop (巢狀迴圈)或複雜結構卻相對複雜 | Character | C | Meaning | | -------- | -------- | -------- | | > | ++p | increment the data pointer (to point to the next cell to the right) | | < | - -p |decrement the data pointer (to point to the next cell to the left)| | + | ++*p |increment (increase by one) the byte at the data pointer.| | - | - -*p |decrement (decrease by one) the byte at the data pointer.| | . | putchar(*p)|output the byte at the data pointer.| | , | *p=putchar() |accept one byte of input, storing its value in the byte at the data pointer.| | [ | while(*p){ |if the byte at the data pointer is zero, then instead of moving the instruction pointer forward to the next command, jump it forward to the command after the matching ] command.| | ] | } |if the byte at the data pointer is nonzero, then instead of moving the instruction pointer forward to the next command, jump it back to the command after the matching [ command.| * 舉例 ~~~ brainfunction: [-] C: Clean while(cell[0]) { --cell[0]; } ~~~ * 影片中 Interpreter 和 Static Compiler 的比較 * 參考資料 [compiler interpreters assemblers 比較](http://www.microcontrollertips.com/compilers-translators-interpreters-assemblers/) / [wiki](https://en.wikipedia.org/wiki/Just-in-time_compilation) * Interpreter: translates code like a compiler but reads the code and immediately executes on that code, and therefore is initially faster than a compiler(中間不產 object file) * Compiler: Compilers convert high-level language code to machine (object) code in one session * Assembler: An assembler translates a program written in assembly language into machine language * 影片中提到的 JIT compiler( Just-in-time compilation ) * IN-TIME: 及時 (特性:收到 work 就馬上去做,因為有 delay 才有相對的 in-time) * REAL-TIME: 即時 (特性:在 deadline 以前完成 work) * 而通常程序有兩種進行方式是上述的: Dynamic Interpreter 和 Static Compiler, 但 JIT compiler 集合兩種特性: * dynamic translation, is compilation done during execution of a program – at run time – rather than prior to execution. * 然後呈現的結果是 Dynamic(JIT compiler) 不做最佳化的效率贏過 Static Compiler 做最佳化 * 碎形 (fractal) ### 研究程式碼 - [ ] 研究給定的 [full-stack-hello](https://github.com/sysprog21/full-stack-hello) - [ ] 學習其中 ISA 和模擬器實作,並在 GitHub 上 fork [full-stack-hello](https://github.com/sysprog21/full-stack-hello) - [x] 對照研讀 [你所不知道的 C 語言:編譯器和最佳化原理篇](https://hackmd.io/s/Hy72937Me) 教材和觀看直播錄影,以得知編譯器運作原理 * 搭配影片 [你所不知道的 C 語言:編譯器和最佳化原理 (上)](https://www.youtube.com/watch?v=XY4YkuX_a3k) / [(下)](https://www.youtube.com/watch?v=J2lZmh4PXTc) * 編譯器的限制 * 太多 #if 的 macro 會導致不能處理(行號錯亂) * 有些嵌入式系統的限制 (不准使用太多的 STACK 或 MALLOC) * 如果我們理解編譯器,就可以從中修改 * 如果不理解最佳化的話,在對實驗開最佳化可能造成錯誤 * [Compiler design] * DFA (DETERMINISTIC FINITE AUTOMATA) * 每一個 input 的集合一定要對應到一個 status * 每一個 input 的集合只能要對應到一個 status * NFA (NONDETERMINISTIC FINITE AUTOMATA) * NFA 轉 DFA 的所付出的成本會比直接找 DFA 來的低 * 簡易的 NFA 轉 DFA ~~~ ~~~ * shell 會執行 fork + execve() * 在系統呼叫程式時,都會有一個 main 的程式進入點 [(source)](http://refspecs.linuxbase.org/LSB_2.0.1/LSB-PDA/LSB-PDA/baselib---libc-start-main-.html) ~~~ __libc_start_main : function shall initialize the process, call the main function with appropriate arguments, and handle the return from main. __libc_start_main is not in the source standard; it is only in the binary standard. ~~~ * 並不是每一個都是 Compiler -> Assembler -> Linker 是因為除錯方便和有效的模組化( 有些沒有 Assembler ,直接由 Compiler 產生的 object code -> linker) * gcc : compiler driver * ld : GNU linker * (DSL) domain specific language * 原來的 vm 所支援的功能 ~~~ Usage: as_exec [-w] [-x] [-o <out_file>] <in_file> -w Assemble <in_file> and write to an ELF file, see -o below -o if -w is specifed, <out_file> is used to store the object code -x Load <in_file> and execute it <in_file> the file name to be used by commands above ~~~ ### 實作 Fibonacci 數列 - [ ] 閱讀 [你所不知道的C語言:遞迴呼叫篇](https://hackmd.io/s/rJ8BOjGGl) 和觀看對應講座錄影,用 [full-stack-hello](https://github.com/sysprog21/full-stack-hello) 的指令實作 Fibonacci 數列 - [x] 需要包含 recursive 和 non-recursive 實作,分別置於檔案 `tests/fib-recursive.s` 和 `tests/fib-iterative.s` - [x] 修改 `vm.c` (和對應的原始程式碼),允許執行時期由使用者 (和 `Makefile`) 帶入參數,如 `--input`,這數值應該透過暫存器作為上述 Fibonacci 數列程式的 `Fib(N)` - [ ] 研究現有 [full-stack-hello](https://github.com/sysprog21/full-stack-hello) 的自動測試程式,整合上述兩種 Fibonacci 程式實作,並且比較效能 (需要包含 vm 內部的 cycle count) * [Fibonacci number](https://en.wikipedia.org/wiki/Fibonacci_number) : the Fibonacci numbers are the numbers in the following integer sequence, called the Fibonacci sequence * 定義: every number after the first two is the sum of the two preceding ones * 像這樣 1,1,2,3,5,8,13,21,24.. * 但現在的使用,有時候會給 0 當初始值 0,1,1,2,3,5,8,13,24... * Fib(N) 就是要求出數列中的第 N 個為多少 : * 補 : 目前的 N 是輸入的 N 減 2 ,還沒扣 R_1 R_2 * line8 到 line13 相當於 for 迴圈 #### iteration ~~~= or $0 $0 #1 ;R_1 : initial the first number or $0 $1 #2 ;R_2 : initial the second number or $0 $0 #3 ;R_3 for temp or $0 $0 #0 ;R_0 for input jz #0 #12 ;if 0,print 0 or $0 #2 #3 ;temp = R_2 add #1 #2 #2 ;R_2 = R_1 + R_2 or $0 #3 #1 ;R_1 = temp sub #0 $1 #0 ;input-1 jnz #0 #5 ;jump back until it become 0 print #2 halt print $0 halt ~~~ #### recursive * 一開始如果用 C 去想,會長這樣 ~~~c= int fib(int x) { if (x == 0) return 0; if (x == 1) return 1; return fib(x-1)+fib(x-2); } ~~~ * 拿 5 為例:我們是一直減直到找到 1 , 0 才 return ~~~ fin(5) / \ fin(4) fin(3) / \ / \ fin(3) fin(2) fin(2) fin(1) / \ / \ / \ / \ fin(2)fin(1)fin(1)fin(0)..... / \ fin(1) fin(0) ~~~ * 但如果我只想用 1 個 register 記錄 N 回傳回去時要 +2 因為做了兩次減 1 像這樣 fib(x-1)+fib(x-2) ~~~= or $0 $0 #1 ;r_1=output or $2 $0 #0 ;r_0=input call #5 print #1 halt xor #0 $1 #2 ;r_2=compare register jnz #2 #9 ;jump,if!=1 add #1 $1 #1 ;if==1,output add ret xor #0 $0 #2 jnz #2 #12 ;jump,if!=1 ret ;if==0,return sub #0 $1 #0 call #5 sub #0 $1 #0 call #5 add #0 $2 #0 ret ~~~ * 關於 `--input` 的設計,我想要他和 `-w` 一起打包成一個 elf ,所以不和 `-x` 一起使用 ~~~c ... } else if (!strcmp(argv[i], "--input")) { if (!argv[i + 1]) FATAL(-1, "Fib(N) need int as a parameter , see -h\n"); if (req == LOAD_ELF_AND_EVAL) FATAL(-1, "-x and --input used together, see -h\n"); int Fib_N = atoi(argv[++i]); } ... ~~~ :::danger `driver.c` 本身不該知道特定程式的存在,這些錯誤訊息應該移到組合語言程式中。`driver.c` 只檢查輸入的型態。注意看 [atoi](http://www.cplusplus.com/reference/cstdlib/atoi/) 的回傳值,我們可能會分不清楚 `0` 來自使用者輸入,抑或是錯誤的表達式 :notes: jserv ::: >>了解了,謝謝老師提點! [name=tina0405] * 先處理如果有 `--input` ,將他的參數放進 register_0 * 這邊我預設 register_0 就是要給 `--input` 參數放置的 * 我使用 sprintf ,如果有 `--input` 如同一開始就有一行組語是在初始化暫存器 * 傳進去的 input 是我再 driver.c 定義,沒用到為 -1 * 20 的空間是包括原本字串長度和可能的數字長度 ~~~c= void assemble_from_fd(vm_env *env, int fd, int input) { ... if (input != -1) { char str[20]; /* INT_MAX = 2147483647 */ sprintf(str, "or #0 $%d #0", input); assemble_line(env, str); } ... } ~~~ * 再來思考如何區分 atoi 的錯誤值(預設是0)和使用者真的輸入 0 * 思考的方向是利用 ASCII code,在 48~57 之間為數字 * 有錯誤的話傳送 error signal 再組語中印出 * 錯誤訊號存在 rigister 5 ~~~c= } else if (!strcmp(argv[i], "--input")) { ... for (int k = 0; k < strlen(argv[i]); k++) { if (argv[i][k] < 48 || argv[i][k] > 57) { error_N = 1; /*error signal for assembly*/ break; } } ... ~~~ ### 製圖和解讀 - [ ] 除了 recursive 和 iterative 版本,應該一併考慮以下 Fibonacci 數列的實作方式,並善用 GNU plot 製圖和解讀 - [x] Tail recursion - [x] Q-Matrix - [x] Fast doubling #### tail recursive * [參考 Tail recursion](http://www.geeksforgeeks.org/tail-recursion-fibonacci/) : A recursive function is tail recursive when the recursive call is the last thing executed by the function. ~~~c= // A tail recursive function to // calculate n th fibnacci number int fib(int n, int a = 0, int b = 1) { if (n == 0) return a; if (n == 1) return b; return fib(n-1, b, a+b); } ~~~ * 例子 : n = 5 帶入,就是這樣層層 call 下去,再 return 回來 ~~~ n a b 5 0 1 4 1 1 3 1 2 2 2 3 1 3 5 ~~~ * 我將 rigister 3 設計為 temp , 他可以用來比較是否不為 0 要 jump 也可以做 result 因為在每一次 ret 時,他當下的任務就完成了,可以被蓋掉 ~~~ or $0 $0 #1 ;a = 0 or $1 $0 #2 ;b = 1 or $0 $0 #3 ;tmp = 0 call #8 print #3 halt xor #0 $0 #3 ;compare with 0 jnz #3 #12 or #1 $0 #3 ret xor #0 $1 #3 ;compare with 1 jnz #3 #16 or #2 $0 #3 ret sub #0 $1 #0 or #1 $0 #3 or #2 $0 #1 add #2 #3 #2 call #8 ret ~~~ #### Fibonacci Q-Matrix 參考 [source](http://mathworld.wolfram.com/FibonacciQ-Matrix.html) $Q=\left [ \begin{array}{cc} F_2 & F_1 \\ F_1 & F_0 \end{array} \right ]=\left[ \begin{array}{cc} 1 & 1 \\ 1 & 0 \end{array} \right ]$ * 然後變成 n 次方 $Q^n=\left [ \begin{array}{cc} F_{n+1} & F_n \\ F_n & F_{n-1} \end{array} \right ]$ * 其實這個 Q-Matrix 就是利用矩症乘法的特性,又要乘的係數剛好都是 1 所以有累加的效果 * 矩陣的實作因為有 4 個元素,但是是 3 個值,用 3 個 rigister 表示 $F_0$ $F_1$ $F_2$ 其中 $F_1$ 會是結果 * 然後我們做矩陣的乘法,例子: $\left[ \begin{array}{cc} 1 & 1 \\ 1 & 0 \end{array} \right ]$$\left[ \begin{array}{cc} 1 & 1 \\ 1 & 0 \end{array} \right ]=\left[ \begin{array}{cc} 2 & 1 \\ 1 & 1 \end{array} \right ]$ * 所以我們每次只做 4 件事,但至少要兩個暫存器來存被蓋掉之前的值 $1\times F_2+1\times F_1=F_3$ 蓋掉舊的 $F_2$ $1\times F_1+1\times F_0=F_2$ 蓋掉舊的 $F_1$ $1\times F_2+0\times F_1=F_2$ 蓋掉舊的 $F_1$ $1\times F_1+0\times F_0=F_1$ 蓋掉舊的 $F_0$ * 原本 4 行的運算式,用組語表示就必須一直考量暫存器是否會被蓋掉: ~~~ or $0 $0 #1 ;F0 = 0 or $1 $0 #2 ;F1 = 1 or $1 $0 #3 ;F2 = 1 or $0 $0 #4 ;tmp or $0 $0 #6 ;tmp or $0 $0 #7 ;tmp or $0 $0 #8 ;tmp xor #0 $0 #4 jnz #4 #13 print #1 ;input == 0 ,print result halt or #3 $0 #7 ;record F2 mul $1 #3 #4 ;1*F2 mul $1 #2 #6 ;1*F1 add #4 #6 #3 ;1*F2+1*F1=F2 or #2 $0 #8 ;record F1 mul $1 #2 #4 ;1*F1 mul $1 #1 #6 ;1*F0 add #4 #6 #2 ;1*F1+1*F0=F1 mul $1 #7 #4 ;1*F2 mul $0 #8 #6 ;0*F1 add #4 #6 #3 ;1*F2+0*F1=F1 mul $1 #8 #4 ;1*F1 mul $0 #1 #6 ;0*F0 add #4 #6 #1 ;1*F1+0*F0=F0 sub #0 $1 #0 ;input sub 1 jmp #9 ~~~ * 在 Q-matrix 還有一個特性 : 之後可以用這些特性來擴大 fibonacci 的允許範圍 $Q^m Q^{n-1}=Q^{n+m-1}$ #### Fast doubling * 參考了這篇證明 [Fibonacci Fast Doubling Proof](https://math.stackexchange.com/questions/1124590/need-help-understanding-fibonacci-fast-doubling-proof) * 我們可以利用以上證明寫出下列的算式 : $F(2n)=2F(n+1)F(n)-F(n)^2$ $F(2n+1)=F(n+1)^2+F(n)^2$ * 把奇數偶數分開處理 ~~~ or $0 $0 #1 ;R_1 : initial the first number or $0 $1 #2 ;R_2 : initial the second number or $0 $0 #3 ;R_3 for temp or $0 $0 #4 ;R_4 for n or $0 $0 #6 ;F(n) or $0 $0 #7 ;F(n+1) or $0 $0 #8 ;R_8 for temp mod #0 $2 #8 jnz #8 #13 ;if even div #0 $2 #4 ;register_4 = n jmp #24 sub #0 $1 #4 ;if odd div #4 $2 #4 ;register_4 = n jmp #24 jnz #4 #18 ;if 0,print 0 ret or $0 #2 #3 ;temp = R_2 add #1 #2 #2 ;R_2 = R_1 + R_2 or $0 #3 #1 ;R_1 = temp sub #4 $1 #4 ;input-1 jnz #4 #18 ;jump back until it become 0 ret call #16 or #1 $0 #6 ;register_6 = F(n) add #4 $1 #4 ;n-1 call #16 or #1 $0 #7 ;register_7 = F(n-1) jnz #8 #36 mul $2 #6 #3 ;even mul #3 #7 #3 ;2*F(n)*F(n+1)-F(n)*F(n) mul #6 #6 #1 sub #3 #1 #3 print #3 halt mul #6 #6 #3 ;odd mul #7 #7 #1 ;F(n+1)*F(n+1)+F(n)*F(n) add #3 #1 #3 print #3 halt ~~~ ### 加入追蹤 / 除錯用途的指令 - [ ] 模擬器的程式碼,加入追蹤 / 除錯用途的指令 (instruction) - [ ] 探討模擬器的運作機制並說明自己修改的方式 ### 提點 * 注意 Fib(63) 之後的數值會超過 2^32^ 可表達的範圍,需要設計有效的數值表示機制來處理 (如用兩個 32-bit 暫存器保存 2^64^ 數值) * 觀察 [Add label support](https://github.com/jserv/full-stack-hello/pull/30) 的實作方式,試著在[full-stack-hello](https://github.com/sysprog21/full-stack-hello) 給定的組譯器中新增 label 的支援