46

调用C标准库的exit函数

 4 years ago
source link: https://www.tuicool.com/articles/uIjmAry
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 表达式的编译也就是信手拈来的事了,不解释太多。在本篇中,将会讲述一下如何产生可以调用来自于C语言标准库的 exit(3) 函数的汇编代码。

在Common Lisp中并没有一个叫做 EXIT 的内置函数,所以如同之前实现的 _exit 一样,我会新增一种需要识别的 (first expr) ,即符号 exit 。为了可以调用C语言标准库中的 exit 函数,需要遵循调用约定。对于 exit 这种只有一个参数的函数而言,情形比较简单,只需要跟对 _exit 一样处理即可。刚开始,我写下的代码是这样的

(defun jjcc2 (expr globals)
  ;; 省略不必要的内容
  (cond ;; 省略不必要的内容
        ((member (first expr) '(_exit exit))
         ;; 暂时以硬编码的方式识别一个函数是否来自于C语言的标准库
         `((movl ,(get-operand expr 0) %edi)
           (call :|_exit|)))))

(exit 1) 进行编译,会得到如下的代码

        .data
        .section __TEXT,__text,regular,pure_instructions
        .globl _main
_main:
        MOVL $1, %EDI
        CALL _exit

不过这样的代码经过编译链接之后,一运行就会遇到段错误(segmentation fault)。经过一番放狗搜索后,才知道原来在macOS上调用C函数的时候,需要先将栈对齐到16字节——我将其理解为将指向栈顶的指针对齐到16字节。于是乎,我将 jjcc2 修改为如下的形式

(defun jjcc2 (expr globals)
  ;; 省略不必要的内容
  (cond ;; 省略不必要的内容
        ((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" #XFFFFFFF0) %esp)
           (call :|_exit|)))))

结果发现还是不行。最后,实在没辙了,只好先写一段简单的C代码,然后用 gcc -S 生成汇编代码,来看看究竟应当如何处理这个栈的对齐要求。一番瞎折腾之后,发现原来是要处理 RSP 寄存器而不是 ESP 寄存器——我也不晓得这是为什么, ESP 不就是 RSP 的低32位而已么。

最后,把 jjcc2 写成下面这样后,终于可以成功编译 (exit 1)

(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|)))))

生成的汇编代码如下

        .data
        .section __TEXT,__text,regular,pure_instructions
        .globl _main
_main:
        MOVL $1, %EDI
        AND $0xFFFFFFFFFFFFFFF0, %RSP
        CALL _exit

好了,这个时候我就在想,如果想要支持其它来自C语言标准库的函数的话,只要依葫芦画瓢就好了,好像还挺简单的——天真的我如此天真地想着。

全文完


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK