您好,登錄后才能下訂單哦!
package main import "fmt" func main() { fmt.Println("Hello, world") }
在本文中,我們將介紹初學者比較關心的話題:go語言如何編譯為機器碼
本文的目標是希望讀者對go語言的編譯過程有一個全面的理解
一段程序要運行起來,需要將go代碼生成機器能夠識別的二進制代碼
go代碼生成機器碼需要編譯器經歷:
詞法分析 => 語法分析 => 類型檢查 => 中間代碼 => 代碼優化 => 生成機器碼
Go語言的編譯器入口是 src/cmd/compile/internal/gc
包中的 main.go
文件,此函數會先獲取命令行傳入的參數并更新編譯的選項和配置
隨后就會開始運行 parseFiles 函數對輸入的所有文件進行詞法與語法分析
func Main(archInit func(*Arch)) { // ... lines := parseFiles(flag.Args())
接下來我們將對各個階段做深入介紹
所有的編譯過程都是從解析代碼的源文件開始的
詞法分析的作用就是解析源代碼文件,它將文件中的字符串序列轉換成Token
序列,方便后面的處理和解析
我們一般會把執行詞法分析的程序稱為詞法解析器(lexer)
Token
可以是關鍵字,字符串,變量名,函數名
有效程序的"單詞"都由Token
表示,具體來說,這意味著"package","main","func" 等單詞都為Token
Go語言允許我們使用go/scanner和go/token包在Go程序中執行解析程序,從而可以看到類似被編譯器解析后的結構
如果在語法解析的過程中發生了任何語法錯誤,都會被語法解析器發現并將消息打印到標準輸出上,整個編譯過程也會隨著錯誤的出現而被中止
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 序列,這些序列會按照順序被語法分析器進行解析,語法的解析過程就是將詞法分析生成的 Token 按照語言定義好的文法(Grammar)自下而上或者自上而下的進行規約,每一個 Go 的源代碼文件最終會被歸納成一個 SourceFile 結構:
SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" }
標準的 Golang 語法解析器使用的就是 LALR(1) 的文法,語法解析的結果生成了抽象語法樹(Abstract Syntax Tree,AST)
抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每個節點都表示源代碼中的一種結構。
之所以說語法是“抽象”的,是因為這里的語法并不會表示出真實語法中出現的每個細節。比如,嵌套括號被隱含在樹的結構中,并沒有以節點的形式呈現;而類似于 if-condition-then 這樣的條件跳轉語句,可以使用帶有三個分支的節點來表示。
與AST相對應的是CST(Concrete Syntax Trees),讀者可以在參考資料中拓展閱讀二者的差別
在AST中,我們能夠看到程序結構,例如函數和常量聲明
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,常量,變量和函數
為了進一步理解,我們看一下對其的圖形化抽象表示
紅色表示與節點相對應的代碼
main函數包含了3個部分,名稱, 聲明, 主體
名稱是單詞main的標識
由Type字段指定的聲明將包含參數列表和返回類型
主體由一系列語句組成,其中包含程序的所有行。在本例中,只有一行
fmt.Println語句由AST中的很多部分組成,由ExprStmt
聲明。
ExprStmt
代表一個表達式,其可以是本例中的函數調用,也可以是二進制運算(例如加法和減法等)
我們的ExprStmt
包含一個CallExpr
,這是我們的實際函數調用。這又包括幾個部分,其中最重要的是Fun
和Args
Fun包含對函數調用的引用,由SelectorExpr
聲明。在AST中,編譯器尚未知道fmt是一個程序包,它也可能是AST中的變量
Args
包含一個表達式列表,這些表達式是該函數的參數。在本例中,我們已將文字字符串傳遞給函數,因此它由類型為STRING
的BasicLit
表示。
構造AST之后,將會對所有import的包進行解析
接著Go語言的編譯器會對語法樹中定義和使用的類型進行檢查,類型檢查分別會按照順序對不同類型的節點進行驗證,按照以下的順序進行處理:
常量、類型和函數名及類型
變量的賦值和初始化
函數和閉包的主體
哈希鍵值對的類型
導入函數體
外部的聲明
通過對每一棵抽象節點樹的遍歷,我們在每一個節點上都會對當前子樹的類型進行驗證保證當前節點上不會出現類型錯誤的問題,所有的類型錯誤和不匹配都會在這一個階段被發現和暴露出來。
類型檢查的階段不止會對樹狀結構的節點進行驗證,同時也會對一些內建的函數進行展開和改寫,例如 make 關鍵字在這個階段會根據子樹的結構被替換成 makeslice 或者 makechan 等函數。
類型檢查不止對類型進行了驗證工作,還對 AST 進行了改寫以及處理Go語言內置的關鍵字
在上面的步驟完成之后,可以明確代碼是正確有效的
接著將AST轉換為程序的低級表示形式,即靜態單一賦值形式(Static Single Assignment Form,SSA)形式,核心代碼位于gc/ssa.go
SSA不是程序的最終狀態,其可以更輕松地應用優化,其中最重要的是始終在使用變量之前定義變量,并且每個變量只分配一次
例如下面的代碼我們可以看到第一個x的賦值沒有必要的
x = 1 x = 2 y = 7
編輯器會將上面的代碼變為如下,從而會刪除x_1
x_1 = 1 x_2 = 2 y_1 = 7
生成SSA的初始版本后,將應用許多優化過程。這些優化應用于某些代碼段,這些代碼段可以使處理器執行起來更簡單或更快速。
例如下面的代碼是永遠不會執行的,因此可以被消除。
if (false) { fmt.Println(“test”) }
優化的另一個示例是可以刪除某些nil檢查,因為編譯器可以證明這些檢查永遠不會出錯
在對SSA進行優化的過程中使用了S表達式(S-expressions)進行描述, S-expressions 是嵌套列表(樹形結構)數據的一種表示法,由編程語言Lisp發明并普及
SSA優化過程中對于S表達式的應用如下所示,將8位的常量乘法組合起來
(Mul8 (Const8 [c]) (Const8 [d])) -> (Const8 [int64(int8(c*d))])
具體的優化包括
常數傳播(constant propagation)
值域傳播(value range propagation)
稀疏有條件的常數傳播(sparse conditional constant propagation)
消除無用的程式碼(dead code elimination)
全域數值編號(global value numbering)
消除部分的冗余(partial redundancy elimination)
強度折減(strength reduction)
寄存器分配(register allocation)
我們可以用下面的簡單代碼來查看SSA及其優化過程
對于如下程序
package main import "fmt" func main() { fmt.Println(2) }
我們需要在命令行運行如下指令來查看SSA
GOSSAFUNC環境變量代表我們需要查看SSA的函數并創建ssa.html文件
GOOS、GOARCH代表編譯為在Linux 64-bit平臺運行的代碼
go build用-ldflags給go編譯器傳入參數
-S 標識將打印匯編代碼
$ GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags "-S" simple.go
下面的命令等價
GOSSAFUNC=main GOOS=linux GOARCH=amd64 go tool compile main.go
當打開ssa.html時,將顯示許多代碼片段,其中一些片段是隱藏的。
Start片段是從AST生成的SSA。genssa片段是最終生成的Plan9匯編代碼
Start片段如下
start b1:- v1 (?) = InitMem <mem> v2 (?) = SP <uintptr> v3 (?) = SB <uintptr> v4 (?) = Addr <*uint8> {type.int} v3 v5 (?) = Addr <*int> {""..stmp_0} v3 v6 (6) = IMake <interface {}> v4 v5 (~arg0[interface {}]) v7 (?) = ConstInterface <interface {}> v8 (?) = ArrayMake1 <[1]interface {}> v7 v9 (6) = VarDef <mem> {.autotmp_11} v1 v10 (6) = LocalAddr <*[1]interface {}> {.autotmp_11} v2 v9 v11 (6) = Store <mem> {[1]interface {}} v10 v8 v9 v12 (6) = LocalAddr <*[1]interface {}> {.autotmp_11} v2 v11 v13 (6) = NilCheck <void> v12 v11 v14 (?) = Const64 <int> [0] (fmt..autotmp_3[int], fmt.n[int]) v15 (?) = Const64 <int> [1] v16 (6) = PtrIndex <*interface {}> v12 v14 v17 (6) = Store <mem> {interface {}} v16 v6 v11 v18 (6) = NilCheck <void> v12 v17 v19 (6) = Copy <*interface {}> v12 v20 (6) = IsSliceInBounds <bool> v14 v15 v25 (?) = ConstInterface <error> (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 <int> v15 v14 v24 (6) = SliceMake <[]interface {}> v19 v23 v23 (fmt.a[[]interface {}]) v26 (6) = Copy <mem> v17 v27 (+6) = InlMark <void> [0] v26 v31 (274) = Load <*os.File> v30 v26 v32 (274) = IMake <io.Writer> v29 v31 v33 (274) = Store <mem> {io.Writer} v28 v32 v26 v35 (274) = Store <mem> {[]interface {}} v34 v24 v33 v36 (274) = StaticCall <mem> {fmt.Fprintln} [64] v35 v38 (274) = Load <int> v37 v36 (fmt.n[int], fmt..autotmp_3[int]) v40 (274) = Load <error> v39 v36 (fmt.err[error], fmt..autotmp_4[error]) Plain → b4 (+6) b3: ← b1- v21 (6) = Copy <mem> v17 v22 (6) = PanicBounds <mem> [6] v14 v15 v21 Exit v22 (6) b4: ← b2- v41 (7) = Copy <mem> 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
每個v是一個新變量,可以單擊以查看使用它的位置。
b是代碼塊,本例中我們有3個代碼塊:b1, b2和 b3
b1將始終被執行,b2和b3是條件塊,如b1最后一行所示:If v20 → b2 b3 (likely) (6)
,只有v20為true會執行b2,v20為false會執行b3
我們可以點擊v20查看其定義,其定義是v20 (6) = IsSliceInBounds v14 v15
IsSliceInBounds
會執行如下檢查:0 <= v14 <= v15 是否成立
我們可以單擊v14和v15來查看它們的定義:v14 = Const64 [0]
,v15 = Const64 [1]
Const64為64位常量,因此 0 <= 0 <= 1 始終成立,因此v20始終成立
當我們在opt片段查看v20時,會發現v20 (6) = ConstBool [true]
,v20變為了始終為true
因此,我們會看到在opt deadcode片段中,b3塊被刪除了
生成SSA之后,Go編譯器還會進行一系列簡單的優化,例如無效和無用代碼的刪除
我們將用同樣的ssa.html文件,比較lower 和 lowered deadcode片段
在HTML文件中,某些行顯示為灰色,這意味著它們將在下一階段之一中被刪除或更改
例如v15 = MOVQconst [1]
為灰色,因為其在后面根本沒有被使用。MOVQconst與我們之前看到的指令Const64相同,僅適用于amd64平臺
完成以上步驟,最終還會生成跨平臺的plan9匯編指令,并進一步根據目標的 CPU 架構生成二進制機器代碼
Go語言源代碼的 cmd/compile/internal 目錄中包含了非常多機器碼生成相關的包
不同類型的 CPU 分別使用了不同的包進行生成 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm
Go語言能夠在幾乎全部常見的 CPU 指令集類型上運行。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。