一个简陋的四则运算编译器实现

34次阅读

共计 1422 个字符,预计需要花费 4 分钟才能阅读完成。

本文很水。

有一天,我心血来潮想要写一个将 Common Lisp 编译成汇编(x64 那种)的编译器。我喜欢 Common Lisp 这门语言,它非常好玩,有许多有趣的特性(宏、condition system 等),并且它的生态很贫瘠,有很多造轮子的机会。因为我懂的还不够多,所以没法从源代码一步到位生成可执行文件,只好先输出汇编代码,再利用现成的汇编器(比如 as、nasm)从这些输出内容生成可执行文件。至于这东西是不是真的算是编译器,我也不是很在意。

好了,我要开始表演了。

你可能看过龙书,或者其它比较经典的编译原理和实践方面的书。那你应该会知道,编译器还蛮复杂的。但我水平有限,把持不住工业级的产品那么精妙的结构和代码,所以我的编译器很简陋——简陋到起码这个版本一眼就看到尽头了。

尽管简陋,但身为一名业余爱好者,尝试开发这么一个玩具还是很 excited 的。由于编译器本身也是用 Common Lisp 写的,所以就偷个懒不写 front end 的部分了,聚焦于从 CL 代码到汇编代码的实现。

先从最简单的一种情况——二元整数的加法入手,比如下面这段代码

(+ 1 2)

对于加法,可以输出 ADDL 指令,两个参数则随便找两个寄存器放进去就好了。一段简单得不能再简单的代码一下子就写出来了

(defun jjcc2 (expr)
  "支持两个数的四则运算的编译器"
  (cond ((eq (first expr) '+)
         `((movl ,(second expr) %eax)
           (movl ,(third expr) %ebx)
           (addl %eax %ebx)))))

(defun stringify (asm)
  "根据 jjcc2 产生的 S 表达式生成汇编代码字符串"
  (format t ".section __TEXT,__text,regular,pure_instructions~%")
  (format t ".globl _main~%")
  (format t "_main:~%")
  (dolist (ins asm)
    (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))))
  (format t "movl %ebx, %edi~%")
  (format t "movl $0x2000001, %eax~%")
  (format t "syscall~%"))

在 REPL 中像下面这样运行

(stringify (jjcc2 '(+ 1 2)))

它会输出这些内容

        .section __TEXT,__text,regular,pure_instructions
        .globl _main
_main:
        MOVL $1, %EAX
        MOVL $2, %EBX
        ADDL %EAX, %EBX
        movl %ebx, %edi
        movl $0x2000001, %eax
        syscall

把上面这段汇编代码保存到名为 jjcc.s 的文件,再运行下列的命令,就可以得到一个能跑的 a.out 文件了

as -o jjcc.o jjcc.s
gcc jjcc.o

运行之后,再输出上一个命令的退出码,就可以看到结果 3 了。

.section那一行太长,其实可以用 .text 来代替;指令和寄存器的名字大小写混用;stringify函数中对第二第三个操作数的处理代码很冗余,等等,都是可以吐槽的问题 XD

全文完。

阅读原文

正文完
 0