C-編譯器的實現


  寫這個編譯器的目的,是為了完成編譯原理課上老師布置的大作業,實際上該大作業並不是真的實現一個編譯器,而我選擇硬剛,是為了完成我的小願望--手寫內核,編譯器和CPU。我花了整個上半學期,寫完了WeiOS,為了讓它支持更多的用戶態程序,甚至是基本的程序開發,必須給它量身打造一個編譯器。於是這個編譯器被提上日程。

  因為我要復習考研和專業課過多,我打消了手寫詞法分析和語法分析的念頭,轉而使用FLEXYACC,等到有時間再完成手工的版本--我認為也不是很難,如果用遞歸下降的話。

  全部代碼都在該處 https://github.com/mynamevhinf/CMinusCompiler

 

詞法分析

  因為是精簡的C語言,所以只支持基本的符號(Token),像”++”, “--”和位操作都不予考慮。另外,只支持intvoid類型。於是便構成了以下符號集:

  number   [1-9][0-9]*

  letter     [a-zA-Z]

  Identifier  {letter}({letter}|{number})*

  Newline   \n

  whitespace [ \t\r]+

保留字:

  If else while int void return

 

運算和界限符號:

  <= >= != == + - * / < > ; , ( ) { } [ ]

 

主體函數是getToken(),這個函數封裝了FLEX原生的yylex函數。而yyparser也將直接調用該函數。它的主要工作是在開始第一次詞的檢索前,初始化相關變量。然后在每次被調用的時候,返回符號的類型給yyparser,並且把構成符號的字符串臨時保存在tokenString字符數組中。所以這函數相當於什么事情都沒有干。

另外注意的是注釋的兩個符號,我直接在詞法分析處理注釋了。行注釋是”//”,利用FLEX自帶的input()函數(如果有的話,沒有就寫一個)一直讀到’\n’出現。然后就是段注釋符”/*”和”*/”,相似的做法。

 

語法分析

  以下是BNF格式的語法規則:

  Program -> declaration_list

  declaration_list -> declaration_list  declaration | declaration

  declaration ->  var_declaration | func_declaration

  type_specifier ->  INT | VOID

  var_declaration ->  type_specifier VARIABLE ;  |  type_specifier VARIABLE [ NUM ] ;

  func_declaration ->  type_specifier VARIABLE ( params ) compound_stmt

  Params -> params_list | VOID

  params_list -> params_list , param | param

  Param -> type_specifier VARIABLE  | type_specifier VARIABLE [ ]

  compound_stmt -> { local_declarations stmt_list }

  local_declarations -> local_declarations var_declaration | /* empty */

  stmt_list ->stmt_list stmt  |   stmt

  Stmt -> expr_stmt | if_stmt | return_stmt | compound_stmt | iteration_stmt

  expr_stmt -> expr ; | ;

  if_stmt ->IF ( expr ) stmt | IF ( expr )  stmt ELSE stmt

  iteration_stmt -> WHILE ( expr ) stmt

  return_stmt ->RET ; | RET expr ;

  Expr -> var = expr | simple_expr

  Var -> VARIABLE | VARIABLE [ NUM ]

  Call -> VARIABLE ( args )

  Args -> arg_list |  /* empty */

  arg_list -> arg_list , expr | expr

  simple_expr -> NUM  | var | call  | ( expr )  | - simple_expr

            | simple_expr + simple_expr

            | simple_expr - simple_expr

            | simple_expr * simple_expr

            | simple_expr / simple_expr

            | simple_expr < simple_expr

            | simple_expr > simple_expr

            | simple_expr >=simple_expr

            | simple_expr <= simple_expr

            | simple_expr != simple_expr

            | simple_expr == simple_expr

 

  我用globl.h中的TreeNode結構來保存語法樹中的每一個節點。而一些為空的轉換,我打算還是用一個該結構來表示,但是類型標記為None(也許有點浪費內存).

  我實現的C-還算是個比較完整的程序語言,所以很有必要生成AST(抽象語法樹),那么語法樹中共有幾種類型的節點呢?按理說應每種語法規則對應一種類型,例如參數列表,聲明語句,普通語句和表達式等都對應一個節點類型,詳細可以參見NodeType枚舉類型。Parser.c文件是處理與語法樹相關的函數,目前來說當中幾個函數還沒寫清楚,TreeNode需要大改一下我估計,過幾天也許就明了了。--2018/05/29

  2018/05/30

  沒想到只過了一天不到,就完成了語法分析部分,總體上來說還是很簡單的。

  有些語法規則導出兩種相似的子規則,用專業術語來講就是就是要做左因子消除,但好像yacc已經代替我們做了這個工作--我猜測它在底下優化了我們混亂的語法規則,包括左遞歸也解決了。據來說就是if_stmt導出的兩種情況,我並不打算在StmtType枚舉中添加新的枚舉類型來處理,而是利用原有的結構,用nkids來辨別是哪種情況。而在處理var_declaration第二種導出的規則時,原有的結構不夠用了,因為我要存儲VARIABLE和NUM,很顯然一個attr聯合體不夠用,所以我引入了第二個聯合體分別來存儲兩個值。分別叫attrAattrB。有些時候這樣做也無法解決結構上的問題,才不得不用兩個枚舉類型來解決。現在我也不確定這樣做是否多余,畢竟我也是第一次寫編譯器,但它確實解決了當下的問題。

  我刪除了封裝yylex()getToken()函數,並解決了注釋代碼的問題,現在可以支持/**/的段注釋和//行注釋了。另外,我不想讓我代碼變得冗余,所以我把構建符號表(Symbol table)的任務放在了語義分析,直接放在語法分析中雖然節約了時間,但實在是難以維護。

  最后根目錄下source*.c都成功地進行了語法分析,可能也只是表面上...

  語義分析還沒學,翻書去了...

 

  2018/06/02

  經過兩天的學習,我知道了符號表基本的構建方式和運行時環境的搭建。但是還來不及完全轉換成代碼。今天我抽了一點時間寫了符號表的管理函數。我的編譯器有多級符號表:一個全局符號表以及每個函數一個局部符號表。在全局符號表中,每個屬於函數的變量名,都有一個指針指向其局部符號表。而局部符號表通過Parent字段與父符號表相連,在當前符號表無法檢索到符號信息的時候,就會去搜尋父符號表,層層遞增。由於C語言本身的規定,和我的懶惰,我不支持動態作用域,也就是說對一切外部(相對與當前代碼塊)的符號引用而言,其偏移和效果都是固定的;也不支持函數聲明嵌套,也就是說對於任何局部符號表,其父符號表都指向全局符號表。因此我覺得這樣簡單的實現方式是可行的。

  符號表的基本數據結構是哈希表。具體實現不做闡述,我只寫了插入和尋找的函數,因為這個爛編譯器暫時不支持類型聲明和類型別名,所以我沒有寫刪除。

  這里留下一個問題,除了全局聲明函數,代碼塊--也就是被{}包圍起來的部分,也需要局部符號表,而且是支持嵌套的。那么我該如何解決這種相當於“匿名”函數的符號表問題,我還沒有思路。或許可以給它們各自賦予決不會引起沖突的名字?又或者是留到代碼生成階段再處理,這樣就不必緩存“匿名”符號表了,直接生成代碼就完事了。

  主要代碼都在symbol.c symbol.h里面。

 

  2018/06/07

  經過幾天的咸魚和跟進,我完成了類型檢查和符號表生成,中間代碼生成。

  關於類型檢查,是完全仿造gcc的,檢查順序是后序遍歷,例如表達式:

    a = test() + b; 

  我們必須先檢查test()是否有返回值,然后b是什么類型的數據,如果test()返回值是void,那么這個語句就可以直接判斷為錯誤了。若兩個加數通過檢查,而a是數組類型的話,也會報錯。

  由上所述,類型檢查必須是后序遍歷,而我生成符號表的時候采取先序遍歷,這里就產生了矛盾。解決的方法是,我對語法樹中的表達式(expr)類型的節點,通通執行后續遍歷,而其他如If語句,while語句等采取常規做法。原因是我在表達式的語句中,不可能出現聲明新函數或者變量的語句,反而是要檢查所用的變量和函數在之前是否聲明,如果沒有就報錯,因此后序遍歷是可行的。

  也就是說我的buildSymtab()函數不是安裝樹狀遞歸展開的,而且名不副實,因為它除開生成符號表,還同時完成了類型檢查工作。其實我是想把這兩個步驟分開的,用2-pass來完成,代碼也比較精簡,想砍掉哪部分也很方便。但由於當時寫的太爽了,不想再寫類似於buildSymtab()這樣結構的函數了,所以直接做完了。

  上一篇遺留的問題,匿名代碼塊怎么辦?我的處理方法是為它們生成單獨的符號表,並且在語法分析階段,調用randomFuncName()函數為每一個匿名代碼塊生成一個名字,類似真正的函數那樣管理,這個名字不是真正隨機的,因為它是AAAnonymous*的形式,但無所謂了,反正這編譯器也爛。

  具體的代碼在analyzer.c里,相關結構的定義在analyzer.h中。

  接下來是一張類型檢查的貼圖: 左邊是我的fcc,右邊是gcc,錯誤和警告都檢測出來了,但是他們比我提示信息更加多...

   

  運行時環境(run-time):

  當然是選擇和C語言一樣啊,沒有什么好說的。

  要提的一點還是匿名代碼塊的問題,對於這種代碼塊中聲明和定義的“局部變量”,我觀察了gcc的做法,他們把這些“局部變量”當作了其所屬的函數的局部變量,舉個例子:

  void test()

  {

    int j;

    {

      int i;

    }

  }

  那么在函數test()為局部變量分配空間的時候,不是分配4個字節,而是8個字節。所以我也這么干。因為語法的原因,函數體本身聲明局部變量肯定在匿名代碼塊之前,故當我們處理到匿名代碼塊的時候,必須要返回去更改所屬函數的局部變量信息(用於代碼生辰的時候分配具體空間)。之前我的FuncProperty結構並沒有維護參數個數和局部變量大小的信息,所以我做了一些更改,添加了nparamsnlocalv域。

 

  中間代碼

  本來我都不打算寫中間代碼,直接生成x86的匯編代碼,但老師說不給我分,我就寫了。然后我看了一下vsl的中間代碼,我很郁悶遂決定自己創造一套新的三地址碼,我覺得還挺有意思的,我甚至都考慮用這套三地址碼寫一個虛擬機,直接在虛擬機上面跑。。。

  折中了一下,我的這套三地址碼已經非常像x86的匯編代碼了,除了一些沒用的臨時寄存器。其對於我來說,最大的作用就是把樹形的信息,轉換成了鏈狀的,接下來順着三地址碼構成的鏈表翻譯就好了。相關代碼見irgen.c而數據結構等在irgen.h.下面是一個源代碼文件(source2.c)以及其生成的三地址碼(midcode.f)的部分。

source2.c:

int jb;
int x[10];

/*
void test(void)
{
}
*/

int minloc(int a[], int low, int high)
{
    int i; int x; int k;
        
    k = low;
    x = a[low];
    i = low + i;

/*
    k = a;
    ok = 10;
    i = a[test()];
*/
    while (i < high) {
        if (a[i] < x) {
            x = a[i];
            k = i;
        }
        i = i + 1;
    }
    return k;
}   

void sort(int a[], int low, int high)
{
    int i;
    int k;

 

 

 

midcode.f:

 

 1 program:
 2     glob_var jb 4
 3     glob_var x 40
 4 
 5 _minloc:
 6     loc_var 16
 7     k = low 
 8     mull t0 low 4 
 9     addl t1 &a t0 
10     x = t1 
11     addl t2 low i 
12     i = t2 
13 L0:
14     lt t3 i high 
15     if_false t3 goto L1
16     mull t4 i 4 
17     addl t5 &a t4 
18     lt t6 t5 x 
19     if_false t6 goto L2
20     mull t7 i 4 
21     addl t8 &a t7 
22     x = t8 
23     k = i 
24 L2:
25     addl t9 i 1 
26     i = t9 
27     goto L0
28 L1:
29     ret k 
30 
31 _sort:
32     loc_var 16
33     i = low 
34 L3:
35     subl t10 high 1 
36     lt t11 i t10 
37     if_false t11 goto L4
38     loc_var 16
39     begin_args

 

  可以看到真的是一一對應的,雖然還有個小Bug,但是我懶得管了。

 

  2018/06/11

  我的寄存器分配函數寫得有點混亂,當然如果我不想做那些奇奇怪怪的優化,就不會存在這個問題。我寫得很難受,但也比較享受。我現在是考慮把對中間變量的處理也統一起來,但是還沒有思路...

  isReside():判斷一個地址是否駐留在寄存器中,如果該地址類型為TempPtr或TempReg,

  regWriteBack(RegPtr Rp, RegPtr Rbp,FILE *fp): 把Rp寄存器的內容寫會所屬變量的內存地址。一定要先判斷Hp是否為空,若為空就表示當前存了一個中間變量!!!! 如果不為空,但是沒被修改過--就是dirty為0,也沒必要寫回去!!!

  regReadIn(RegPtr Address *addr, FILE *fp): 把地址addr所屬的變量的值讀到到Rp指向的寄存器中,並更新相關內容.如果該變量/中間變量已存在寄存器中,則調用regMoveReg()直接從原寄存器移動.如果不在,則根據addr的類型采取不同的操作.我暫時處理了一般變量和常數的讀取。

  regBindVar(RegPtr Rp, Address *addr, HashList Hp, int dirty):如函數名,把addr,Hp,dirty賦給寄存器Rp.函數體內檢測是否有其他寄存器已保存了該變量,調用FreeReg()把其addr和Hp域清空.

  RegPtr regSearchOne(HashList Hp, FILE *fp):為第一個操作數搜尋合適的寄存器.我判斷的是addr這個域,但感覺好像不夠圓滿...

  

  2018/06/14

  懶得寫了,終於完成了代碼生成,我自己寫的那套寄存器分配,也很好用,甚至超過了vsl作者們寫的算法= =

  但是遺留了一個問題,就是我在代碼生成的時候,忘記考慮到代碼重入性的問題了,在生成循環代碼塊的時候,根據前文的數據流生成了代碼,本意是盡量減少內存存取的次數,但是我沒有考慮到,在真正運行的過程中,每次循環開始的時候,寄存器中存儲的信息不一定是“第一次"進入循環塊時的信息,因此我生成的代碼有語義錯誤,只有偶爾情況下才是正確的。我不打算改這個bug了,老師說這個涉及到龍書上講解的數據流分析,但我還要考研,所以推遲了吧,等真正需要的時候再搞這個。

  下面是測試代碼,就是一個簡單的選擇排序 source2.c

/* A program to perform selection sort on a 10
   element array. */

/*
void test(void)
{
}
*/

int minloc(int a[], int low, int high)
{
    int i; int x; int k;
        
    k = low;
    x = a[low];
    i = low + i;

    while (i < high) {
        if (a[i] < x) {
            x = a[i];
            k = i;
        }
        i = i + 1;
    }
    return k;
}   

void sort(int a[], int low, int high)
{
    int i;
    int k;

    i = low;
    while (i < high - 1) {
        int t;
        k = minloc(a, i, high);
        t = a[k];
        a[k] = a[i];
        a[i] = t;
        i = i + 1;
    }
}

  我生成的匯編代碼 source2.s:

  

	.file	"testfile/source2.c"
	.text
	.globl	minloc
	.type	minloc, @function
minloc:
	pushl %ebp
	movl %esp, %ebp
	subl $16, %esp
	movl 12(%ebp), %eax
	movl %eax, -12(%ebp)
	movl 12(%ebp), %edx
	leal 0(,%edx,4), %ebx
	movl 8(%ebp), %ecx
	addl %ecx, %ebx
	movl (%ebx), %ebx
	movl %ebx, -8(%ebp)
	movl -4(%ebp), %eax
	addl %eax, %edx
	movl %edx, -4(%ebp)
.L0:
	cmpl 16(%ebp), %edx
	jge .L1
	leal 0(,%edx,4), %eax
	addl %ecx, %eax
	movl (%eax), %eax
	cmpl -8(%ebp), %eax
	jge .L2
	leal 0(,%edx,4), %ebx
	addl %ecx, %ebx
	movl (%ebx), %ebx
	movl %ebx, -8(%ebp)
	movl %edx, -12(%ebp)
.L2:
	movl -4(%ebp), %edx
	addl $1, %edx
	movl %edx, -4(%ebp)
	jmp .L0
.L1:
	movl -12(%ebp), %eax
	leave
	ret

	.globl	sort
	.type	sort, @function
sort:
	pushl %ebp
	movl %esp, %ebp
	subl $16, %esp
	movl 12(%ebp), %eax
	movl %eax, -4(%ebp)
.L3:
	movl 16(%ebp), %edx
	subl $1, %edx
	cmpl %edx, %eax
	jge .L4
	movl 16(%ebp), %ebx
	pushl %ebx
	pushl %eax
	movl 8(%ebp), %ecx
	pushl %ecx
	call minloc
	addl $12, %esp
	movl %eax, -8(%ebp)
	leal 0(,%eax,4), %edx
	addl %ecx, %edx
	movl (%edx), %edx
	movl %edx, -12(%ebp)
	movl -4(%ebp), %eax
	leal 0(,%eax,4), %edx
	addl %ecx, %edx
	movl -8(%ebp), %eax
	leal 0(,%eax,4), %ebx
	addl %ecx, %ebx
	movl (%edx), %edx
	movl %edx, (%ebx)
	movl -4(%ebp), %eax
	leal 0(,%eax,4), %ecx
	movl 8(%ebp), %eax
	addl %eax, %ecx
	movl -12(%ebp), %eax
	movl %eax, (%ecx)
	movl -4(%ebp), %eax
	addl $1, %eax
	movl %eax, -4(%ebp)
	jmp .L3
.L4:
	leave
	ret

  

最后上測試結果,難得的正確了一次:

 

  2018/06/24

  本來不打算寫了,但是在滿分的激勵下我做了以下修改,並最終解決了上面的Bug

    1.被調用函數保存可能改變的所有寄存器。

    2.實現循環代碼的語義正確(其實就是保證了可重入性)。

  當然還有一個問題就是,棧內容初始化。因為物理頁面分配的不確定性,因此會留下上一個進程的數據,所以必須在程序一開始對可能用到的棧空間進行初始化。嚴格來說,這並不是我的問題。

  最后附上一張成功運行的圖:

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM