• C语言解释器的实现让脚本跑起来(六)


     目录:

    1.脚本的执行要素

    2.栈的模拟.

    3.变量在栈中的地址计算

    4.函数的调用过程

    5.命令的解析

    6.C的库函数调用

      在前面的文章中,我主要讲解了语言的解析部分,最终我们生产了脚本的中间代码。接下来,将是一个最困难的时刻,怎么解析执行中间代码!
      执行代码其实是经过一定处理后的中间代码的另外一种表示。正如前面提到的,我们的中间代码是三元组的形式,比如:c = a + b * c; 可以表示成 @1 = b * c; @2 = a + @1; @3 = c = @2;但是,这种中间代码还得经过一定的转换才能更方便我们解析执行。接下来,我将一步步的说明,中间代码被执行的每个过程。

    1.脚本的执行要素
      一个脚本要被执行,必须要为它创建一个环境,就想操作系统中为没有程序创建一个进程一样。
      一个C语言程序,其实只有几个要素:运算符,变量,函数。所以,一个C脚本要被执行,首先,它必须具备中间代码命令的解析;其次,必须要有变量的内存空间;再次,必须要有函数的调用解析,即函数调用栈的模拟。所以,一个脚本的执行,最重要的是变量内存的分配和栈的维护,还有命令的执行。

    2.栈的模拟.
      如果你熟悉C的调用栈,那么这个就很容易理解了。我们先不说函数调用时,栈的变化,姑且先说明一个函数的执行过程。还是这个例子:

        int add( int a, int b )
    {
    int c, d, e;
    c = a + b;
    }

    那么它的中间代码是这样的:

      @1 = a + b;
    @2 = c = @1;

      在执行时,我们不能直接根据变量名去查找变量,这样既麻烦,而且效率也很低;而是应该根据变量的地址去存取变量。但是变量保存在哪里,怎么计算,这就是引入栈的原因了。我们首先看看上面的函数对应的栈:

      address  var
    --------------
    -20 a
    -16 b
    -12 eip
    -8 esp
    -4 return-address
    0 <-------------------esp
    0 c
    4 d
    8 e
    12 @1
    16 @2
    --------------

      eip表示调用该函数时,当前的命令位置,当函数返回时,我们要pop出这个eip,继续执行eip的下一条命令。
      esp表示调用该函数时,当前函数的变量空间的开始位置,即调用者的esp,当函数返回时,我们要还原该esp。esp的意思是,一个函数的变量空间在栈中的基地址。每个函数在执行时,我们都会有一个固定的esp,每个变量在栈中都有具体的位置,这些变量相对于esp的距离都是固定。
      return-address主要是保存函数返回值得地址,即函数在被调用时产生的临时变量。在函数返回时,返回值会被填入该地址中。这样调用者就可以从这个临时变量中获取调用结果了。例如:int a = add( 3, 4 );  那么,return-address就应该是a的地址,或者是另一个临时变量的地址,总之,最后要为a赋值,必须依赖于return-address。

      有了这个栈,我们的中间代码就应该被处理成这样:

      @1 = a + b   对应于  [esp+12] = [esp-20] + [esp-16];
    @2 = c = @1 对应于 [esp+16] = [esp+0] = [esp+12];

      上述的代码中"[xx]"表示地址xx中的值,因为esp在执行时,每个函数在实现时esp是固定的,所以我们可以省略esp不写,所以上面的代码可以改为:

      [12] = [-20] + [-16];
    [16] = [0] = [12];

      为了方便处理,我们将中间变量也放到栈里面,但是,中间变量的地址是可以被重用的,因为一条语句被执行完后,这条语句的中间变量就不会再被用到,所以,上一条语句的中间变量是可以被回收的。

    3.变量在栈中的地址计算
      首先,每个函数中,都有一个固定的esp,可以视为该函数在栈中的起始位置。然后其他的变量都被表示为距离esp的值,即偏移量。例如上面的例子,我们在解析出一个函数的中间代码时,就知道了这个函数的所有的局部变量,形参列表,并且知道这些变量的类型。所有我们可以根据类型的大小,计算他们在栈中的位置。

    4.函数的调用过程
      例如有下面的代码:

        int add( int a, int b )
    {
    int c, d, e;
    c = a + b;
    return c;
    }
    int main(){
    add( 4, 5 ); <---------①
    }

    当执行到①的时候,他的栈空间是这样的:

      address offset    var
    --------------------------
    ....
    15988 -12 eip
    15992 -8 esp
    15996 -4 return-address
    16000 0 <-------------------(main-esp假设为16000)

    16000 -20 4
    16004 -16 5
    16008 -12 eip eip指向add(4,5)的下一条命令
    16012 -8 main-esp 16000
    16016 -4 return-address
    0 <-------------------(add-esp = 160000+20 = 160020 )
    16020 0 c
    16024 4 d
    16028 8 e
    16032 12 @1
    16036 16 @2
    ....
    ---------------------------

    当add函数返回时,该函数的栈会被回收。即变成:

      address offset    var
    --------------------------
    ....
    15988 -12 eip
    15992 -8 esp
    15996 -4 return-address
    16000 0 <-------------------(main-esp假设为16000)
    --------------------------

    5.命令的解析
      每条中间变量都由一个操作符和若干个操作数组成,这里没办法罗列出所有的操作符的解析。仅仅说明一个最简单的情况:
          @1 = a + b   对应于  [esp+12] = [esp-20] + [esp-16];
      这条中间代码,它的操作符是"+", 操作数是[-20],[-16], 目标操作数是[12]。所以解析过程相当简单,变成C代码就是这样的:
          *(int*)(esp+12) = *(int*)(esp-12) + *(int*)(esp-16);
      实际上我就是这么干的,只不过是为了适应各种命令的解析,显得比较的烦死,但是原理都是一样的。这里的int类型,是操作数中包含的类型信息,这是必须的,在中间代码的处理时,每个变量的类型都必须被确定,否则代码在执行时,没办法知道它所占的内存空间。
     
      这是每条命令的定义,它其实是一个双向链表,这有利于跳转语句的跳转。

    typedef struct _cmd{
    char cmd;
    struct{
    char type;
    int size;
    union{
    int64 i;
    double d;
    }d;
    }d[3];
    int ex;
    struct _cmd * next;
    struct _cmd * pre;
    }cmd_t;

    cmd 操作符
    d[3] 操作数
    ex 某些命令的附加信息
    next 下一条命令
    pre 前一条命令

    6.C的库函数调用
      C语言有它的库函数,如果我们的解释器要自己实现这些库函数的话,那么工作量就大大增加了,有什么办法直接调用系统的库函数呢。如果能做到这点,那么也就能解释器的使用者提供更加强大的交换方式----即使用者可以注册自己的函数,供脚本使用。想了很多方法,唯有用汇编了。具体的做法就是:
      例如,脚本有一行代码 fopen( "test", "r" );
      那么,我们获取了函数名fopen,发现他是被注册的函数,所以我们得到fopen的函数指针,假设是fptr.所以这条语句的执行是这样的:
      push 0x123243     ; "test"的地址
      push 0x894982     ; "r"的地址
      call fptr         ; 调用系统的fopen函数
      ...
     

    我写了一个汇编代码,为了在liunx下顺利的移植代码,使用了nasm(我原来是使用masm)。:

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;
    ;nasm -fcoff call.asm -o outfile
    ;
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    [bits 32] ;使用32位模式的处理器
    [section .text]

    %define WIN32
    %ifdef WIN32
    %define _funptr _asm_funptr ;保存函数指针
    %define _argtab _asm_argtab ;参数列表
    %define _argtye _asm_argtye ;参数类型列表
    %define _argnum _asm_argnum ;参数个数
    %define _call _asm_call
    %else
    %define _funptr asm_funptr
    %define _argtab asm_argtab
    %define _argtye asm_argtye
    %define _argnum asm_argnum
    %define _call asm_call
    %endif

    extern _funptr
    extern _argtab
    extern _argtye
    extern _argnum
    global _call

    _call:
    xor edx, edx
    xor ecx, ecx
    mov ebx, [_argnum]
    cmp ebx, 0
    jz end
    beg:
    cmp dword[_argtye + ecx], 1
    jz ft
    push dword[_argtab+ecx]
    add edx,4
    jmp fe
    ft:
    fld dword [_argtab+ecx]
    sub esp,8
    fstp qword [esp]
    add edx,8
    fe:
    add ecx, 8
    sub ebx, 1
    jnz beg
    end:
    mov [_argnum], edx
    mov eax, [_funptr]
    call eax
    add esp, [_argnum]
    ret
  • 相关阅读:
    Linux任务前后台的切换
    如何给html元素的onclick事件传递参数即如何获取html标签的data
    关键词多空格处理
    tp3常量
    php 正则判断是否是手机号码
    thinkphp 初始化
    删除图标
    time() 在thinkphp 3.2.3 模板格式化输出
    iOS工程如何支持64-bit
    调试instruments
  • 原文地址:https://www.cnblogs.com/linxr/p/2398635.html
Copyright © 2020-2023  润新知