- https://github.com/chai2010/go-ast-book?hmsr=codercto.com&utm_medium=codercto.com&utm_source=codercto.com
- https://www.sohu.com/a/293962794_99930294
- https://www.kancloud.cn/cfun_good/golang/2033481
AST全稱Abstract Syntax Tree抽象語法樹宛渐,即以樹狀形式表現(xiàn)變成語言的語法結(jié)構(gòu),樹上每個節(jié)點都表示源代碼中一種結(jié)構(gòu)。之所以說語法是抽象的垒棋,是因為這里的語法并不會表示出真實語法中出現(xiàn)的每個細(xì)節(jié)采呐。
Golang語法樹是Go源代碼的另一種語義等價的表現(xiàn)形式外厂,Go自帶的go fmt
蛤售、go doc
等命令都是在Go語法樹基礎(chǔ)之上的分析工具,將Go語言程序作為輸入數(shù)據(jù)瓜贾,從語法樹的維度重新審視Go語言程序。
$ go run
Golang的go run
命令會完成源代碼從編譯到執(zhí)行的過程吴叶,簡單來說可將go run
等價于go build
+ 執(zhí)行阐虚。
$ go build
參數(shù) | 描述 |
---|---|
go build -n | 不執(zhí)行地打印流程中用的命令 |
go build -x | 執(zhí)行并打印流程中使用到的命令 |
go build -work | 打印編譯時的臨時目錄路徑,結(jié)束時保留蚌卤,默認(rèn)編譯結(jié)束會刪除实束。 |
Golang中go build
主要完成源碼的編譯和可執(zhí)行文件的生成。go build
接收參數(shù)為.go
文件或目錄逊彭,默認(rèn)情況下編譯當(dāng)前目錄下所有的.go
文件咸灿。在main
包下執(zhí)行會生成相應(yīng)的可執(zhí)行文件,在非main
包下會做一些檢查侮叮,生成的庫文件放在緩存目錄下避矢,在工作目錄下并無新文件生成。
編譯過程
Golang是需要編譯才能執(zhí)行的語言囊榜,代碼在運行前需要通過編譯器生成二進(jìn)制機(jī)器碼审胸,隨后二進(jìn)制機(jī)器碼才能在目標(biāo)機(jī)器上運行。
Golang編碼程序首先經(jīng)過編譯器生成plan9
匯編卸勺,再由匯編器和鏈接處理得到最終的可執(zhí)行程序砂沛。
Golang編譯器的源代碼位于cmd/compile
目錄下,目錄下的文件共同構(gòu)成了Golang的編譯器曙求,編譯器分為前端和后端碍庵,前端承擔(dān)著詞法分析映企、語法分析、類型檢查静浴、中間代碼生成的工作堰氓,后端主要負(fù)責(zé)目標(biāo)代碼的生成和優(yōu)化,就是將中間代碼翻譯成機(jī)器能夠運行的機(jī)器碼苹享。
源代碼從編譯到執(zhí)行的過程會經(jīng)過:源碼->編譯->可執(zhí)行文件->執(zhí)行輸出
Golang的編譯器對Golang源代碼的處理在邏輯上可分為四個階段
1.詞法語法分析
2.類型檢查和AST轉(zhuǎn)換
3.SSA優(yōu)化和降級轉(zhuǎn)換
4.Go源碼生成對應(yīng)的plan9
匯編
Golang程序的編譯入口是compile/internal/gc/main.go
文件的Main
函數(shù)双絮,Main
函數(shù)獲取命令行參數(shù)并更新編譯選項和配置,然后運行parseFiles
函數(shù)對輸入的所有文件進(jìn)行詞法和語法分析富稻,得到對應(yīng)的AST抽象語法樹掷邦。
第一階段:詞法和語法分析
編譯過程是從解析代碼的源文件開始,詞法分析的作用是解析源代碼文件椭赋,將文件中的字符串序列轉(zhuǎn)換為Token序列抚岗,以方便后續(xù)的處理和解析,一般會把執(zhí)行詞法分析的程序稱為詞法解析器(lexer)哪怔。
順序 | 階段 | 工具 | 描述 |
---|---|---|---|
1 | 詞法分析 | 詞法分析器 | 源代碼被token 化 |
2 | 語法分析 | 解析器 | 解析 |
3 | 生成語法樹 | 抽象語法樹 | 為每個源構(gòu)造語法樹文件 |
1.詞法分析
- 詞法分析是將字符串轉(zhuǎn)換為標(biāo)記(Token)序列的過程
詞法分析的輸入是詞法分析器輸出的Token序列宣蔚,Token序列會按順序被語法分析器進(jìn)行解析,語法的解析過程是將詞法分析生成的Token按照語言定義好的文法(Grammar)自下而上或自上而下的進(jìn)行規(guī)約认境,每個Go的源代碼文件最終會被歸納成一個SourceFile結(jié)構(gòu)胚委。
1SourceFile = PackageClause ";" {ImportDecl";"} {TopLevelDecl";"}
源代碼被詞法分析器Token
化后進(jìn)行詞法分析,解析器解析后進(jìn)行語法分析叉信,最后為每個源構(gòu)造語法樹文件亩冬。每個語法樹都由與之對應(yīng)的源文件上元素的節(jié)點,比如表達(dá)式硼身、聲明硅急、陳述。
"json.go":SourceFile {
PackageName:"json",
ImportDecl: []Import{"io"},
TopLevelDecl:...
}
Golang中compile/internal/syntax/tokens.go
文件定義了Golang支持的全部token
類型佳遂,比如名稱和文本营袜、操作符、定界符丑罪、關(guān)鍵字等荚板。詞法分析會將文本中的字符序列轉(zhuǎn)換為標(biāo)記序列,比如將關(guān)鍵字package
轉(zhuǎn)換為_package
標(biāo)記吩屹,fun
轉(zhuǎn)換為_fun
標(biāo)記等跪另。
Golang的詞法解析是通過src/cmd/compile/internal/syntax/scanner.go
文件中的syntax.scanner
結(jié)構(gòu)體實現(xiàn)的,由syntax.scanner.next
方法驅(qū)動煤搜。
src/cmd/compile/internal/syntax/scanner.go
文件實現(xiàn)了詞法解析器免绿,使用scanner
結(jié)構(gòu)的next
方法實現(xiàn).go
文件的掃描并轉(zhuǎn)換為token
序列。next
方法獲取文件中未被解析的字符宅楞,進(jìn)入switch/case
分支進(jìn)行詞法解析针姿。
2.語法分析
- 語法分析是根據(jù)某種特定的形式文法(Grammer)對Token序列構(gòu)成的輸入文本進(jìn)行分析并確定其語法結(jié)構(gòu)的過程。
標(biāo)準(zhǔn)的Golang語法分析器使用的是LALR的文法厌衙,語法解析的結(jié)果是抽象語法樹距淫,每個AST都對應(yīng)著一個單獨的Golang文件,這個抽象語法樹包括當(dāng)前文件屬于的包名婶希、定義的常量榕暇、結(jié)構(gòu)體、函數(shù)等喻杈。
語法分析會通過文法分析彤枢,構(gòu)建輸入的token
序列的語法結(jié)構(gòu),得到對應(yīng)的語法樹筒饰。
如果在語法解析過程中發(fā)生了任何語法錯誤缴啡,都會被語法解析器發(fā)現(xiàn)并將消息打印到標(biāo)準(zhǔn)輸出上,整個編譯過程也會隨著錯誤的出現(xiàn)而被中止瓷们。
語法解析過程會調(diào)用goroutine
业栅,語法解析器位于cmd/compile/internal/syntax.parser
,其中syntax.parser.fileOrNil
為一個核心解析方法谬晕。解析的產(chǎn)物是文件對應(yīng)的抽象語法樹碘裕。
3.抽象語法樹
抽象語法樹(AST)是源代碼語法結(jié)構(gòu)一種抽象的表示,抽象語法樹使用樹狀的方式表示編程語言中的語法結(jié)構(gòu)攒钳。抽象語法樹中每個節(jié)點表示源代碼的一個元素帮孔,每個子樹表示一個語法元素。
作為編譯器中常用的數(shù)據(jù)結(jié)構(gòu)不撑,抽象語法樹會抹去源代碼中不重要的字符文兢,比如空格、分號燎孟、括號等禽作。編譯器在執(zhí)行完語法分析后會輸出一個抽象語法樹,抽象語法樹會輔助編譯器進(jìn)行語義分析揩页,以此來確定結(jié)構(gòu)正確的程序是否存在類型不匹配或不一致的問題旷偿。
第二階段:語義分析
語義分析主要包括對抽象語法樹(AST)進(jìn)行類型檢查和變換
語義分析過程中重要的操作:逃逸分析、變量捕獲爆侣、函數(shù)內(nèi)聯(lián)萍程、閉包處理
1. 類型檢查
- 通過名稱解析和類型推斷以確定對象所屬的標(biāo)識符,以及每個表達(dá)式具有的類型兔仰。
- 類型檢查包括某些額外的檢查茫负,比如“聲明和未使用”以及確定函數(shù)是否終止。
類型檢查
當(dāng)拿到一組文件的抽象語法樹之后乎赴,Golang的編譯器會對語法樹中定義和使用的類型進(jìn)行檢查忍法,類型檢查分別會按照順序?qū)Σ煌愋偷墓?jié)點進(jìn)行驗證潮尝,會按照如下順序進(jìn)行處理:
- 常量、類型饿序、函數(shù)名及類型
- 變量的賦值和初始化
- 函數(shù)和閉包的主體
- 哈希鍵值對的類型
- 導(dǎo)入函數(shù)體
- 外部的聲明
通過對每顆抽象結(jié)點樹的遍歷勉失,會在每個節(jié)點上都會對當(dāng)前子樹的類型進(jìn)行驗證保證當(dāng)前節(jié)點上不會出現(xiàn)類型錯誤的問題,所有的類型錯誤和不匹配都會在此階段被發(fā)現(xiàn)和暴露出來原探。
類型檢查不止會對樹狀結(jié)構(gòu)的節(jié)點進(jìn)行驗證乱凿,同時也會對一些內(nèi)建的函數(shù)進(jìn)行展開和改寫。比如make
關(guān)鍵字會在此階段會根據(jù)子樹的結(jié)構(gòu)被替換成為makeslice
或makechan
等函數(shù)咽弦。
2. 變換
- 在抽象語法樹上進(jìn)行某些轉(zhuǎn)換徒蟆,某些節(jié)點基于類型信息會被細(xì)化。比如:從算術(shù)加法節(jié)點類型分割的字符串添加型型、死代碼消除段审、函數(shù)調(diào)用內(nèi)聯(lián)、轉(zhuǎn)義分析...
第三階段:SSA生成(中間代碼生成)
中間代碼是指一種應(yīng)用于抽象機(jī)器的編程語言输莺,其設(shè)計目的是用來幫助我們分析計算機(jī)程序戚哎。在編譯過程中,編譯器會將源代碼轉(zhuǎn)換成目標(biāo)機(jī)器上機(jī)器的過程中嫂用,先把原地阿瑪轉(zhuǎn)換成一種中間的表述形式型凳。
當(dāng)將源文件轉(zhuǎn)換成抽象語法樹、對整顆樹的語法進(jìn)行解析并進(jìn)行類型檢查之后嘱函,可以認(rèn)為當(dāng)前文件中的代碼基本上不存在無法編譯或語法錯誤的問題甘畅,Golang的編譯器會將輸入的AST轉(zhuǎn)換成為中間代碼。
- Golang編譯器的中間代碼具有靜態(tài)單賦值(SSA)的特性
Golang編譯器的中間代碼使用了SSA(Static Single Assignment Form往弓,靜態(tài)單一分配)的特性疏唾,如果在中間代碼生成的過程中使用此特性,就能夠很容易的分析出代碼中無用的變量和片段并對代碼進(jìn)行優(yōu)化函似。
類型檢查之后會通過名為compileFunctions
的函數(shù)開始對整個Golang中的全部函數(shù)進(jìn)行編譯槐脏,這些函數(shù)會在一個編譯隊列中等待幾個后端工作goroutine
的消費,這些goroutine
會將所有函數(shù)對應(yīng)的AST轉(zhuǎn)換為使用SSA特性的中間代碼撇寞。
抽象語法樹(AST)將轉(zhuǎn)換為靜態(tài)單一分配(SSA)形式顿天,SSA是一種具有特定屬性的低級中間表示,可以更加輕松地實現(xiàn)優(yōu)化并最終從中生成機(jī)器代碼蔑担。
在此轉(zhuǎn)換期間會將應(yīng)用函數(shù)內(nèi)在函數(shù)牌废,對于這些特殊功能,編譯器會教導(dǎo)它們根據(jù)具體情況使用大量優(yōu)化的代碼替代啤握。
在AST到SSA轉(zhuǎn)換期間鸟缕,某些節(jié)點會被降級為更為簡單的組件,因此編譯器的其余部分可使用它們。例如內(nèi)置復(fù)制替代為內(nèi)存移動懂从,并且范圍循環(huán)被重寫為for
循環(huán)授段。
然后,應(yīng)用一系列與機(jī)器無關(guān)的傳遞和規(guī)則番甩。這些不會涉及任何單個計算機(jī)體系結(jié)構(gòu)畴蒲,因此看在所有GOARCH變體上運行。
這些通用過程的示例包括消除死代碼对室,刪除不需要的零檢查、刪除未使用的分支咖祭。
通過重寫規(guī)則主要涉及表達(dá)式掩宜,比如使用常量值替換某些表達(dá)式,以及優(yōu)化乘法和浮點運算么翰。
第四階段:機(jī)器碼生成
- 底層SSA和結(jié)構(gòu)特定的傳遞
- 生成機(jī)器碼
Golang源代碼的cmd/compile/internal
中包含了許多機(jī)器代生成相關(guān)的包牺汤,不同類型的CPU分別使用不同的包進(jìn)行生成amd64
、arm
浩嫌、arm64
檐迟、mips
、mips64
码耐、ppc64
追迟、s390x
、x86
骚腥、wasm
敦间。Golang能夠在上述的CPU指令集類型上運行。
作為一種在棧虛擬機(jī)上使用的二進(jìn)制指令格式束铭,它的設(shè)計目標(biāo)是在Web瀏覽器上提供一種具有高可移植性的目標(biāo)語言廓块。Golang編譯器既能生成WASM格式的指令,就能運行在常見的主流瀏覽器中契沫。
1$ GOARCH=wasm GOOS=js gobuild -o lib.wasm main.go
編譯器的機(jī)器相關(guān)階段以底層傳遞開始带猴,該傳遞將通用值重寫為其機(jī)器特定的變體。例如在AMD64存儲器操作數(shù)上是可能的懈万,因此可以組合許多加載存儲操作拴清。
需要注意的是,較低的通道運行所有特定于機(jī)器的重寫規(guī)則钞速,因此它當(dāng)前也應(yīng)用了大量優(yōu)化贷掖。
一旦SSA降低且更加特定于目標(biāo)體系結(jié)構(gòu),就會運行最終的代碼優(yōu)化過程渴语。這包括另一個四代碼消除傳遞苹威,移動值更接近它們的使用,刪除從未讀取的局部變量驾凶,以及寄存器分配牙甫。
在SSA生成節(jié)點結(jié)束時掷酗,Go函數(shù)已轉(zhuǎn)換為一些列obj.Prog
指令。它們會被傳遞給裝載器cmd/internal/obj
窟哺,將它們轉(zhuǎn)換為機(jī)器代碼并寫出最終的目標(biāo)文件泻轰。目標(biāo)文件還將包含反射數(shù)據(jù),導(dǎo)出數(shù)據(jù)和調(diào)試信息且轨。