在上一篇文章中,实现了对大于号(>
)的处理,那么对if
表达式的编译也就是信手拈来的事了,不解释太多。在本篇中,将会讲述一下如何产生可以调用来自于C语言标准库的exit(3)
函数的汇编代码。
在Common Lisp中并没有一个叫做EXIT
的内置函数,所以如同之前实现的_exit
一样,我会新增一种需要识别的(first expr)
,即符号exit
。为了可以调用C语言标准库中的exit
函数,需要遵循调用约定。对于exit
这种只有一个参数的函数而言,情形比较简单,只需要跟对_exit
一样处理即可。刚开始,我写下的代码是这样的
1 2 3 4 5 6 7
| (defun jjcc2 (expr globals) (cond ((member (first expr) '(_exit exit)) `((movl ,(get-operand expr 0) %edi) (call :|_exit|)))))
|
对(exit 1)
进行编译,会得到如下的代码
1 2 3 4 5 6
| .data .section __TEXT,__text,regular,pure_instructions .globl _main _main: MOVL $1, %EDI CALL _exit
|
不过这样的代码经过编译链接之后,一运行就会遇到段错误(segmentation fault)。经过一番放狗搜索后,才知道原来在macOS上调用C函数的时候,需要先将栈对齐到16字节——我将其理解为将指向栈顶的指针对齐到16字节。于是乎,我将jjcc2
修改为如下的形式
1 2 3 4 5 6 7 8 9 10
| (defun jjcc2 (expr globals) (cond ((member (first expr) '(_exit exit)) `((movl ,(get-operand expr 0) %edi) ;; 据这篇回答(https://stackoverflow.com/questions/12678230/how-to-print-argv0-in-nasm)所说,在macOS上调用C语言函数,需要将栈对齐到16位 ;; 假装要对齐的是栈顶地址。因为栈顶地址是往低地址增长的,所以只需要将地址的低16位抹掉就可以了 (and ,(format nil "$0x~X" #XFFFFFFF0) %esp) (call :|_exit|)))))
|
结果发现还是不行。最后,实在没辙了,只好先写一段简单的C代码,然后用gcc -S
生成汇编代码,来看看究竟应当如何处理这个栈的对齐要求。一番瞎折腾之后,发现原来是要处理RSP
寄存器而不是ESP
寄存器——我也不晓得这是为什么,ESP
不就是RSP
的低32位而已么。
最后,把jjcc2
写成下面这样后,终于可以成功编译(exit 1)
了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| (defun jjcc2 (expr globals) "支持两个数的四则运算的编译器" (check-type globals hash-table) (cond ((eq (first expr) '+) `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (addl %ebx %eax))) ((eq (first expr) '-) `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (subl %ebx %eax))) ((eq (first expr) '*) `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (imull %ebx %eax))) ((eq (first expr) '/) `((movl ,(get-operand expr 0) %eax) (cltd) (movl ,(get-operand expr 1) %ebx) (idivl %ebx))) ((eq (first expr) 'progn) (let ((result '())) (dolist (expr (rest expr)) (setf result (append result (jjcc2 expr globals)))) result)) ((eq (first expr) 'setq) (setf (gethash (second expr) globals) 0) (values (append (jjcc2 (third expr) globals) `((movl %eax ,(get-operand expr 0)))) globals)) ((eq (first expr) '>) (let ((label-greater-than (intern (symbol-name (gensym)) :keyword)) (label-end (intern (symbol-name (gensym)) :keyword))) `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (cmpl %ebx %eax) (jg ,label-greater-than) (movl $0 %eax) (jmp ,label-end) ,label-greater-than (movl $1 %eax) ,label-end))) ((eq (first expr) 'if) (let ((label-else (intern (symbol-name (gensym)) :keyword)) (label-end (intern (symbol-name (gensym)) :keyword))) (append (jjcc2 (second expr) globals) `((cmpl $0 %eax) (je ,label-else)) (jjcc2 (third expr) globals) `((jmp ,label-end) ,label-else) (jjcc2 (fourth expr) globals) `(,label-end)))) ((member (first expr) '(_exit exit)) `((movl ,(get-operand expr 0) %edi) ;; 据这篇回答(https://stackoverflow.com/questions/12678230/how-to-print-argv0-in-nasm)所说,在macOS上调用C语言函数,需要将栈对齐到16位 ;; 假装要对齐的是栈顶地址。因为栈顶地址是往低地址增长的,所以只需要将地址的低16位抹掉就可以了 (and ,(format nil "$0x~X" #XFFFFFFFFFFFFFFF0) %rsp) (call :|_exit|)))))
|
生成的汇编代码如下
1 2 3 4 5 6 7
| .data .section __TEXT,__text,regular,pure_instructions .globl _main _main: MOVL $1, %EDI AND $0xFFFFFFFFFFFFFFF0, %RSP CALL _exit
|
好了,这个时候我就在想,如果想要支持其它来自C语言标准库的函数的话,只要依葫芦画瓢就好了,好像还挺简单的——天真的我如此天真地想着。
全文完