在 Clozure Common Lisp 中使用汇编(一) - 编写 LAP 代码
Common Lisp (简称“CL”)诞生于1984年,最一开始的目标是期望把70年代末80年代初的那些 lisp 家族的语言做一番综合和统一。CL 很成功的实现了这个目标,这使得 CL 的各种设计来自于当时各种各样的 lisp 家族的语言。几年以后,CL 迎来了一次升级,核心语言基本上没有太大的变动,主要是加入了CLOS。1994年,ANSI 发布了第一个 CL 标准,这次标准化过程是对当时 CL 市场的一次概括,内容基本上和上一次升级差别不大。在这以后, CL 标准委员会工作重心转移,跑去支援 ISLISP[1] 标准的制定。2004年,由于确实没人了,CL 标准委员会宣布解散。从 1994 - 2004 年间,CL 标准文档的变化仅仅只是对语言表达不清晰、“错别字”、语法错误这种类型的修改,语言本身并没有改变。CL 界至今仍旧严格的遵循其 ANSI 标准 “ANSI INCITS 226”[2]。
CL 的这种历史发展过程,使得它成为今天少见的保留着 80 年左右语言设计的一门编程语言,堪称编程语言界的“活化石”。
反汇编
可能是由于 CL 的设计都是从 80 年左右的那些 LISP 家族“抄”来的, CL 标准规定,每个 CL 实现都必须实现一个 DISASSEMBLE
函数,用于反汇编[3]。例如,对于函数 '(lambda ())
,反汇编的结果如下。
1 | CL-USER> (disassemble '(lambda ())) |
这是 Clozure Common Lisp (简称“CCL”)的反汇编结果。换一种实现呢?
1 | * (disassemble '(lambda ())) |
这是 Steel Bank Common Lisp (简称“SBCL”)的反汇编结果。由于是 CL 标准规定必须存在的,所以即使是非 native 平台的 CL 实现,都必须提供对应的反汇编功能。
在 CCL 中写汇编代码
即使 CL 可以对任何编译过的函数进行反汇编,标准里却没有提供汇编的接口。但是每个主流 CL 实现都带汇编器,因而可以在 CL 中嵌入汇编语言代码。下面用 CCL 和 x86_64 linux 平台为例子,讲解怎么在 CL 中插入汇编。
LAP
CCL 中,用汇编实现的东西,称为 LAP 。至于 LAP 全称到底是什么,可能也没什么人记得了,有的说是 Lisp Assembly Program ,官方给出的文档却也有 Lisp Assembly Parser 的解释[4]。
普通的CL函数,可以通过 CCL::DEFX86LAPFUNCTION
定义。例如,对于两个 FIXNUM
类型的相加,一个简单的实现如下。(注意CCL用的汇编是LISP风格的语法)
1 | (ccl::defx86lapfunction my-add-1 ((x1 arg_y) (x2 arg_z)) |
这个程序首先检查参数个数是不是2,如果不是,抛出异常。接下来,检查两个参数的类型是不是都是 FIXNUM
,如果不是,抛出异常。然后才是做加法。这个函数用 CCL 反汇编结果如下。
1 | CL-USER> (disassemble 'my-add-1) |
然而这些都是 虚假的汇编。真正的汇编,可以用 gdb 反汇编得到。如下所示。
1 | # 由于对齐等因素,这里的反汇编不是百分百正确。不过大体上和CCL反汇编结果是对得上的。 |
把 LAP 包装成闭包
使用 LAP 方式定义出来的是一个有名有姓的 CL 函数,那么,也可以通过 CCL::X86-LAP-FUNCTION
把汇编代码包装成一个CL闭包。如下所示。
1 | CL-USER> (funcall |
Foreign Function Interface
由于 LAP 做出来的函数,是标准的 CCL 函数,遵循 CCL 内部的各种标准,必须对 CCL 的内部运作相当了解才能很好的写出来。但是,真的只有这种方法吗?也许,我们要的只是在内存中生成一段机器码,然后丢给 CCL去执行即可。这时候可以换一种思路,一方面利用 CCL 的汇编器帮我们生成一段机器码,同时,丢给 FFI 一个指针,让 CCL 把这段代码当做普通的机器码那样去执行。这样,我们只需要遵循通用的 ABI 标准,以及少数 FFI 的约定,即可写出任何汇编代码。CCL 在调用我们的汇编代码的时候,也会按照这些通用的准则为我们准备相应的数据。
下面这个例子,汇编是直接从 gcc 生成的代码抄来的。它传入一个int
类型的参数,把参数加一后返回,这个程序遵循通用的 ABI 标准。
1 | CL-USER> (ff-call |
总结
本文介绍了在 CCL 中编写汇编代码的方法。在 CCL 中,可以通过 LAP 来编写汇编代码。如果是 LAP function ,则需要遵循 CCL 的各种约定。如果通过 FFI 来调用编写的汇编代码,则只需要遵循通用的 ABI 约定。
ISO Standarized Lisp,ISO 推出的标准化的 LISP 家族语言。 ↩︎
有兴趣的同学可以在 ANSI 的在线商店购买,也就 30 美元,1000+ 页的一本书,平均一页纸密密麻麻的知识只要2毛钱,赚翻了! ↩︎
可能当时 CL “抄”的某些 lisp 家族的语言,就有这个功能。那是一个内存特别小机器性能特别差要非常小心计划着用的年代,反汇编可以让人更好的发现底层的性能、资源使用等方面的问题。 ↩︎
详见 https://trac.clozure.com/ccl/wiki/Internals/Compiler 。 ↩︎