【關(guān)注公眾號】「syd3600520」 回復(fù)002 獲取Go相關(guān)學(xué)習(xí)資料
從計算機(jī)誕生到現(xiàn)在掌眠,編程語言的發(fā)展大致分為了三個階段
- 從打孔程序的機(jī)器語言
- 一系列指令夏醉、寄存器代碼的匯編語言
- 再到我們?nèi)粘J褂玫母呒壵Z言
機(jī)器語言一堆的0/1代碼確實反人類,匯編語言指令繁雜 不同機(jī)器設(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
- 反編譯對二進(jìn)制包進(jìn)行分析
- 繞過go系統(tǒng)限制 訪問私有方法
- ......
常用指令
匯編
其實跟Go
Java
這些語言類似無非是變量、方法等巷疼。的確匯編存在比較多的指令晚胡、寄存器代碼。我對待匯編語言就像是對待學(xué)習(xí)的日語一樣嚼沿,雖然不少晦澀難記的單詞 但是先掌握好五十音行 再搞懂語法估盘,單詞的問題可以回頭查閱,常用的也就那么多
常數(shù)定義
plan9匯編中使用0x123的形式表示十六進(jìn)制
操作方向
plan9匯編操作數(shù)方向 與intel匯編方向相反
//plan9 匯編
MOVQ $123, AX
//intel匯編
mov rax, 123
棧擴(kuò)大攀细、縮小
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個寄存器時編譯器用來維護(hù)上下文菊卷、特殊標(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(全局?jǐn)?shù)據(jù))訪問洁闰。通常情況下歉甚,不會對SB/FP寄存器進(jìn)行運算操作,通常情況會以SB/FP/SP作為基準(zhǔn)地址扑眉,進(jìn)行偏移纸泄、解引用等操作
其中
- SP是棧指針,用來指向局部變量和函數(shù)調(diào)用的參數(shù)腰素,通過symbol+offset(SP)的方式使用刃滓。SP指向local stack frame的棧頂,所以使用時需要使用負(fù)偏移量耸弄,取之范圍為[-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ù)刂?/li>
他們的關(guān)系為:
- 若沒有本地變量: 偽SP=硬件SP+8
- 若有本地變量:偽SP=硬件SP+16+本地變量空間大小
- FP偽寄存器
FP偽寄存器:用來標(biāo)識函數(shù)參數(shù)扶歪、返回值理肺,編譯器維護(hù)了基于FP偏移的棧上參數(shù)指針,0(FP)表示function的第一個參數(shù)善镰,8(FP)表示第二個參數(shù)(64位系統(tǒng)上)后臺加上偏移量就可以訪問更多的參數(shù)妹萨。要訪問具體function的參數(shù),編譯器強(qiáng)制要求必須使用name來訪問FP炫欺,比如 foo+0(FP)獲取foo的第一個參數(shù)乎完,foo+8(FP)獲取第二個參數(shù)。
與偽SP寄存器的關(guān)系是:
- 若本地變量或者棧調(diào)用存嚴(yán)格split關(guān)系(無NOSPLIT)品洛,偽FP=偽SP+16
- 否則 偽FP=偽SP+8
- FP是訪問入?yún)⑹饕獭⒊鰠⒌幕纺ν埃话阌谜蚱苼韺ぶ罚琒P是訪問本地變量的起始基址帽揪,一般用負(fù)向偏移來尋址
- 修改硬件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)的方式進(jìn)行使用,例如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寄存器打交道的情況較少。
- BP寄存器
還有BP寄存器台颠,表示已給調(diào)用棧的起始棧底(棧的方向從大到小褐望,SP表示棧頂);一般用的不多串前,若需要做手動維護(hù)調(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" hello.go
//方法二
go tool compile -N -l -S hello.go //禁止優(yōu)化
//方法三
go build -gcflags="-N -l -m" -o xx xx.go
go tool objdump <binary>
go tool objdump -s <method name> <binary> //反匯編指定函數(shù)
//方法一、二生成的過程中的匯編
//方法三 生成的事最終機(jī)器碼的匯編
棧結(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ù)椑甙危空間自動加8,用于存儲BP寄存器呀酸,跳過這8字節(jié)后才是函數(shù)棧上局部變量的內(nèi)存凉蜂。邏輯上的FP/SP位置就是我們在寫匯編代碼時,計算便宜量時性誉,F(xiàn)P/SP的基準(zhǔn)位置跃惫,因此局部變量的內(nèi)存在邏輯SP的低地址側(cè),因此我們訪問時艾栋,需要向負(fù)方向偏移爆存。
實際上,在該函數(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) | 保存在進(jìn)入函數(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ù)的偏移地址 |
操作指令
用于指導(dǎo)匯編如何進(jìn)行厅瞎。以下指令后綴<mark>Q</mark>說明是64位上的匯編指令。
助記符 | 指令種類 | 用途 | 示例 |
---|---|---|---|
MOVQ | 傳送 | 數(shù)據(jù)傳送 |
MOVQ 48, AX 表示把48傳送AX中 |
LEAQ | 傳送 | 地址傳送 |
LEAQ AX, BX 表示把AX有效地址傳送到BX中 |
PUSHQ AX 表示先修改棧頂指針初坠,將AX內(nèi)容送入新的棧頂位置SUBQ 代替 |
|||
POPQ AX 表示先彈出棧頂?shù)臄?shù)據(jù)和簸,然后修改棧頂指針ADDQ 代替 |
|||
ADDQ | 運算 | 相加并賦值 |
ADDQ BX, AX 表示BX和AX的值相加并賦值給AX |
SUBQ | 運算 | 相減并賦值 | 略,同上 |
IMULQ | 運算 | 無符號乘法 | 略碟刺,同上 |
IDIVQ | 運算 | 無符號除法 |
IDIVQ CX 除數(shù)是CX锁保,被除數(shù)是AX,結(jié)果存儲到AX中 |
CMPQ | 運算 | 對兩數(shù)相減南誊,比較大小 |
CMPQ SI CX 表示比較SI和CX的大小。與SUBQ類似蜜托,只是不返回相減的結(jié)果 |
CALL | 轉(zhuǎn)移 | 調(diào)用函數(shù) |
CALL runtime.printnl(SB) 表示通過<mark>println</mark>函數(shù)的內(nèi)存地址發(fā)起調(diào)用 |
JMP | 轉(zhuǎn)移 | 無條件轉(zhuǎn)移指令 |
JMP 389 無條件轉(zhuǎn)至0x0185 地址處(十進(jìn)制389轉(zhuǎn)換成十六進(jìn)制0x0185) |
JLS | 轉(zhuǎn)移 | 條件轉(zhuǎn)移指令 |
JLS 389 上一行的比較結(jié)果抄囚,左邊小于右邊則執(zhí)行跳到0x0185 地址處(十進(jìn)制389轉(zhuǎn)換成十六進(jìn)制0x0185) |
可以看到,表中的PUSHQ
和POPQ
被去掉了橄务,這是因為在go匯編中幔托,對棧的操作并不是出棧入棧,而是通過對SP進(jìn)行運算來實現(xiàn)的蜂挪。
標(biāo)志位
助記符 | 名字 | 用途 |
---|---|---|
OF | 溢出 | 0為無溢出 1為溢出 |
CF | 進(jìn)位 | 0為最高位無進(jìn)位或錯位 1為有 |
PF | 奇偶 | 0表示數(shù)據(jù)最低8位中1的個數(shù)為奇數(shù)重挑,1則表示1的個數(shù)為偶數(shù) |
AF | 輔助進(jìn)位 | |
ZF | 零 | 0表示結(jié)果不為0 1表示結(jié)果為0 |
SF | 符號 | 0表示最高位為0 1表示最高位為1 |
如有錯誤懇請指正。
參考文檔: