42

编译return语句

 5 years ago
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.
neoserver,ios ssh client

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 的时候,编译器甚至已经给出了提醒

J3iqyqI.png!web

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

全文完。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK