輸出HELLO WORLD——如何編譯通用的函數調用表達式

這篇的東西比較多。html

首先要處理一下inside-out/auxinside-out這兩個函數。以前的inside-out/aux其實一直不支持對progn的處理,須要先補充;而inside-out則能夠優化一下,避免在只有一個表達式的狀況下,也用progn將其包裹起來。修改後的inside-out/auxinside-out分別以下git

(defun inside-out/aux (expr result)
  "將嵌套的表達式EXPR由內而外地翻出來"
  (check-type expr list)
  ;; 出於簡單起見,暫時只處理加法運算
  (cond ((member (first expr) '(+ - * / _exit > exit))
         (let ((operands '()))
           ;; 對參數列表中的全部表達式都遞歸地進行【外翻】處理
           (dolist (arg (rest expr))
             (if (listp arg)
                 (let ((var (gensym)))
                   (setf result (inside-out/aux arg result))
                   (let ((val (pop result)))
                     (push `(setq ,var ,val) result)
                     (push var operands)))
                 (push arg operands)))
           (push (cons (first expr) (nreverse operands)) result)
           result))
        ((eq (first expr) 'if)
         (push `(if ,(inside-out (second expr))
                    ,(inside-out (third expr))
                    ,(inside-out (fourth expr)))
               result)
         result)
        ((eq (first expr) 'progn)
         (dolist (e (rest expr))
           (push (inside-out e) result))
         result)
        (t
         (push expr result)
         result)))

(defun inside-out (expr)
  (let ((forms (nreverse (inside-out/aux expr '()))))
    (if (> (length forms) 1)
        (cons 'progn forms)
        (car forms))))

實際上能夠更進一步:inside-out/auxinside-out大能夠合併到一塊兒,結果以下github

(defun inside-out (expr)
  "將嵌套的表達式EXPR由內而外地翻出來"
  (check-type expr list)
  (cond ((eq (first expr) 'if)
         `(if ,(inside-out (second expr))
              ,(inside-out (third expr))
              ,(inside-out (fourth expr))))
        ((eq (first expr) 'progn)
         (cons 'progn
               (mapcar #'inside-out (rest expr))))
        (t
         (let ((assignments '())
               (operands '()))
           ;; 對參數列表中的全部表達式都遞歸地進行【外翻】處理
           (dolist (arg (rest expr))
             (if (listp arg)
                 (let ((val (inside-out arg))
                       (var (gensym)))
                   (push `(setq ,var ,val) assignments)
                   (push var operands))
                 (push arg operands)))
           (if (null assignments)
               expr
               `(progn
                  ,@(nreverse assignments)
                  (,(first expr) ,@(nreverse operands))))))))

好了,接下來纔是本文的重點:如何編譯全部的函數調用表達式。ide

儘管我在上面誇下海口,說要編譯「全部」的函數調用表達式,但事實上,如今我還作不到——我只能把全部的函數調用表達式,都映射到對C標準庫中的函數的調用。所以,若是想要調用C標準庫中的putchar函數,那麼必須寫下以下的代碼函數

(|_putchar| #.(char-code #\A))

這裏用了雙豎線的語法來確保這個符號的symbol-name是全小寫的putchar,開始的下劃線是由於在macOS中,調用C函數的時候必需要加上這個前綴的下劃線。#.是個Common Lisp中的reader macro,可讓後面的表達式在讀取期被求值,這樣我就不須要手寫字母A的code-point啦——好吧,是在炫技。優化

要編譯這種函數調用表達式,只須要模仿一下此前對_exit的處理就能夠啦。首先,是求值函數調用表達式中的各個參數,而後將它們放入恰當的位置中——有的要放入寄存器中,有的要壓棧。做爲一個野路子的編譯器愛好者,我固然是沒有正兒八經地看過牙膏廠或者按摩店出品的ABI手冊的,我看的是這一份資料:https://www3.nd.edu/~dthain/c...rest

因此我瞭解到的是:code

  • 前六個參數,分別要從左到右地依次放入RDIRSIRDXRCXR8,以及R9這些寄存器中的;
  • 剩下的參數,統統壓棧

而後因爲macOS的任性要求,在調用前還須要將RSP寄存器對齊到16字節的內存地址。我在這裏折騰了好久,最後才發現,原來我要在函數調用結束以後,把修改過的RSP寄存器恢復原狀才行_(:з」∠)_orm

因此,這一部分的代碼是這樣子的(精簡了一下)htm

(defun jjcc2 (expr globals)
  "支持兩個數的四則運算的編譯器"
  (check-type globals hash-table)
  (cond (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))))))

先用pushqRSP保存起來,待call指令結束返回以後,再popq出來恢復它XD

到這裏爲止,就能夠來寫經典的Hello World了,代碼以下

(fb `(progn ,@(mapcar #'(lambda (c) `(|_putchar| ,(char-code c))) (coerce "Hello, world!" 'list)) (_exit 0)))

生成的彙編代碼以下

.data
        .section __TEXT,__text,regular,pure_instructions
        .globl _main
_main:
        MOVQ $72, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $101, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $108, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $108, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $111, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $44, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $32, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $119, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $111, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $114, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $108, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $100, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVQ $33, %RDI
        PUSHQ %RSP
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _putchar
        POPQ %RSP
        MOVL $0, %EDI
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _exit

使用GAS編譯上述代碼,並藉助gcc連接後,運行它就能夠看到Hello, world!

全文完

閱讀原文

相關文章
相關標籤/搜索