编译return语句
source link: https://www.tuicool.com/articles/UvqiQbz
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Common Lisp中有一个叫做 return
的宏,它的作用和平常在C、Java,或者Node.js里面见到的 return
关键字完全不一样。Common Lisp中的 return
用于从一个块( block
)中返的,而不是从一个函数中返回。用 return
可以写出下面这样的代码,符号 YOU-WILL-NOT-SEE-ME
永远不会被打印
(defun foo () (block nil (return 123) (print 'you-will-not-see-me)))
求值 return
,就将123作为 block
的返回值从中返回了,后面的 print
并没有机会执行——在SBCL中编译上面这段 defun
的时候,编译器甚至已经给出了提醒
return
是一个宏,它可以展开为一个 return-from
,并带有一个名为 NIL
的块名。用 return-from
可以直接从函数 foo
中返回而不需要多一层 block
,示例代码如下
(defun foo2 () (return-from foo2 123) (print 'you-will-not-see-me))
除了要多写一个函数的名称之外, return-from
跟C、Java,或者Node.js中的 return
语句是差不多的——没错,只是差不多而已。实际上, return-from
也是从一个 block
中返回的,上面的代码之所以有效,是因为 defun
会隐式地定义一个跟函数同名的块。
这一次要在 jjcc2
中支持的 return
,比起Common Lisp,更接近于C语言中的 return
语句——是用来直接从函数调用中返回的。
编译 return
其实很简单。在目前的 inside-out
中, return
会落入到最后的分支,因此它的唯一一个参数会被翻出来先编译,并且其结果是放入到 %EAX
寄存器中的。所以,编译 return
只需要生成一道简单的 RET
指令就足够了。修改后的 jjcc2
如下
(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) '*) ;; 将两个数字相乘的结果放到第二个操作数所在的寄存器中 ;; 因为约定了用EAX寄存器作为存放最终结果给continuation用的寄存器,所以第二个操作数应当为EAX `((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) ;; 编译赋值语句的方式比较简单,就是将被赋值的符号视为一个全局变量,然后将eax寄存器中的内容移动到这里面去 ;; TODO: 这里expr的second的结果必须是一个符号才行 ;; FIXME: 不知道应该赋值什么比较好,先随便写个0吧 (setf (gethash (second expr) globals) 0) (values (append (jjcc2 (third expr) globals) ;; 为了方便stringify函数的实现,这里直接构造出RIP-relative形式的字符串 `((movl %eax ,(get-operand expr 0)))) globals)) ;; ((eq (first expr) '_exit) ;; ;; 因为知道_exit只需要一个参数,所以将它的第一个操作数塞到EDI寄存器里面就可以了 ;; ;; TODO: 更好的写法,应该是有一个单独的函数来处理这种参数传递的事情(以符合calling convention的方式) ;; `((movl ,(get-operand expr 0) %edi) ;; (movl #x2000001 %eax) ;; (syscall))) ((eq (first expr) '>) ;; 为了可以把比较之后的结果放入到EAX寄存器中,以我目前不完整的汇编语言知识,可以想到的方法如下 (let ((label-greater-than (intern (symbol-name (gensym)) :keyword)) (label-end (intern (symbol-name (gensym)) :keyword))) ;; 根据这篇文章(https://en.wikibooks.org/wiki/X86_Assembly/Control_Flow#Comparison_Instructions)中的说法,大于号左边的数字应该放在CMP指令的第二个操作数中,右边的放在第一个操作数中 `((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) ;; 假定if语句的测试表达式的结果也是放在%eax寄存器中的,所以只需要拿%eax寄存器中的值跟0做比较即可(类似于C语言) (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)) ;; 暂时以硬编码的方式识别一个函数是否来自于C语言的标准库 `((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|))) ((eq (first expr) 'return) ;; 由于经过inside-out的处理之后,return的参数就是一个“原子”了,因此不再需要调用jjcc2来处理一遍 `((movl ,(get-operand expr 0) %eax) (ret))) (t ;; 按照这里(https://www3.nd.edu/~dthain/courses/cse40243/fall2015/intel-intro.html)所给的函数调用约定来传递参数 (let ((instructions '()) (registers '(%rdi %rsi %rdx %rcx %r8 %r9))) (dotimes (i (length (rest expr))) (if (nth i registers) (push `(movq ,(get-operand expr i) ,(nth i registers)) instructions) (push `(pushq ,(get-operand expr i)) instructions))) ;; 经过一番尝试后,我发现必须在完成函数调用后恢复RSP寄存器才不会导致段错误 `(,@(nreverse instructions) (pushq %rsp) (and ,(format nil "$0x~X" #XFFFFFFFFFFFFFFF0) %rsp) (call ,(first expr)) (popq %rsp))))))
现在,就不需要总是依靠 exit
函数来退出了。下列的代码可以使用 RET
指令从 _main
函数中返回
(fb '(return (+ 1 2)))
生成的汇编代码如下
.data G565: .long 0 .section __TEXT,__text,regular,pure_instructions .globl _main _main: MOVL $1, %EAX MOVL $2, %EBX ADDL %EBX, %EAX MOVL %EAX, G565(%RIP) MOVL G565(%RIP), %EAX RET
全文完。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK