肝了一上午的Golang之Plan9入門
從計算機誕生到現(xiàn)在,編程語言的發(fā)展大致分為了三個階段
從打孔程序的機器語言 一系列指令、寄存器代碼的匯編語言 再到我們?nèi)粘J褂玫母呒壵Z言
機器語言一堆的0/1代碼確實反人類,匯編語言指令繁雜 不同機器設(shè)備還有較大差異。比如x86架構(gòu)的匯編指令一般有兩種格式:
Intel匯編
DOS、Windows包括我們之前了解的8086處理器 Windows:VC編譯器 AT&T匯編
Linux、Unix、Mac OS Unix:GCC編譯器
而Go使用的匯編叫做plan9匯編這些東西的確我們現(xiàn)在使用的高級語言的編譯器都幫助我們屏蔽掉了,但是今天我們要來學(xué)學(xué)Go的plan9匯編,要是硬扛為什么?沒錯 我是為了炫技!!!
對于一只老鳥來說,我覺得搞搞Plan9匯編還是有不少益處的:
可以搞懂一段代碼底層到底是如何運行的 性能極致追求的優(yōu)化 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)如何運行 比如hashmap、channel 反編譯對二進制包進行分析 繞過go系統(tǒng)限制 訪問私有方法 ......
常用指令
匯編其實跟Go Java這些語言類似無非是變量、方法等。的確匯編存在比較多的指令、寄存器代碼。我對待匯編語言就像是對待學(xué)習(xí)的日語一樣,雖然不少晦澀難記的單詞 但是先掌握好五十音行 再搞懂語法,單詞的問題可以回頭查閱,常用的也就那么多
常數(shù)定義
plan9匯編中使用0x123的形式表示十六進制
操作方向
plan9匯編操作數(shù)方向 與intel匯編方向相反
//plan9 匯編
MOVQ $123, AX
//intel匯編
mov rax, 123
棧擴大、縮小
plan9中棧操作并沒有push pop,而是采用sub和add SP
SUBQ $0x18, SP //對SP做減法 為函數(shù)分配函數(shù)棧幀
ADDQ $0x18, SP //對SP做加法 清楚函數(shù)棧幀
數(shù)據(jù)copy
MOVB $1, DI // 1 byte
MOVW $0x10, BX // 2bytes
MOVD $1, DX // 4 bytes
MOVQ $-10, AX // 8 bytes
計算指令
ADDQ AX, BX // BX += AX
SUBQ AX, BX // BX -= AX
IMULQ AX, BX // BX *= AX
跳轉(zhuǎn)
//無條件跳轉(zhuǎn)
JMP addr // 跳轉(zhuǎn)到地址,地址可為代碼中的地址 不過實際上手寫不會出現(xiàn)這種東西
JMP label // 跳轉(zhuǎn)到標(biāo)簽 可以跳轉(zhuǎn)到同一函數(shù)內(nèi)的標(biāo)簽位置
JMP 2(PC) // 以當(dāng)前置頂為基礎(chǔ),向前/后跳轉(zhuǎn)x行
JMP -2(PC) //同上
//有條件跳轉(zhuǎn)
JNZ target // 如果zero flag被set過,則跳轉(zhuǎn)
變量聲明
匯編中的變量一般是存儲在.rodata或者.data段中的只讀值。對應(yīng)到應(yīng)用層就是已經(jīng)初始化的全局的const、var變量/常量
DATA symbol+offset(SB)/width,value
上面的語句初始化symbol+offset(SB)的數(shù)據(jù)中width bytes,賦值為value,相對于棧操作,SB的操作都是增地址,棧時減地址
GLOBL runtime·tlsoffset(SB), NOPTR, $4
// 聲明一個全局變量tlsoffset,4byte,沒有DATA部分,因其值為0。
// NOPTR 表示這個變量數(shù)據(jù)中不存在指針,GC不需要掃描。
(使用DATA結(jié)合GLOBL來定義一個變量,GLOBL必須跟在DATA指令之后)當(dāng)時我嘗試了下發(fā)現(xiàn)GLOBL不放在DATA之后 也沒啥問題,如果知道的小伙伴可以分享一下。
舉個栗子:
pkg.go
package?pkg
var?Id?int
var?Name?string
pkg_amd64.s
GLOBL ·Id(SB),$8
DATA ·Id+0(SB)/1,$0x37
DATA ·Id+1(SB)/1,$0x25
DATA ·Id+2(SB)/1,$0x00
DATA ·Id+3(SB)/1,$0x00
DATA ·Id+4(SB)/1,$0x00
DATA ·Id+5(SB)/1,$0x00
DATA ·Id+6(SB)/1,$0x00
DATA ·Id+7(SB)/1,$0x00
GLOBL ·Name(SB),$24
DATA ·Name+0(SB)/8,$·Name+16(SB)
DATA ·Name+8(SB)/8,$6
DATA ·Name+16(SB)/8,$"gopher"
函數(shù)聲明
舉個栗子:
fun.go
package?fun
//go:noinline
func?Swap(a,?b?int)?(int,?int)
fun_amd64.s
#include "textflag.h"
// func Swap(a,b int) (int,int)
告訴匯編器該數(shù)據(jù)放到TEXT區(qū)
| 告訴匯編器這是基于靜態(tài)地址的數(shù)據(jù)(static base)
| |
TEXT fun·Swap(SB),NOSPLIT,$0-32
MOVQ a+0(FP), AX // FP(Frame pointer)棧幀指針 這里指向棧幀最低位
MOVQ b+8(FP), BX
MOVQ BX ,ret0+16(FP)
MOVQ AX ,ret1+24(FP)
RET
上述代碼存儲在TEXT段中。pkgname可以省略,比如你的方法是fun·Swap(這里的· 是個unicode的中點 mac下的輸入方式為 option+shift+9),在編譯后的程序里的符號則是fun.Swap,總結(jié)起來如下:

stack frame size 棧幀大小(局部變量+可能需要的額外調(diào)用函數(shù)的參數(shù)空間的總大小,但不不包含調(diào)用其他函數(shù)時的ret address的大小)
arguments size 參數(shù)及返回值大小
若不指定NOSPLIT,arguments size必須指定。
測試代碼
func?main()?{
?println(pkg.Id)
?println(pkg.Name)
?a,?b?:=?1,?2
?a,?b?=?fun.Swap(a,?b)
?fmt.Println(a,?b)
}
寄存器
Go匯編引入了4個偽寄存器,這4個寄存器是編譯器用來維護上下文、特殊標(biāo)識等作用的:
FP(Frame pointer):arguments and locals
PC(Program counter): jumps and branches
SB(Static base pointer):global symbols
SP(Stack pointer):top of stack
所有用戶空間的數(shù)據(jù)都可以通過FP/SP(局部數(shù)據(jù)、輸入?yún)?shù)、返回值)和SB(全局數(shù)據(jù))訪問。通常情況下,不會對SB/FP寄存器進行運算操作,通常情況會以SB/FP/SP作為基準地址,進行偏移、解引用等操作
其中
SP是棧指針,用來指向局部變量和函數(shù)調(diào)用的參數(shù),SP指向local stack frame的棧頂,所以使用時需要使用負偏移量,取之范圍為[-framesize,0)。foo-8(SP)表示foo的棧第8byte。SP有偽SP和硬件SP的區(qū)分,如果硬件支持SP寄存器,那么不加name的時候就是訪問硬件寄存器,因此 x-8(SP)和-8(SP)訪問的會是不同的內(nèi)存空間。對SP和PC的訪問都應(yīng)該帶上name,若要訪問對應(yīng)的硬件寄存器可以使用RSP。
偽SP:本地變量最高起始地址 硬件SP:函數(shù)棧真實棧頂?shù)刂?/section>
他們的關(guān)系為:
若沒有本地變量: 偽SP=硬件SP+8 若有本地變量:偽SP=硬件SP+16+本地變量空間大小
FP偽寄存器
FP偽寄存器:用來標(biāo)識函數(shù)參數(shù)、返回值,編譯器維護了基于FP偏移的棧上參數(shù)指針,0(FP)表示function的第一個參數(shù),8(FP)表示第二個參數(shù)(64位系統(tǒng)上)后臺加上偏移量就可以訪問更多的參數(shù)。要訪問具體function的參數(shù),編譯器強制要求必須使用name來訪問FP,比如 foo+0(FP)獲取foo的第一個參數(shù),foo+8(FP)獲取第二個參數(shù)。
與偽SP寄存器的關(guān)系是:
若本地變量或者棧調(diào)用存嚴格split關(guān)系(無NOSPLIT),偽FP=偽SP+16 否則 偽FP=偽SP+8 FP是訪問入?yún)?、出參的基址,一般用正向偏移來尋址,SP是訪問本地變量的起始基址,一般用負向偏移來尋址 修改硬件SP,會引起偽SP、FP同步變化
SUBQ $16, SP // 這里golang解引用時,偽SP/FP都會-16
SB偽寄存器可以理解為原始內(nèi)存,foo(SB)的意思是用foo來代表內(nèi)存中的一個地址。foo(SB)可以用來定義全局的function和數(shù)據(jù),foo<>(SB)表示foo只在當(dāng)前文件可見,跟C中的static效果類似。此外可以在引用上加偏移量,如foo+4(SB)表示foo+4bytes的地址 參數(shù)/本地變量訪問
通過symbol+/-offset(FP/SP)的方式進行使用,例如arg0+0(FP)表示函數(shù)第一個參數(shù)的位置,arg1+8(FP)表示函數(shù)參數(shù)偏移8byte的另一個參數(shù)。arg0/arg1用于助記,但是必須存在,否則無法通過編譯(golang會識別并做處理)。
其中對于SP來說,還有一種訪問方式: +/-offset(FP) 這里SP前面沒有symbol修飾,代表這是硬件SP???
PC寄存器
實際上就是在體系結(jié)構(gòu)的知識中常見的PC寄存器,在x86平臺下對應(yīng)ip寄存器,amd64上則是rip。除了個別跳轉(zhuǎn)之外,手寫代碼與PC寄存器打交道的情況較少。
SP寄存器
SP是棧指針寄存器,指向當(dāng)前函數(shù)棧的棧頂,通過symbol+offset(SP)的方式使用。offset的合法取值是[-framesize,0),注意這是一個左閉右開區(qū)間。假如局部變量都是8字節(jié),那么第一個局部變量就可以用localvar0-8(SP)來表示
BP寄存器
還有BP寄存器,表示已給調(diào)用棧的起始棧底(棧的方向從大到小,SP表示棧頂);一般用的不多,若需要做手動維護調(diào)用棧關(guān)系,需要用到BP寄存器,手動split調(diào)用棧。
通用寄存器
在plan9匯編里還可以直接使用amd64的通用寄存器,應(yīng)用代碼層面會用到的通用寄存器主要是:
rax,rbx,rcx,rdx,rdi,rsi,r8~r15這14個寄存器。plan9中使用寄存器不需要帶r或e的前綴,例如rax,只要寫AX即可:
MOVQ $101, AX
示例:
func?Add(a?,b?int)?(c?int){
??sum?:=?0
??return?a?+?b?+?sum
}
各變量通用寄存器解引用如下:(偽FP=偽SP+16=硬件SP+24)
a: a+0(SP)或者a+16(SP) b: b+8(SP)或者a+24(SP) c: c+16(SP)或者a+32(SP) sum:sum-8(SP)或者a-24(FP)
TLS偽寄存器
該寄存器存儲當(dāng)前goroutine g結(jié)構(gòu)地址
Go程序如何轉(zhuǎn)換為plan9?
//編譯
go?build?-gcflags="-S"
go?tool?compile?-S?hello.go
go?tool?compile?-N?-S?hello.go?//禁止優(yōu)化
//反編譯
go?tool?objdump?
棧結(jié)構(gòu)

