其實,現在的編譯器早已經突破了原來的概念。比如說,編譯器最終的**不一定在實際機器上執行,可能是虛擬機器;編譯器編譯語言時不一定需要生成可執行檔案,能解釋就行;編譯器最好並行編譯;編譯器不一定很大,可能十幾個檔案就可以,比如說lua等等。不過,我們今天說的編譯器還是比較傳統的c編譯器,有興趣的同學可以看看編譯器是怎麼幫助我們生成可執行檔案的。我們按照詞法、語法、語義、優化的順序逐一展開。現在假設有這樣一段**,
#include #define max_value 7
int test(int value)
int main(int argc, char* argv)
(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
popset argc[param]
set argv[param]
set 3
call test
pop
get result
mov p[inner], result
set p
set string "p = %d"
poppop
mov result, 1
poppop
這裡需要解釋一下,語義轉換的結構和形式其實是各個編譯器自己定義的,未必有通用的結構。這裡的語句只是我自己想出來的,可能和實際的形式有很大的出入,但是基本方法應該是一樣的。主要解釋如下,
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)調整變數和函式**的位置,填寫檔案結構,生成最終可執行檔案
隨想錄(編譯器是怎麼工作的)
其實,現在的編譯器早已經突破了原來的概念。比如說,編譯器最終的 不一定在實際機器上執行,可能是虛擬機器 編譯器編譯語言時不一定需要生成可執行檔案,能解釋就行 編譯器最好並行編譯 編譯器不一定很大,可能十幾個檔案就可以,比如說lua等等。不過,我們今天說的編譯器還是比較傳統的c編譯器,有興趣的同學可以...
隨想錄(lcc編譯器)
lcc編譯器是一款開源編譯器,和我們之前談過的ucc差不多。一開始的時候,這款編譯器是用來進行教學使用的,但是後來越來越多的人開始了解它 使用它,並且將這款編譯器用到實際專案當中。當前一般的用法就是利用lcc將c檔案轉變成asm彙編檔案,這種使用方法是最常見的。如果我們自己開發的程式是微控制器軟體 ...
隨想錄(c編譯器的實現)
無意中在github上發現乙個很有意思的專案,比如這裡它利用flex實現了字串的識別,利用bison實現了ast語法樹的構建,最後直接利用ast進行計算和識別。ast節點遍歷的時候,作者應該是根左右遍歷的,其實左右根遍歷或許更好一些。注意,語法解析的時候肯定是樹的結構,但是不一定是二叉樹。比如,它的...