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