編譯器怎樣把C來源程式翻譯成機器代碼呢?相信你一定很好奇並想看看具體的例子。好,下面就以一個非常簡單例子來說一下編譯器的整個工作過程。
來源程式:
int round (f) float f; {
return f+0.5;
}
第一階段:預先處理。
預先處理是指宏擴充、引入標頭檔、選擇條件編譯代碼等工作。其實就是你經常使用的#define、#include<xxx.h>、#ifdef xxx 等語句。預先處理程式是作為獨立的進程執行的,而且可以混用,比如lcc編譯器可以使用GCC的前置處理器(這是因為大家都遵循同一個標準ANSI C,所以說標準這東西就是好,一有標準大家就不會亂套)。但對本例而言,經過預先處理後原始碼還是這個樣子,所以這裡就不打算深入介紹前置處理器了,而且它也不在討論之內,不會影響對原始碼的閱讀。
第二階段:詞法分析。
詞法分析的任務就是分解出一個個的單詞(token)。就像我們英漢翻譯一樣,你不妨把C來源程式當做英語,把機器碼當做漢語。好,現在給你一段英文(C來源程式),要你把它翻譯成中文(機器碼)。你想一下你會怎麼做?當然你首先要把這一段英文分解成一個個的單詞,逐個對照牛津詞典(C語言詞法規則)。同樣,詞法分析無非就是把來源程式肢解成一個個詞法單位,即單詞,然後用一張表記下來,等文法分析的時候再拿出來看看。現在這個例子就會被lcc肢解成下面的這張單詞表:
單詞編碼 附加值
INT inttype
ID "round"
'('
ID "f"
')'
FLOAT floattype
ID "f"
';'
'{'
RETURN
ID "f"
'+'
FCON 0.5
';'
'}'
EOI
其中,EOI代表結束符。附加值給出了單詞的更多資訊。雖然你現在可能還在對上面表中的某些符號如FCON是什麼東西耿耿於懷,其實大可不必,你現在只須對詞法分析有一個大致的瞭解即可,即知道詞法分析到底是幹什麼的就可以了,至於它具體怎麼運作,那是日後的分析了。
第三階段:文法及語義分析。
文法分析就是分析是不是符合C語言的文法要求和規定,關於C語言本身的文法可以參見K&R的經典《The C Programming Language》的附錄A,那裡對ANSI C進行全面的解釋。語義分析就是分析是不是符合語義,比如某些句子可能沒有語法錯誤但有語義錯誤,比如代碼 a = b+3; 使用了未定義的變數a,這就是語義錯誤。文法分析最終會產生一片森林(資料結構應該學過了吧),森林裡有很多樹,每棵樹都叫做抽象文法樹(Abstract Syntax Tree,簡稱AST)。就本例而言,lcc會產生如下兩棵AST:
其中,每個節點的形式是“操作符+類型”。詳細如下:
ASGN+F: Assignment + Float 即浮點數賦值運算。
ADDRF+P: Address-Function + Pointer 即指向函數參數的指標,也就是參數的地址。
CVD+F: Convert Double to Float 即將Double轉換為Float,同理CVD+I 就是Double轉換為Int 以此類推。
INDIR+D: Indirection Double 即取值,值的類型為double。其中INDIR代表取值操作,+號後面代表值的類型。如INDIR+F就是取浮點值。
第一個AST應該從右下角的" caller "f" ---> double " 逆著箭頭並逆時針看。總的流程是這樣:從調用者(caller)的f處取值,此值是 double類型,將其轉換成float類型並賦值給函數round(被調用者,callee)的參數f。
第二個AST應該從左下角的" callee "f" ---> float " 逆著箭頭並順時針看。總的流程是這樣:從參數f處取值,並轉換成double,然後與double類型的常量0.5進行雙精確度的加運算。運算完後將結果轉換成 int型並返回。
看到這裡是不是把原始碼給忘了?回過頭看看開頭處的原始碼,傳回值是 int 類型沒錯,參數類型是 float 類型,常量0.5預設是 double 類型。所以一系列的類型強轉就在程式員不知不覺的情況下發生了:先是把double類型的實參(假設的)轉換成 float 類型的傳給參數,由於0.5是double類型的,不得已又再次轉換成double類型的,然後才進行運算,最後看看傳回型別,不好,是int類型,無奈之下又強轉為int類型,最終把結果返回給調用者。
所以說現在使用進階語言編程的程式員真是幸福啊!隨隨便便寫下像這樣的代碼:
int a = 5; double b = 2.34; char c = a + b;
還沒事。因為C編譯器已經默默無聞地忍受了這一切!這也是使用進階語言的好處之一啊。但是機器始終是機器,你要告訴它是什麼類型的運算它才能算。學過彙編的都知道,就加法而言,就有整型加,浮點型加等等。編譯器的很重要的任務之一就是隱藏這些細節,使編程簡單。
第四階段:中間代碼的產生。
中間代碼階段將上面的AST轉換成DAG(Directed Acyclic graph,無迴圈有向圖)。如:
這張圖與之前的區別在於將形如 ASGN+F 的改成 ASGNF 以示區別。其中CNST+D變成引用標號為2的靜態變數。標號1表示round的結尾。
第五階段:彙編代碼的產生。
你可以看到,DAG已經可以很形象地描述執行代碼了。lcc的代碼產生器通過對DAG加註釋的方法產生彙編代碼。注釋結果如:
每個函數的入口和出口都是一樣的彙編代碼,所以後端就會準備好一張代碼模板,我們只要把產生的程式碼插進合適的位置就可以了。就X86和DOS/WinNT而言,lcc與MASM或TASM彙編器協同合作,將產生的彙編代碼串連成在特定的體繫結構和作業系統上的機器碼。
關於彙編的知識自己掌握,這裡不討論了,以免陷入不必要的細節。
小結:
從C來源程式到彙編代碼,我們很快地走了一遍,瞭解了lcc的工作過程。下一節就進入正題,進行代碼注釋。注釋的順序與作者著作《A retargetable.....》一樣,因為照常理應該是從詞法分析開始,但詞法分析出來的單詞如何管理呢?這就涉及到符號表,符號表怎樣動態增長怎樣儲存呢?這就涉及到記憶體管理。同樣符號表與類型表有千絲萬縷的聯絡。符號表的符號名字又涉及到字串的管理。所以自底向上的順序是:
記憶體管理---->字串管理----->符號表管理、類型表管理----->詞法分析