50

调用C标准库的exit函数

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

在上一篇文章中,实现了对大于号( > )的处理,那么对 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语言标准库的函数的话,只要依葫芦画瓢就好了,好像还挺简单的——天真的我如此天真地想着。

全文完


Recommend

  • 152
    • www.cocoachina.com 7 years ago
    • Cache

    OC限制函数调用的频率

    website upgrading… 京ICP备110065...

  • 72
    • draveness.me 6 years ago
    • Cache

    理解 Golang 的函数调用

    函数是 Go 语言中的一等公民,理解和掌握函数的调用过程是深入学习 Golang 时无法跳过的步骤,这里会介绍 Go 语言中函数调用的过程和实现原理并与 C 语言中函数执行的过程进行对比,同时对参数传递的原理进行剖析,让读者能够清楚地知道 G...

  • 101

  • 81

    原标题:Understanding JavaScript Function Invocation and "this",出自Yehuda的这篇博客,是在Typescript的中文教程里看到的。 JS的函数调用一直以来给不少人带来疑惑,其中this的语义是人们

  • 32

    Fracker是一套PHP函数调用追踪与分析的工具,其目标是在PHP应用程序的手动安全评估期间协助安全研究人员。 它包括: 需要安装在目标Web应用程序环境中的

  • 52

    理解 Go 语言的函数调用4.1 函数调用函数是 Go 语言的一等公民,掌握和理解函数的调用过程是我们深入学习 Go 无法跳过的,本节将从函数的调用惯例和参数传递方法两个方面分别介绍函数的执行过程。...

  • 34
    • www.tuicool.com 5 years ago
    • Cache

    swift函数定义跟调用总结

    swift提供了大量方便快捷的定义方式,今天在这里做一个总结。 1、无参数无返回值函数 func test1() { print("无参数无返回值函数") } 调用:test1() 2、多个参数无返回值 func...

  • 43
    • segmentfault.com 5 years ago
    • Cache

    c# 函数调用 - - SegmentFault 思否

    这周我们学院做大实验,简单来说就是用c#语言来编写三个小应用,分别是简单计算器,学生成绩管理界面和超市选址问题,让我们去体会可视化编程的思想。再没有学过c#的情况下,我们靠着老师发的参考资料和上网查询,勉强按时完成了。这次主要来讲讲我从做成绩管理系...

  • 26

    除了使用GDB 启动调试、 暂停/恢复程序执行 和GDB查看变量外,另外一个重要的调试方法便是查看程序的函数调用堆栈情况。 调用堆栈是当前函数之前的所有已调用函数的...

  • 14

    V2EX  ›  程序员 Http 协议或(其他)协议的调用换标准 SQL  

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK