0.1 引言
? ? ? ? 工作之余,閑來無事祭刚,便根據(jù)多方搜集的資料,基于Python實現(xiàn)了一個簡易的C語言編譯器好啰,可以稱之為SCC(Simplified C Compiler)。整理了這段時間的學(xué)習(xí)過程儿奶,也分享出來框往,讓更多愿意了解編譯器的人少走一些彎路,提供更多可以參考的資料闯捎。
? ? ? ? 相信如果開始學(xué)習(xí)這部分知識椰弊,可能大都是從《??書》這類經(jīng)典書籍開始的。但是相信很多人屏住呼吸翻開一頁又一頁瓤鼻,又學(xué)到了多少知識秉版,就因人而異了。反正茬祷,我沒有看完那本書清焕,反倒是這一系列文章(Let's Build A Simple Interpreter)淺顯易懂地解釋了Pascal語言解釋器的實現(xiàn)方法,對我非常有啟發(fā)和幫助祭犯。編譯器并不是深不可測秸妥,只是從小坑里面好爬出來一些罷了。
? ? ? ? 在進入下一個部分之前盹憎,讓我們先想一想筛峭,為什么要學(xué)習(xí)編譯器知識铐刘。
- 沒事干陪每,像我一樣,可以找點虐心的事情做镰吵。
- 以后寫程序遇到bug檩禾,就可以拓寬debug的范圍了。
- 成為寫出(C++)++的那個人疤祭。
- ......
? ? ? ? 一切都得有一個目標(biāo)盼产,不然就沒辦法堅持下去。對于我自己而言勺馆,學(xué)習(xí)底層的知識戏售,讓自己能夠系統(tǒng)性地思考,去面對各種上層調(diào)用帶來問題草穆,非常具有挑戰(zhàn)性灌灾。
? ? ? ? 好了,閑話少許悲柱,下面進入正題锋喜。
0.2 初識編譯器
? ? ? ? 這里簡單介紹一下編譯器的組成:
0.2.1 前處理
? ? ? ? 這部分主要做三件事情:
- 處理頭文件
#include "stdio.h"
按照頭文件引用順序嵌套地將頭文件的內(nèi)容展開到當(dāng)前文件中。如果嵌套引用到的文件很多,最終參與編譯的源文件內(nèi)容肯定超過了文件中原本的那些代碼嘿般。只是大部分時候段标,我們將聲明(.h
文件)與實現(xiàn)(.c
文件)分離,而.c
文件可以單獨生成目標(biāo)文件(后綴名為.o
)炉奴,只需要在鏈接的時候添加上即可逼庞。因此并不需要全部展開到當(dāng)前文件中。 - 處理預(yù)編譯指令
C語言有很多的預(yù)編譯指令瞻赶。比如往堡,非常常用的:
實際上,現(xiàn)在的IDE工具已經(jīng)能夠直接進行辨識共耍,直接就能告訴你用哪一塊代碼虑灰,剩下的就直接忽略了,不會進入編譯過程痹兜。#if XXX ... #elif XXX ... #else ... #endif
- 展開宏定義
#define add(x, y) ((x) + (y))
用((x) + (y))
將代碼中的add(x, y)
全部替換穆咐,這也是為什么在學(xué)習(xí)C語言的過程中,不要吝惜用括號的緣故字旭;同時对湃,宏定義末尾也不能加分號等等。因此遗淳,當(dāng)明白編譯器怎么處理宏定義的時候拍柒,那么使用宏定義就能游刃有余了。
0.2.2 編譯
? ? ? ? 經(jīng)過前處理過程處理的代碼就開始進入編譯過程屈暗〔鹧叮回顧一下,我們遇到的編譯錯誤主要有哪些养叛?以下面這段代碼為例:
struct Point
{
int x;
int y;
} // <- missing ';' (2
struct Point pt = {1, 2};
int main()
{
if (pt.x <> 2) // <- '<>' no such operator (1
b = 2; // <- 'b' is undefined (3
return 0;
}
? ? ? ? 我在這里列舉了三類錯誤种呐,已經(jīng)分別標(biāo)注在上面對應(yīng)的代碼后面。那么弃甥,再設(shè)想一下爽室,我們應(yīng)該如何編寫代碼將這些錯誤找出來呢?
? ? ? ? 很明顯淆攻,第一種錯誤阔墩,也就是<>
這種符號性質(zhì)的錯誤,只需要從頭到尾遍歷一遍瓶珊,就可以發(fā)現(xiàn)啸箫,根本不用做額外的工作。這就是我們將要介紹的詞法分析艰毒。
? ? ? ? 對于第二種錯誤筐高,如果不是結(jié)構(gòu)體,而只是一般的函數(shù)塊,也是不需要分號的柑土。這時蜀肘,我們必須要能夠知道這里應(yīng)該出現(xiàn)什么符號,不應(yīng)該出現(xiàn)什么符號稽屏。這就需要對代碼的結(jié)構(gòu)有一定的認知扮宠,也就是語法分析。
? ? ? ? 那前面分析手段辦不到的狐榔,自然就留給語義分析去做了:進行變量的聲明檢查坛增。
0.2.2.1 詞法分析
? ? ? ? 詞法分析是一個化整為零的過程。它從頭到尾將源代碼拆分成一個個的單元薄腻,稱之為token收捣。這些token按照空格、換行符和引號等進行拆分庵楷,可以是變量名罢艾、關(guān)鍵字、運算符號和其它字符尽纽。由于C語言并沒有定義<>
這樣的二元比較操作符咐蚯,此處就會產(chǎn)生錯誤提示信息。
0.2.2.2 語法分析
? ? ? ? 語法分析則是一個化零為整的相反過程弄贿。它將token按照定義的語法要求組成表達式春锋,語句和程序段。由于C語言要求結(jié)構(gòu)體定義必須以;
結(jié)尾差凹,此處就會產(chǎn)生語法錯誤期奔。這是很多人開始學(xué)C語言容易忘記的地方。
? ? ? ? 一些時候直奋,我們可能會遇到IDE提示一大堆錯誤能庆,然后去出錯的地方看,覺得也沒有錯誤脚线。其實這個時候,就是在最開始出錯的地方前面弥搞,缺少;
所致邮绿。不過,現(xiàn)在編譯器功能越來越強大攀例,很多時候能夠直接準(zhǔn)確定位錯誤船逮。
0.2.2.3 語義分析
? ? ? ? 詞法分析只是將token組成了符合語法邏輯結(jié)構(gòu)的片段,還需要語義分析進行上下文檢查粤铭,即判斷變量挖胃、函數(shù)是否已經(jīng)定義或者類型是否匹配。顯然,變量b
開始使用的時候并沒有定義酱鸭,此處便是第三種語法錯誤吗垮。
0.2.2.4 匯編語言生成
? ? ? ? 當(dāng)然,經(jīng)過了上面三個過程的仔細檢查凹髓,我們可以放心地為源代碼生成匯編語言代碼了烁登。目前,主流的匯編語言格式有Intel和AT&T兩種蔚舀,雖然格式還是有一定的差別饵沧,但是萬變不離其中,本質(zhì)上是相通的赌躺。
? ? ? ? 這一步狼牺,也是最終影響程序運行性能的關(guān)鍵。我們將在后面詳細討論礼患。
0.2.3 匯編
? ? ? ? 匯編語言代碼還需要經(jīng)過匯編過程生成二進制代碼锁右,每條匯編指令都會生成一個相對于某個基地址的偏移地址⊙忍基地址大多數(shù)情況下都不是實際的物理地址咏瑟。因此,并不能直接運行痪署。
0.2.4 鏈接
? ? ? ? 直到通過鏈接器對多個二進制代碼的地址偏移重新編排码泞,得到具有正確物理地址的二進制代碼,這個時候狼犯,才能直接運行余寥。
0.3 編譯器命令行
? ? ? ? 考慮hello.c
文件下的代碼:
#include "stdio.h"
int main(int argc, char* argv[])
{
printf("hello world!");
return 0;
}
接下來我們將使用成熟的C語言編譯器對每一個過程進行命令行操作,從而與后面我們實際編寫的代碼生成的結(jié)果相比較悯森。
- 前處理過程
clang -E hello.c -o hello.e
- 語法分析和語義分析
clang -fsyntax-only hello.c
- 匯編語言生成
clang -S hello.c -o hello.s
- 匯編
clang -o hello.o hello.s
- 鏈接
clang -o hello hello.o
更多的內(nèi)容可以詳見LLVM的官方文檔宋舷。
? ? ? ? 這樣一看,編譯器其實承擔(dān)了非常繁雜的工作瓢姻。在接下來的部分祝蝠,這些內(nèi)容都會一一呈現(xiàn)。
實現(xiàn)簡易的C語言編譯器(part 1)
實現(xiàn)簡易的C語言編譯器(part 2)
實現(xiàn)簡易的C語言編譯器(part 3)
實現(xiàn)簡易的C語言編譯器(part 4)
實現(xiàn)簡易的C語言編譯器(part 5)
實現(xiàn)簡易的C語言編譯器(part 6)
實現(xiàn)簡易的C語言編譯器(part 7)
實現(xiàn)簡易的C語言編譯器(part 8)
實現(xiàn)簡易的C語言編譯器(part 9)
實現(xiàn)簡易的C語言編譯器(part 10)
實現(xiàn)簡易的C語言編譯器(part 11)
實現(xiàn)簡易的C語言編譯器(part 12)