如何編譯defun

本文講解如何編譯defun。在Common Lisp中,defun用於定義函數。例如,下列的代碼定義了函數foohtml

(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。

全文完

閱讀原文

相關文章
相關標籤/搜索