package main import "fmt" func main() { fmt.Println("Hello, world") }
在本文中,我們將介紹初學(xué)者比較關(guān)心的話題:go語言如何編譯為機(jī)器碼
創(chuàng)新互聯(lián)公司云計(jì)算的互聯(lián)網(wǎng)服務(wù)提供商,擁有超過13年的服務(wù)器租用、四川雅安電信機(jī)房、云服務(wù)器、網(wǎng)絡(luò)空間、網(wǎng)站系統(tǒng)開發(fā)經(jīng)驗(yàn),已先后獲得國家工業(yè)和信息化部頒發(fā)的互聯(lián)網(wǎng)數(shù)據(jù)中心業(yè)務(wù)許可證。專業(yè)提供云主機(jī)、網(wǎng)絡(luò)空間、域名注冊、VPS主機(jī)、云服務(wù)器、香港云服務(wù)器、免備案服務(wù)器等。本文的目標(biāo)是希望讀者對go語言的編譯過程有一個(gè)全面的理解
一段程序要運(yùn)行起來,需要將go代碼生成機(jī)器能夠識別的二進(jìn)制代碼
go代碼生成機(jī)器碼需要編譯器經(jīng)歷:
詞法分析 => 語法分析 => 類型檢查 => 中間代碼 => 代碼優(yōu)化 => 生成機(jī)器碼
Go語言的編譯器入口是 src/cmd/compile/internal/gc
包中的 main.go
文件,此函數(shù)會(huì)先獲取命令行傳入的參數(shù)并更新編譯的選項(xiàng)和配置
隨后就會(huì)開始運(yùn)行 parseFiles 函數(shù)對輸入的所有文件進(jìn)行詞法與語法分析
func Main(archInit func(*Arch)) { // ... lines := parseFiles(flag.Args())
接下來我們將對各個(gè)階段做深入介紹
所有的編譯過程都是從解析代碼的源文件開始的
詞法分析的作用就是解析源代碼文件,它將文件中的字符串序列轉(zhuǎn)換成Token
序列,方便后面的處理和解析
我們一般會(huì)把執(zhí)行詞法分析的程序稱為詞法解析器(lexer)
Token
可以是關(guān)鍵字,字符串,變量名,函數(shù)名
有效程序的"單詞"都由Token
表示,具體來說,這意味著"package","main","func" 等單詞都為Token
Go語言允許我們使用go/scanner和go/token包在Go程序中執(zhí)行解析程序,從而可以看到類似被編譯器解析后的結(jié)構(gòu)
如果在語法解析的過程中發(fā)生了任何語法錯(cuò)誤,都會(huì)被語法解析器發(fā)現(xiàn)并將消息打印到標(biāo)準(zhǔn)輸出上,整個(gè)編譯過程也會(huì)隨著錯(cuò)誤的出現(xiàn)而被中止
helloworld程序解析后如下所示
1:1 package "package" 1:9 IDENT "main" 1:13 ; "\n" 2:1 import "import" 2:8 STRING "\"fmt\"" 2:13 ; "\n" 3:1 func "func" 3:6 IDENT "main" 3:10 ( "" 3:11 ) "" 3:13 { "" 4:3 IDENT "fmt" 4:6 . "" 4:7 IDENT "Println" 4:14 ( "" 4:15 STRING "\"Hello, world!\"" 4:30 ) "" 4:31 ; "\n" 5:1 } "" 5:2 ; "\n" 5:3 EOF ""
我們可以看到,詞法解析器添加了分號,分號常常是在C語言等語言中一條語句后添加的
這解釋了為什么Go不需要分號:詞法解析器可以智能地加入分號
語法分析的輸入就是詞法分析器輸出的 Token 序列,這些序列會(huì)按照順序被語法分析器進(jìn)行解析,語法的解析過程就是將詞法分析生成的 Token 按照語言定義好的文法(Grammar)自下而上或者自上而下的進(jìn)行規(guī)約,每一個(gè) Go 的源代碼文件最終會(huì)被歸納成一個(gè) SourceFile 結(jié)構(gòu):
SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" }
標(biāo)準(zhǔn)的 Golang 語法解析器使用的就是 LALR(1) 的文法,語法解析的結(jié)果生成了抽象語法樹(Abstract Syntax Tree,AST)
抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。
之所以說語法是“抽象”的,是因?yàn)檫@里的語法并不會(huì)表示出真實(shí)語法中出現(xiàn)的每個(gè)細(xì)節(jié)。比如,嵌套括號被隱含在樹的結(jié)構(gòu)中,并沒有以節(jié)點(diǎn)的形式呈現(xiàn);而類似于 if-condition-then 這樣的條件跳轉(zhuǎn)語句,可以使用帶有三個(gè)分支的節(jié)點(diǎn)來表示。
與AST相對應(yīng)的是CST(Concrete Syntax Trees),讀者可以在參考資料中拓展閱讀二者的差別
在AST中,我們能夠看到程序結(jié)構(gòu),例如函數(shù)和常量聲明
Go為我們提供了用于解析程序和查看AST的軟件包:go/parser 和 go/ast
helloworld程序生成的AST如下所示
0 *ast.File { 1 . Package: 1:1 2 . Name: *ast.Ident { 3 . . NamePos: 1:9 4 . . Name: "main" 5 . } 6 . Decls: []ast.Decl (len = 2) { 7 . . 0: *ast.GenDecl { 8 . . . TokPos: 3:1 9 . . . Tok: import 10 . . . Lparen: - 11 . . . Specs: []ast.Spec (len = 1) { 12 . . . . 0: *ast.ImportSpec { 13 . . . . . Path: *ast.BasicLit { 14 . . . . . . ValuePos: 3:8 15 . . . . . . Kind: STRING 16 . . . . . . Value: "\"fmt\"" 17 . . . . . } 18 . . . . . EndPos: - 19 . . . . } 20 . . . } 21 . . . Rparen: - 22 . . } 23 . . 1: *ast.FuncDecl { 24 . . . Name: *ast.Ident { 25 . . . . NamePos: 5:6 26 . . . . Name: "main" 27 . . . . Obj: *ast.Object { 28 . . . . . Kind: func 29 . . . . . Name: "main" 30 . . . . . Decl: *(obj @ 23) 31 . . . . } 32 . . . } 33 . . . Type: *ast.FuncType { 34 . . . . Func: 5:1 35 . . . . Params: *ast.FieldList { 36 . . . . . Opening: 5:10 37 . . . . . Closing: 5:11 38 . . . . } 39 . . . } 40 . . . Body: *ast.BlockStmt { 41 . . . . Lbrace: 5:13 42 . . . . List: []ast.Stmt (len = 1) { 43 . . . . . 0: *ast.ExprStmt { 44 . . . . . . X: *ast.CallExpr { 45 . . . . . . . Fun: *ast.SelectorExpr { 46 . . . . . . . . X: *ast.Ident { 47 . . . . . . . . . NamePos: 6:2 48 . . . . . . . . . Name: "fmt" 49 . . . . . . . . } 50 . . . . . . . . Sel: *ast.Ident { 51 . . . . . . . . . NamePos: 6:6 52 . . . . . . . . . Name: "Println" 53 . . . . . . . . } 54 . . . . . . . } 55 . . . . . . . Lparen: 6:13 56 . . . . . . . Args: []ast.Expr (len = 1) { 57 . . . . . . . . 0: *ast.BasicLit { 58 . . . . . . . . . ValuePos: 6:14 59 . . . . . . . . . Kind: STRING 60 . . . . . . . . . Value: "\"Hello, world!\"" 61 . . . . . . . . } 62 . . . . . . . } 63 . . . . . . . Ellipsis: - 64 . . . . . . . Rparen: 6:29 65 . . . . . . } 66 . . . . . } 67 . . . . } 68 . . . . Rbrace: 7:1 69 . . . } 70 . . } 71 . } .. . .. // Left out for brevity 83 }
如上的輸出中我們能夠看出一些信息
在Decls字段中,包含文件中所有聲明的列表,例如import,常量,變量和函數(shù)
為了進(jìn)一步理解,我們看一下對其的圖形化抽象表示
紅色表示與節(jié)點(diǎn)相對應(yīng)的代碼
main函數(shù)包含了3個(gè)部分,名稱, 聲明, 主體
名稱是單詞main的標(biāo)識
由Type字段指定的聲明將包含參數(shù)列表和返回類型
主體由一系列語句組成,其中包含程序的所有行。在本例中,只有一行
fmt.Println語句由AST中的很多部分組成,由ExprStmt
聲明。
ExprStmt
代表一個(gè)表達(dá)式,其可以是本例中的函數(shù)調(diào)用,也可以是二進(jìn)制運(yùn)算(例如加法和減法等)
我們的ExprStmt
包含一個(gè)CallExpr
,這是我們的實(shí)際函數(shù)調(diào)用。這又包括幾個(gè)部分,其中最重要的是Fun
和Args
Fun包含對函數(shù)調(diào)用的引用,由SelectorExpr
聲明。在AST中,編譯器尚未知道fmt是一個(gè)程序包,它也可能是AST中的變量
Args
包含一個(gè)表達(dá)式列表,這些表達(dá)式是該函數(shù)的參數(shù)。在本例中,我們已將文字字符串傳遞給函數(shù),因此它由類型為STRING
的BasicLit
表示。
構(gòu)造AST之后,將會(huì)對所有import的包進(jìn)行解析
接著Go語言的編譯器會(huì)對語法樹中定義和使用的類型進(jìn)行檢查,類型檢查分別會(huì)按照順序?qū)Σ煌愋偷墓?jié)點(diǎn)進(jìn)行驗(yàn)證,按照以下的順序進(jìn)行處理:
常量、類型和函數(shù)名及類型
變量的賦值和初始化
函數(shù)和閉包的主體
哈希鍵值對的類型
導(dǎo)入函數(shù)體
外部的聲明
通過對每一棵抽象節(jié)點(diǎn)樹的遍歷,我們在每一個(gè)節(jié)點(diǎn)上都會(huì)對當(dāng)前子樹的類型進(jìn)行驗(yàn)證保證當(dāng)前節(jié)點(diǎn)上不會(huì)出現(xiàn)類型錯(cuò)誤的問題,所有的類型錯(cuò)誤和不匹配都會(huì)在這一個(gè)階段被發(fā)現(xiàn)和暴露出來。
類型檢查的階段不止會(huì)對樹狀結(jié)構(gòu)的節(jié)點(diǎn)進(jìn)行驗(yàn)證,同時(shí)也會(huì)對一些內(nèi)建的函數(shù)進(jìn)行展開和改寫,例如 make 關(guān)鍵字在這個(gè)階段會(huì)根據(jù)子樹的結(jié)構(gòu)被替換成 makeslice 或者 makechan 等函數(shù)。
類型檢查不止對類型進(jìn)行了驗(yàn)證工作,還對 AST 進(jìn)行了改寫以及處理Go語言內(nèi)置的關(guān)鍵字
在上面的步驟完成之后,可以明確代碼是正確有效的
接著將AST轉(zhuǎn)換為程序的低級表示形式,即靜態(tài)單一賦值形式(Static Single Assignment Form,SSA)形式,核心代碼位于gc/ssa.go
SSA不是程序的最終狀態(tài),其可以更輕松地應(yīng)用優(yōu)化,其中最重要的是始終在使用變量之前定義變量,并且每個(gè)變量只分配一次
例如下面的代碼我們可以看到第一個(gè)x的賦值沒有必要的
x = 1 x = 2 y = 7
編輯器會(huì)將上面的代碼變?yōu)槿缦?,從而?huì)刪除x_1
x_1 = 1 x_2 = 2 y_1 = 7
生成SSA的初始版本后,將應(yīng)用許多優(yōu)化過程。這些優(yōu)化應(yīng)用于某些代碼段,這些代碼段可以使處理器執(zhí)行起來更簡單或更快速。
例如下面的代碼是永遠(yuǎn)不會(huì)執(zhí)行的,因此可以被消除。
if (false) { fmt.Println(“test”) }
優(yōu)化的另一個(gè)示例是可以刪除某些nil檢查,因?yàn)榫幾g器可以證明這些檢查永遠(yuǎn)不會(huì)出錯(cuò)
在對SSA進(jìn)行優(yōu)化的過程中使用了S表達(dá)式(S-expressions)進(jìn)行描述, S-expressions 是嵌套列表(樹形結(jié)構(gòu))數(shù)據(jù)的一種表示法,由編程語言Lisp發(fā)明并普及
SSA優(yōu)化過程中對于S表達(dá)式的應(yīng)用如下所示,將8位的常量乘法組合起來
(Mul8 (Const8 [c]) (Const8 [d])) -> (Const8 [int64(int8(c*d))])
具體的優(yōu)化包括
常數(shù)傳播(constant propagation)
值域傳播(value range propagation)
稀疏有條件的常數(shù)傳播(sparse conditional constant propagation)
消除無用的程式碼(dead code elimination)
全域數(shù)值編號(global value numbering)
消除部分的冗余(partial redundancy elimination)
強(qiáng)度折減(strength reduction)
寄存器分配(register allocation)
我們可以用下面的簡單代碼來查看SSA及其優(yōu)化過程
對于如下程序
package main import "fmt" func main() { fmt.Println(2) }
我們需要在命令行運(yùn)行如下指令來查看SSA
GOSSAFUNC環(huán)境變量代表我們需要查看SSA的函數(shù)并創(chuàng)建ssa.html文件
GOOS、GOARCH代表編譯為在Linux 64-bit平臺(tái)運(yùn)行的代碼
go build用-ldflags給go編譯器傳入?yún)?shù)
-S 標(biāo)識將打印匯編代碼
$ GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags "-S" simple.go
下面的命令等價(jià)
GOSSAFUNC=main GOOS=linux GOARCH=amd64 go tool compile main.go
當(dāng)打開ssa.html時(shí),將顯示許多代碼片段,其中一些片段是隱藏的。
Start片段是從AST生成的SSA。genssa片段是最終生成的Plan9匯編代碼
Start片段如下
start b1:- v1 (?) = InitMemv2 (?) = SP v3 (?) = SB v4 (?) = Addr <*uint8> {type.int} v3 v5 (?) = Addr <*int> {""..stmp_0} v3 v6 (6) = IMake v4 v5 (~arg0[interface {}]) v7 (?) = ConstInterface v8 (?) = ArrayMake1 <[1]interface {}> v7 v9 (6) = VarDef {.autotmp_11} v1 v10 (6) = LocalAddr <*[1]interface {}> {.autotmp_11} v2 v9 v11 (6) = Store {[1]interface {}} v10 v8 v9 v12 (6) = LocalAddr <*[1]interface {}> {.autotmp_11} v2 v11 v13 (6) = NilCheck v12 v11 v14 (?) = Const64 [0] (fmt..autotmp_3[int], fmt.n[int]) v15 (?) = Const64 [1] v16 (6) = PtrIndex <*interface {}> v12 v14 v17 (6) = Store {interface {}} v16 v6 v11 v18 (6) = NilCheck v12 v17 v19 (6) = Copy <*interface {}> v12 v20 (6) = IsSliceInBounds v14 v15 v25 (?) = ConstInterface (fmt..autotmp_4[error], fmt.err[error]) v28 (?) = OffPtr <*io.Writer> [0] v2 v29 (?) = Addr <*uint8> {go.itab.*os.File,io.Writer} v3 v30 (?) = Addr <**os.File> {os.Stdout} v3 v34 (?) = OffPtr <*[]interface {}> [16] v2 v37 (?) = OffPtr <*int> [40] v2 v39 (?) = OffPtr <*error> [48] v2 If v20 → b2 b3 (likely) (6) b2: ← b1- v23 (6) = Sub64 v15 v14 v24 (6) = SliceMake <[]interface {}> v19 v23 v23 (fmt.a[[]interface {}]) v26 (6) = Copy v17 v27 (+6) = InlMark [0] v26 v31 (274) = Load <*os.File> v30 v26 v32 (274) = IMake v29 v31 v33 (274) = Store {io.Writer} v28 v32 v26 v35 (274) = Store {[]interface {}} v34 v24 v33 v36 (274) = StaticCall {fmt.Fprintln} [64] v35 v38 (274) = Load v37 v36 (fmt.n[int], fmt..autotmp_3[int]) v40 (274) = Load v39 v36 (fmt.err[error], fmt..autotmp_4[error]) Plain → b4 (+6) b3: ← b1- v21 (6) = Copy v17 v22 (6) = PanicBounds [6] v14 v15 v21 Exit v22 (6) b4: ← b2- v41 (7) = Copy v36 Ret v41 name ~arg0[interface {}]: v6 name fmt.a[[]interface {}]: v24 name fmt.n[int]: v14 v38 name fmt.err[error]: v25 v40 name fmt..autotmp_3[int]: v14 v38 name fmt..autotmp_4[error]: v25 v40
每個(gè)v是一個(gè)新變量,可以單擊以查看使用它的位置。
b是代碼塊,本例中我們有3個(gè)代碼塊:b1, b2和 b3
b1將始終被執(zhí)行,b2和b3是條件塊,如b1最后一行所示:If v20 → b2 b3 (likely) (6)
,只有v20為true會(huì)執(zhí)行b2,v20為false會(huì)執(zhí)行b3
我們可以點(diǎn)擊v20查看其定義,其定義是v20 (6) = IsSliceInBounds v14 v15
IsSliceInBounds
會(huì)執(zhí)行如下檢查:0 <= v14 <= v15 是否成立
我們可以單擊v14和v15來查看它們的定義:v14 = Const64 [0]
,v15 = Const64 [1]
Const64為64位常量,因此 0 <= 0 <= 1 始終成立,因此v20始終成立
當(dāng)我們在opt片段查看v20時(shí),會(huì)發(fā)現(xiàn)v20 (6) = ConstBool [true]
,v20變?yōu)榱耸冀K為true
因此,我們會(huì)看到在opt deadcode片段中,b3塊被刪除了
生成SSA之后,Go編譯器還會(huì)進(jìn)行一系列簡單的優(yōu)化,例如無效和無用代碼的刪除
我們將用同樣的ssa.html文件,比較lower 和 lowered deadcode片段
在HTML文件中,某些行顯示為灰色,這意味著它們將在下一階段之一中被刪除或更改
例如v15 = MOVQconst [1]
為灰色,因?yàn)槠湓诤竺娓緵]有被使用。MOVQconst與我們之前看到的指令Const64相同,僅適用于amd64平臺(tái)
完成以上步驟,最終還會(huì)生成跨平臺(tái)的plan9匯編指令,并進(jìn)一步根據(jù)目標(biāo)的 CPU 架構(gòu)生成二進(jìn)制機(jī)器代碼
Go語言源代碼的 cmd/compile/internal 目錄中包含了非常多機(jī)器碼生成相關(guān)的包
不同類型的 CPU 分別使用了不同的包進(jìn)行生成 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm
Go語言能夠在幾乎全部常見的 CPU 指令集類型上運(yùn)行。
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)cdcxhl.cn,海內(nèi)外云服務(wù)器15元起步,三天無理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務(wù)可用性高、性價(jià)比高”等特點(diǎn)與優(yōu)勢,專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場景需求。