目錄:
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