函數(shù)調(diào)用棧關(guān)系

X86平臺上BP寄存器,通常用來指示函數(shù)棧的起始位置,僅僅起一個指示作用,現(xiàn)代編譯器生成的代碼通常不會用到BP寄存器,但是可能某些debug工具會用到該寄存器來尋找函數(shù)參數(shù)、局部變量等。因此我們寫匯編代碼時,也最好將棧起始位置存儲在BP寄存器中。因此amd64平臺上,會在函數(shù)返回值之后插入8byte來放置CALLER BP寄存器。
此外需要注意的是,CALLER BP是在編譯期由編譯器插入的,用戶手寫匯編代碼時,計算framesize時是不包括這個CALLER BP部分的,但是要計算函數(shù)返回值的8byte。是否插入CALLER BP的主要判斷依據(jù)是:
函數(shù)的棧幀大小大于0 下述函數(shù)返回true
func?Framepointer_enabled(goos,?goarch?string)?bool?{
??return?framepointer_enabled?!=?0?&&?goarch?==?"amd64"?&&?goos?!=?"nacl"
}
此外需要注意,go編譯器會將函數(shù)??臻g自動加8,用于存儲BP寄存器,跳過這8字節(jié)后才是函數(shù)棧上局部變量的內(nèi)存。邏輯上的FP/SP位置就是我們在寫匯編代碼時,計算變量時,F(xiàn)P/SP的基準位置,因此局部變量的內(nèi)存在邏輯SP的低地址側(cè),因此我們訪問時,需要向負方向偏移。
實際上,在該函數(shù)被調(diào)用后,編譯器會添加SUBQ/LEAQ代碼修改物理SP指向的位置。我們在反匯編的代碼中能看到這部分操作,因此我們需要注意物理SP與偽SP指向位置的差別。
舉個栗子:
func?zzz(a,?b,?c?int)?[3]int{
?var?d?[3]int
?d[0],?d[1],?d[2]?=?a,?b,?c
?return?d
}

總結(jié)
| 助記符 | 名字 | 用途 |
|---|---|---|
| AX | 累加寄存器(AccumulatorRegister) | 用于存放數(shù)據(jù),包括算術(shù)、操作數(shù)、結(jié)果和臨時存放地址 |
| BX | 基址寄存器(BaseRegister) | 用于存放訪問存儲器時的地址 |
| CX | 計數(shù)寄存器(CountRegister) | 用于保存計算值,用作計數(shù)器 |
| DX | 數(shù)據(jù)寄存器(DataRegister) | 用于數(shù)據(jù)傳遞,在寄存器間接尋址中的I/O指令中存放I/O端口的地址 |
| SP | 堆棧頂指針(StackPointer) | 如果是symbol+offset(SP)的形式表示go匯編的偽寄存器;如果是offset(SP)的形式表示硬件寄存器 |
| BP | 堆?;羔?BasePointer) | 保存在進入函數(shù)前的棧頂基址 |
| SB | 靜態(tài)基指針(StaticBasePointer) | go匯編的偽寄存器。foo(SB)用于表示變量在內(nèi)存中的地址,foo+4(SB)表示foo起始地址往后偏移四字節(jié)。一般用來聲明函數(shù)或全局變量 |
| FP | 棧幀指針(FramePointer) | go匯編的偽寄存器。引用函數(shù)的輸入?yún)?shù),形式是symbol+offset(FP),例如arg0+0(FP) |
| SI | 源變址寄存器(SourceIndex) | 用于存放源操作數(shù)的偏移地址 |
| DI | 目的寄存器(DestinationIndex) | 用于存放目的操作數(shù)的偏移地址 |
如有錯誤懇請指正。
參考文檔:
go asm
golang 匯編
推薦閱讀
