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