調(diào)用約定 (calling convention) 是調(diào)用方 (caller) 與被調(diào)用方 (callee) 對于函數(shù)調(diào)用的參數(shù)與返回值的傳遞方式糖权、傳遞順序的約定绽淘,只有雙方遵守相同的約定,才能保證參數(shù)的正確傳遞测萎,以及保證函數(shù)棧的平衡亡电。
Go 1.17 版本之前,采用基于棧的調(diào)用約定硅瞧,即函數(shù)的參數(shù)與返回值都通過棧來傳遞份乒,這種方式的優(yōu)點(diǎn)是實(shí)現(xiàn)簡單,無需考慮不同架構(gòu)下cpu寄存器的差異腕唧,更容易實(shí)現(xiàn)跨平臺(tái)或辖。然而由于內(nèi)存的訪問時(shí)延遠(yuǎn)高于寄存器,導(dǎo)致 Golang 的函數(shù)調(diào)用性能要低于 C/C++ 等使用寄存器傳遞參數(shù)的編程語言枣接。
Golang 在 1.17 版本中引入了基于寄存器的函數(shù)調(diào)用約定颂暇,本文將簡要介紹該調(diào)用約定函似,并通過具體的實(shí)驗(yàn)來驗(yàn)證并加深理解磁奖。
Go internal ABI specification
Go internal ABI specification 詳細(xì)介紹了 Go 1.17 之后的應(yīng)用程序二進(jìn)制接口 (Application Binary Interface, ABI)糜值。
首先是各種基礎(chǔ)類型的大小與內(nèi)存對齊約定满钟,例如 int
類型在 64 位架構(gòu)下的大小為 8 字節(jié)鸟缕,并且需要對齊到 8 字節(jié)瘫筐。棧上的變量如果不對齊矩桂,編譯器會(huì)插入 padding 使之對齊软族。
Type | 64-bit | 32-bit | ||
---|---|---|---|---|
Size | Align | Size | Align | |
bool, uint8, int8 | 1 | 1 | 1 | 1 |
uint16, int16 | 2 | 2 | 2 | 2 |
uint32, int32 | 4 | 4 | 4 | 4 |
uint64, int64 | 8 | 8 | 8 | 4 |
int, uint | 8 | 8 | 4 | 4 |
float32 | 4 | 4 | 4 | 4 |
float64 | 8 | 8 | 8 | 4 |
complex64 | 8 | 4 | 8 | 4 |
complex128 | 16 | 8 | 16 | 4 |
uintptr, *T, unsafe.Pointer | 8 | 8 | 4 | 4 |
棧幀結(jié)構(gòu)如下:棧幀由高地址向低地址增長添谊,依次是上一個(gè)函數(shù)的程序計(jì)數(shù)器 (Program Counter, PC)财喳、上一個(gè)函數(shù)的基址寄存器 (Base Pointer, BP)、函數(shù)的局部變量、調(diào)用函數(shù)的返回值耳高、調(diào)用函數(shù)的參數(shù)扎瓶。
+------------------------------+
| return PC |
| RBP on entry |
| ... locals ... |
| ... outgoing arguments ... |
+------------------------------+ ↓ lower addresses
寄存器約定 (amd64):
- 用于整型參數(shù)和返回值的通用寄存器:RAX、RBX泌枪、RCX栗弟、RDI、RSI工闺、R8乍赫、R9、R10陆蟆、R11
- 用于浮點(diǎn)參數(shù)和返回值的浮點(diǎn)寄存器:X0~X14
- 其他是用于特殊目的的寄存器:
寄存器 | 名稱含義 | 值含義 |
---|---|---|
RSP | 棧頂指針 (Stack pointer) | 固定 |
RBP | 棧幀指針(基址)(Frame pointer) | 固定 |
RDX | 閉包上下文指針 | 臨時(shí) |
R12 | 無 | 臨時(shí) |
R13 | 無 | 臨時(shí) |
R14 | 當(dāng)前 go routine | 臨時(shí) |
R15 | GOT reference temporary | 動(dòng)態(tài)鏈接時(shí)固定 |
X15 | 零值寄存器 | 固定為0 |
變量在棧上的排列順序:從低地址到高地址依次為通過棧傳遞的 receiver 參數(shù)雷厂、通過棧傳遞的若干個(gè)參數(shù)、通過棧傳遞的函數(shù)返回值叠殷、寄存器參數(shù)溢出空間改鲫。其中所有參數(shù)都會(huì)根據(jù)其類型處理內(nèi)存對齊,不對齊的情況下編譯器計(jì)算對齊所需的 padding 空間使之對齊林束。
+------------------------------+
| . . . |
| 2nd reg argument spill space |
| 1st reg argument spill space |
| <pointer-sized alignment> |
| . . . |
| 2nd stack-assigned result |
| 1st stack-assigned result |
| <pointer-sized alignment> |
| . . . |
| 2nd stack-assigned argument |
| 1st stack-assigned argument |
| stack-assigned receiver |
+------------------------------+ ↓ lower addresses
函數(shù)的調(diào)用者負(fù)責(zé)保存寄存器像棘,被調(diào)用者無需保存寄存器,因此可以會(huì)覆蓋任何沒有固定含義的寄存器壶冒,包括參數(shù)寄存器缕题。
代碼示例
示例代碼如下,保存為 main.go
胖腾。
package main
func main() {
r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12 := test(71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83)
ret := r0 + r1 + r2 + r3 + r4 + r5 + r6 + r7 + r8 + r9 + r10 + r11 + r12
println(ret)
}
func test(a0 int, a1 int, a2 int, a3 int, a4 int, a5 int, a6 int, a7 int, a8 int, a9 int, a10 int, a11 int, a12 int) (int, int, int, int, int, int, int, int, int, int, int, int, int) {
var arr [13]int
arr[0] = a0 + 31
arr[1] = a1 + 32
arr[2] = a1 + 33
arr[3] = a1 + 34
arr[4] = a1 + 35
arr[5] = a1 + 36
arr[6] = a0 + 37
arr[7] = a1 + 38
arr[8] = a1 + 39
arr[9] = a1 + 40
arr[10] = a1 + 41
arr[11] = a1 + 42
arr[12] = a1 + 43
arr[2] = arr[0] + arr[1]
arr[4] = arr[2] + arr[3]
arr[6] = arr[4] + arr[5]
arr[8] = arr[6] + arr[7]
arr[10] = arr[8] + arr[9]
arr[12] = arr[10] + arr[11]
arr[1] = arr[1] * 3
arr[1] = arr[1] + 11
return arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], arr[6], arr[7], arr[8], arr[9], arr[10], arr[11], arr[12]
}
編譯并輸出到 main.s
文件中烟零。go 版本為 go1.18.7 linux/amd64
。
go tool compile -S main.go > main.s
在 go 代碼中咸作,main 函數(shù)首先調(diào)用 test 函數(shù)锨阿,傳遞了 13 個(gè) int 參數(shù),分別為 71~83 之間依次遞增的數(shù)字记罚。
在匯編代碼中墅诡,main 函數(shù)通過 SUBQ $152, SP
開辟了 152 字節(jié)的棧空間桐智,然后將函數(shù)調(diào)用方的 BP
保存到棧底即 144(SP)
處末早,接著將 144(SP)
處設(shè)置為新的 BP
。FUNCDATA
指令和 PCDATA
指令與垃圾回收有關(guān)酵使,這里不討論荐吉。
main 函數(shù)的主要邏輯中焙糟,首先將 80 移動(dòng)到棧頂口渔,然后將 81 移動(dòng)到棧頂?shù)南乱粋€(gè) slot,將 82 和 83 移動(dòng)到接下來的 slot 中穿撮,接著依次將 71~79 移動(dòng)到 AX
~R11
寄存器中缺脉,然后調(diào)用 test 函數(shù)痪欲。在 test 函數(shù)返回之后,從 AX
~R11
攻礼、32(SP)~56(SP)
中取出返回值业踢,累加到 DX
寄存器上,隨機(jī)將 DX
中的值寫入 136(SP)
也就是 ret
變量礁扮,后面的一段代碼是內(nèi)聯(lián)的 println
代碼知举,printlock
與 printunlock
表示加鎖與解鎖,加鎖后將 136(SP)
寫入 AX
作為參數(shù)太伊,調(diào)用 runtime.printnl
函數(shù)輸出雇锡,忽略返回值。最后將 144(SP)
即調(diào)用方 BP
寫回 BP
僚焦,通過 ADDQ $152, SP
釋放 main 函數(shù)的椕烫幔空間,然后執(zhí)行 RET
指令恢復(fù) PC
"".main STEXT size=235 args=0x0 locals=0x98 funcid=0x0 align=0x0
0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $152-0
0x0000 00000 (main.go:3) LEAQ -24(SP), R12
0x0005 00005 (main.go:3) CMPQ R12, 16(R14)
0x0009 00009 (main.go:3) PCDATA $0, $-2
0x0009 00009 (main.go:3) JLS 225
0x000f 00015 (main.go:3) PCDATA $0, $-1
0x000f 00015 (main.go:3) SUBQ $152, SP
0x0016 00022 (main.go:3) MOVQ BP, 144(SP)
0x001e 00030 (main.go:3) LEAQ 144(SP), BP
0x0026 00038 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0026 00038 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0026 00038 (main.go:4) MOVQ $80, (SP)
0x002e 00046 (main.go:4) MOVQ $81, 8(SP)
0x0037 00055 (main.go:4) MOVQ $82, 16(SP)
0x0040 00064 (main.go:4) MOVQ $83, 24(SP)
0x0049 00073 (main.go:4) MOVL $71, AX
0x004e 00078 (main.go:4) MOVL $72, BX
0x0053 00083 (main.go:4) MOVL $73, CX
0x0058 00088 (main.go:4) MOVL $74, DI
0x005d 00093 (main.go:4) MOVL $75, SI
0x0062 00098 (main.go:4) MOVL $76, R8
0x0068 00104 (main.go:4) MOVL $77, R9
0x006e 00110 (main.go:4) MOVL $78, R10
0x0074 00116 (main.go:4) MOVL $79, R11
0x007a 00122 (main.go:4) PCDATA $1, $0
0x007a 00122 (main.go:4) CALL "".test(SB)
0x007f 00127 (main.go:5) LEAQ (BX)(AX*1), DX
0x0083 00131 (main.go:5) ADDQ CX, DX
0x0086 00134 (main.go:5) ADDQ DI, DX
0x0089 00137 (main.go:5) ADDQ SI, DX
0x008c 00140 (main.go:5) ADDQ R8, DX
0x008f 00143 (main.go:5) ADDQ R9, DX
0x0092 00146 (main.go:5) ADDQ R10, DX
0x0095 00149 (main.go:5) ADDQ R11, DX
0x0098 00152 (main.go:5) ADDQ 32(SP), DX
0x009d 00157 (main.go:5) ADDQ 40(SP), DX
0x00a2 00162 (main.go:5) ADDQ 48(SP), DX
0x00a7 00167 (main.go:5) ADDQ 56(SP), DX
0x00ac 00172 (main.go:5) MOVQ DX, "".ret+136(SP)
0x00b4 00180 (main.go:6) CALL runtime.printlock(SB)
0x00b9 00185 (main.go:6) MOVQ "".ret+136(SP), AX
0x00c1 00193 (main.go:6) CALL runtime.printint(SB)
0x00c6 00198 (main.go:6) CALL runtime.printnl(SB)
0x00cb 00203 (main.go:6) CALL runtime.printunlock(SB)
0x00d0 00208 (main.go:7) MOVQ 144(SP), BP
0x00d8 00216 (main.go:7) ADDQ $152, SP
0x00df 00223 (main.go:7) NOP
0x00e0 00224 (main.go:7) RET
0x00e1 00225 (main.go:7) NOP
0x00e1 00225 (main.go:3) PCDATA $1, $-1
0x00e1 00225 (main.go:3) PCDATA $0, $-2
0x00e1 00225 (main.go:3) CALL runtime.morestack_noctxt(SB)
0x00e6 00230 (main.go:3) PCDATA $0, $-1
0x00e6 00230 (main.go:3) JMP 0
test 函數(shù)中申請了一個(gè)長度為 13 的數(shù)組芳悲,首先將參數(shù)依次加上一個(gè)不同的值立肘,復(fù)制給數(shù)組中對應(yīng)順序的元素。
test 函數(shù)的匯編代碼如下名扛,首先通過 SUBQ $112, SP
開辟了 112 字節(jié)的椓履辏空間,然后將調(diào)用方也就是 main
函數(shù)的 BP
放入棧底 104(SP)
的位置肮韧,然后將 104(SP)
的地址設(shè)置為新的 BP
踢故。
接著開始計(jì)算 ,LEAQ 31(AX), R12
可以理解為 R12 = AX + 31
惹苗,隨后 MOVQ R12, "".arr(SP)
將 R12
保存到 (SP)
處殿较,即 arr[0]
,故這兩條匯編指令實(shí)現(xiàn)了 arr[0] = a0 + 31
桩蓉。接下來的匯編指令以此類推淋纲,使用 R12
或 DX
作為數(shù)據(jù)寄存器,存放中間結(jié)果院究,接著將結(jié)果保存到棧中洽瞬。有一個(gè)例外是 DI
,由于數(shù)據(jù)清零指令 DUFFZERO
會(huì)修改 DI
业汰,因此先將 DI
保存到 DX
伙窃,并用 DX
完成對應(yīng)的計(jì)算。從 arr[9] = a9 + 40
開始样漆,變成先 MOVQ "".a9+120(SP), DX
從棧中取出參數(shù)存到 DX
为障,然后 ADDQ $40, DX
對 DX
做加法,最后 MOVQ DX, "".arr+72(SP)
將結(jié)果寫入數(shù)組。
接下來是將數(shù)組中的兩個(gè)元素相加并賦值給另一個(gè)元素鳍怨,例如 arr[2] = arr[0] + arr[1]
呻右。匯編代碼中,首先 MOVQ "".arr(SP), DX
將 arr[0]
存入 DX
鞋喇,然后 ADDQ "".arr+8(SP), DX
將 8(SP)
添加到 DX
上声滥,最后 MOVQ DX, "".arr+16(SP)
將 DX
寫入 16(SP)
。
對于 arr[1] = arr[1] * 3
侦香,首先 MOVQ "".arr+8(SP), R13
將 arr[1]
存入 R13
落塑,接著 LEAQ (R13)(R13*2), R15
相當(dāng)于 R15=R13+R13*2
,從而實(shí)現(xiàn) 3 倍的乘法罐韩,然后 MOVQ R15, "".arr+8(SP)
將 R15
寫回棧中芜赌。
計(jì)算完成后依次將 a[0]
~ a[12]
寫入 AX
`R15`、`152(SP)`176(SP)
伴逸。最后 MOVQ 104(SP), BP
恢復(fù) BP
缠沈,ADDQ $112, SP
恢復(fù) SP
,RET
恢復(fù) PC
错蝴。
注1:RET
指令與 CALL
指令配對使用洲愤,每一個(gè)函數(shù)都應(yīng)該以 RET
結(jié)尾。CALL
指令首先將 CALL
指令的下一條指令入棧(等價(jià)于PUSH
顷锰,會(huì)修改 SP
)柬赐,然后修改 PC
跳轉(zhuǎn)到被調(diào)函數(shù)處執(zhí)行,被調(diào)函數(shù)返回時(shí)調(diào)用 RET
指令官紫,從棧中彈出 PC
(等價(jià)于 POP
肛宋,會(huì)修改SP
)從而執(zhí)行 CALL
指令的下一條指令。
注2:MOVUPS X15, "".arr(SP)
指令表示 arr
位于棧頂束世,X15
是零值寄存器酝陈,MOVUPS
從 X15
中取出 0,賦值到 arr 的前 16 個(gè)字節(jié)中毁涉。DUFFZERO
用于清空內(nèi)存地址沉帮,從而實(shí)現(xiàn)棧上變量的 0 值初始化。參數(shù)沒看懂贫堰。
"".test STEXT nosplit size=395 args=0x88 locals=0x70 funcid=0x0 align=0x0
0x0000 00000 (main.go:9) TEXT "".test(SB), NOSPLIT|ABIInternal, $112-136
0x0000 00000 (main.go:9) SUBQ $112, SP
0x0004 00004 (main.go:9) MOVQ BP, 104(SP)
0x0009 00009 (main.go:9) LEAQ 104(SP), BP
0x000e 00014 (main.go:9) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (main.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (main.go:9) FUNCDATA $5, "".test.arginfo1(SB)
0x000e 00014 (main.go:9) FUNCDATA $6, "".test.argliveinfo(SB)
0x000e 00014 (main.go:9) PCDATA $3, $1
0x000e 00014 (main.go:10) MOVUPS X15, "".arr(SP)
0x0013 00019 (main.go:9) MOVQ DI, DX
0x0016 00022 (main.go:10) LEAQ "".arr+8(SP), DI
0x001b 00027 (main.go:10) PCDATA $0, $-2
0x001b 00027 (main.go:10) LEAQ -32(DI), DI
0x001f 00031 (main.go:10) NOP
0x0020 00032 (main.go:10) DUFFZERO $331
0x0033 00051 (main.go:11) PCDATA $0, $-1
0x0033 00051 (main.go:11) LEAQ 31(AX), R12
0x0037 00055 (main.go:11) MOVQ R12, "".arr(SP)
0x003b 00059 (main.go:12) LEAQ 32(BX), R12
0x003f 00063 (main.go:12) MOVQ R12, "".arr+8(SP)
0x0044 00068 (main.go:13) LEAQ 33(CX), R12
0x0048 00072 (main.go:13) MOVQ R12, "".arr+16(SP)
0x004d 00077 (main.go:14) ADDQ $34, DX
0x0051 00081 (main.go:14) MOVQ DX, "".arr+24(SP)
0x0056 00086 (main.go:15) LEAQ 35(SI), DX
0x005a 00090 (main.go:15) MOVQ DX, "".arr+32(SP)
0x005f 00095 (main.go:16) LEAQ 36(R8), DX
0x0063 00099 (main.go:16) MOVQ DX, "".arr+40(SP)
0x0068 00104 (main.go:17) LEAQ 37(R9), DX
0x006c 00108 (main.go:17) MOVQ DX, "".arr+48(SP)
0x0071 00113 (main.go:18) LEAQ 38(R10), DX
0x0075 00117 (main.go:18) MOVQ DX, "".arr+56(SP)
0x007a 00122 (main.go:19) LEAQ 39(R11), DX
0x007e 00126 (main.go:19) MOVQ DX, "".arr+64(SP)
0x0083 00131 (main.go:20) MOVQ "".a9+120(SP), DX
0x0088 00136 (main.go:20) ADDQ $40, DX
0x008c 00140 (main.go:20) MOVQ DX, "".arr+72(SP)
0x0091 00145 (main.go:21) MOVQ "".a10+128(SP), DX
0x0099 00153 (main.go:21) ADDQ $41, DX
0x009d 00157 (main.go:21) MOVQ DX, "".arr+80(SP)
0x00a2 00162 (main.go:22) MOVQ "".a11+136(SP), DX
0x00aa 00170 (main.go:22) ADDQ $42, DX
0x00ae 00174 (main.go:22) MOVQ DX, "".arr+88(SP)
0x00b3 00179 (main.go:23) MOVQ "".a12+144(SP), DX
0x00bb 00187 (main.go:23) ADDQ $43, DX
0x00bf 00191 (main.go:23) MOVQ DX, "".arr+96(SP)
0x00c4 00196 (main.go:25) MOVQ "".arr(SP), DX
0x00c8 00200 (main.go:25) ADDQ "".arr+8(SP), DX
0x00cd 00205 (main.go:25) MOVQ DX, "".arr+16(SP)
0x00d2 00210 (main.go:26) MOVQ "".arr+24(SP), R12
0x00d7 00215 (main.go:26) ADDQ R12, DX
0x00da 00218 (main.go:26) MOVQ DX, "".arr+32(SP)
0x00df 00223 (main.go:27) MOVQ "".arr+40(SP), R12
0x00e4 00228 (main.go:27) ADDQ R12, DX
0x00e7 00231 (main.go:27) MOVQ DX, "".arr+48(SP)
0x00ec 00236 (main.go:28) MOVQ "".arr+56(SP), R12
0x00f1 00241 (main.go:28) ADDQ R12, DX
0x00f4 00244 (main.go:28) MOVQ DX, "".arr+64(SP)
0x00f9 00249 (main.go:29) MOVQ "".arr+72(SP), R12
0x00fe 00254 (main.go:29) ADDQ R12, DX
0x0101 00257 (main.go:29) MOVQ DX, "".arr+80(SP)
0x0106 00262 (main.go:30) MOVQ "".arr+88(SP), R12
0x010b 00267 (main.go:30) ADDQ DX, R12
0x010e 00270 (main.go:30) MOVQ R12, "".arr+96(SP)
0x0113 00275 (main.go:32) MOVQ "".arr+8(SP), R13
0x0118 00280 (main.go:32) LEAQ (R13)(R13*2), R15
0x011d 00285 (main.go:32) MOVQ R15, "".arr+8(SP)
0x0122 00290 (main.go:33) LEAQ (R13)(R13*2), BX
0x0127 00295 (main.go:33) LEAQ 11(BX), BX
0x012b 00299 (main.go:33) MOVQ BX, "".arr+8(SP)
0x0130 00304 (main.go:35) MOVQ "".arr(SP), AX
0x0134 00308 (main.go:35) MOVQ "".arr+16(SP), CX
0x0139 00313 (main.go:35) MOVQ "".arr+24(SP), DI
0x013e 00318 (main.go:35) MOVQ "".arr+32(SP), SI
0x0143 00323 (main.go:35) MOVQ "".arr+40(SP), R8
0x0148 00328 (main.go:35) MOVQ "".arr+48(SP), R9
0x014d 00333 (main.go:35) MOVQ "".arr+56(SP), R10
0x0152 00338 (main.go:35) MOVQ "".arr+64(SP), R11
0x0157 00343 (main.go:35) MOVQ "".arr+72(SP), R13
0x015c 00348 (main.go:35) MOVQ "".arr+88(SP), R15
0x0161 00353 (main.go:35) MOVQ R13, "".~r9+152(SP)
0x0169 00361 (main.go:35) MOVQ DX, "".~r10+160(SP)
0x0171 00369 (main.go:35) MOVQ R15, "".~r11+168(SP)
0x0179 00377 (main.go:35) MOVQ R12, "".~r12+176(SP)
0x0181 00385 (main.go:35) MOVQ 104(SP), BP
0x0186 00390 (main.go:35) ADDQ $112, SP
0x018a 00394 (main.go:35) RET
棧幀圖示
上述代碼中兩個(gè)函數(shù)對應(yīng)的棧幀可以由下圖表示穆壕。
左邊為 main 函數(shù)的棧幀,從高地址到低地址依次為:
- main 函數(shù)的調(diào)用者的
PC
:這是由CALL
指令自動(dòng)壓入棧的其屏,并因此導(dǎo)致調(diào)用者的SP
增加了 8 字節(jié)喇勋。 - main 函數(shù)的調(diào)用者的
BP
:main 函數(shù)在申請棧空間后偎行,將上一個(gè)函數(shù)的BP
放到棧底川背,并且調(diào)整BP
指向該位置贰拿。 - main 函數(shù)的局部變量
ret
:占 8 個(gè)字節(jié)的int
類型。 - 寄存器參數(shù)的溢出區(qū)域:如果被調(diào)函數(shù)中參數(shù)非常多渗常,寄存器不夠用了,寄存器參數(shù)就存到這些位置汗盘。
- 棧上返回值:存放在棧上的 test 函數(shù)的返回值皱碘。
- 棧上入?yún)ⅲ捍娣旁跅I系?test 函數(shù)的入?yún)ⅰ?/li>
右邊為 test 函數(shù)的棧幀,從高地址到低地址依次為:
- main 函數(shù)的
PC
:這是由CALL
指令自動(dòng)壓入棧的隐孽,并因此導(dǎo)致SP
增加了 8 字節(jié)癌椿。 - main 函數(shù)的
BP
:test 函數(shù)在申請棧空間后菱阵,將 main 函數(shù)的BP
放到棧底踢俄,并且調(diào)整BP
指向該位置。 - test 函數(shù)的局部變量
arr
:一共 13 個(gè)int
類型的數(shù)組晴及。
由于沒有調(diào)用其他函數(shù)都办,因此 test 函數(shù)的棧幀中沒有參數(shù)、返回值虑稼、寄存器溢出區(qū)琳钉。
值得注意的是,這兩個(gè)函數(shù)的棧幀在內(nèi)存中是連續(xù)的蛛倦,即途中虛線連接的部分歌懒,在內(nèi)存中是完全相同的區(qū)域,只是由于兩個(gè)函數(shù)的 SP
值不同溯壶,相對的偏移量也不同及皂。
函數(shù)的參數(shù)與返回值存放在調(diào)用方的棧幀中,棧上變量的訪問通常不是使用 PUSH/POP
而是使用 SP
加上偏移量訪問且改,類似于數(shù)組验烧。
棧的平衡由被調(diào)方負(fù)責(zé),編譯器計(jì)算函數(shù)所需的椨瞩耍空間噪窘,函數(shù)首先將 SP
減去所需棧空間用于調(diào)整棧頂效扫,實(shí)現(xiàn)“批量入椌蠹啵”的效果,隨后將調(diào)用方的 BP
保存到棧底菌仁。函數(shù)返回之前先從棧中恢復(fù) BP
浩习,然后將 SP
加上最初減去的值,實(shí)現(xiàn)“批量出椉们穑”的效果谱秽。
函數(shù)的調(diào)用與返回過程中的跳轉(zhuǎn)由 CALL/RET
指令負(fù)責(zé)洽蛀。CALL
指令首先將 CALL
指令的下一條指令的地址通過 PUSH
入棧,然后 JUMP
到被調(diào)函數(shù)的地址(修改 PC
)運(yùn)行疟赊。被調(diào)函數(shù)返回之前最后一條指令是 RET
郊供,它將棧中存放的指令地址 POP
到 PC
寄存器中,跳轉(zhuǎn)到 CALL
指令的下一條指令開始執(zhí)行近哟。
小結(jié)
Golang 函數(shù)調(diào)用過程中同時(shí)使用寄存器與棧進(jìn)行參數(shù)傳遞驮审,優(yōu)先使用寄存器,寄存器不夠用了才使用棧吉执。函數(shù)的第 1~9 個(gè)參數(shù)依次使用 AX疯淫、BX、CX戳玫、DI熙掺、SI、R8咕宿、R9币绩、R10、R11 寄存器府阀。從第 10 個(gè)參數(shù)開始类浪,使用棧傳遞,并且是從棧頂向棧低依次排列肌似。執(zhí)行順序方面费就,入棧操作先執(zhí)行,然后再對寄存器賦值川队。由于寄存器溢出區(qū)域的存在力细,寄存器傳參并不會(huì)減小棧內(nèi)存占用。
使用寄存器傳遞參數(shù)指令數(shù)量比使用棧傳遞參數(shù)更少固额。在上面的例子中眠蚂,使用寄存器傳遞的參數(shù)在計(jì)算時(shí)可以直接計(jì)算,然后存到棧上斗躏,而使用棧傳遞的參數(shù)則需要先從棧中加載到寄存器上逝慧,然后計(jì)算,最后存到棧上啄糙。更重要的是笛臣,寄存器的讀寫速度遠(yuǎn)高于 CPU Cache 與內(nèi)存,而棧則是存放在內(nèi)存中隧饼。一組具有代表性的 Go 包和程序的基準(zhǔn)顯示性能提高了約 5%沈堡,二進(jìn)制文件大小通常減少了約 2%。