c语言写脚本6,C语言解释器的实现(6)--让脚本跑起来

目录:

在前面的文章中,我主要讲解了语言的解析部分,最终我们生产了脚本的中间代码。接下来,将是一个最困难的时刻,怎么解析执行中间代码!

执行代码其实是经过一定处理后的中间代码的另外一种表示。正如前面提到的,我们的中间代码是三元组的形式,比如: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;

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

96819587_1.gif

address var

--------------

-20 a

-16 b

-12 eip

-8 esp

-4 return-address

0

0 c

4 d

8 e

12 @1

16 @2

--------------

96819587_1.gif

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.函数的调用过程

例如有下面的代码:

96819587_1.gif

int add( int a, int b )

{

int c, d, e;

c = a + b;

return c;

}

int main(){

add( 4, 5 );

}

96819587_1.gif

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

96819587_1.gif

address offset var

--------------------------

....

15988 -12 eip

15992 -8 esp

15996 -4 return-address

16000 0

16000 -20 4

16004 -16 5

16008 -12 eip eip指向add(4,5)的下一条命令

16012 -8 main-esp 16000

16016 -4 return-address

0

16020 0 c

16024 4 d

16028 8 e

16032 12 @1

16036 16 @2

....

---------------------------

96819587_1.gif

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

96819587_1.gif

address offset var

--------------------------

....

15988 -12 eip

15992 -8 esp

15996 -4 return-address

16000 0

--------------------------

96819587_1.gif

5.命令的解析

每条中间变量都由一个操作符和若干个操作数组成,这里没办法罗列出所有的操作符的解析。仅仅说明一个最简单的情况:

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

这条中间代码,它的操作符是"+", 操作数是[-20],[-16], 目标操作数是[12]。所以解析过程相当简单,变成C代码就是这样的:

*(int*)(esp+12) = *(int*)(esp-12) + *(int*)(esp-16);

实际上我就是这么干的,只不过是为了适应各种命令的解析,显得比较的烦死,但是原理都是一样的。这里的int类型,是操作数中包含的类型信息,这是必须的,在中间代码的处理时,每个变量的类型都必须被确定,否则代码在执行时,没办法知道它所占的内存空间。

这是每条命令的定义,它其实是一个双向链表,这有利于跳转语句的跳转。

96819587_1.gif

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 前一条命令

96819587_1.gif

6.C的库函数调用

C语言有它的库函数,如果我们的解释器要自己实现这些库函数的话,那么工作量就大大增加了,有什么办法直接调用系统的库函数呢。如果能做到这点,那么也就能解释器的使用者提供更加强大的交换方式----即使用者可以注册自己的函数,供脚本使用。想了很多方法,唯有用汇编了。具体的做法就是:

例如,脚本有一行代码 fopen( "test", "r" );

那么,我们获取了函数名fopen,发现他是被注册的函数,所以我们得到fopen的函数指针,假设是fptr.所以这条语句的执行是这样的:

push 0x123243     ; "test"的地址

push 0x894982     ; "r"的地址

call fptr         ; 调用系统的fopen函数

...

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

96819587_1.gif

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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

96819587_1.gif

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值