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

本文很水。

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

好了,我要开始表演了。

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

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

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

1
(+ 1 2)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(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 中像下面这样运行

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

它会输出这些内容

1
2
3
4
5
6
7
8
9
        .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 文件了

1
2
as -o jjcc.o jjcc.s
gcc jjcc.o

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

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

全文完。