編譯器,是把高級(jí)語言轉(zhuǎn)化為機(jī)器語言的工具軟件。
高級(jí)語言的代碼也是個(gè)文本字符串,所以編譯器的前端與sed、gawk、grep是差不多的,都是廣義上的字符串匹配。
編譯器把代碼轉(zhuǎn)化為機(jī)器碼的過程如下:
1,詞法分析,
這是編譯器里最簡單的模塊。
詞法分析,就是通過查看下一個(gè)字符來確定怎么把代碼字符串分割成一個(gè)個(gè)的語法詞匯。
起始符、終止符,是詞法分析的主要概念。
intday =24 * 3600;
這行代碼的第1個(gè)詞是int,起始符是i,終止符是空格。
在詞法分析時(shí)它是一個(gè)標(biāo)志符,也就是字母、下劃線、數(shù)字組成的一個(gè)字符串,必須以字母或下劃線開頭。
在分析出int這個(gè)標(biāo)志符之后,它后面的空格就沒用了,直接丟棄它。
第2個(gè)詞day也是一個(gè)標(biāo)志符,起始符是d,終止符也是空格。
習(xí)慣把代碼寫得密集的人可能這么寫:int day=24*3600;
這時(shí)day的終止符是=,它同時(shí)還是下一個(gè)詞的起始符,在把day加入詞匯序列之后需要從=開始接著分析。
第3個(gè)詞是=,第4個(gè)詞是24,第5個(gè)詞是*,第6個(gè)詞是3600,第7個(gè)詞是分號(hào);
在詞法分析時(shí)要把數(shù)字字符串24和3600轉(zhuǎn)化為整數(shù)24和3600,這兩個(gè)在程序里是不同的。
10進(jìn)制、16進(jìn)制、8進(jìn)制、2進(jìn)制、浮點(diǎn)數(shù)的支持,都是詞法分析時(shí)的任務(wù)。
另外,轉(zhuǎn)義字符串也要在這里支持。
'' 在源代碼里是字符串文本,包含著4個(gè)字符' 0 ',要轉(zhuǎn)義成單個(gè)字符0。
' ' ' ' ' '的處理和''一樣。
詞法分析還是很好寫的。
2,語法分析,
這是編譯器前端最難寫的模塊,它需要把源代碼轉(zhuǎn)化成一棵描述整個(gè)程序結(jié)構(gòu)的多叉樹。
這個(gè)多叉樹叫做抽象語法樹(英文縮寫AST)。
類型、變量、運(yùn)算符、函數(shù)定義、函數(shù)調(diào)用、if語句、for/while循環(huán),都是這個(gè)這棵樹的一部分。
抽象語法樹的層次結(jié)構(gòu),與源代碼的結(jié)構(gòu)是一樣的。
如果是這樣的源代碼的話:
int sum = 0;
for (int i = 0; i < 8; i++) {
if (i % 2 == 0)
sum += i;
}
那么語法樹是這樣的:
語法樹
初始化語句sum = 0與后續(xù)的for循環(huán)是順序執(zhí)行的,它們屬于同一個(gè)順序塊,在語法樹上有同一個(gè)父節(jié)點(diǎn)。
for循環(huán)有4個(gè)子節(jié)點(diǎn):初始化表達(dá)式i = 0,條件i < 8,循環(huán)體if語句、更新表達(dá)式i++。
其中循環(huán)體又是個(gè)if語句,具有2個(gè)子節(jié)點(diǎn):條件表達(dá)式i % 2 == 0,主體sum += i。
while循環(huán)的結(jié)構(gòu)與for類似,只要去掉初始化表達(dá)式和更新表達(dá)式就行,只有2個(gè)節(jié)點(diǎn)。
把詞法分析之后的詞匯序列轉(zhuǎn)化成抽象語法樹時(shí),常用的方法是有限自動(dòng)機(jī)。
也可以把代碼直接寫成遞歸函數(shù)調(diào)用,但是后續(xù)改起語法來就比較麻煩。
我一開始就是把scf的parse模塊寫成了遞歸函數(shù)調(diào)用,后來為了可以編輯語法,又自己做了個(gè)簡單的有限自動(dòng)機(jī)。
3,語義分析,
把語法樹遍歷一遍,檢查一下類型是否匹配,就是語義分析。
如果要支持面向?qū)ο?/strong>的話,就可以在這里進(jìn)行函數(shù)重載和運(yùn)算符重載。
常量表達(dá)式也要在這里計(jì)算出來,int day = 24 *3600要轉(zhuǎn)化成day = 86400。
常量表達(dá)式的計(jì)算
對(duì)語法樹進(jìn)行遍歷時(shí),不同的語法節(jié)點(diǎn)使用的處理函數(shù)是不同的,這就是語義。
符號(hào)=要當(dāng)作賦值,符號(hào)+要當(dāng)作加法,其他類似。
C語言里常見的函數(shù)調(diào)用,語法樹是這樣的:
int printf(const char* fmt, ...);
int main()
{
printf("hello world ");
}
函數(shù)調(diào)用也是一個(gè)運(yùn)算符,具有一個(gè)單獨(dú)的語法節(jié)點(diǎn),它的子節(jié)點(diǎn)都是它的參數(shù):
其中函數(shù)名也是一個(gè)參數(shù),要轉(zhuǎn)化為對(duì)應(yīng)的函數(shù)體的節(jié)點(diǎn)的指針。
通過這個(gè)指針才可以找到函數(shù)的代碼,進(jìn)行內(nèi)聯(lián)優(yōu)化(inline)。
函數(shù)調(diào)用和定義
如果要是調(diào)用的外部函數(shù),只有聲明沒有實(shí)現(xiàn),那就沒法內(nèi)聯(lián)了。
4,中間代碼生成(三地址碼),從這里開始就是編譯器的后端了。
這一步也是對(duì)語法樹進(jìn)行遍歷,把對(duì)應(yīng)的表達(dá)式、函數(shù)、if語句、for循環(huán)都變成類似匯編的三地址碼。
上面那段for循環(huán),這時(shí)會(huì)被變成如下的三地址碼序列:
assign sum, 0
assign i, 0
start: // for循環(huán)的開頭
cmp i, 8
jge end //條件不成立,則結(jié)束循環(huán)
assignt, i % 2 // t是編譯器生成的臨時(shí)變量
cmp t, 0
jne next
add sum, sum, i // 這行才是三地址碼
next: // 下一輪for循環(huán)
inc i // 循環(huán)變量+1
jmp start //跳轉(zhuǎn)回開頭,繼續(xù)循環(huán)
end:// for循環(huán)結(jié)束
到了這里,那個(gè)復(fù)雜的樹型結(jié)構(gòu)已經(jīng)變成線形結(jié)構(gòu)了,可以按順序?qū)懙揭粋€(gè)文本文件里,這就是匯編代碼。
到這里,編譯器就可以生成類似gcc -S的匯編代碼了。
5,中間代碼優(yōu)化,
這是編譯器后端的主要部分,屬于機(jī)器無關(guān)優(yōu)化,這部分的優(yōu)化是不依賴于CPU平臺(tái)的。
scf框架的這部分包含以下功能:
1)內(nèi)聯(lián)函數(shù),
2)有向無環(huán)圖DAG的生成,
3)帶二級(jí)指針參數(shù)的函數(shù)調(diào)用分析,
4)指針別名分析,也就是分析指針指向的變量,
5)活躍變量分析,
6)變量的加載保存分析,
7)需要自動(dòng)內(nèi)存管理的變量分析,
8)代碼流程圖的深度優(yōu)先排序,
9)自動(dòng)內(nèi)存管理代碼的添加,
10)基本塊內(nèi)的優(yōu)化,
11)循環(huán)分析,
會(huì)把一些變量盡量在循環(huán)的入口加載,在循環(huán)出口保存,減少循環(huán)內(nèi)的內(nèi)存讀寫。
沒有常量傳播的優(yōu)化,哪天有空我把它添上
6,寄存器分配,
使用圖的著色算法,之前的文章寫過。
7,指令選擇,
直接寫在代碼里的,沒做龍書里提到的那個(gè)樹的覆蓋。
8,機(jī)器碼生成,
根據(jù)intel x64的手冊(cè)編寫機(jī)器碼就行。
9,目標(biāo)文件生成,
也就是gcc -c 得到那個(gè).o文件。
Linux上的elf文件是什么樣的就怎么寫,可以參考linux的man手冊(cè)里對(duì)elf的講解。
10,可執(zhí)行文件的生成,
這是連接器的功能,它把多個(gè).o .a .so文件連接成一個(gè)可執(zhí)行文件。
這一步的代碼在scf/elf目錄,有興趣的可以看看。
連接之后的文件就可以在shell命令行里運(yùn)行了。
審核編輯:湯梓紅
-
機(jī)器碼
+關(guān)注
關(guān)注
0文章
12瀏覽量
8312 -
代碼
+關(guān)注
關(guān)注
30文章
4788瀏覽量
68601 -
編譯器
+關(guān)注
關(guān)注
1文章
1634瀏覽量
49129
原文標(biāo)題:編譯器的代碼架構(gòu)
文章出處:【微信號(hào):yikoulinux,微信公眾號(hào):一口Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論