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