本文很水。
有一天,我心血来潮想要写一个将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
全文完。
阅读原文