本文很水。

有一天,我心血来潮想要写一个将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.sgcc jjcc.o

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

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

全文完。

阅读原文