前言
我們知道 Go 語言的三位領(lǐng)導(dǎo)者中有兩位來自 Plan 9 項目,這直接導(dǎo)致了 Go 語言的匯編采用了比較有個性的 Plan 9 風(fēng)格。不過,我們不能因咽廢食而放棄無所不能的匯編。
1、 Go 匯編基礎(chǔ)知識
1.1、通用寄存器
不同體系結(jié)構(gòu)的 CPU,其內(nèi)部寄存器的數(shù)量、種類以及名稱可能大不相同,這里我們只介紹 AMD64 的寄存器。AMD64 有 20 多個可以直接在匯編代碼中使用的寄存器,其中有幾個寄存器在操作系統(tǒng)代碼中才會見到,而應(yīng)用層代碼一般只會用到如下三類寄存器。

AMD64 的通用通用寄存器的名字在 plan9 中的對應(yīng)關(guān)系:
| AMD64 | RAX | RBX | RCX | RDX | RDI | RSI | RBP | RSP | R8 | R9 | R10 | R11 | R12 | R13 | R14 | RIP |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Plan9 | AX | BX | CX | DX | DI | SI | BP | SP | R8 | R9 | R10 | R11 | R12 | R13 | R14 | PC |
Go 語言中寄存器一般用途:

1.2、偽寄存器
偽寄存器是 plan9 偽匯編中的一個助記符, 也是 Plan9 比較有個性的語法之一。常見偽寄存器如下表所示:

SB:指向全局符號表。相對于寄存器,SB 更像是一個聲明標(biāo)識,用于標(biāo)識全局變量、函數(shù)等。通過 symbol(SB) 方式使用,symbol<>(SB)表示 symbol 只在當(dāng)前文件可見,跟 C 中的 static 效果類似。此外可以在引用上加偏移量,如 symbol+4(SB) 表示 symbol+4bytes 的地址。
PC:程序計數(shù)器(Program Counter),指向下一條要執(zhí)行的指令的地址,在 AMD64 對應(yīng) rip 寄存器。個人覺得,把他歸為偽寄存器有點令人費解,可能是因為每個平臺對應(yīng)的物理寄存器名字不一樣。
SP:SP 寄存器比較特殊,既可以當(dāng)做物理寄存器也可以當(dāng)做偽寄存器使用,不過這兩種用法的使用語法不同。其中,偽寄存器使用語法是 symbol+offset(SP),此場景下 SP 指向局部變量的起始位置(高地址處);x-8(SP) 表示函數(shù)的第一個本地變量;物理 SP(硬件SP) 的使用語法則是 +offset(SP),此場景下 SP 指向真實棧頂?shù)刂罚畹偷刂诽帲?/p>
FP:用于標(biāo)識函數(shù)參數(shù)、返回值。被調(diào)用者(callee)的 FP 實際上是調(diào)用者(caller)的棧頂,即 callee.SP(物理SP) == caller.FP;x+0(FP) 表示第一個請求參數(shù)(參數(shù)返回值從右到左入棧)。
實際上,生成真正可執(zhí)行代碼時,偽 SP、FP 會由物理 SP 寄存器加上偏移量替換。所以執(zhí)行過程中修改物理 SP,會引起偽 SP、FP 同步變化,比如執(zhí)行 SUBQ $16, SP 指令后,偽 SP 和偽 FP 都會 -16。而且,反匯編二進制而生成的匯編代碼中,只有物理 SP 寄存器。即 go tool objdump/go tool compile -S 輸出的匯編代碼中,沒有偽 SP 和 偽 FP 寄存器,只有物理 SP 寄存器。
另外還有 1 個比較特殊的偽寄存器:TLS:存儲當(dāng)前 goroutine 的 g 結(jié)構(gòu)體的指針。實際上,X86 和 AMD64 下的 TLS 是通過段寄存器 FS 或 GS 實現(xiàn)的線程本地存儲基地址,而當(dāng)前 g 的指針是線程本地存儲的第一個變量。
比如 github.com/petermattis/goid.Get 函數(shù)的匯編實現(xiàn)如下:
//funcGet()int64
TEXT·Get(SB),NOSPLIT,$0-8
MOVQ(TLS),R14
MOVQg_goid(R14),R13
MOVQR13,ret+0(FP)
RET
編譯成二進制之后,再通過 go tool objdump 反編譯成匯編(Go 1.18),得到如下代碼:
TEXTgithub.com/petermattis/goid.Get.abi0(SB)/Users/bytedance/go/pkg/mod/github.com/petermattis/goid@v0.0.0-20221215004737-a150e88a970d/goid_go1.5_amd64.s
goid_go1.5_amd64.s:280x108adc0654c8b342530000000MOVQGS:0x30,R14
goid_go1.5_amd64.s:290x108adc94d8bae98000000MOVQ0x98(R14),R13
goid_go1.5_amd64.s:300x108add04c896c2408MOVQR13,0x8(SP)
goid_go1.5_amd64.s:310x108add5c3RET
可以知道 MOVQ (TLS), R14 指令最終編譯成了 MOVQ GS:0x30, R14 ,使用了 GS 段寄存器實現(xiàn)相關(guān)功能。
操作系統(tǒng)對內(nèi)存的一般劃分如下圖所示:
高地址+------------------+
||
|內(nèi)核空間|
||
--------------------
||
|棧|
||
--------------------
||
|.......|
||
--------------------
||
|堆|
||
--------------------
|全局數(shù)據(jù)|
|------------------|
||
|靜態(tài)代碼|
||
|------------------|
|系統(tǒng)保留|
低地址|------------------|
這里提個疑問,我們知道協(xié)程分為有棧協(xié)程和無棧協(xié)程,go 語言是有棧協(xié)程。那你知道普通 gorutine 的調(diào)用棧是在哪個內(nèi)存區(qū)嗎?
1.3、函數(shù)調(diào)用棧幀
我們先熟悉幾個名詞。
caller:函數(shù)調(diào)用者。callee:函數(shù)被調(diào)用者。比如函數(shù) main 中調(diào)用 sum 函數(shù),那么 main 就是 caller,而 sum 函數(shù)就是 callee。棧幀:stack frame,即執(zhí)行中的函數(shù)所持有的、獨立連續(xù)的棧區(qū)段。一般用來保存函數(shù)參數(shù)、返回值、局部變量、返回 PC 值等信息。golang 的 ABI 規(guī)定,由 caller 管理函數(shù)參數(shù)和返回值。
下圖是 golang 的調(diào)用棧,源于曹春暉老師的 github 文章《匯編 is so easy》 ,做了簡單修改:
caller
+------------------+
||
+---------------------->+------------------+
|||
||callerparentBP|
|BP(pseudoSP)+------------------+
|||
||LocalVar0|
|+------------------+
|||
||.......|
|+------------------+
|||
||LocalVarN|
+------------------+
callerstackframe||
|calleearg2|
|+------------------+
|||
||calleearg1|
|+------------------+
|||
||calleearg0|
|SP(RealRegister)->+------------------+--------------------------+FP(virtualregister)
||||
||returnaddr|parentreturnaddress|
+---------------------->+------------------+--------------------------+<-----------------------+?????????
????????????????????????????????????????????????????|??caller?BP???????????????|????????????????????????????|?????????
????????????????????????????????????????????????????|??(caller?frame?pointer)??|????????????????????????????|?????????
?????????????????????????????????????BP(pseudo?SP)??+--------------------------+????????????????????????????|?????????
????????????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
????????????????????????????????????????????????????|?????Local?Var0???????????|????????????????????????????|?????????
????????????????????????????????????????????????????+--------------------------+????????????????????????????|?????????
????????????????????????????????????????????????????|??????????????????????????|??????????????????????????????????????
????????????????????????????????????????????????????|?????Local?Var1???????????|??????????????????????????????????????
????????????????????????????????????????????????????+--------------------------+????????????????????callee?stack?frame
????????????????????????????????????????????????????|??????????????????????????|??????????????????????????????????????
????????????????????????????????????????????????????|???????.....??????????????|??????????????????????????????????????
????????????????????????????????????????????????????+--------------------------+????????????????????????????|?????????
????????????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
????????????????????????????????????????????????????|?????Local?VarN???????????|????????????????????????????|?????????
?????High?????????????????????????SP(Real?Register)?+--------------------------+????????????????????????????|?????????
??????^?????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
??????|?????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
??????|?????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
??????|?????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
??????|?????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
??????|?????????????????????????????????????????????+--------------------------+????<-----------------------+?????????
?????Low??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
??????????????????????????????????????????????????????????????callee
需要指出的是,上圖中的 CALLER BP 是在編譯期由編譯器在符合條件時自動插入。所以手寫匯編時,計算 framesize 時不應(yīng)包括 CALLER BP 的空間。是否插入 CALLER BP 的主要判斷依據(jù)如下:
//Mustagreewithinternal/buildcfg.FramePointerEnabled.
constframepointer_enabled=GOARCH=="amd64"||GOARCH=="arm64"
以下是 Go 語言函數(shù)棧展開邏輯的一段代碼,它側(cè)面驗證了 BP 插入的條件:
- 函數(shù)的棧幀大小大于 0;
- 常量 framepointer_enabled 值為 true。
//Forarchitectureswithframepointers,ifthere's
//aframe,thenthere'sasavedframepointerhere.
//
//NOTE:Thiscodeisnotasgeneralasitlooks.
//Onx86,theABIistosavetheframepointerwordatthe
//topofthestackframe,sowehavetobackdownoverit.
//Onarm64,theframepointershouldbeatthebottomof
//thestack(withR29(akaFP)=RSP),inwhichcasewewould
//notwanttodothesubtractionhere.Butwestartedoutwithout
//anyframepointer,andwhenwewantedtoaddit,wedidn't
//wanttobreakalltheassemblydoingdirectwritesto8(RSP)
//tosetthefirstparametertoacalledfunction.
//SowedecidedtowritetheFPlink*below*thestackpointer
//(withR29=RSP-8inGofunctions).
//ThisistechnicallyABI-compatiblebutnotstandard.
//Andithappenstoendupmimickingthex86layout.
//Otherarchitecturesmaymakedifferentdecisions.
ifframe.varp>frame.sp&&framepointer_enabled{
frame.varp-=goarch.PtrSize
}
//Mustagreewithinternal/buildcfg.FramePointerEnabled.
constframepointer_enabled=GOARCH=="amd64"||GOARCH=="arm64"
1.4、golang常用匯編指令
參考文檔:
Go支持的 X86 指令
https://github.com/golang/arch/blob/v0.2.0/x86/x86.csv
Go支持的 ARM64 指令
https://github.com/golang/arch/blob/v0.2.0/arm64/arm64asm/inst.json
Go支持的 ARM 指令
https://github.com/golang/arch/blob/v0.2.0/arm/arm.csv
常用指令:

例如
MOVB$1, DI // 1 byte;將 DI 的第一個 Byte 的值設(shè)置為 1
MOVW$0x10,BX//2bytes
MOVD$1,DX//4bytes
MOVQ$-10,AX//8bytes
SUBQ$0x18,SP//對SP做減法,擴棧
ADDQ$0x18,SP//對SP做加法,縮棧
ADDQAX,BX//BX+=AX
SUBQAX,BX//BX-=AX
IMULQAX,BX//BX*=AX
JMPaddr//跳轉(zhuǎn)到地址,地址可為代碼中的地址,不過實際上手寫一般不會出現(xiàn)
JMPlabel//跳轉(zhuǎn)到標(biāo)簽,可以跳轉(zhuǎn)到同一函數(shù)內(nèi)的標(biāo)簽位置
JMP2(PC)//向前轉(zhuǎn)2行
JMP-2(PC)//向后跳轉(zhuǎn)2行
JNZtarget//如果zeroflag被set過,則跳轉(zhuǎn)
常用標(biāo)志位:

1.5 全局變量
參考文檔:《Go語言高級編程》的章節(jié) 3.3 常量和全局變量
https://github.com/chai2010/advanced-go-programming-book/blob/master/ch3-asm/ch3-03-const-and-var.md
1.5.1 使用語法
使用 GLOBL 關(guān)鍵字聲明全局變量,用 DATA 定義指定內(nèi)存的值:
//DATA匯編指令指定對應(yīng)內(nèi)存中的值;width必須是1、2、4、8幾個寬度之一
DATAsymbol+offset(SB)/width,value//symbol+offset偏移量,width寬度,value初始值
//GLOBL指令聲明一個變量對應(yīng)的符號,以及變量對應(yīng)的內(nèi)存大小
GLOBLsymbol(SB),flag,width//名為symbol,內(nèi)存寬度為width,flag可省略
例子:
DATAage+0x00(SB)/4,$18//age=18
GLOBLage(SB),RODATA,$4//聲明全局變量age,占用4Byte內(nèi)存空間
DATApi+0(SB)/8,$3.1415926
GLOBLpi(SB),RODATA,$8
DATAbio<>+0(SB)/8,$"hellowo"//<>表示只在當(dāng)前文件生效
DATAbio<>+8(SB)/8,$"old!!!!"//bio="helloworld!!!!"
GLOBLbio<>(SB),RODATA,$16
其中 flag 的字面量定義在 Go 標(biāo)準庫下 src/runtime/textflag.h 文件中,需要在匯編文件中 #include "textflag.h",其類型有有如下幾個:
| flag | value | 說明 |
|---|---|---|
| NOPROF | 1 | (TEXT項使用) 不優(yōu)化NOPROF標(biāo)記的函數(shù)。這個標(biāo)志已廢棄。(For TEXT items.) Don't profile the marked function. This flag is deprecated. |
| DUPOK | 2 | 在二進制文件中允許一個符號的多個實例。鏈接器會選擇其中之一。It is legal to have multiple instances of this symbol in a single binary. The linker will choose one of the duplicates to use. |
| NOSPLIT | 4 | (TEXT項使用) 不插入檢測棧分裂(擴張)的前導(dǎo)指令代碼(減少開銷,一般用于葉子節(jié)點函數(shù)(函數(shù)內(nèi)部不調(diào)用其他函數(shù)))。程序的棧幀中,如果調(diào)用其他函數(shù)會增加棧幀的大小,必須在棧頂留出可用空間。用來保護程序,例如堆棧拆分代碼本身。(For TEXT items.) Don't insert the preamble to check if the stack must be split. The frame for the routine, plus anything it calls, must fit in the spare space at the top of the stack segment. Used to protect routines such as the stack splitting code itself. |
| RODATA | 8 | (DATA和GLOBAL項使用) 將這個數(shù)據(jù)放在只讀的塊中。(For DATA and GLOBL items.) Put this data in a read-only section. |
| NOPTR | 16 | (用于DATA和GLOBL項目)這個數(shù)據(jù)不包含指針?biāo)跃筒恍枰占鱽頀呙琛?For DATA and GLOBL items.) This data contains no pointers and therefore does not need to be scanned by the garbage collector. |
| WRAPPER | 32 | (TEXT項使用)這是包裝函數(shù) (For TEXT items.) This is a wrapper function and should not count as disabling recover. |
| NEEDCTXT | 64 | (TEXT項使用)此函數(shù)是一個閉包,因此它將使用其傳入的上下文寄存器。(For TEXT items.) This function is a closure so it uses its incoming context register. |
| TLSBSS | 256 | (用于DATA和GLOBL項目)將此數(shù)據(jù)放入線程本地存儲中。Allocate a word of thread local storage and store the offset from the thread local base to the thread local storage in this variable. |
| NOFRAME | 512 | (TEXT項使用)不要插入指令為此函數(shù)分配棧幀。僅在聲明幀大小為0的函數(shù)上有效。(函數(shù)必須是葉子節(jié)點函數(shù),且以0標(biāo)記堆棧函數(shù),沒有保存幀指針(或link寄存器架構(gòu)上的返回地址))TODO(mwhudson):目前僅針對 ppc64x 實現(xiàn)。Do not insert instructions to allocate a stack frame for this function. Only valid on functions that declare a frame size of 0. TODO(mwhudson): only implemented for ppc64x at present. |
| REFLECTMETHOD | 1024 | 函數(shù)可以調(diào)用 reflect.Type.Method 或 reflect.Type.MethodByName。Function can call reflect.Type.Method or reflect.Type.MethodByName. |
| TOPFRAME | 2048 | (TEXT項使用)函數(shù)是調(diào)用堆棧的頂部。?;厮輵?yīng)在此功能處停止。Function is the outermost frame of the call stack. Call stack unwinders should stop at this function. |
| ABIWRAPPER | 4096 | 函數(shù)是一個 ABI 包裝器。Function is an ABI wrapper. |
其中 NOSPLIT 需要特別注意,它表示該函數(shù)運行不會導(dǎo)致棧分裂,用戶也可以使用 //go:nosplit 強制給 go 函數(shù)指定 NOSPLIT 屬性。例如:
//go:nosplit
funcsomeFunc(){
}
匯編中直接給函數(shù)標(biāo)記 NOSPLIT 即可:
//表示someFunc函數(shù)執(zhí)行時最多需要24字節(jié)本地變量和8字節(jié)參數(shù)空間
TEXT·someFunc(SB),NOSPLIT,$24-8
RET
鏈接器認為標(biāo)記為 NOSPLIT 的函數(shù),最多需要使用 StackLimit 字節(jié)空間,所以不需要插入棧分裂(溢出)檢查,函數(shù)調(diào)用損耗更小。不過,使用該標(biāo)志的時候要特別小心,萬一發(fā)生意外容易導(dǎo)致棧溢出錯誤,溢出時會在執(zhí)行期報 nosplit stack overflow 錯。Go 1.18 標(biāo)準庫下 go/src/runtime/HACKING.md 中有如下說明:
nosplitfunctions
MostfunctionsstartwithaprologuethatinspectsthestackpointerandthecurrentG'sstackboundandcallsmorestackifthestackneedstogrow.
Functionscanbemarked//go:nosplit(orNOSPLITinassembly)toindicatethattheyshouldnotgetthisprologue.Thishasseveraluses:
-Functionsthatmustrunontheuserstack,butmustnotcallintostackgrowth,forexamplebecausethiswouldcauseadeadlock,orbecausetheyhaveuntypedwordsonthestack.
-Functionsthatmustnotbepreemptedonentry.
-FunctionsthatmayrunwithoutavalidG.Forexample,functionsthatruninearlyruntimestart-up,orthatmaybeenteredfromCcodesuchascgocallbacksorthesignalhandler.
Splittablefunctionsensurethere'ssomeamountofspaceonthestackfornosplitfunctionstoruninandthelinkerchecksthatanystaticchainofnosplitfunctioncallscannotexceedthisbound.
Anyfunctionwitha//go:nosplitannotationshouldexplainwhyitisnosplitinitsdocumentationcomment.
另外,當(dāng)函數(shù)處于調(diào)用鏈的葉子節(jié)點,且棧幀小于 StackSmall(128)字節(jié)時,則自動標(biāo)記為 NOSPLIT。此邏輯的代碼如下:
//constStackSmall=128
ifctxt.Arch.Family==sys.AMD64&&autoffsettrue
LeafSearch:
forq:=p;q!=nil;q=q.Link{
switchq.As{
caseobj.ACALL:
//Treatcommonruntimecallsthattakenoarguments
//thesameasduffcopyandduffzero.
if!isZeroArgRuntimeCall(q.To.Sym){
leaf=false
breakLeafSearch
}
fallthrough
caseobj.ADUFFCOPY,obj.ADUFFZERO:
ifautoffset>=objabi.StackSmall-8{
leaf=false
breakLeafSearch
}
}
}
ifleaf{
p.From.Sym.Set(obj.AttrNoSplit,true)
}
}
1.5.2 Go 語言中的常用用法
在匯編代碼中使用 go 變量:
#include"textflag.h"
TEXT·get(SB),NOSPLIT,$0-8
MOVQ·a(SB),AX//把go代碼定義的全局變量讀到AX中
MOVQAX,ret+0(FP)//把AX的值寫入返回值位置
RET
packagemain
vara=999
funcget()intfuncmain(){
println(get())
}
go 代碼中使用匯編定義的變量:
// string 定義形式 1:在 String 結(jié)構(gòu)體后多分配一個[n]byte 數(shù)組存放靜態(tài)字符串
DATA·Name+0(SB)/8,$·Name+16(SB)//StringHeader.Data
DATA·Name+8(SB)/8,$6//StringHeader.Len
DATA·Name+16(SB)/8,$"gopher"//[6]byte{'g','o','p','h','e','r'}
GLOBL·Name(SB),NOPTR,$24//struct{Datauintptr,Lenint,str[6]byte}
// string 定義形式 2:獨立分配一個僅當(dāng)前文件可見的[n]byte 數(shù)組存放靜態(tài)字符串
DATAstr<>+0(SB)/8,$"HelloWo"//str[0:8]={'H','e','l','l','o','','W','o'}
DATAstr<>+8(SB)/8,$"rld!"//str[9:12]={'r','l','d','!''}
GLOBLstr<>(SB),NOPTR,$16//定義全局數(shù)組varstr<>[16]byte
DATA·Helloworld+0(SB)/8,$str<>(SB)//StringHeader.Data=&str<>
DATA·Helloworld+8(SB)/8,$12//StringHeader.Len=12
GLOBL·Helloworld(SB),NOPTR,$16//struct{Datauintptr,Lenint}
varName,Helloworldstring
funcdoSth(){
fmt.Printf("Name:%s
",Name)//讀取匯編中初始化的變量Name
fmt.Printf("Helloworld:%s
",Helloworld)//讀取匯編中初始化的變量Helloworld
}
//輸出:
//Name:gopher
//Helloworld:HelloWorld!
1.6 函數(shù)調(diào)用
1.6.1 使用語法
Go 語言匯編中,函數(shù)聲明格式如下:
告訴匯編器該數(shù)據(jù)放到TEXT區(qū)
^靜態(tài)基地址指針(告訴匯編器這是基于靜態(tài)地址的數(shù)據(jù))
|^
||標(biāo)簽函數(shù)入?yún)?返回值占用空間大小
||^^
||||
TEXTpkgname·funcname(SB),TAG,$16-24
^^^^
||||
函數(shù)所屬包名函數(shù)名表示ABI類型函數(shù)棧幀大小(本地變量占用空間大小)

一些說明:
- 棧幀大小包括局部變量和可能需要的額外調(diào)用函數(shù)的參數(shù)空間的總大小,但不不包含調(diào)用其他函數(shù)時的 ret address 的大小。
- 匯編文件中,函數(shù)名以 '·' 開頭或連接 pkgname 是固定格式。
- go 函數(shù)采用的是 caller-save 模式,被調(diào)用者的參數(shù)、返回值、棧位置都由調(diào)用者維護。
go 語言編譯成匯編:
gotoolcompile-Sxxx.go
gobuild-gcflags-Sxxx.go
從二進制反編譯為匯編:
gotoolobjdump-s"main.main"main.out>main.S
1.6.2 使用例子
Go 函數(shù)調(diào)用匯編函數(shù):
//add.go
packagemain
import"fmt"
funcadd(x,yint64)int64
funcmain(){
fmt.Println(add(2,3))
}
//add_amd64.s
//add(x,y)->x+y
TEXT·add(SB),NOSPLIT,$0
MOVQx+0(FP),BX
MOVQy+8(FP),BP
ADDQBP,BX
MOVQBX,ret+16(FP)
RET
匯編調(diào)用 go 語言函數(shù):
packagemain
import"fmt"
funcadd(x,yint)int{
returnx+y
}
funcoutput(a,bint)int
funcmain(){
s:=output(10,13)
fmt.Println(s)
}
#include"textflag.h"
//funcoutput(a,bint)int
TEXT·output(SB),NOSPLIT,$24-24
MOVQa+0(FP),DX//arga
MOVQDX,0(SP)//argx
MOVQb+8(FP),CX//argb
MOVQCX,8(SP)//argy
CALL·add(SB)//在調(diào)用add之前,已經(jīng)把參數(shù)都通過物理寄存器SP搬到了函數(shù)的棧頂
MOVQ16(SP),AX//add函數(shù)會把返回值放在這個位置
MOVQAX,ret+16(FP)//returnresult
RET
1.6.1 匯編函數(shù)中用到的一些特殊命令(偽指令)
GO_RESULTS_INITIALIZED:如果 Go 匯編函數(shù)返回值含指針,則該指針信息必須由 Go 源文件中的函數(shù)的 Go 原型提供,即使對于未直接從 Go 調(diào)用的匯編函數(shù)也是如此。如果返回值將在調(diào)用指令期間保存實時指針,則該函數(shù)中應(yīng)首先將結(jié)果歸零, 然后執(zhí)行偽指令 GO_RESULTS_INITIALIZED。表明該堆棧位置應(yīng)該執(zhí)行進行 GC 掃描,避免其指向的內(nèi)存地址唄 GC 意外回收。
NO_LOCAL_POINTERS: 就是字面意思,表示函數(shù)沒有指針類型的局部變量。
PCDATA: Go 語言生成的匯編,利用此偽指令表明匯編所在的原始 Go 源碼的位置(file&line&func),用于生成 PC 表格。runtime.FuncForPC 函數(shù)就是通過 PC 表格得到結(jié)果的。一般由編譯器自動插入,手動維護并不現(xiàn)實。
FUNCDATA: 和 PCDATA 的格式類似,用于生成 FUNC 表格。FUNC 表格用于記錄函數(shù)的參數(shù)、局部變量的指針信息,GC 依據(jù)它來跟蹤棧中指針指向內(nèi)存的生命周期,同時棧擴縮容的時候也是依據(jù)它來確認是否需要調(diào)整棧指針的值(如果指向的地址在需要擴縮容的棧中,則需要同步修改)。
1.7 條件編譯
Go 語言僅支持有限的條件編譯規(guī)則:
- 根據(jù)文件名編譯。
- 根據(jù) build 注釋編譯。
根據(jù)文件名編譯類似 *_test.go,通過添加平臺后綴區(qū)分,比如: asm_386.s、asm_amd64.s、asm_arm.s、asm_arm64.s、asm_mips64x.s、asm_linux_amd64.s、asm_bsd_arm.s 等.
根據(jù) build 注釋編譯,就是在源碼中加入?yún)^(qū)分平臺和編譯器版本的注釋。比如:
//go:build(darwin||freebsd||netbsd||openbsd)&&gc
//+builddarwinfreebsdnetbsdopenbsd
//+buildgc
Go 1.17 之前,我們可以通過在源碼文件頭部放置 +build 構(gòu)建約束指示符來實現(xiàn)構(gòu)建約束,但這種形式十分易錯,并且它并不支持&&和||這樣的直觀的邏輯操作符,而是用逗號、空格替代,下面是原 +build 形式構(gòu)建約束指示符的用法及含義:

Go 1.17 引入了 //go:build 形式的構(gòu)建約束指示符,支持&&和||邏輯操作符,如下代碼所示:
//go:buildlinux&&(386||amd64||arm||arm64||mips64||mips64le||ppc64||ppc64le)
//go:buildlinux&&(mips64||mips64le)
//go:buildlinux&&(ppc64||ppc64le)
//go:buildlinux&&!386&&!arm
考慮到兼容性,Go 命令可以識別這兩種形式的構(gòu)建約束指示符,但推薦 Go 1.17 之后都用新引入的這種形式。
gofmt 可以兼容處理兩種形式,處理原則是:如果一個源碼文件只有 // +build 形式的指示符,gofmt 會將與其等價的 //go:build 行加入。否則,如果一個源文件中同時存在這兩種形式的指示符行,那么 //+build 行的信息將被 //go:build 行的信息所覆蓋。
2、 go 語言 ABI
參考文檔:
Go internal ABI specification
https://go.googlesource.com/go/+/refs/heads/dev.regabi/src/cmd/compile/internal-abi.md
Proposal: Create an undefined internal calling convention
https://go.googlesource.com/proposal/+/master/design/27539-internal-abi.md
名詞解釋:ABI: application binary interface, 應(yīng)用程序二進制接口,規(guī)定了程序在機器層面的操作規(guī)范和調(diào)用規(guī)約。調(diào)用規(guī)約: calling convention, 所謂“調(diào)用規(guī)約”是調(diào)用方和被調(diào)用方對于函數(shù)調(diào)用的一個明確的約定,包括:函數(shù)參數(shù)與返回值的傳遞方式、傳遞順序。只有雙方都遵守同樣的約定,函數(shù)才能被正確地調(diào)用和執(zhí)行。如果不遵守這個約定,函數(shù)將無法正確執(zhí)行。
Go 從1.17.1版本開始支持多 ABI:1. 為了兼容性各平臺保持通用性,保留歷史版本 ABI,并更名為 ABI0。2. 為了更好的性能,增加新版本 ABI 取名 ABIInternal。ABI0 遵循平臺通用的函數(shù)調(diào)用約定,實現(xiàn)簡單,不用擔(dān)心底層cpu架構(gòu)寄存器的差異;ABIInternal 可以指定特定的函數(shù)調(diào)用規(guī)范,可以針對特定性能瓶頸進行優(yōu)化,在多個 Go 版本之間可以迭代,靈活性強,支持寄存器傳參提升性能。Go 匯編為了兼容已存在的匯編代碼,保持使用舊的 ABI0。
Go 為什么在有了 ABI0 之后,還要引入 ABIInternal?當(dāng)然是為了性能!據(jù)官方測試,寄存器傳參可以帶來 5% 的性能提升。
我們看一個例子:
packagemain
import_"fmt"
funcPrint(deltastring)
funcmain(){
Print("hello")
}
#include"textflag.h"
TEXT·Print(SB),NOSPLIT,$8
CALLfmt·Println(SB)
RET
運行上面代碼會報錯:main.Print: relocation target fmt.Println not defined for ABI0 (but is defined for ABIInternal)
原因是,fmt·Println 函數(shù)默認使用的 ABI 標(biāo)準是 ABIInternal,而 Go 語言手寫的匯編使用的 ABI 格式是 ABI0,二者標(biāo)準不一樣不能直接調(diào)用。不過 Go 語言可以通過 //go:linkname 的方式為 ABIInternal 生成 ABI0 包裝。
packagemain
import(
"fmt"
)
//go:linknamePrintlnfmt.Println
funcPrintln(a...any)(nint,errerror)
funcPrint(deltainterface{})funcmain(){
Print("hello")
}
#include"textflag.h"
TEXT·Print(SB),NOSPLIT,$48-16
LEAQstrp+0(FP),AX
MOVQAX,0(SP)//[]interface{}slice的pointer
MOVQ$1,BX
MOVQBX,8(SP)//slice的len
MOVQBX,16(SP)//slice的cap
CALLfmt·Println(SB)////go:linkname為fmt.Println生成一個ABI0包裝后,匯編可以直接調(diào)用
RET
簡單說明:函數(shù) fmt.Println 是一個變參函數(shù),變參(a ...any)實際上是 (a []any)的語法糖。參數(shù)中,slice 占 24Byte,int 占 8Byte,error 是 interface 類型,占 16Byte,加起來是 48 Byte。所以,調(diào)用此函數(shù)時,caller 需要再棧上準備 24Byte 空間。而 Print 的入?yún)偤檬且粋€ interface{} 類型,和 any 一致,所以只要把 Print 函數(shù)的入?yún)⒌牡刂焚x給 a 的指針,并把 a 的 len 和 cap 設(shè)置為 1,就可以調(diào)用 fmt·Println 函數(shù)了。如以上代碼所示。
3、內(nèi)存管理和 GC 對匯編的影響
3.1 調(diào)用棧擴縮容對匯編的影響
為了減少對內(nèi)存的占用,goroutine 啟動時 runtime 只給它分配了很少的棧內(nèi)存。所有函數(shù)(標(biāo)記 go:nosplit 的除外)的序言部分(啟動指令)會插入分段檢查,當(dāng)發(fā)現(xiàn)棧溢出(棧空間不足)時,就會調(diào)用 runtime.morestack,執(zhí)行棧拓展邏輯:
- 舊版本的 Go 編譯器采用了分段棧機制實現(xiàn)棧拓展,當(dāng)一個 goroutine 的執(zhí)行棧溢出時,就增加一個棧內(nèi)存作為調(diào)用棧的補充,新舊棧彼此沒有連續(xù)。這種設(shè)計的缺陷很容易破壞緩存的局部性原理,從而降低程序的運行時性能。
- Go 1.3 版本開始,引入了連續(xù)棧(拷貝棧)機制,并把 goroutine 的初始棧大小由 8KB 降低到了 2KB。當(dāng)一個執(zhí)行棧發(fā)生溢出時,新建一個兩倍于原棧大小的新棧,并將原棧整個拷貝到新棧上,保證整個棧是連續(xù)的。
棧的拷貝有些副作用:
- 如果棧上存在指向當(dāng)前被拷貝棧的指針,當(dāng)??截悎?zhí)行完成后,這個指針還是指向原棧,需要更新。
- goroutine 的 g 結(jié)構(gòu)體上的 gobuf 成員也還是指向舊的棧,也需要更新。
除了正在拷貝的棧中可能存在指向自己的的指針外,還有沒有其他存活中的內(nèi)存有指向即將失效的??臻g的指針呢?答案在 go 逃逸分析源碼 中,代碼如下:
//Escapeanalysis.
//
//HereweanalyzefunctionstodeterminewhichGovariables
//(includingimplicitallocationssuchascallsto"new"or"make",
//compositeliterals,etc.)canbeallocatedonthestack.Thetwo
//keyinvariantswehavetoensureare:(1)pointerstostackobjects
//cannotbestoredintheheap,and(2)pointerstoastackobject
//cannotoutlivethatobject(e.g.,becausethedeclaringfunction
//returnedanddestroyedtheobject'sstackframe,oritsspaceis
//reusedacrossloopiterationsforlogicallydistinctvariables).
//
其中 “(1) pointers to stack objects cannot be stored in the heap” 表明指向棧對象的指針不能存儲在堆中。
拷貝棧理論上沒有上限,但是一般都設(shè)置了上限。當(dāng)新的棧大小超過了 maxstacksize 就會拋出”stack overflow“的異常。maxstacksize 是在 runtime.main 中設(shè)置的。64 位系統(tǒng)下棧的最大值 1GB、32 位系統(tǒng)是 250MB。參考代碼:
ifnewsize>maxstacksize||newsize>maxstackceiling{
ifmaxstacksizeprint("runtime:goroutinestackexceeds",maxstacksize,"-bytelimit
")
}else{
print("runtime:goroutinestackexceeds",maxstackceiling,"-bytelimit
")
}
print("runtime:sp=",hex(sp),"stack=[",hex(gp.stack.lo),",",hex(gp.stack.hi),"]
")
throw("stackoverflow")
}
由拷貝棧的原理可知,拷貝棧對 Go 匯編是透明的。
3.2 GC 對匯編的影響
由于 GC 會動態(tài)回收沒有被引用的堆內(nèi)存,而 goroutine 的調(diào)用棧在堆空間,所以如果調(diào)用棧中存了堆內(nèi)存的指針,就需要告訴 GC 棧中含指針。上文中說到的偽指令 FUNCDATA、GO_RESULTS_INITIALIZED、NO_LOCAL_POINTERS 就是干這個事的。由于 FUNCDATA 偽指令幾乎只能由編譯器維護,所以在手寫的匯編函數(shù)本地內(nèi)存棧中保存指向動態(tài)內(nèi)存的指針幾乎是一種奢望。
4、 函數(shù)內(nèi)聯(lián)和匯編
參考文檔:
Go: Inlining Strategy & Limitation
https://medium.com/a-journey-with-go/go-inlining-strategy-limitation-6b6d7fc3b1be
4.1 查看內(nèi)聯(lián)情況
可以通過執(zhí)行以下命令,輸出被內(nèi)聯(lián)的函數(shù):
gobuild-gcflags="-m"main.go
#輸出結(jié)果:
#./op.go6:caninlineadd
#./op.go6:caninlinesub
#./main.go11:inliningcalltosub
#./main.go11:inliningcalltoadd
#./main.go12:inliningcalltofmt.Printf
或者使用參數(shù) -gflags="-m -m" 運行,查看編譯器的詳細優(yōu)化策略:
gobuild-gcflags="-m-m"main.go
輸出很詳細:
#command-line-arguments
./main.go6:cannotinlinemain:functiontoocomplex:cost106exceedsbudget80
./main.go12:inliningcalltofmt.Printf
./main.go6:caninlinetoEfacewithcost0as:func(){}
./main.go2:shlxescapestoheap:
./main.go2:flow:i=&{storageforshlx}:
./main.go2:fromshlx(spill)at./main.go2
./main.go2:fromi=shlx(assign)at./main.go4
./main.go2:flow:{storagefor...argument}=i:
./main.go2:from...argument(slice-literal-element)at./main.go12
./main.go2:flow:fmt.a=&{storagefor...argument}:
./main.go2:from...argument(spill)at./main.go12
./main.go2:fromfmt.format,fmt.a:="%+v",...argument(assign-pair)at./main.go12
./main.go2:flow:{heap}=*fmt.a:
./main.go2:fromfmt.Fprintf(io.Writer(os.Stdout),fmt.format,fmt.a...)(callparameter)at./main.go12
./main.go2:xescapestoheap:
./main.go2:flow:i=&{storageforx}:
./main.go2:fromx(spill)at./main.go2
./main.go2:fromi=x(assign)at./main.go4
./main.go2:flow:{storagefor...argument}=i:
./main.go2:from...argument(slice-literal-element)at./main.go12
./main.go2:flow:fmt.a=&{storagefor...argument}:
./main.go2:from...argument(spill)at./main.go12
./main.go2:fromfmt.format,fmt.a:="%+v",...argument(assign-pair)at./main.go12
./main.go2:flow:{heap}=*fmt.a:
./main.go2:fromfmt.Fprintf(io.Writer(os.Stdout),fmt.format,fmt.a...)(callparameter)at./main.go12
./main.go2:xescapestoheap
./main.go2:shlxescapestoheap
./main.go12:...argumentdoesnotescape
Go 編譯器默認將進行內(nèi)聯(lián)優(yōu)化,可以通過 -gcflags="-l" 選項全局禁用內(nèi)聯(lián),與一個-l禁用內(nèi)聯(lián)相反,如果傳遞兩個或兩個以上的-l則會打開內(nèi)聯(lián),并啟用更激進的內(nèi)聯(lián)策略。例如以下代碼:
//3.1:varclosure=NewClosure()
funcmain(){
//3.2:varclosurefunc()int
varclosure=NewClosure()
closure()
//3.3:closure=NewClosure()
closure()
}
funcNewClosure()func()int{
i:=0returnfunc()int{
i++
returni
}
}
命令 go build -gcflags="-m" main.go 和 go build -gcflags="-m -l -l" main.go 都是輸出:
./main.go6:caninlineNewClosure
./main.go9:caninlineNewClosure.func1
./main.go26:inliningcalltoNewClosure
./main.go9:caninlinemain.func1
./main.go9:inliningcalltomain.func1
./main.go9:inliningcalltomain.func1
./main.go26:funcliteraldoesnotescape
./main.go2:movedtoheap:i
./main.go9:funcliteralescapestoheap
命令 go build -gcflags="-m" main.go 輸出:
./main.go2:movedtoheap:i
./main.go9:funcliteralescapestoheap
4.2 內(nèi)聯(lián)前后性能對比
首先,看一下函數(shù)內(nèi)聯(lián)與非內(nèi)聯(lián)的性能差異。內(nèi)聯(lián)可以避免函數(shù)調(diào)用過程中的一些開銷:創(chuàng)建棧幀,讀寫寄存器。不過,對函數(shù)體進行拷貝也會增大二進制文件的大小。據(jù) Go 官方宣傳,內(nèi)聯(lián)大概會有 5~6% 的性能提升。
//go:noinline
funcmaxNoinline(a,bint)int{
ifareturnb
}
returna
}
funcmaxInline(a,bint)int{
ifareturnb
}
returna
}
funcBenchmarkInline(b*testing.B){
x,y:=1,2
b.Run("BenchmarkNoInline",func(b*testing.B){
b.ResetTimer()
fori:=0;i"BenchmarkInline",func(b*testing.B){
b.ResetTimer()
fori:=0;i
在程序代碼中,想要禁止編譯器內(nèi)聯(lián)優(yōu)化很簡單,在函數(shù)定義前一行添加 //go:noinline 即可。以下是性能對比結(jié)果:
BenchmarkInline/BenchmarkNoInline-128861373981.248ns/op0B/op0allocs/op
BenchmarkInline/BenchmarkInline-1210000000000.2506ns/op0B/op0allocs/op
因為函數(shù)體內(nèi)部的執(zhí)行邏輯非常簡單,此時內(nèi)聯(lián)與否的性能差異主要體現(xiàn)在函數(shù)調(diào)用的固定開銷上。顯而易見,該差異是非常大的。
4.3 內(nèi)聯(lián)條件
Go 語言代碼函數(shù)內(nèi)聯(lián)的策略每個編譯器版本都有細微差別,比如新版已支持含 for 和 閉包 的函數(shù)內(nèi)聯(lián)。1.18 版本的部分無法內(nèi)聯(lián)的規(guī)則如下:
- 函數(shù)標(biāo)注 "go:noinline" 注釋。
- 函數(shù)標(biāo)注 "go:norace" 注釋,且使用 "-gcflags=-d checkptr" 參數(shù)編譯。
- 函數(shù)標(biāo)注 "go:cgo_unsafe_args" 注釋。
- 函數(shù)標(biāo)注 "go:uintptrescapes" 注釋。
- 函數(shù)只有聲明而沒有函數(shù)體:比如函數(shù)實體在匯編文件 xxx.s 中。
- 超過小代碼量邊界的函數(shù):內(nèi)聯(lián)的小代碼量邊界是 80 個節(jié)點(抽象語法樹AST的節(jié)點)。
- 函數(shù)中含某些關(guān)鍵字的函數(shù):比如 select、defer、go、recover 等。
- 一些特殊的內(nèi)部函數(shù):比如 runtime.getcallerpc、runtime.getcallersp (這倆太特殊了)。
- 函數(shù)內(nèi)部使用 type 關(guān)鍵字重定義了類型:比如 "type Int int" 或 "type Int = int"。
- 作為尾遞歸調(diào)用時。
此外,還有一些編譯器覺得內(nèi)聯(lián)成本很低,所以必然內(nèi)聯(lián)的函數(shù):
- "runtime" package 下的 "heapBits.nextArena" 和 "builtin" package 下的 "append"。
- "encoding/binary" package 下的:"littleEndian.Uint64", "littleEndian.Uint32", "littleEndian.Uint16","bigEndian.Uint64", "bigEndian.Uint32", "bigEndian.Uint16","littleEndian.PutUint64", "littleEndian.PutUint32", "littleEndian.PutUint16","bigEndian.PutUint64", "bigEndian.PutUint32", "bigEndian.PutUint16", "append"。
由規(guī)則 5 可知,Go 語言匯編是無法內(nèi)聯(lián)的。
此外,關(guān)于閉包內(nèi)聯(lián)是一個比較復(fù)雜的話題,據(jù)筆者測試,1.18 有如上規(guī)則:
- 滿足條件的閉包可以內(nèi)聯(lián)。
- 閉包通用部分在內(nèi)聯(lián)統(tǒng)計的時候,占用函數(shù)的 15 個 AST 節(jié)點。
- 變量保存的閉包,如果是局部變量且沒有重新賦值過,則可以被內(nèi)聯(lián)。
關(guān)于閉包內(nèi)聯(lián)的第 3 條規(guī)則,有如下例子:
//3.1:varclosure=NewClosure()
funcmain(){
//3.2:varclosurefunc()int
varclosure=NewClosure()
closure()
//3.3:closure=NewClosure()
closure()
}
funcNewClosure()func()int{
i:=0
returnfunc()int{
i++
returni
}
}
執(zhí)行 go build -gcflags="-m" ./ 輸出如下
./main.go6:caninlineNewClosure
./main.go9:caninlineNewClosure.func1
./main.go26:inliningcalltoNewClosure
./main.go9:caninlinemain.func1
./main.go9:inliningcalltomain.func1
./main.go9:inliningcalltomain.func1
./main.go26:funcliteraldoesnotescape
./main.go2:movedtoheap:i
./main.go9:funcliteralescapestoheap
表明閉包 closure 可以內(nèi)聯(lián)。如果把 3.1 或 3.2 或 3.3 的注釋打開,則將會輸出:
./main.go6:caninlineNewClosure
./main.go9:caninlineNewClosure.func1
./main.go22:inliningcalltoNewClosure
./main.go22:funcliteraldoesnotescape
./main.go2:movedtoheap:i
./main.go9:funcliteralescapestoheap
表明閉包 closure 無法內(nèi)聯(lián)。
此外,如果想禁用閉包內(nèi)聯(lián),可以使用 -gcflags="-d=inlfuncswithclosures=0" 或-gcflags="-d inlfuncswithclosures=0" 參數(shù)編譯。
gobuild-gcflags="-d=inlfuncswithclosures=0"main.go
gobuild-gcflags="-dinlfuncswithclosures=0"main.go
如果想了解 go 1.18 的內(nèi)聯(lián)檢查邏輯,可以看這個源碼:inline.CanInline 和 (*inline.hairyVisitor).doNode。其調(diào)用順序是:inline.CanInline --> inline.hairyVisitor.tooHairy --> inline.hairyVisitor.doNode。
//CanInlinedetermineswhetherfnisinlineable.
//Ifso,CanInlinesavescopiesoffn.Bodyandfn.Dclinfn.Inl.
//fnandfn.Bodywillalreadyhavebeentypechecked.
funcCanInline(fn*ir.Func){
...
//Ifmarked"go:noinline",don'tinline
iffn.Pragma&ir.Noinline!=0{
reason="markedgo:noinline"
return
}
//Ifmarked"go:norace"and-racecompilation,don'tinline.
ifbase.Flag.Race&&fn.Pragma&ir.Norace!=0{
reason="markedgo:noracewith-racecompilation"
return
}
//Ifmarked"go:nocheckptr"and-dcheckptrcompilation,don'tinline.
ifbase.Debug.Checkptr!=0&&fn.Pragma&ir.NoCheckPtr!=0{
reason="markedgo:nocheckptr"
return
}
//Ifmarked"go:cgo_unsafe_args",don'tinline,sincethe
//functionmakesassumptionsaboutitsargumentframelayout.
iffn.Pragma&ir.CgoUnsafeArgs!=0{
reason="markedgo:cgo_unsafe_args"
return
}
//Ifmarkedas"go:uintptrescapes",don'tinline,sincethe
//escapeinformationislostduringinlining.
iffn.Pragma&ir.UintptrEscapes!=0{
reason="markedashavinganescapinguintptrargument"
return
}
//Thenowritebarrierreccheckercurrentlyworksatfunction
//granularity,soinliningyeswritebarrierrecfunctionscan
//confuseit(#22342).Asaworkaround,disallowinlining
//themfornow.
iffn.Pragma&ir.Yeswritebarrierrec!=0{
reason="markedgo:yeswritebarrierrec"
return
}
//Iffnhasnobody(isdefinedoutsideofGo),cannotinlineit.
iflen(fn.Body)==0{
reason="nofunctionbody"
return
}
...
visitor:=hairyVisitor{
budget:inlineMaxBudget,//inlineMaxBudget==80
extraCallCost:cc,
}
ifvisitor.tooHairy(fn){
reason=visitor.reason
return
}
...
}
func(v*hairyVisitor)tooHairy(fn*ir.Func)bool{
v.do=v.doNode//cacheclosure
ifir.DoChildren(fn,v.do){
returntrue
}
...
}
func(v*hairyVisitor)doNode(nir.Node)bool{
...
caseir.OSELECT,
ir.OGO,
ir.ODEFER,
ir.ODCLTYPE,//can'tprintyet
ir.OTAILCALL:
v.reason="unhandledop"+n.Op().String()
returntrue
...
}
5、 有哪些有意思的使用場景
5.1、 獲取 goid
goid 即 goroutine id,最常用三方庫應(yīng)該就是 petermattis/goid, 里通過匯編獲取 goid 的代碼關(guān)鍵邏輯如下:
runtime_go1.9.go 代碼:
//go:buildgc&&go1.9//+buildgc,go1.9packagegoid
typestackstruct{
louintptr
hiuintptr
}
typegobufstruct{
spuintptr
pcuintptr
guintptr
ctxtuintptr
retuintptr
lruintptr
bpuintptr
}
typegstruct{
stackstack
stackguard0uintptr
stackguard1uintptr
_panicuintptr
_deferuintptr
muintptr
schedgobuf
syscallspuintptr
syscallpcuintptr
stktopspuintptr
paramuintptr
atomicstatusuint32
stackLockuint32
goidint64//Hereitis!
}
goid_go1.5_amd64.go 代碼:
//go:build(amd64||amd64p32)&&gc&&go1.5//+buildamd64amd64p32//+buildgc//+buildgo1.5packagegoid
funcGet()int64
goid_go1.5_amd64.s 代碼:
//go:build(amd64||amd64p32)&&gc&&go1.5
//+buildamd64amd64p32
//+buildgc
//+buildgo1.5
#include"go_asm.h"
#include"textflag.h"
//funcGet()int64
TEXT·Get(SB),NOSPLIT,$0-8
MOVQ(TLS),R14
MOVQg_goid(R14),R13
MOVQR13,ret+0(FP)
RET
不過這樣獲取 goid 有一個局限性,就是如果當(dāng)前處于 g0 調(diào)用棧(系統(tǒng)調(diào)用或CGO函數(shù)中)時,拿到的不是當(dāng)前 g 的 goid,而是 是 g0 的 goid。在這種情況下 g.m.curg.goid 才是當(dāng)前 g 的 goid。參考Go1.18 標(biāo)準庫下go/src/runtime/HACKING.md 文件里的說明:
getg()andgetg().m.curg
Togetthecurrentuserg,usegetg().m.curg.
getg()alonereturnsthecurrentg,butwhenexecutingonthesystemorsignalstacks,thiswillreturnthecurrentM's"g0"or"gsignal",respectively.Thisisusuallynotwhatyouwant.
Todetermineifyou'rerunningontheuserstackorthesystemstack,usegetg()==getg().m.curg.
除了 goid,pid也可以用匯編獲?。篶holeraehyq/pid 是一個 fork petermattis/goid 的倉庫,里面增加了獲取 pid 的實現(xiàn),實現(xiàn)代碼如下:
p_m_go1.19.go 代碼:
//go:buildgc&&go1.19&&!go1.21//+buildgc,go1.19,!go1.21packagegoid
typepstruct{
idint32//Hereispid
}
typemstruct{
g0uintptr//goroutinewithschedulingstack
morebufgobuf//gobufargtomorestack
divmoduint32//div/moddenominatorforarm-knowntoliblink
_uint32//Fieldsnotknowntodebuggers.
prociduint64//fordebuggers,butoffsetnothard-coded
gsignaluintptr//signal-handlingg
goSigStackgsignalStack//Go-allocatedsignalhandlingstack
sigmasksigset//storageforsavedsignalmask
tls[6]uintptr//thread-localstorage(forx86externregister)
mstartfnfunc()
curguintptr//currentrunninggoroutine
caughtsiguintptr//goroutinerunningduringfatalsignal
p*p//attachedpforexecutinggocode(nilifnotexecutinggocode)
}
pid_go1.5.go 代碼:
//go:build(amd64||amd64p32||arm64)&&!windows&&gc&&go1.5//+buildamd64amd64p32arm64//+build!windows//+buildgc//+buildgo1.5packagegoid
//go:nosplitfuncgetPid()uintptr//go:nosplitfuncGetPid()int{
returnint(getPid())
}
pid_go1.5_amd64.s 代碼:
//+buildamd64amd64p32
//+buildgc,go1.5
#include"go_asm.h"
#include"textflag.h"
//funcgetPid()int64
TEXT·getPid(SB),NOSPLIT,$0-8
MOVQ(TLS),R14
MOVQg_m(R14),R13
MOVQm_p(R13),R14
MOVLp_id(R14),R13
MOVQR13,ret+0(FP)
RET
不過,通過這種方式獲取的 pid 也有一個局限性:在持有 pid 之后的時間里,可能當(dāng)前 goroutine 已經(jīng)被調(diào)度到其他 P 上了,也就是在使用 pid 的時候當(dāng)前 pid 已經(jīng)改變了。如果想要持有在持有 pid 的過程中持續(xù)幫當(dāng)當(dāng)前 P,可以使用一下方式:
import"unsafe"
var_=unsafe.Sizeof(0)
//go:linknameprocPinruntime.procPin
//go:nosplit
funcprocPin()int
//go:linknameprocUnpinruntime.procUnpin
//go:nosplit
funcprocUnpin()
runtime.procPin 和 runtime.procUnpin的實現(xiàn)代碼在Go 標(biāo)準庫下的 src/runtime/proc.go 文件中:
//go:nosplit
funcprocPin()int{
_g_:=getg()
mp:=_g_.m
mp.locks++//鎖定P的調(diào)度
returnint(mp.p.ptr().id)
}
//go:nosplit
funcprocUnpin(){
_g_:=getg()
_g_.m.locks--
}
通過 procPin 函數(shù)鎖定 P 的調(diào)度后再使用 pid,然后通過 procUnpin 釋放 P。不過這里也需要謹慎使用,使用不當(dāng)會對性能產(chǎn)生嚴重影響。
以上獲取 goid 的方式還有一個比較大的缺點,就是如果 Go 編譯器修改了 g 的結(jié)構(gòu)體,就需要重新適配。
《Go語言高級編程》第三章第8節(jié) 的實現(xiàn)可以避免這個問題。其原理是,通過匯編構(gòu)建一個 g 類型的 interface{},然后通過反射獲取 goid 成員的偏移量。根據(jù)原理,可以如下實現(xiàn):
funcGetg()int64funcgetgi()interface{}
varg_goid_offsetuintptr=func()uintptr{
g:=getgi()
iff,ok:=reflect.TypeOf(g).FieldByName("goid");ok{
returnf.Offset
}
panic("cannotfindg.goidfield")
}()
TEXT·Getg(SB),NOSPLIT,$0-8
MOVQ(TLS),AX
ADDQ·g_goid_offset(SB),AX
MOVQ(AX),BX
MOVQBX,ret+0(FP)
RET
//funcgetgi()interface{}
TEXT·getgi(SB),NOSPLIT,$32-16
NO_LOCAL_POINTERS
MOVQ$0,ret_type+0(FP)
MOVQ$0,ret_data+8(FP)
GO_RESULTS_INITIALIZED
//getruntime.g
//MOVQ(TLS),AX
MOVQ$0,AX
//getruntime.gtype
MOVQ$type·runtime·g(SB),BX
//MOVQBX,·runtime_g_type(SB)
//returninterface{}
MOVQBX,ret_type+0(FP)
MOVQAX,ret_data+8(FP)
RET
實際上還可以繼續(xù)簡化:
varruntime_g_typeuint64//go源碼中聲明
vargGoidOffsetuintptr=func()uintptr{//nolint
varifaceinterface{}
typeefacestruct{
_typeuint64
dataunsafe.Pointer
}
//結(jié)構(gòu)iface后,修改他的類型為g
(*eface)(unsafe.Pointer(&iface))._type=runtime_g_type
iff,ok:=reflect.TypeOf(iface).FieldByName("goid");ok{
returnf.Offset
}
panic("cannotfindg.goidfield")
}()
GLOBL·runtime_g_type(SB),NOPTR,$8
DATA·runtime_g_type+0(SB)/8,$type·runtime·g(SB)//匯編中初始化。匯編中可以訪問 package 的私有變量
5.2、Monkey Patch
Go 語言實現(xiàn)猴子打點的 package 不一定需要使用匯編,比如 bouk/monkey 和 go-kiss/monkey。不過字節(jié)開源的 monkey 和 內(nèi)部的 mockito 都使用了匯編。他們有一個同源的依賴庫,分別在 mockey/internal/monkey 目錄和 mockito/monkey 目錄下。
其 Patch() 的調(diào)用路徑如下:Build() -> Patch() -> PatchValue() -> WriteWithSTW() -> Write() -> do_replace_code() 其中 do_replace_code() 是匯編實現(xiàn)的,作用是使用 mprotect 系統(tǒng)調(diào)用來修改內(nèi)存權(quán)限(mprotect系統(tǒng)調(diào)用是修改內(nèi)存頁屬性的)。原因是:可執(zhí)行代碼區(qū)是只讀的,需要修改為可讀寫后才能修改,修改為可執(zhí)行后才能執(zhí)行(有想用 Go 寫病毒的,可以參考一下)。
func(builder*MockBuilder)Build()*Mocker{
mocker:=Mocker{target:reflect.ValueOf(builder.target),builder:builder}
mocker.buildHook(builder)
mocker.Patch()
return&mocker
}
func(mocker*Mocker)Patch()*Mocker{
mocker.lock.Lock()
defermocker.lock.Unlock()
ifmocker.isPatched{
returnmocker
}
mocker.patch=monkey.PatchValue(mocker.target,mocker.hook,reflect.ValueOf(mocker.proxy),mocker.builder.unsafe)
mocker.isPatched=true
addToGlobal(mocker)
mocker.outerCaller=tool.OuterCaller()
returnmocker
}
//PatchValuereplacethetargetfunctionwithahookfunction,andstoresthetargetfunctionintheproxyfunction//forfuturerestore.Targetandhookarevaluesoffunction.Proxyisavalueofproxyfunctionpointer.funcPatchValue(target,hook,proxyreflect.Value,unsafebool)*Patch{
tool.Assert(hook.Kind()==reflect.Func,"'%s'isnotafunction",hook.Kind())
tool.Assert(proxy.Kind()==reflect.Ptr,"'%v'isnotafunctionpointer",proxy.Kind())
tool.Assert(hook.Type()==target.Type(),"'%v'and'%s'mismatch",hook.Type(),target.Type())
tool.Assert(proxy.Elem().Type()==target.Type(),"'*%v'and'%s'mismatch",proxy.Elem().Type(),target.Type())
targetAddr:=target.Pointer()
//ThefirstfewbytesofthetargetfunctioncodeconstbufSize=64
targetCodeBuf:=common.BytesOf(targetAddr,bufSize)
//constructthebranchinstruction,i.e.jumptothehookfunction
hookCode:=inst.BranchInto(common.PtrAt(hook))
//searchthecuttingpointofthetargetcode,i.e.theminimumlengthoffullinstructionsthatislongerthanthehookCode
cuttingIdx:=inst.Disassemble(targetCodeBuf,len(hookCode),!unsafe)
//constructtheproxycode
proxyCode:=common.AllocatePage()
//savetheoriginalcodebeforethecuttingpointcopy(proxyCode,targetCodeBuf[:cuttingIdx])
//constructthebranchinstruction,i.e.jumptothecuttingpointcopy(proxyCode[cuttingIdx:],inst.BranchTo(targetAddr+uintptr(cuttingIdx)))
//injecttheproxycodetotheproxyfunction
fn.InjectInto(proxy,proxyCode)
tool.DebugPrintf("PatchValue:hookcodelen(%v),cuttingIdx(%v)
",len(hookCode),cuttingIdx)
//replacetargetfunctioncodesbeforethecuttingpoint
mem.WriteWithSTW(targetAddr,hookCode)
return&Patch{base:targetAddr,code:proxyCode,size:cuttingIdx}
}
//WriteWithSTWcopiesdatabytestothetargetaddressandreplacestheoriginalbytes,duringwhichitwillstopthe//world(onlythecurrentgoroutine'sPisrunning).funcWriteWithSTW(targetuintptr,data[]byte){
common.StopTheWorld()
defercommon.StartTheWorld()
err:=Write(target,data)
tool.Assert(err==nil,err)
}
而 Write 函數(shù)的實現(xiàn)在 github.com/bytedance/mockey/internal/monkey/mem/write_linux.go,其代碼如下:
packagemem
import(
"syscall""github.com/bytedance/mockey/internal/monkey/common"
)
funcWrite(targetuintptr,data[]byte)error{
do_replace_code(target,common.PtrOf(data),uint64(len(data)),syscall.SYS_MPROTECT,
syscall.PROT_READ|syscall.PROT_WRITE,syscall.PROT_READ|syscall.PROT_EXEC))
returnnil
}
funcdo_replace_code(
_uintptr,//void*addr
_uintptr,//void*data
_uint64,//size_tsize
_uint64,//intmprotect
_uint64,//intprot_rw
_uint64,//intprot_rx
)
do_replace_code 函數(shù)的匯編實現(xiàn)在 github.com/bytedance/mockey/internal/monkey/mem/write_linux_amd64.s,代碼如下:
#include"textflag.h"
#defineNOP8BYTE$0x90;BYTE$0x90;BYTE$0x90;BYTE$0x90;BYTE$0x90;BYTE$0x90;BYTE$0x90;BYTE$0x90;
#defineNOP64NOP8;NOP8;NOP8;NOP8;NOP8;NOP8;NOP8;NOP8;
#defineNOP512NOP64;NOP64;NOP64;NOP64;NOP64;NOP64;NOP64;NOP64;
#defineNOP4096NOP512;NOP512;NOP512;NOP512;NOP512;NOP512;NOP512;NOP512;
#defineaddrarg+0x00(FP)
#definedataarg+0x08(FP)
#definesizearg+0x10(FP)
#definemprotectarg+0x18(FP)
#defineprot_rwarg+0x20(FP)
#defineprot_rxarg+0x28(FP)
#defineCMOVNEQ_AX_CX
BYTE$0x48
BYTE$0x0f
BYTE$0x45
BYTE$0xc8
TEXT·do_replace_code(SB),NOSPLIT,$0x30-0
JMPSTART
NOP4096
START:
MOVQaddr,DI
MOVQsize,SI
MOVQDI,AX
ANDQ$0x0fff,AX
ANDQ$~0x0fff,DI
ADDQAX,SI
MOVQSI,CX
ANDQ$0x0fff,CX
MOVQ$0x1000,AX
SUBQCX,AX
TESTQCX,CX
CMOVNEQ_AX_CX
ADDQCX,SI
MOVQDI,R8
MOVQSI,R9
MOVQmprotect,AX
MOVQprot_rw,DX
SYSCALL
MOVQaddr,DI
MOVQdata,SI
MOVQsize,CX
REP
MOVSB
MOVQR8,DI
MOVQR9,SI
MOVQmprotect,AX
MOVQprot_rx,DX
SYSCALL
JMPRETURN
NOP4096
RETURN:
RET
5.3、 優(yōu)化獲取行號性能
筆者另一篇掘金文章 《golang文件行號探索》 中有詳細說明,代碼如下:
//stack_amd64.gotypeLineuintptrfuncNewLine()Line
varrcuCacheunsafe.Pointer=func()unsafe.Pointer{
m:=make(map[Line]string)
returnunsafe.Pointer(&m)
}()
func(lLine)LineNO()(linestring){
mPCs:=*(*map[Line]string)(atomic.LoadPointer(&rcuCache))
line,ok:=mPCs[l]
if!ok{
file,n:=runtime.FuncForPC(uintptr(l)).FileLine(uintptr(l))
line=file+":"+strconv.Itoa(n)
mPCs2:=make(map[Line]string,len(mPCs)+10)
mPCs2[l]=line
for{
p:=atomic.LoadPointer(&rcuCache)
mPCs=*(*map[Line]string)(p)
fork,v:=rangemPCs{
mPCs2[k]=v
}
swapped:=atomic.CompareAndSwapPointer(&rcuCachep,unsafe.Pointer(&mPCs2))
ifswapped{
break
}
}
}
return
}
#stack_amd64.s
TEXT·NewLine(SB),NOSPLIT,$0-8
MOVQretpc-8(FP),AX
SUBQ$1,AX//注意,這里要-1
MOVQAX,ret+0(FP)
RET
該代碼除了使用匯編獲取行號外,還是用了無鎖的 RCU(Read-copy update) 算法提升并發(fā)查詢速度。還有一點要注意的,retpc-8(FP) 是函數(shù)返回地址,也就是調(diào)用指令 CALL 的下一行指令, 所以需要 -1 才能得到 CALL 指令的 pc,參考Go 源碼 src/runtime/traceback.g 的這段注釋:
//file/lineinformationusingpc-1,becausethatisthepcofthe
//callinstruction(moreprecisely,thelastbyteofthecallinstruction).
//Callersexpectthepcbuffertocontainreturnaddressesanddothe
//same-1themselves,sowekeeppcunchanged.
//Whenthepcisfromasignal(e.g.profilerorsegv)thenwewant
//tolookupfile/lineinformationusingpc,andwestorepc+1inthe
//pcbuffersocallerscanunconditionallysubtract1beforelookingup.
//Seeissue34123.
//Thepccanbeatfunctionentrywhentheframeisinitializedwithout
//actuallyrunningcode,likeruntime.mstart.
5.4、 優(yōu)化獲取調(diào)用棧性能
筆者另一篇掘金文章 《關(guān)于 golang 錯誤處理的一些優(yōu)化想法》 中有詳細說明。stack_amd64.go 代碼:
//go:buildamd64//+buildamd64packageerrors
import(
_"unsafe"
)
funcbuildStack(s[]uintptr)int
stack_amd64.s 代碼:
//go:buildamd64||amd64p32||arm64
//+buildamd64amd64p32arm64
#include"go_asm.h"
#include"textflag.h"
#include"funcdata.h"
//funcbuildStack(s[]uintptr)int
TEXT·buildStack(SB),NOSPLIT,$24-8
NO_LOCAL_POINTERS
MOVQcap+16(FP),DX//s.cap
MOVQp+0(FP),AX//s.ptr
MOVQ$0,CX//loop.i
loop:
MOVQ+8(BP),BX//lastpc->BX
SUBQ$1,BX
MOVQBX,0(AX)(CX*8)//s[i]=BX
ADDQ$1,CX//CX++/i++
CMPQCX,DX//ifs.len>=s.cap{return}
JAEreturn//無符號大于等于就跳轉(zhuǎn)
MOVQ+0(BP),BP//lastBP;展開調(diào)用棧至上一層
CMPQBP,$0//if(BP)<=?0?{?return}
JAloop//無符號大于就跳轉(zhuǎn)
return:
MOVQCX,n+24(FP)//retn
RET
5.5、 字符串比較
Go 語言源碼里的字符串比較函數(shù),實際上使用了 SIMD 指令加速,由匯編實現(xiàn)。源碼在 Go 源碼文件中:src/cmd/compile/internal/typecheck/builtin/runtime.go :
funccmpstring(string,string)int
src/internal/bytealg/compare_amd64.s:
TEXT·Compare(SB),NOSPLIT,$0-56
//AX=a_base(wantinSI)
//BX=a_len(wantinBX)
//CX=a_cap(unused)
//DI=b_base(wantinDI)
//SI=b_len(wantinDX)
//R8=b_cap(unused)
MOVQSI,DX
MOVQAX,SI
JMPcmpbody<>(SB)
TEXTruntime·cmpstring(SB),NOSPLIT,$0-40
//AX=a_base(wantinSI)
//BX=a_len(wantinBX)
//CX=b_base(wantinDI)
//DI=b_len(wantinDX)
MOVQAX,SI
MOVQDI,DX
MOVQCX,DI
JMPcmpbody<>(SB)
//input:
//SI=a
//DI=b
//BX=alen
//DX=blen
//output:
//AX=output(-1/0/1)
TEXTcmpbody<>(SB),NOSPLIT,$0-0
CMPQSI,DI
...
loop:
CMPQR8,$16
JBE_0through16
MOVOU(SI),X0
MOVOU(DI),X1
PCMPEQBX0,X1
PMOVMSKBX1,AX
XORQ$0xffff,AX//convertEQtoNE
JNEdiff16//branchifatleastonebyteisnotequal
ADDQ$16,SI
ADDQ$16,DI
SUBQ$16,R8
JMPloop
···
這里 MOVOU、PCMPEQB、PMOVMSKB 等就是 SIMD 指令。如果想詳細了解 SIMD 指令可以看一下 Intel 的官方文檔 《Intel Intrinsics Guide》。另,據(jù)筆者的嘗試,SSE 和 SSE2 指令是可以直接在 Go 語言會便利使用的。有想法的同學(xué)可以自己驗證一下其他 SIMD 指令。
5.6、 字符串搜索
我們常用的字符串搜索函數(shù) strings.Index,也使用了匯編實現(xiàn)的 SIMD 指令加速。代碼在 Go 源碼文件 src/strings/strings.go 下:
//Indexreturnstheindexofthefirstinstanceofsubstrins,or-1ifsubstrisnotpresentins.funcIndex(s,substrstring)int{
n:=len(substr)
switch{
casen==0:
return0casen==1:
returnIndexByte(s,substr[0])
casen==len(s):
ifsubstr==s{
return0
}
return-1casen>len(s):
return-1casen<=?bytealg.MaxLen:
????????//Usebruteforcewhensandsubstrbotharesmalliflen(s)<=?bytealg.MaxBruteForce?{
returnbytealg.IndexString(s,substr)
...
}
//IndexBytereturnstheindexofthefirstinstanceofcins,or-1ifcisnotpresentins.funcIndexByte(sstring,cbyte)int{
returnbytealg.IndexByteString(s,c)
}
IndexByteString 函數(shù)聲明在 src/internal/bytealg/indexbyte_native.go
//go:build386||amd64||s390x||arm||arm64||ppc64||ppc64le||mips||mipsle||mips64||mips64le||riscv64||wasm
packagebytealg
//go:noescape
funcIndexByte(b[]byte,cbyte)int
//go:noescape
funcIndexByteString(sstring,cbyte)int
src/internal/bytealg/index_native.go
//go:buildamd64||arm64||s390x||ppc64le||ppc64
packagebytealg
//go:noescape
//Indexreturnstheindexofthefirstinstanceofbina,or-1ifbisnotpresentina.
//Requires2<=?len(b)?<=?MaxLen.
funcIndex(a,b[]byte)int
//go:noescape
//IndexStringreturnstheindexofthefirstinstanceofbina,or-1ifbisnotpresentina.
//Requires2<=?len(b)?<=?MaxLen.
funcIndexString(a,bstring)int
匯編實現(xiàn)在 src/internal/bytealg/indexbyte_amd64.s
#include"go_asm.h"
#include"textflag.h"
TEXT·IndexByte(SB),NOSPLIT,$0-40
MOVQb_base+0(FP),SI
MOVQb_len+8(FP),BX
MOVBc+24(FP),AL
LEAQret+32(FP),R8
JMPindexbytebody<>(SB)
TEXT·IndexByteString(SB),NOSPLIT,$0-32
MOVQs_base+0(FP),SI
MOVQs_len+8(FP),BX
MOVBc+16(FP),AL
LEAQret+24(FP),R8
JMPindexbytebody<>(SB)
//input:
//SI:data
//BX:datalen
//AL:bytesought
//R8:addresstoputresult
TEXTindexbytebody<>(SB),NOSPLIT,$0
//ShuffleX0aroundsothateachbytecontains
//thecharacterwe'relookingfor.
MOVDAX,X0
PUNPCKLBWX0,X0
PUNPCKLBWX0,X0
PSHUFL$0,X0,X0
...
src/internal/bytealg/index_amd64.s
#include"go_asm.h"
#include"textflag.h"
TEXT·Index(SB),NOSPLIT,$0-56
MOVQa_base+0(FP),DI
MOVQa_len+8(FP),DX
MOVQb_base+24(FP),R8
MOVQb_len+32(FP),AX
MOVQDI,R10
LEAQret+48(FP),R11
JMPindexbody<>(SB)
TEXT·IndexString(SB),NOSPLIT,$0-40
MOVQa_base+0(FP),DI
MOVQa_len+8(FP),DX
MOVQb_base+16(FP),R8
MOVQb_len+24(FP),AX
MOVQDI,R10
LEAQret+32(FP),R11
JMPindexbody<>(SB)
//AX:lengthofstring,thatwearesearchingfor
//DX:lengthofstring,inwhichwearesearching
//DI:pointertostring,inwhichwearesearching
//R8:pointertostring,thatwearesearchingfor
//R11:address,wheretoputreturnvalue
//Note:WewantleninDXandAX,becausePCMPESTRIimplicitlyconsumesthem
TEXTindexbody<>(SB),NOSPLIT,$0
...
筆者驗證了一下 IndexByte 和自定義通過 for 循環(huán)實現(xiàn)建的差別:
funcBenchmarkIndexByte(b*testing.B){
b.Run("IndexByte",func(b*testing.B){
b.ReportAllocs()
fori:=0;i0
k:=0for{
j:=strings.IndexByte(str[k:],']')
ifj0{
break
}
n++
k+=j+1
}
_=n
}
b.SetBytes(int64(b.N))
b.StopTimer()
})
b.Run("for",func(b*testing.B){
b.ReportAllocs()
str:=testdata.TwitterJsonOut
fori:=0;i0fori:=0;ilen(str);i++{
ifstr[i]==']'{
n++
}
}
_=n
}
b.SetBytes(int64(b.N))
b.StopTimer()
})
}
結(jié)果如下:
BenchmarkIndexByte/IndexByte
BenchmarkIndexByte/IndexByte-123072980387.5ns/op7929621.19MB/s0B/op0allocs/op
BenchmarkIndexByte/for
BenchmarkIndexByte/for-125166632417ns/op213777.66MB/s0B/op0allocs/op
由結(jié)果可知,SIMD 的加速性能還是挺好的。不過,實際上如果 strings.IndexByte() 字符串很短 或 所查找的字符在字符串中大量存在的話,性能甚至?xí)?for 循環(huán)慢。這個可以自行驗證一下。
5.7、 自定義SIMD優(yōu)化
如果感興趣,可以照著 Go 編譯器里的匯編抄,慢慢嘗試。github 上也有許多項目可以抄,比如:minio/sha256-simd
5.8、 隨意跳轉(zhuǎn)
這段代碼個人覺得很有意思,雖然有缺陷,但不失為一次大膽的嘗試。筆者另一篇掘金文章《關(guān)于 golang 錯誤處理的一些優(yōu)化想法》 中有詳細說明,實現(xiàn)原理類似 C 語言的棧溢出攻擊,就是替換函數(shù)的 RET 返回地址。
測試代碼如下:
funcTestTagTry0(t*testing.T){
deferfunc(){
fmt.Printf("1->")
}()
tag,err1:=NewTag()//當(dāng)tag.Try(err)時,跳轉(zhuǎn)此處并返回err1
fmt.Printf("2->")
iferr1!=nil{
fmt.Printf("3->")
return
}
deferfunc(){
fmt.Printf("4->")//由于的缺陷:這里 debug 下 defer 不內(nèi)聯(lián),會執(zhí)行;release 下 defer 內(nèi)聯(lián),不會執(zhí)行
}()
fmt.Printf("5->")
err2:=errors.New("err2")
tag.Try(err2)//這里err2!=nil,則會跳轉(zhuǎn)到tag創(chuàng)建處的下一行指令執(zhí)行,即fmt.Printf("2->")
fmt.Printf("6->")
return
}
測試結(jié)果:
#release下defer內(nèi)聯(lián),不會輸出4
2->5->2->3->1->
#debug下defer不內(nèi)聯(lián),會輸出4
2->5->2->3->4->1
5.9 調(diào)用其他 package 的私有函數(shù)
通過過擺脫 golang 編譯器的一些約束,調(diào)用其他 package 的私有函數(shù)。如這篇文章《How to call private functions (bind to hidden symbols) in GoLang》。
上面 goid 的例子的最后,也講了通過匯編使用 package 私有的類型,即 DATA ·runtime_g_type+0(SB)/8,$type·runtime·g(SB) ,這里不在重復(fù)。
5.10 提高 CGO 調(diào)用的性能
我們知道,CGO 和系統(tǒng)調(diào)用時,Go 語言需要把 goroutine 的調(diào)用棧切換回 g0 調(diào)用棧,并使用 g0 調(diào)用,整個過程性能損耗比較大。實際上,我們可以通過匯編適配 C 語言的 ABI 來直接調(diào)用 C 語言的函數(shù),參考 github 下的這個庫: petermattis/fastcgo。不過,這么做也有很大的局限性,比如導(dǎo)致棧溢出、因 goroutine 無法被搶占而影響 GC 性能等。
審核編輯 :李倩
-
寄存器
+關(guān)注
關(guān)注
31文章
5620瀏覽量
130455 -
計數(shù)器
+關(guān)注
關(guān)注
32文章
2321瀏覽量
98616 -
Go
+關(guān)注
關(guān)注
0文章
45瀏覽量
12620
原文標(biāo)題:5、 有哪些有意思的使用場景
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
labview基礎(chǔ)知識
關(guān)于C語言的基礎(chǔ)知識
匯編指令基礎(chǔ)知識
通信基礎(chǔ)知識教程
匯編語言學(xué)習(xí)課件_微處理器基礎(chǔ)知識
使用Eclipse基礎(chǔ)知識
電源管理基礎(chǔ)知識電源管理基礎(chǔ)知識電源管理基礎(chǔ)知識
匯編基礎(chǔ)知識教程之?dāng)?shù)據(jù)類型與寄存器
匯編基礎(chǔ)知識教程之ARM匯編簡介
Go匯編基礎(chǔ)知識
評論