cmd编译可以通过执行没有结果_Go语言是如何完成编译的
Go語(yǔ)言是一門需要編譯才能運(yùn)行的編程語(yǔ)言,也就說代碼在運(yùn)行之前需要通過編譯器生成二進(jìn)制機(jī)器碼,隨后二進(jìn)制文件才能在目標(biāo)機(jī)器上運(yùn)行,如果我們想要了解Go語(yǔ)言的實(shí)現(xiàn)原理,理解它的編譯過程就是一個(gè)沒有辦法繞過的事情。
預(yù)備知識(shí)
想要深入了解Go語(yǔ)言的編譯過程,需要提前了解一下編譯過程中涉及的一些術(shù)語(yǔ)和專業(yè)知識(shí)。這些知識(shí)其實(shí)在我們的日常工作和學(xué)習(xí)中比較難用到,但是對(duì)于理解編譯的過程和原理還是非常重要的。
1) 抽象語(yǔ)法樹
抽象語(yǔ)法樹(AST)是源代碼語(yǔ)法的結(jié)構(gòu)的一種抽象表示,它用樹狀的方式表示編程語(yǔ)言的語(yǔ)法結(jié)構(gòu)。抽象語(yǔ)法樹中的每一個(gè)節(jié)點(diǎn)都表示源代碼中的一個(gè)元素,每一顆子樹都表示一個(gè)語(yǔ)法元素,例如一個(gè) if else 語(yǔ)句,我們可以從 2 * 3 + 7 這一表達(dá)式中解析出下圖所示的抽象語(yǔ)法樹。
抽象語(yǔ)法樹
作為編譯器常用的數(shù)據(jù)結(jié)構(gòu),抽象語(yǔ)法樹抹去了源代碼中不重要的一些字符,比如空格、分號(hào)或者括號(hào)等等。編譯器在執(zhí)行完語(yǔ)法分析之后會(huì)輸出一個(gè)抽象語(yǔ)法樹,這棵樹會(huì)輔助編譯器進(jìn)行語(yǔ)義分析,我們可以用它來確定結(jié)構(gòu)正確的程序是否存在一些類型不匹配或不一致的問題。
2) 靜態(tài)單賦值
靜態(tài)單賦值(SSA)是中間代碼的一個(gè)特性,如果一個(gè)中間代碼具有靜態(tài)單賦值的特性,那么每個(gè)變量就只會(huì)被賦值一次,在實(shí)踐中我們通常會(huì)用添加下標(biāo)的方式實(shí)現(xiàn)每個(gè)變量只能被賦值一次的特性,這里以下面的代碼舉一個(gè)簡(jiǎn)單的例子:
x := 1x := 2y := x根據(jù)分析,我們其實(shí)能夠發(fā)現(xiàn)上述的代碼其實(shí)并不需要第一個(gè)將 1 賦值給 x 的表達(dá)式,也就是這一表達(dá)式在整個(gè)代碼片段中是沒有作用的:
x1 := 1x2 := 2y1 := x2從使用 SSA 的中間代碼我們就可以非常清晰地看出變量 y1 的值和 x1 是完全沒有任何關(guān)系的,所以在機(jī)器碼生成時(shí)其實(shí)就可以省略第一步,這樣就能減少需要執(zhí)行的指令來優(yōu)化這一段代碼。
根據(jù) Wikipedia(維基百科)對(duì) SSA 的介紹來看,在中間代碼中使用 SSA 的特性能夠?yàn)檎麄€(gè)程序?qū)崿F(xiàn)以下的優(yōu)化:
- 常數(shù)傳播(constant propagation)
- 值域傳播(value range propagation)
- 稀疏有條件的常數(shù)傳播(sparse conditional constant propagation)
- 消除無用的程式碼(dead code elimination)
- 全域數(shù)值編號(hào)(global value numbering)
- 消除部分的冗余(partial redundancy elimination)
- 強(qiáng)度折減(strength reduction)
- 寄存器分配(register allocation)
從 SSA 的作用我們就能看出,因?yàn)樗闹饕饔镁褪谴a的優(yōu)化,所以是編譯器后端(主要負(fù)責(zé)目標(biāo)代碼的優(yōu)化和生成)的一部分;當(dāng)然,除了 SSA 之外代碼編譯領(lǐng)域還有非常多的中間代碼優(yōu)化方法,優(yōu)化編譯器生成的代碼是一個(gè)非常古老并且復(fù)雜的領(lǐng)域,這里就不展開介紹了。
3) 指令集架構(gòu)
最后要介紹的一個(gè)預(yù)備知識(shí)就是指令集的架構(gòu)了,很多開發(fā)者都會(huì)遇到在生產(chǎn)環(huán)境運(yùn)行的結(jié)果和本地不同的問題,導(dǎo)致這種情況的原因其實(shí)非常復(fù)雜,不同機(jī)器使用不同的指令就是可能的原因之一。
我們大多數(shù)開發(fā)者都會(huì)使用 x86_64 的 Macbook 作為工作上主要使用的硬件,在命令行中輸入 uname -m 就能夠獲得當(dāng)前機(jī)器上硬件的信息:
x86_64 是目前比較常見的指令集架構(gòu)之一,除了 x86_64 之外,還有其他類型的指令集架構(gòu),例如 amd64、arm64 以及 mips 等等,不同的處理器使用了大不相同的機(jī)器語(yǔ)言,所以很多編程語(yǔ)言為了在不同的機(jī)器上運(yùn)行需要將源代碼根據(jù)架構(gòu)翻譯成不同的機(jī)器代碼。
復(fù)雜指令集計(jì)算機(jī)(CISC)和精簡(jiǎn)指令集計(jì)算機(jī)(RISC)是目前的兩種 CPU 區(qū)別,它們的在設(shè)計(jì)理念上會(huì)有一些不同,從名字我們就能看出來這兩種不同的設(shè)計(jì)有什么區(qū)別,復(fù)雜指令集通過增加指令的數(shù)量減少需要執(zhí)行的質(zhì)量數(shù),而精簡(jiǎn)指令集能使用更少的指令完成目標(biāo)的計(jì)算任務(wù)。早期的 CPU 為了減少機(jī)器語(yǔ)言指令的數(shù)量使用復(fù)雜指令集完成計(jì)算任務(wù),這兩者之前的區(qū)別其實(shí)就是設(shè)計(jì)上的權(quán)衡。
編譯原理
Go語(yǔ)言編譯器的源代碼在 cmd/compile 目錄中,目錄下的文件共同構(gòu)成了Go語(yǔ)言的編譯器,學(xué)過編譯原理的人可能聽說過編譯器的前端和后端,編譯器的前端一般承擔(dān)著詞法分析、語(yǔ)法分析、類型檢查和中間代碼生成幾部分工作,而編譯器后端主要負(fù)責(zé)目標(biāo)代碼的生成和優(yōu)化,也就是將中間代碼翻譯成目標(biāo)機(jī)器能夠運(yùn)行的機(jī)器碼。
Go的編譯器在邏輯上可以被分成四個(gè)階段:詞法與語(yǔ)法分析、類型檢查和 AST 轉(zhuǎn)換、通用 SSA 生成和最后的機(jī)器代碼生成,下面我們來分別介紹一下這四個(gè)階段做的工作。
1) 詞法與語(yǔ)法分析
所有的編譯過程其實(shí)都是從解析代碼的源文件開始的,詞法分析的作用就是解析源代碼文件,它將文件中的字符串序列轉(zhuǎn)換成 Token 序列,方便后面的處理和解析,我們一般會(huì)把執(zhí)行詞法分析的程序稱為詞法解析器(lexer)。
而語(yǔ)法分析的輸入就是詞法分析器輸出的 Token 序列,這些序列會(huì)按照順序被語(yǔ)法分析器進(jìn)行解析,語(yǔ)法的解析過程就是將詞法分析生成的 Token 按照語(yǔ)言定義好的文法(Grammar)自下而上或者自上而下的進(jìn)行規(guī)約,每一個(gè) Go 的源代碼文件最終會(huì)被歸納成一個(gè) SourceFile 結(jié)構(gòu):
標(biāo)準(zhǔn)的Go 語(yǔ)法解析器使用的就是 LALR(1) 的文法,語(yǔ)法解析的結(jié)果其實(shí)就是上面介紹過的抽象語(yǔ)法樹(AST),每一個(gè) AST 都對(duì)應(yīng)著一個(gè)單獨(dú)的Go語(yǔ)言文件,這個(gè)抽象語(yǔ)法樹中包括當(dāng)前文件屬于的包名、定義的常量、結(jié)構(gòu)體和函數(shù)等。
如果在語(yǔ)法解析的過程中發(fā)生了任何語(yǔ)法錯(cuò)誤,都會(huì)被語(yǔ)法解析器發(fā)現(xiàn)并將消息打印到標(biāo)準(zhǔn)輸出上,整個(gè)編譯過程也會(huì)隨著錯(cuò)誤的出現(xiàn)而被中止。
2) 類型檢查
當(dāng)拿到一組文件的抽象語(yǔ)法樹 AST 之后,Go語(yǔ)言的編譯器會(huì)對(duì)語(yǔ)法樹中定義和使用的類型進(jìn)行檢查,類型檢查分別會(huì)按照順序?qū)Σ煌愋偷墓?jié)點(diǎn)進(jìn)行驗(yàn)證,按照以下的順序進(jìn)行處理:
- 常量、類型和函數(shù)名及類型;
- 變量的賦值和初始化;
- 函數(shù)和閉包的主體;
- 哈希鍵值對(duì)的類型;
- 導(dǎo)入函數(shù)體;
- 外部的聲明;
通過對(duì)每一棵抽象節(jié)點(diǎn)樹的遍歷,我們?cè)诿恳粋€(gè)節(jié)點(diǎn)上都會(huì)對(duì)當(dāng)前子樹的類型進(jìn)行驗(yàn)證保證當(dāng)前節(jié)點(diǎn)上不會(huì)出現(xiàn)類型錯(cuò)誤的問題,所有的類型錯(cuò)誤和不匹配都會(huì)在這一個(gè)階段被發(fā)現(xiàn)和暴露出來。
類型檢查的階段不止會(huì)對(duì)樹狀結(jié)構(gòu)的節(jié)點(diǎn)進(jìn)行驗(yàn)證,同時(shí)也會(huì)對(duì)一些內(nèi)建的函數(shù)進(jìn)行展開和改寫,例如 make 關(guān)鍵字在這個(gè)階段會(huì)根據(jù)子樹的結(jié)構(gòu)被替換成 makeslice 或者 makechan 等函數(shù)。
我們其實(shí)能夠看出類型檢查不止做了驗(yàn)證類型的工作,還對(duì) AST 進(jìn)行了改寫和處理Go語(yǔ)言內(nèi)置關(guān)鍵字的活,所以,這一過程在整個(gè)編譯流程中還是非常重要的,沒有這個(gè)步驟很多關(guān)鍵字其實(shí)就沒有辦法工作。
3) 中間代碼生成
當(dāng)我們將源文件轉(zhuǎn)換成了抽象語(yǔ)法樹、對(duì)整棵樹的語(yǔ)法進(jìn)行解析并進(jìn)行類型檢查之后,就可以認(rèn)為當(dāng)前文件中的代碼基本上不存在無法編譯或者語(yǔ)法錯(cuò)誤的問題了,Go語(yǔ)言的編譯器就會(huì)將輸入的 AST 轉(zhuǎn)換成中間代碼。
Go語(yǔ)言編譯器的中間代碼使用了 SSA(Static Single Assignment Form) 的特性,如果我們?cè)谥虚g代碼生成的過程中使用這種特性,就能夠比較容易的分析出代碼中的無用變量和片段并對(duì)代碼進(jìn)行優(yōu)化。
在類型檢查之后,就會(huì)通過一個(gè)名為 compileFunctions 的函數(shù)開始對(duì)整個(gè)Go語(yǔ)言項(xiàng)目中的全部函數(shù)進(jìn)行編譯,這些函數(shù)會(huì)在一個(gè)編譯隊(duì)列中等待幾個(gè)后端工作協(xié)程的消費(fèi),這些 Goroutine 會(huì)將所有函數(shù)對(duì)應(yīng)的 AST 轉(zhuǎn)換成使用 SSA 特性的中間代碼。
4) 機(jī)器碼生成
Go語(yǔ)言源代碼的 cmd/compile/internal 中包含了非常多機(jī)器碼生成相關(guān)的包,不同類型的 CPU 分別使用了不同的包進(jìn)行生成 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm,也就是說Go語(yǔ)言能夠在上述的 CPU 指令集類型上運(yùn)行,其中比較有趣的就是 WebAssembly 了。
作為一種在棧虛擬機(jī)上使用的二進(jìn)制指令格式,它的設(shè)計(jì)的主要目標(biāo)就是在 Web 瀏覽器上提供一種具有高可移植性的目標(biāo)語(yǔ)言。Go語(yǔ)言的編譯器既然能夠生成 WASM 格式的指令,那么就能夠運(yùn)行在常見的主流瀏覽器中。
我們可以使用上述的命令將 Go 的源代碼編譯成能夠在瀏覽器上運(yùn)行的匯編語(yǔ)言,除了這種新興的指令之外,Go語(yǔ)言還支持了幾乎全部常見的 CPU 指令集類型,也就是說它編譯出的機(jī)器碼能夠在使用上述指令集的機(jī)器上運(yùn)行。
編譯器入口
Go語(yǔ)言的編譯器入口在 src/cmd/compile/internal/gc 包中的 main.go 文件,這個(gè) 600 多行的 Main 函數(shù)就是Go語(yǔ)言編譯器的主程序,這個(gè)函數(shù)會(huì)先獲取命令行傳入的參數(shù)并更新編譯的選項(xiàng)和配置,隨后就會(huì)開始運(yùn)行 parseFiles 函數(shù)對(duì)輸入的所有文件進(jìn)行詞法與語(yǔ)法分析得到文件對(duì)應(yīng)的抽象語(yǔ)法樹:
func Main(archInit func(*Arch)) { // ... lines := parseFiles(flag.Args())接下來就會(huì)分九個(gè)階段對(duì)抽象語(yǔ)法樹進(jìn)行更新和編譯,就像我們?cè)谏厦娼榻B的,整個(gè)過程會(huì)經(jīng)歷類型檢查、SSA 中間代碼生成以及機(jī)器碼生成三個(gè)部分:
- 檢查常量、類型和函數(shù)的類型;
- 處理變量的賦值;
- 對(duì)函數(shù)的主體進(jìn)行類型檢查;
- 決定如何捕獲變量;
- 檢查內(nèi)聯(lián)函數(shù)的類型;
- 進(jìn)行逃逸分析;
- 將閉包的主體轉(zhuǎn)換成引用的捕獲變量;
- 編譯頂層函數(shù);
- 檢查外部依賴的聲明;
了解了剩下的編譯過程之后,我們重新回到詞法和語(yǔ)法分析后的具體流程,在這里編譯器會(huì)對(duì)生成語(yǔ)法樹中的節(jié)點(diǎn)執(zhí)行類型檢查,除了常量、類型和函數(shù)這些頂層聲明之外,它還會(huì)對(duì)變量的賦值語(yǔ)句、函數(shù)主體等結(jié)構(gòu)進(jìn)行檢查:
類型檢查會(huì)對(duì)傳入節(jié)點(diǎn)的子節(jié)點(diǎn)進(jìn)行遍歷,這個(gè)過程會(huì)對(duì) make 等關(guān)鍵字進(jìn)行展開和重寫,類型檢查結(jié)束之后并沒有輸出新的數(shù)據(jù)結(jié)構(gòu),只是改變了語(yǔ)法樹中的一些節(jié)點(diǎn),同時(shí)這個(gè)過程的結(jié)束也意味著源代碼中已經(jīng)不存在語(yǔ)法錯(cuò)誤和類型錯(cuò)誤,中間代碼和機(jī)器碼也都可以正常的生成了。
initssaconfig() peekitabs() for i := 0; i < len(xtop); i++ { n := xtop[i] if n.Op == ODCLFUNC { funccompile(n) } } compileFunctions() for i, n := range externdcl { if n.Op == ONAME { externdcl[i] = typecheck(externdcl[i], ctxExpr) } } checkMapKeys()}在主程序運(yùn)行的最后,會(huì)將頂層的函數(shù)編譯成中間代碼并根據(jù)目標(biāo)的 CPU 架構(gòu)生成機(jī)器碼,不過這里其實(shí)也可能會(huì)再次對(duì)外部依賴進(jìn)行類型檢查以驗(yàn)證正確性。
總結(jié)
Go語(yǔ)言的編譯過程其實(shí)是非常有趣并且值得學(xué)習(xí)的,通過對(duì)Go語(yǔ)言四個(gè)編譯階段的分析和對(duì)編譯器主函數(shù)的梳理,我們能夠?qū)?Golang 的實(shí)現(xiàn)有一些基本的理解,掌握編譯的過程之后,Go語(yǔ)言對(duì)于我們來講也不再是一個(gè)黑盒,所以學(xué)習(xí)其編譯原理的過程還是非常讓人著迷的。
總結(jié)
以上是生活随笔為你收集整理的cmd编译可以通过执行没有结果_Go语言是如何完成编译的的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 扫码枪怎么安装电脑如何安装扫描仪
- 下一篇: atheros蓝牙设备驱动 小米_小米A