目錄:
在前面的文章中,我主要講解了語言的解析部分,最終我們生產了腳本的中間代碼。接下來,將是一個最困難的時刻,怎么解析執行中間代碼!
執行代碼其實是經過一定處理后的中間代碼的另外一種表示。正如前面提到的,我們的中間代碼是三元組的形式,比如: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的值,即偏移量。例如上面的例子,我們在解析出一個函數的中間代碼時,就知道了這個函數的所有的局部變量,形參列表,並且知道這些變量的類型。所有我們可以根據類型的大小,計算他們在棧中的位置。
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