17

支持四则运算中的变量

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

上一篇文章 中, jjcc2 函数实现了对 setq 这个语句的编译。这么一来,便可以将加减乘除运算中的嵌套表达式都替换为变量了。比如,将

(+ (+ 1 2) 3)

中的嵌套的表达式 (+ 1 2) 用一个变量 G564 代替,变成

(PROGN
  (SETQ #:G564 (+ 1 2))
  (+ #:G564 3))

PS:上面的结果中的 #:G564 只是打印出来的时候长这个样子而已,实际地输入这段代码的话,两个 #:G564 其实是不同的符号,会导致未绑定的变量的错误的。

言归正传。既然如此,现在就要来支持编译 (+ #:G564 3) 这样的表达式了。其实这个真的是太简单了,只需要将这个符号塞入到 jjcc2 的第二个参数的 globals 中,然后在生成的“汇编指令”的S表达式中,嵌入这个符号即可。

我刚开始的时候也是这么想的,后来发现这样出来的代码编译不过,哭

折腾了一小段时间后,才知道原来有一种叫做“RIP-relative”的东西——好吧,我的X64的汇编语言知识也是赶鸭子上架的,遇到什么问题就放狗搜,所以完全不成体系——总之,我找到了解决办法,就是将原本放入一个符号的操作数,替换为类似于下面这样的内容

G564(%RIP)

所以对于操作数,实际上还需要先判断一下其类型。如果是整数,就按照原来的方式原样输出;如果是符号,就生成像上面这样的RIP-relative的结构。这部分太经常出现了,于是提炼出了一个专门处理四则运算的操作数的辅助函数 get-operand

(defun get-operand (expr n)
  "从EXPR中提取出第N个操作数,操作数的下标从0开始计算"
  (check-type expr list)
  (check-type n integer)
  (let ((e (nth (1+ n) expr)))
    (etypecase e
      (integer e)
      (symbol (format nil "~A(%RIP)" e)))))

借助它重写 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))))

现在,如果运行下面的这个 example1 函数

(defun example1 ()
  "验证jjcc2确实可以处理含有变量的加减乘除运算"
  (let ((ht (make-hash-table)))
    (setf (gethash 'a ht) 1)
    (let ((asm (jjcc2 '(+ a a) ht)))
      (stringify asm ht))))

便可以得到下面这段汇编代码了

        .data
A: .long 1
        .section __TEXT,__text,regular,pure_instructions
        .globl _main
_main:
        MOVL A(%RIP), %EAX
        MOVL A(%RIP), %EBX
        ADDL %EBX, %EAX
        movl %eax, %edi
        movl $0x2000001, %eax
        syscall

全文完。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK