1.代碼塊
代碼塊是由多個運算式組成的一組代碼。它可以看成是以下的形式:
{
exp1
exp2
...
}
它由"{"開始,由"}"結束,中間包含多條運算式,或者是控制語句。如果不是以"{"開始,那麼,一個代碼塊就是一條運算式。在上面的章節,我們已經介紹過了,每個運算式會產生一個中間代碼。它是一個鏈表 struct _code * ,而一個代碼塊,是由多個運算式組成的,所以我們將每個運算式的中間代碼鏈表連到一起就成了代碼塊的中間代碼了。
如果代碼塊中包含控制語句,那麼,我們必須做一些處理,即在代碼鏈表中插入跳躍陳述式,和跳轉位置(Lab)。
2.控制語句
2.1 C語言中,控制語句有這些:
a. if( exp ) stmt else stmt
b. do stmt while( exp )
c. while( exp ) stmt
d. for( exp1; exp2; exp3 ) stmt
e. switch( exp ) stmt
f. goto lab
其中,stmt表示一個代碼塊。我們如何為這些代碼產生中間代碼呢?這裡還要說明的是跳躍陳述式。比如一個if語句:
if( exp ) stmt1 else stmt2
那麼,它的意思是,當 exp == 0 時,跳轉到stmt2位置;當exp != 0的時候不做跳轉,但是stmt1執行完成後要跳轉到stmt2的後面。所以,這中間涉及了兩個東西:跳躍陳述式 和 跳轉的位置。跳躍陳述式我們用三種命令表示:JE、JNE、JMP,即不等於跳轉,等於跳轉,無條件跳轉。 跳轉的位置我們用Lab表示,即在代碼鏈表中插入一個標籤,供跳躍陳述式尋找要跳轉的位置。
還是上面的if語句,它產生後的代碼應該是這樣的:
A. if( exp ) stmt1 else stmt2 -->
exp
JE L1
stmt1
JMP L2
L1:
stmt2
L2:
其中,L1 L2分別佔用代碼鏈表的一個節點,在code_t結構體中,用lab域表示。
2.2 控制語句中的break和continue.
在一些控制語句中,他們支援break和continue,即如果在代碼塊總出現break,那麼他應該跳轉到代碼塊的外面,如果是continue,那麼跳轉到條件陳述式繼續執行。例如下面的do while語句:
B. do stmt while( exp ) -->
L1:
stmt <-- 如果這裡出現break,那麼JMP L3; 如果出現continue, 那麼JMP L2
L2:
exp
JNE L1
L3:
因為在解析stmt的時候,L1,L2和L3都已經固定好了,所以,在處理break和continue的時候,跳轉的LAB都已經明確,可以用參數將L2和L3傳遞個stmt()函數,stmt函數中解析break和continue的時候,僅僅是添加一條跳躍陳述式。
2.3 其他控制語句的代碼形式:
C. while( exp ) stmt -->
JMP L2
L1:
stmt
L2:
exp
JNE L1
L3:
D. for( exp1; exp2; exp3 ) stmt -->
exp1
JMP L3
L1:
stmt
L2:
exp3
L3:
exp2
JNE goto L1
L4:
E. switch( exp ){
case 1: stmt1
case i: stmti
default: stmt
...
}
exp
selete i and jmp(L1..Ln,L)
Li: stmti
L: stmt
LL:
selete i and jmp(L1..Ln,L) 表示 如果exp的結果是i,那麼跳轉到Li,否則跳轉到L。switch語句跟別的控制語句不一樣,其他的控制語句在還沒解析代碼塊的時候,我們就已經知道應該建立幾個Lab了,所以我們可以事先建立好Lab,然後在適當的位置插入JMP語句,這個JMP語句中跳轉到的Lab這時候已經確定了。但是對於switch語句,我們事先不知道case在什麼地方,所以不知道"selete i and jmp(L1..Ln,L)"應該對應什麼代碼。所以,我們必須解析完stmt(代碼塊)之後才能產生代碼。 具體的做法是在解析代碼快的時候記錄下所以的Lab,解析完成後再做相應的處理,即構造"selete i and jmp(L1..Ln,L)"代碼,將它串連到中間代碼的前面。
F. goto Lab -->
JMP Lab
在解析goto的時候,必須將"Lab"名稱轉換成我們的Lab的表示形式。
3.局部變數的生命週期
在一個函數中定義的變數稱之為局部變數,但是局部變數有自己的生命週期,即在自己的代碼塊中定義的,那麼它只對這個代碼塊的代碼可見。例如有下面的代碼:
{
int a;
{
int a;
}
printf("%d\n", a);
}
那麼第二個a對printf語句處是不可見的。為了表示變數的生命週期,我們為每個變數加入了begin和end域,用來儲存該變數對[begin,end]區間的代碼是可見的。所以,這裡begin,和end怎麼解析是個問題,begin不難,在解析定義的時候就可以確定,但是end確實比較難,因為必須在一個代碼塊中結束後(即解析到"}"後),才知道end的值。所以為了確定end的值,棧在這裡又被徵用了。
{ <-- 代碼塊開始,建立一個stack1
int a; <-- 解析完a,將a壓入stack1, 此時 a.begin已經確定
{ <-- 遇到"{" 遞迴調用解析函數,建立一個stack2
int a; <-- 解析完a,將a壓入stack2, 此時 a.begin已經確定
} <-- 遇到"}",表示該代碼塊完成,將a從stack2中pop出來,設定a.end !
此時,遞迴調用結束。返回到上一個代碼塊處理函數。
printf("%d\n", a); <--
} <-- 到"}",表示該代碼塊完成,將a從stack1中pop出來,設定a.end !
經過上面的過程,第一個a和第二個a的begin和end值都被確定。在代碼的處理過程中,我們根據變數名尋找變數時,必鬚根據當前代碼的位置,來判斷位置是否屬於[begin,end]區間,而不僅僅是判斷變數名。
4.函數解析
一個函數包括這幾個部分:
a. 傳回值類型
b. 形參列表
c. 局部變數
d. 代碼塊
例如下面的函數:
int add( int a, int b )
{
int c;
c = a + b;
return c;
}
那麼它的傳回值類型是int, 參數列表是a、b,局部變數有c, 執行代碼是 " c = a + b; return c; " 。仔細觀察,它其實是由函式宣告和一個代碼塊組成的。所以解析這個函數也很簡單,其實就是解析聲明,得到函數名,參數列表和傳回值類型。然後執行上一章節描述的解析代碼塊函數,得到該函數的中間代碼鏈。
5.附
比如有如下的代碼:
int main( int argc, char **argv ){
int a, b;
b = 1;
for( a=0; a<10; a++ ){
b *= 2;
}
return b;
}
那麼這個函數所對應的中間代碼是這樣的:
fun: main 2-args: argc argv b a
@0 = b = 1
@1 = a = 0
JMP 7
LAB_5:
@4 = b *= 2
LAB_6:
@3 = a ++
LAB_7:
@2 = a < 10
JNE 5
LAB_8:
@5 = b