五一假期在家沒事逛論壇的時候淘这,發(fā)現(xiàn)了一個寶藏網(wǎng)站另锋,傳送門 這個網(wǎng)站可以在線生成多種語言的匯編代碼,有這個好東西,那必須拿go實驗一番埠况。
很久之前我寫過一篇go通過go匯編看多返回值實現(xiàn)的文章傳送門。當時寫的時候比較早棵癣,后來 go 1.17 對函數(shù)調(diào)用時辕翰,傳遞參數(shù)做了修改,簡單說就是go1.17之前浙巫,函數(shù)參數(shù)是通過椊鹗瘢空間來傳遞的,在go1.17時做出了改變的畴,在一些平臺上(AMD64)可以像C,C++那樣使用寄存器傳遞參數(shù)和函數(shù)返回值了渊抄。為什么做出這個改變呢,原因就是寄存器更快丧裁。雖然內(nèi)存已經(jīng)很快了护桦,但是還是沒法和寄存器相比。之前為啥不用寄存器煎娇,用椂郑空間,原因是實現(xiàn)簡單缓呛,不用考慮不同平臺催享,不用架構(gòu)的區(qū)別。
簡單總結(jié)一下兩種方式
- 椨窗恚空間:
- 優(yōu)點:實現(xiàn)簡單因妙,不用區(qū)分不同的平臺,通用性強
- 缺點:效率低
- 寄存器:
- 優(yōu)點:速度快
- 缺點:通用性差,不同的平臺需要單獨處理
當然攀涵,這里說的通用性差是對于編譯器來說的
go匯編基礎(chǔ)知識
再來總結(jié)一次go匯編的基礎(chǔ)知識吧铣耘,現(xiàn)在回頭看之前總結(jié)的還是不全面的
go使用的 plan9 匯編,這個和 AT&T 的匯編差別還是有點大的以故,我個人感覺plan9匯編比較重要的就是四個寄存器蜗细,只要理解了這四個寄存器,匯編就理解了一半了
匯編中個幾個術(shù)語:
- 棧:進程怒详、線程炉媒、goroutine 都有自己的調(diào)用棧,先進后出(FILO)
- 棧幀:可以理解是函數(shù)調(diào)用時棘利,在棧上為函數(shù)所分配的內(nèi)存區(qū)域
- 調(diào)用者:caller橱野,比如:A 函數(shù)調(diào)用了 B 函數(shù),那么 A 就是調(diào)用者
- 被調(diào)者:callee善玫,比如:A 函數(shù)調(diào)用了 B 函數(shù)水援,那么 B 就是被調(diào)者
寄存器 | 說明 |
---|---|
SB(Static base pointer) | global symbols 全局靜態(tài)指針 |
FP(Frame pointer) | arguments and locals 指向棧幀的開始 |
SP(Stack pointer) | top of stack 指向棧頂 |
PC(Program counter) | jumps and branches 簡單說程序計數(shù)器 |
簡單展開說一下幾個寄存器吧
- SB:全局靜態(tài)指針,即程序地址空間的開始地址茅郎。一般用在聲明函數(shù)蜗元、全局變量中。
- FP:指向的是 caller 調(diào)用 callee 時傳遞的第一個參數(shù)的位置系冗,可以看作是指向兩個函數(shù)棧的分割位置奕扣;但是FP指向的位置不在 callee 的 stack frame 之內(nèi)。而是在 caller 的 stack frame 上掌敬,指向調(diào)用 add 函數(shù)時傳遞的第一個參數(shù)的位置惯豆;可以在 callee 中用
symbol+offset(FP)
來獲取入?yún)⒌膮?shù)值,比如a+8(FP)
奔害。雖然symbol
沒有什么具體意義楷兽,但是不加編譯器會報錯。 - SP:這個是最常用的寄存器了华临,同樣也是最復(fù)雜的寄存器了芯杀。不同的引用方式,代表不同的位置雅潭。SP寄存器 分為偽 SP 寄存器和硬件 SP 寄存器揭厚。
symbol+offset(SP)
形式,則表示偽寄存器 SP (這個也簡稱為 BP)扶供。如果是offset(SP)
則表示硬件寄存器 SP筛圆。偽 SP 寄存器指向當前棧幀第一個局部變量的結(jié)束位置;硬件SP指向的是整個函數(shù)棧結(jié)束的位置椿浓。有個比較坑的地方:對于編譯輸出(go tool compile -S / go tool objdump)的代碼來講顽染,所有的 SP 都是硬件 SP 寄存器漾岳,無論是否帶 symbol(這一點非常具有迷惑性,需要慢慢理解粉寞。往往在分析編譯輸出的匯編時,看到的就是硬件 SP 寄存器)左腔。 - PC:這個就是計算機常見的 pc 寄存器唧垦,在 x86 平臺下對應(yīng) ip 寄存器,amd64 上則是 rip液样。這個很少有用到振亮。
通過一個棧幀的圖來理解一下這幾個寄存器
大體的棧幀就是圖中的這樣,圖中標注的寄存器都是以 callee 函數(shù)為基準的
通過圖中可知鞭莽,如果callee函數(shù)中沒有局部變量的話坊秸,SP硬寄存器和SP偽寄存器指向的是同一個地方
偽 FP 寄存器對應(yīng)的是 caller 函數(shù)的幀指針,一般用來訪問 callee 函數(shù)的入?yún)?shù)和返回值澎怒。偽 SP 棧指針對應(yīng)的是當前 callee 函數(shù)棧幀的底部(不包括參數(shù)和返回值部分)褒搔,一般用于定位局部變量。硬件 SP 是一個比較特殊的寄存器喷面,因為還存在一個同名的 SP 真寄存器星瘾,硬件 SP 寄存器對應(yīng)的是棧的頂部。
在編寫 Go 匯編時惧辈,當需要區(qū)分偽寄存器和真寄存器的時候只需要記住一點:偽寄存器一般需要一個標識符和偏移量為前綴琳状,如果沒有標識符前綴則是真寄存器。比如(SP)盒齿、+8(SP)沒有標識符前綴為真 SP 寄存器念逞,而 a(SP)、b+8(SP)有標識符為前綴表示偽寄存器边翁。
還有一點
如果callee的楐岢校空間大小是0的話, caller BP 是不會被壓入棧中的倒彰,此時的SP硬件寄存器和偽FP寄存器指向的是同一個位置审洞。
在匯編中,函數(shù)的定義
TEXT "".add(SB), NOSPLIT|ABIInternal, $0-24
TEXT
是一個特殊的指令待讳,定義一個函數(shù)
.add
是函數(shù)的名
NOSPLIT
向編譯器表明不應(yīng)該插入 stack-split 的用來檢查棧需要擴張的前導(dǎo)指令
$0-24
這兩個參數(shù)芒澜,0是聲明這個函數(shù)需要的棧空間的大小创淡,一般來說就是局部變量需要的空間痴晦,單位是位。
24是聲明函數(shù)傳入?yún)?shù)和返回值需要的椓詹剩空間的大小誊酌,單位也是位部凑。
生成匯編
了解了這些基本概念后,上一段代碼碧浊,通過匯編看一下不同版本go是如何處理函數(shù)傳遞參數(shù)的
先通過一段簡單的代碼看一下區(qū)別
package main
func main(){
add(10,20)
}
//go:noinline
func add(a,b int) int{
return a+b
}
//go:noinline
這個是告訴編譯器不要對這個函數(shù)內(nèi)聯(lián)涂邀,這個東西叫g(shù)o的編譯指令,go還有你很多別的指令箱锐,這里就不展開了比勉。想想go也挺有意思的,C++是通過 inline
顯式的指定要內(nèi)聯(lián)驹止,go是告訴編譯器不要內(nèi)聯(lián)浩聋。
先看一下在1.16下的匯編代碼
main_pc0:
.file 1 "<source>"
.loc 1 5 0
TEXT "".main(SB), ABIInternal, $32-0
MOVQ (TLS), CX
CMPQ SP, 16(CX)
PCDATA $0, $-2
JLS main_pc64
PCDATA $0, $-1
SUBQ $32, SP
MOVQ BP, 24(SP)
LEAQ 24(SP), BP
FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
.loc 1 11 0
MOVQ $10, (SP)
MOVQ $20, 8(SP)
PCDATA $1, $0
CALL "".add(SB)
.loc 1 12 0
MOVQ 24(SP), BP
ADDQ $32, SP
RET
NOP
.loc 1 5 0
PCDATA $1, $-1
PCDATA $0, $-2
NOP
main_pc64:
CALL runtime.morestack_noctxt(SB)
PCDATA $0, $-1
JMP main_pc0
.loc 1 21 0
TEXT "".add(SB), NOSPLIT|ABIInternal, $0-24
FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
.loc 1 22 0
MOVQ "".b+16(SP), AX
MOVQ "".a+8(SP), CX
ADDQ CX, AX
MOVQ AX, "".~r2+24(SP)
RET
代碼有很多,只關(guān)注下面圖中標注的
通 MOVQ "".b+16(SP), AX
和 MOVQ "".a+8(SP), CX
可以得知臊恋,函數(shù)是通過SP寄存器偏移完成傳遞參數(shù)的衣洁。
這里要注意,.b+16(SP)
這種寫法看著像是使用的是偽SP寄存器抖仅,實際上用的是硬件SP寄存器
同樣的坊夫,在函數(shù)調(diào)用之前,也會把數(shù)值放到棧的指定位置
MOVQ $10, (SP)
, MOVQ $20, 8(SP)
把編譯器換成最新的 1.18看一下
main_pc0:
.file 1 "<source>"
.loc 1 5 0
TEXT "".main(SB), ABIInternal, $24-0
CMPQ SP, 16(R14)
PCDATA $0, $-2
JLS main_pc47
PCDATA $0, $-1
SUBQ $24, SP
MOVQ BP, 16(SP)
LEAQ 16(SP), BP
FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
.loc 1 11 0
MOVL $10, AX
MOVL $20, BX
PCDATA $1, $0
NOP
CALL "".add(SB)
.loc 1 12 0
MOVQ 16(SP), BP
ADDQ $24, SP
RET
main_pc47:
NOP
.loc 1 5 0
PCDATA $1, $-1
PCDATA $0, $-2
CALL runtime.morestack_noctxt(SB)
PCDATA $0, $-1
JMP main_pc0
.loc 1 21 0
TEXT "".add(SB), NOSPLIT|ABIInternal, $0-16
FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA $5, "".add.arginfo1(SB)
FUNCDATA $6, "".add.argliveinfo(SB)
PCDATA $3, $1
.loc 1 22 0
ADDQ BX, AX
RET
在1.18的匯編代碼中就沒有通過棸妒郏空間來傳遞參數(shù)了践樱,而是直接通過寄存器完成操作,
ADDQ BX, AX
凸丸,并且返回值直接放到寄存器中拷邢。
寄存器數(shù)量是有上限的,如果傳遞的參數(shù)個數(shù)超過了寄存器的上限屎慢,又會怎樣處理呢
package main
func main(){
add(1,2,3,4,5,6,7,8,9,10,11,12)
}
//go:noinline
func add(a,b,c,d,e,f,g,h,i,j,k,l int) int{
return a+b+c+d+e+f+g+h+i+j+k+l
}
對應(yīng)的匯編
main_pc0:
.file 1 "<source>"
.loc 1 5 0
TEXT "".main(SB), ABIInternal, $104-0
CMPQ SP, 16(R14)
PCDATA $0, $-2
JLS main_pc111
PCDATA $0, $-1
SUBQ $104, SP
MOVQ BP, 96(SP)
LEAQ 96(SP), BP
FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
.loc 1 11 0
MOVQ $10, (SP)
MOVQ $11, 8(SP)
MOVQ $12, 16(SP)
MOVL $1, AX
MOVL $2, BX
MOVL $3, CX
MOVL $4, DI
MOVL $5, SI
MOVL $6, R8
MOVL $7, R9
MOVL $8, R10
MOVL $9, R11
PCDATA $1, $0
NOP
CALL "".add(SB)
.loc 1 12 0
MOVQ 96(SP), BP
ADDQ $104, SP
RET
main_pc111:
NOP
.loc 1 5 0
PCDATA $1, $-1
PCDATA $0, $-2
CALL runtime.morestack_noctxt(SB)
PCDATA $0, $-1
JMP main_pc0
.loc 1 21 0
TEXT "".add(SB), NOSPLIT|ABIInternal, $0-96
FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA $5, "".add.arginfo1(SB)
FUNCDATA $6, "".add.argliveinfo(SB)
PCDATA $3, $1
.loc 1 22 0
LEAQ (BX)(AX*1), DX
ADDQ DX, CX
ADDQ DI, CX
ADDQ SI, CX
ADDQ R8, CX
ADDQ R9, CX
ADDQ R10, CX
ADDQ R11, CX
MOVQ "".j+8(SP), DX
ADDQ DX, CX
MOVQ "".k+16(SP), DX
ADDQ DX, CX
MOVQ "".l+24(SP), DX
LEAQ (DX)(CX*1), AX
RET
通過匯編可以看到瞭稼,會先使用寄存器,當寄存器不夠時腻惠,會使用椈分猓空間。
手寫匯編
通過手寫一段匯編代碼集灌,驗證一下各個寄存器的位置悔雹,我用的go版本是 1.14.13,所以傳參數(shù)用的是椥佬空間腌零。
在 main.go 文件中
package main
func add(int, int) int
func main() {
print(add(10, 20))
}
定義一個 main 函數(shù)作為整個程序的入口,聲明一個 add(int, int) int
函數(shù)唆阿,add 函數(shù)的具體實現(xiàn)是用匯編寫的,
在 main.go 同級目錄下創(chuàng)建一個 add_amd64.s 的文件益涧。
使用硬BP寄存器
TEXT ·add(SB), $0-24
MOVQ 8(SP), AX
MOVQ 16(SP), BX
ADDQ BX, AX
MOVQ AX, 24(SP)
RET
使用 go run . 命令,看一下程序執(zhí)行的結(jié)果驯鳖。
這時候的棧幀如圖所示
因為add函數(shù)椣醒空間是0久免,所以偽SP寄存器沒有被壓入棧中扭弧,偽SP寄存器和硬件SP寄存器指向的是同一個位置。
使用偽SP寄存器
TEXT ·add(SB), $16-24
MOVQ a+16(SP), AX
MOVQ b+24(SP), BX
ADDQ BX, AX
MOVQ AX, ret+32(SP)
RET
為了區(qū)分對比丁寄,這時候把add函數(shù)的棽蠢ⅲ空間設(shè)置為16盛正,然后使用偽SP寄存器來獲取值。
執(zhí)行結(jié)果如下:
這時候的椞底蹋空間如圖
使用FP寄存器
代碼如下:
TEXT ·add(SB), $16-24
MOVQ a+0(FP), AX
MOVQ b+8(FP), BX
ADDQ BX, AX
MOVQ AX, ret+16(FP)
RET
執(zhí)行結(jié)果:
說明還是正確的续崖,此時的棧空間沒有變化多艇,和上面是一樣的像吻。
到這,應(yīng)該能理解各個寄存器的相對位置了吧:
在callee棽Υ遥空間不為0的時候,
FP = 硬件SP + framsize + 16
SP = 硬件SP + framsize
在callee椆嵌觯空間為0的時候台腥,
FP = 硬件SP + 8
SP = 硬件SP
匯編簡單分析
通過上面的代碼會發(fā)現(xiàn),手寫匯編的代碼和反匯編的代碼有些不同览爵。反匯編得到的代碼,在函數(shù)前面會有一段
CALL runtime.morestack_noctxt(SB)
其實這個是編譯器自動插入的一段函數(shù)箕母,這段指令會調(diào)用一次 runtime.morestack_noctxt
這個函數(shù)具體的作用是挺復(fù)雜的,主要有 檢查是否需要擴張棧嘶是,go的棧空間是可以動態(tài)擴充的辖源,所以在調(diào)用函數(shù)前會檢查當前的椣L空間是否需要擴充。還有一個功能就是檢查當前協(xié)程需要搶占誊辉。go在1.14之前goroutine的搶占是協(xié)作式搶占模式,怎么判斷一個協(xié)程是否需要搶占呢堕澄?后臺協(xié)程會定時掃描當前運行中的協(xié)程,如果發(fā)現(xiàn)一個協(xié)程運行比較久拍屑,會將其標記為搶占狀態(tài)坑傅。這個掃描的時間點就是函數(shù)調(diào)用期間完成的。
不同類型參數(shù)傳遞
傳結(jié)構(gòu)體
package main
type One struct {
a int
b int
}
func main(){
o := One {
a:10,
b.20,
}
f1(o)
}
//go:noinline
func f1(o One) int {
return o.a + o.b
}
只貼 關(guān)鍵的匯編代碼吧
MOVQ $10, (SP)
MOVQ $0, 8(SP)
PCDATA $1, $0
CALL "".f1(SB)
..........................
TEXT "".f1(SB), NOSPLIT|ABIInternal, $0-24
MOVQ "".o+16(SP), AX
MOVQ "".o+8(SP), CX
ADDQ CX, AX
MOVQ AX, "".~r1+24(SP)
RET
在傳結(jié)構(gòu)體的時候矢渊,只把結(jié)構(gòu)體的內(nèi)容傳進去了枉证。
傳結(jié)構(gòu)體指針
package main
type One struct {
a int
b int
}
func main(){
o := &One{
a:10,
b:20,
}
f1(o)
}
//go:noinline
func f1(o *One) int {
return o.a + o.b
}
匯編
LEAQ ""..autotmp_2+16(SP), AX
MOVQ AX, (SP)
PCDATA $1, $0
CALL "".f1(SB)
.......................
TEXT "".f1(SB), NOSPLIT|ABIInternal, $0-16
MOVQ "".o+8(SP), AX
MOVQ (AX), CX
ADDQ 8(AX), CX
MOVQ CX, "".~r1+16(SP)
RET
可以看到,傳遞指針的時候毡鉴,只是把結(jié)構(gòu)體第一個元素的地址傳遞進去了。
如果是傳一個空的結(jié)構(gòu)體
package main
type One struct {
}
func main(){
o := One{}
f1(o,10,20)
}
//go:noinline
func f1(o One, a,b int) int {
return a + b
}
匯編
TEXT "".f1(SB), NOSPLIT|ABIInternal, $0-24
.loc 1 15 0
MOVQ "".b+16(SP), AX
MOVQ "".a+8(SP), CX
ADDQ CX, AX
MOVQ AX, "".~r3+24(SP)
RET
可以看到猪瞬,當傳遞一個空結(jié)構(gòu)體時入篮,相當于沒有傳遞,因為空結(jié)構(gòu)體的空間大小是0潮售,所以編譯器就給忽略了锅风。
最后
暫時就想到這么多鞍泉,先寫這些吧,后面想起別的再補充吧