本文講解如何編譯defun
。在Common Lisp中,defun
用於定義函數。例如,下列的代碼定義了函數foo
html
(defun foo (a) "一個名爲FOO的函數" (declare (ignorable a)) (1+ 1))
在defun
的語法中,第一行的字符串是這個函數的文檔,能夠用documentation
函數獲取;第二行是declaration。(無論是documentation仍是declaration,也許要等到自舉的那一天才可以支持了)目前只打算支持以下這般樸素的defun
用法:git
(defun a (x) (+ x 1))
能夠想象,編譯上面這段代碼後,首先應當有一個函數名的label,好比就叫作A
。緊接着這個label的是函數體的代碼,按照我這趕鴨子上架的作法看回來的說法,起碼要有參數的處理——好比從寄存器中複製到內存中,還要有callee-saved的寄存器的保護,函數體的處理邏輯代碼,以及收拾殘局並返回到調用者的代碼等等。github
慢着,要將寄存器中的參數值複製到內存中,是須要在棧上開闢空間的。要這麼作的話,就得先計算出一共須要多少字節的存儲空間,還要計算出每個參數在棧上的偏移。而且,爲了能夠在函數體內正確地使用參數的偏移,還須要提供一個環境(相似於編譯原理的教程中經常出現的符號表)以便在遞歸地編譯函數體的過程當中查詢才行——這一系列的東西對jjcc2
的改動比較大。app
因此,我用了一個簡單但侷限性較大的方法:將每個參數都視爲一個同名的全局變量。這樣寄存器中的參數值就不須要複製到棧上,而是直接複製到參數名所表明的內存地址中。ide
如此,要編譯defun
就很簡單了。拓展後的jjcc2
函數的代碼以下函數
(defun jjcc2 (expr globals) "支持兩個數的四則運算的編譯器" (check-type globals hash-table) (cond ((eq (first expr) '+) `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (addl %ebx %eax))) ((eq (first expr) '-) `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (subl %ebx %eax))) ((eq (first expr) '*) ;; 將兩個數字相乘的結果放到第二個操做數所在的寄存器中 ;; 由於約定了用EAX寄存器做爲存放最終結果給continuation用的寄存器,因此第二個操做數應當爲EAX `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (imull %ebx %eax))) ((eq (first expr) '/) `((movl ,(get-operand expr 0) %eax) (cltd) (movl ,(get-operand expr 1) %ebx) (idivl %ebx))) ((eq (first expr) 'progn) (let ((result '())) (dolist (expr (rest expr)) (setf result (append result (jjcc2 expr globals)))) result)) ((eq (first expr) 'setq) ;; 編譯賦值語句的方式比較簡單,就是將被賦值的符號視爲一個全局變量,而後將eax寄存器中的內容移動到這裏面去 ;; TODO: 這裏expr的second的結果必須是一個符號才行 ;; FIXME: 不知道應該賦值什麼比較好,先隨便寫個0吧 (setf (gethash (second expr) globals) 0) (values (append (jjcc2 (third expr) globals) ;; 爲了方便stringify函數的實現,這裏直接構造出RIP-relative形式的字符串 `((movl %eax ,(get-operand expr 0)))) globals)) ;; ((eq (first expr) '_exit) ;; ;; 由於知道_exit只須要一個參數,因此將它的第一個操做數塞到EDI寄存器裏面就能夠了 ;; ;; TODO: 更好的寫法,應該是有一個單獨的函數來處理這種參數傳遞的事情(以符合calling convention的方式) ;; `((movl ,(get-operand expr 0) %edi) ;; (movl #x2000001 %eax) ;; (syscall))) ((eq (first expr) '>) ;; 爲了能夠把比較以後的結果放入到EAX寄存器中,以我目前不完整的彙編語言知識,能夠想到的方法以下 (let ((label-greater-than (intern (symbol-name (gensym)) :keyword)) (label-end (intern (symbol-name (gensym)) :keyword))) ;; 根據這篇文章(https://en.wikibooks.org/wiki/X86_Assembly/Control_Flow#Comparison_Instructions)中的說法,大於號左邊的數字應該放在CMP指令的第二個操做數中,右邊的放在第一個操做數中 `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (cmpl %ebx %eax) (jg ,label-greater-than) (movl $0 %eax) (jmp ,label-end) ,label-greater-than (movl $1 %eax) ,label-end))) ((eq (first expr) 'if) ;; 假定if語句的測試表達式的結果也是放在%eax寄存器中的,因此只須要拿%eax寄存器中的值跟0作比較便可(相似於C語言) (let ((label-else (intern (symbol-name (gensym)) :keyword)) (label-end (intern (symbol-name (gensym)) :keyword))) (append (jjcc2 (second expr) globals) `((cmpl $0 %eax) (je ,label-else)) (jjcc2 (third expr) globals) `((jmp ,label-end) ,label-else) (jjcc2 (fourth expr) globals) `(,label-end)))) ((member (first expr) '(_exit exit)) ;; 暫時以硬編碼的方式識別一個函數是否來自於C語言的標準庫 `((movl ,(get-operand expr 0) %edi) ;; 據這篇回答(https://stackoverflow.com/questions/12678230/how-to-print-argv0-in-nasm)所說,在macOS上調用C語言函數,須要將棧對齊到16位 ;; 僞裝要對齊的是棧頂地址。由於棧頂地址是往低地址增加的,因此只須要將地址的低16位抹掉就能夠了 (and ,(format nil "$0x~X" #XFFFFFFFFFFFFFFF0) %rsp) (call :|_exit|))) ((eq (first expr) 'return) ;; 因爲通過inside-out的處理以後,return的參數就是一個「原子」了,所以再也不須要調用jjcc2來處理一遍 `((movl ,(get-operand expr 0) %eax) (ret))) ((eq (first expr) 'defun) ;; defun的編譯過程是: ;; 1. 根據函數參數生成相應的MOV指令 ;; 2. 編譯body的部分,生成一系列的彙編代碼的S表達式 ;; 3. 以defun的函數名和剛生成的S表達式組成cons ;; 4. 添加到*udfs*中 (let ((init-asm '()) (params (caddr expr)) (registers '(%rdi %rsi %rdx %rcx %r8 %r9))) (dolist (param params) (setf (gethash param globals) 0)) ;; 生成一系列MOV指令,將寄存器中的參數值放入到特定的內存位置中 (dotimes (i (length params)) (when (nth i registers) (push `(movq ,(nth i registers) ,(format nil "~A(%RIP)" (nth i params))) init-asm))) (let ((asm (jjcc2 (cons 'progn (cdddr expr)) globals))) (push (cons (cadr expr) (append init-asm asm '((ret)))) *udfs*) nil))) (t ;; 按照這裏(https://www3.nd.edu/~dthain/courses/cse40243/fall2015/intel-intro.html)所給的函數調用約定來傳遞參數 (let ((instructions '()) (registers '(%rdi %rsi %rdx %rcx %r8 %r9))) (dotimes (i (length (rest expr))) (if (nth i registers) (push `(movq ,(get-operand expr i) ,(nth i registers)) instructions) (push `(pushq ,(get-operand expr i)) instructions))) ;; 通過一番嘗試後,我發現必須在完成函數調用後恢復RSP寄存器纔不會致使段錯誤 `(,@(nreverse instructions) (pushq %rsp) (and ,(format nil "$0x~X" #XFFFFFFFFFFFFFFF0) %rsp) (call ,(first expr)) (popq %rsp))))))
在上面的代碼中,使用了一個叫作*udfs*
的變量。它在個人.lisp
文件中的定義以下測試
(defparameter *udfs* (list (cons '|lt1| '((movl 1 %eax) (ret)))))
實際上它就是一個很簡單的、函數名到函數體代碼的alist而已,在生成彙編代碼字符串的時候,將其一股腦地寫入到流中便可。爲此,stringify
函數也作了一番修改,拆分爲了以下的兩個函數編碼
(defun stringify-asm (asm) "根據彙編代碼ASM生成相應的彙編語言字符串" (dolist (ins asm) (cond ((keywordp ins) (format t "~A:~%" ins)) ((= (length ins) 3) (format t " ~A ~A, ~A~%" (first ins) (if (numberp (second ins)) (format nil "$~A" (second ins)) (second ins)) (if (numberp (third ins)) (format nil "$~A" (third ins)) (third ins)))) ((= (length ins) 2) (format t " ~A ~A~%" (first ins) (if (numberp (second ins)) (format nil "$~A" (second ins)) (second ins)))) ((= (length ins) 1) (format t " ~A~%" (first ins)))))) (defun stringify (asm globals) "根據jjcc2產生的S表達式生成彙編代碼字符串" (check-type globals hash-table) ;; 輸出globals中的全部變量 ;; FIXME: 暫時只支持輸出數字 (format t " .data~%") (maphash (lambda (k v) (format t "~A: .long ~D~%" k v)) globals) (format t " .section __TEXT,__text,regular,pure_instructions~%") (format t " .globl _main~%") ;; 輸出用戶自定義的函數 (dolist (e *udfs*) (destructuring-bind (label . asm) e (format t "~A:~%" label) (stringify-asm asm))) (format t "_main:~%") (stringify-asm asm))
如今,能夠繼續用之前的fb
函數來編譯了,示例代碼以下rest
(setf *udfs* nil) (fb '(progn (defun a (x) (+ x 1)) (_exit (a 2))))
生成的彙編代碼以下code
.data X: .long 0 G606: .long 0 .section __TEXT,__text,regular,pure_instructions .globl _main A: MOVQ %RDI, X(%RIP) MOVL X(%RIP), %EAX MOVL $1, %EBX ADDL %EBX, %EAX RET _main: MOVQ $2, %RDI PUSHQ %RSP AND $0xFFFFFFFFFFFFFFF0, %RSP CALL A POPQ %RSP MOVL %EAX, G606(%RIP) MOVL G606(%RIP), %EDI AND $0xFFFFFFFFFFFFFFF0, %RSP CALL _exit
編譯運行便可獲得返回碼爲3。
全文完