編譯器一直是我比較喜歡的話題。編譯器是個比較神奇的工具,它可以把原來毫無意義的字符數據轉變成一行一行可以執行的代碼。作爲每一個科班出身的同學來說,編譯原理都是專業學習中必須經歷的一個部分。只是在後來的工作中,真正從事編譯器開發的同學少之又少,但是如果你懂得了編譯原理的相關機理,會給你的工作帶來很大的幫助。關於編譯原理的書很多,網上可以搜一下有阿霍版本的《編譯原理》,有陳火旺院士的《編譯原理》,張素琴版本的《編譯原理》,三本書我都覺得不錯。另外,現在關於編譯原理也有很多的開發工具,比如說lex&yacc,只要你會編寫基本的語法範式,設計自己的編譯器也不是什麼難事。
其實,現在的編譯器早已經突破了原來的概念。比如說,編譯器最終的代碼不一定在實際機器上運行,可能是虛擬機;編譯器編譯語言時不一定需要生成可執行文件,能解釋就行;編譯器最好並行編譯;編譯器不一定很大,可能十幾個文件就可以,比如說lua等等。不過,我們今天說的編譯器還是比較傳統的c編譯器,有興趣的同學可以看看編譯器是怎麼幫助我們生成可執行文件的。我們按照詞法、語法、語義、優化的順序逐一展開。現在假設有這樣一段代碼,
- #include <stdio.h>
- #define MAX_VALUE 7
- int test(int value)
- {
- return MAX_VALUE + 1 + value * 4;
- }
- int main(int argc, char* argv)
- {
- int p;
- p = test(3);
- printf("p = %d", p);
- return 1;
- }
(1)詞法分析
詞法分析是整個文件編譯最基本的環節。上面的文件中就存在很多的字符,那我們就需要分別對它們進行處理。比如說,通常的分類很可能是這樣的,
a)字符是否是數字,例如7,1,4
b)字符是否是string類型,例如“p = %d”
c)字符是否是關鍵字,例如define
d)字符是否是變量,例如value,argc, argv, p
e)字符是否是運算符, 例如 +
f)字符是否是圓括號、方括號、花括號等等
(2)語法分析
語法分析的目的就是構建一個語法樹,分析當前的文件是否符合編程語言的文法結構,比如說,
a)整個字符串是否符合表達式要求
b)字符串是否符合判斷語句要求
c)字符串是否符合循環語句要求
d)字符串是否符合函數要求
e)字符串是否符合include語法要求
f) 有沒有沒有未聲明就是用的變量等等;
(3)語義分析
語義分析有的時候和語法分析是聯繫在一起的。但是,這裏我們把它拆開來單獨成了一部分。所謂的語義分析,其實就是把前面生成的語法樹拆解下來,生成原子語句操作的過程。比如說,上面的文件很可能是這樣的形式,
- SET value
- mov temp1[inner], 7
- add temp1[inner], 1
- mul temp2,value[param], 4
- add temp1, temp2
- mov result, temp1
- pop
- SET argc[param]
- SET argv[param]
- SET 3
- call test
- pop
- get result
- mov p[inner], result
- SET p
- SET string "p = %d"
- pop
- pop
- mov result, 1
- pop
- pop
a)SET值爲函數參數
b)call爲函數調用
c)pop爲堆棧平衡使用
d)數據[inner],表示當前變量是函數中的臨時變量
e)數據[param],表示當前變量是入參參數
f)temp表示編譯器爲了自身計算方便,臨時添加的局部變量
g)result表示返回值
(4)代碼優化
代碼優化是編譯器處理的一個重要環節,代碼優化的目的主要是減少不必要的計算和處理,比如
a)計算沒有使用價值的臨時變量
b)除去沒有判斷價值的if語句
c)對於某些const變量,編譯器提前計算,這裏就可以對temp1提前計算
d)其他優化措施等等
(5)生成彙編代碼
在(3)中生成的代碼只是中間代碼,並不是完全意義上的彙編語言。所以,編譯器還需要把它翻譯成對應的二進制代碼,比如說arm語言、x86語言或者是powerpc語言等等。當然這中間還是存在一些技巧的,比如
a)對於多參數的函數,某些cpu可以用寄存器代替,有些cpu用堆棧表示
b)某些cpu需要對字節對齊,某些cpu則不需要
c)某些cpu有字節序的要求,某些cpu則無所謂,而有的cpu則可選
d)對於臨時變量,有的cpu可以寄存器表示,而有的cpu只能自己生成一個temp變量等等,
說到這裏,我們也可以自己小試一下身手,看看代碼怎麼生成,熟悉x86代碼的同學也可以自己試試,
- push ebp
- mov ebp, esp
- push ebx
- push ecx
- mov ebx, 8
- mov ecx, ebp[8]
- mul ecx, 4
- add ebx, ecx
- mov eax, ebx
- pop ecx
- pop ebx
- mov esp, ebp
- pop ebp
- push ebp
- mov ebp, esp
- sub esp, 0x4
- push 3
- call test
- add esp 4
- mov ebp[-4], eax
- push ebp[-4]
- push string "p = %d"
- call printf
- add esp, 8
- mov eax, 1
- sub esp, 0x4
- mov esp, ebp
- pop ebp
(6)彙編級代碼優化
這裏的優化其實還挺多的,但是功能基本有限,無外乎就是,
a)乘法轉變成移位
b)除法轉變成移位
c)寄存器優化使用
d)刪除寄存器的重複操作過程
e)部分函數參數用寄存器代替等等
(7)鏈接和生成可執行文件
在編譯過程中,我們常常看到有些代碼編譯通過了,但是鏈接失敗了。這是很正常的事情,因爲在最後生成的文件當中,每一個變量和函數都應該有出處,否則就會鏈接失敗。不管是什麼系統平臺,鏈接都是個大學問。這個時候,做的事情其實還是比較多的,比如
a)生成執行文件,確定是否帶調試信息
b)鏈接所有的變量和代碼
c)生成map文件
d)確定函數和變量的出處,一旦查找失敗,結束
e)調整變量和函數代碼的位置,填寫文件結構,生成最終可執行文件