C語言解譯器的實現–讓指令碼跑起來(六)

來源:互聯網
上載者:User

 目錄:

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

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.