66

编译大于运算符

 4 years ago
source link: https://www.tuicool.com/articles/BfY3Qr3
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.

原定的计划中这一篇应当是要讲如何编译 if 表达式的,但是我发现没什么东西可以作为if的test-form的部分的表达式,所以觉得,要不还是先实现一下比较两个数字这样子的功能吧。说干就干,我决定用大于运算符来作为例子——大于运算符就是指 > 啦。所以,我的目标是要编译下面这样的代码

并且比较之后的结果要放在 EAX 寄存器中。鉴于现在这门语言还非常地简陋,没有 布尔类型 这样子的东西,所以在此仿照C语言的处置方式,以数值0表示逻辑假,其它的值表示逻辑真。所以上面的表达式在编译成汇编代码并最终运行后,应当可以看到 EAX 寄存器中的值为0。

为了编译大于运算符,并且将结果放入到 EAX 寄存器中,需要用到新的指令 CMPJG ,以及 JMP 了。我的想法是,先将第一个操作数放入到 EAX 寄存器,将第二个操作数放入到 EBX 寄存器。然后,使用 CMP 指令比较这两个寄存器。如果 EAX 中的数值大于 EBX ,那么就使用 JG 指令跳到一个 MOV 指令上,这道 MOV 会将寄存器 EAX 的值修改为1;否则, JG 不被执行,执行后续的一道 MOV 指令,将数值0写入到 EAX 寄存器,然后使用 JMP 跳走,避免又执行到了刚才的第一道 MOV 指令。思路还是挺简单的。

在修改 jjcc2 之前,还需要在 inside-out/aux 中对 > 予以支持,但没什么特别的,就是往 member 的参数中加入 > 这个符号而已。之后,将 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)))))

然后便可以在REPL中运行下列代码了

(let* ((ht (make-hash-table))
       (asm (jjcc2 (inside-out '(_exit (> 1 2))) ht)))
  (stringify asm ht))

输出的汇编代码为

        .data
G809: .long 0
        .section __TEXT,__text,regular,pure_instructions
        .globl _main
_main:
        MOVL $1, %EAX
        MOVL $2, %EBX
        CMPL %EBX, %EAX
        JG G810
        MOVL $0, %EAX
        JMP G811
G810:
        MOVL $1, %EAX
G811:
        MOVL %EAX, G809(%RIP)
        MOVL G809(%RIP), %EDI
        MOVL $33554433, %EAX
        SYSCALL

编译链接运行后,就可以得到预期的结果了。

全文完。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK