go 函数调用

77 阅读10分钟

go 1.17之前

go1.17之前是用栈来传递参数的,下面实验证明,实验环境为

go version go1.14.2 windows/amd64

实验代码为

package main

func main() {
	add(100, 200)
}

func add(a, b int) int {
	c := a + b
	return c
}

经过go tool compil -N -S -l main.go后得到输出如下,删减了一些无关代码

// main 函数
0x0016 00022 (main1.go:3)       SUBQ    $32, SP
0x001a 00026 (main1.go:3)       MOVQ    BP, 24(SP)
0x001f 00031 (main1.go:3)       LEAQ    24(SP), BP
0x0024 00036 (main1.go:4)       MOVQ    $100, (SP)
0x002c 00044 (main1.go:4)       MOVQ    $200, 8(SP)
0x0035 00053 (main1.go:4)       CALL    "".add(SB)
0x003a 00058 (main1.go:5)       MOVQ    24(SP), BP
0x003f 00063 (main1.go:5)       ADDQ    $32, SP
0x0043 00067 (main1.go:5)       RET

// add 函数
0x0000 00000 (main1.go:7)       SUBQ    $16, SP
0x0004 00004 (main1.go:7)       MOVQ    BP, 8(SP)
0x0009 00009 (main1.go:7)       LEAQ    8(SP), BP
0x000e 00014 (main1.go:7)       MOVQ    $0, "".~r2+40(SP)
0x0017 00023 (main1.go:8)       MOVQ    "".a+24(SP), AX
0x001c 00028 (main1.go:8)       ADDQ    "".b+32(SP), AX
0x0021 00033 (main1.go:8)       MOVQ    AX, "".c(SP)
0x0025 00037 (main1.go:9)       MOVQ    AX, "".~r2+40(SP)
0x002a 00042 (main1.go:9)       MOVQ    8(SP), BP
0x002f 00047 (main1.go:9)       ADDQ    $16, SP
0x0033 00051 (main1.go:9)       RET

接下来按照代码的执行顺序,画出对应的栈图。代码顺序大致为

  1. main函数栈空间分配
  2. main函数调用add函数
  3. add函数栈空间分配
  4. add函数计算
  5. add函数返回
  6. main函数返回

main函数栈空间分配

0x0016 00022 (main1.go:3)       SUBQ    $32, SP		// 申请了32字节大小的栈空间
0x001a 00026 (main1.go:3)       MOVQ    BP, 24(SP)	// 保存BP
0x001f 00031 (main1.go:3)       LEAQ    24(SP), BP	// 设置当前栈帧的栈底

这三行代码的作用就是申请栈空间,并保留BP寄存器的值,执行完之后,当前栈中数据为

未命名绘图.png

main调用add函数

0x0024 00036 (main1.go:4)       MOVQ    $100, (SP)
0x002c 00044 (main1.go:4)       MOVQ    $200, 8(SP)
0x0035 00053 (main1.go:4)       CALL    "".add(SB)

前两行代码的作用就是将传递给add函数的100,和200参数值放到栈中,参数顺序是按照函数声明从左到右,从下到上存储,然后通过CALL指令调用add函数,此时栈中数据为
未命名绘图-第 2 页.png

add函数栈空间分配

0x0000 00000 (main1.go:7)       SUBQ    $16, SP
0x0004 00004 (main1.go:7)       MOVQ    BP, 8(SP)
0x0009 00009 (main1.go:7)       LEAQ    8(SP), BP

add函数栈空间分配和前面是一样的,add函数申请了16字节的栈空间,此时栈中数据如下图所示(注:个人认为函数栈空间应该仅仅包含通过SUBQ指令获取的栈空间,但是这个返回地址并没有显式声明在caller和callee的栈空间里面,如果有知道具体的朋友还请不吝赐教)
未命名绘图-第 3 页.png
此时的SP+16到SP+48这段栈空间是属于main函数的,而SP到SP+16是add函数的栈空间的,而中间的sp+16到sp+24这段栈空间存储的是返回地址,用于add函数返回时,跳转回main函数

add函数计算

0x000e 00014 (main1.go:7)       MOVQ    $0, "".~r2+40(SP)	// 返回值初始化
0x0017 00023 (main1.go:8)       MOVQ    "".a+24(SP), AX		// 参数a处理
0x001c 00028 (main1.go:8)       ADDQ    "".b+32(SP), AX		// 参数b,执行加法运算
0x0021 00033 (main1.go:8)       MOVQ    AX, "".c(SP)		// 将运算结果存储到变量c中
0x0025 00037 (main1.go:9)       MOVQ    AX, "".~r2+40(SP)	// 将运算结果放到返回值中

这5行汇编代码就是add函数的主体了,add函数执行完成后栈空间数据为
未命名绘图-第 4 页.png
sp位置就是变量c,而sp+40即为返回值的地址,如果main函数中接收了add函数的返回值,就会从sp+40这个位置取值,汇编代码为MOVQ 40(sp), AX

add函数返回

0x002a 00042 (main1.go:9)       MOVQ    8(SP), BP
0x002f 00047 (main1.go:9)       ADDQ    $16, SP
0x0033 00051 (main1.go:9)       RET

这三行代码对应于之前分配栈空间的代码,恢复BP寄存器,释放之前申请的16字节栈空间

main函数返回

0x003a 00058 (main1.go:5)       MOVQ    24(SP), BP
0x003f 00063 (main1.go:5)       ADDQ    $32, SP
0x0043 00067 (main1.go:5)       RET

同上,main函数恢复BP寄存器,释放之前申请的32字节栈空间

go1.17及之后

go1.17开始,go也采用寄存器传参为主,堆栈辅助的方式,以下是实验环境

go version go1.17.4 windows/amd64

运行go tool compile -N -S -l main.go之后得到的汇编代码如下,删减了一些无关的代码

// main 函数
0x0006 00006 (main1.go:3)       SUBQ    $24, SP
0x000a 00010 (main1.go:3)       MOVQ    BP, 16(SP)
0x000f 00015 (main1.go:3)       LEAQ    16(SP), BP
0x0014 00020 (main1.go:4)       MOVL    $100, AX  	// 第一个参数放到AX寄存器
0x0019 00025 (main1.go:4)       MOVL    $200, BX	// 第二个参数放到BX寄存器
0x0020 00032 (main1.go:4)       CALL    "".add(SB)
0x0025 00037 (main1.go:5)       MOVQ    16(SP), BP
0x002a 00042 (main1.go:5)       ADDQ    $24, SP
0x002e 00046 (main1.go:5)       RET

// add 函数
0x0000 00000 (main1.go:7)       SUBQ    $24, SP
0x0004 00004 (main1.go:7)       MOVQ    BP, 16(SP)
0x0009 00009 (main1.go:7)       LEAQ    16(SP), BP
0x000e 00014 (main1.go:7)       MOVQ    AX, "".a+32(SP)	// 读取第一个参是放到sp+32
0x0013 00019 (main1.go:7)       MOVQ    BX, "".b+40(SP)	// 读取第二个参数放到sp+40
0x0018 00024 (main1.go:7)       MOVQ    $0, "".~r2(SP)
0x0020 00032 (main1.go:8)       MOVQ    "".a+32(SP), AX	
0x0025 00037 (main1.go:8)       ADDQ    "".b+40(SP), AX	// 执行加法运算
0x002a 00042 (main1.go:8)       MOVQ    AX, "".c+8(SP)
0x002f 00047 (main1.go:9)       MOVQ    AX, "".~r2(SP)
0x0033 00051 (main1.go:9)       MOVQ    16(SP), BP
0x0038 00056 (main1.go:9)       ADDQ    $24, SP
0x003c 00060 (main1.go:9)       RET

在第5,6行,main函数将参数放到了AX,BX寄存器中,然后在第7行调用add函数,下图是add函数返回前的栈,其中sp+32到sp+56是main函数申请的24字节栈空间,而sp到sp+24是add函数申请的24字节栈空间。对于上面的代码有一点疑惑的点是,既然通过寄存器传参,为什么还多此一举将参数放到栈上(第16行和第17行),这是因为这里的汇编代码还没有经过编译器优化(被-N参数禁止了),如果去掉后就可以发现这两行代码没有了,整个add函数更简洁
未命名绘图-第 5 页.png
寄存器数量是有限的,接下来验证参数数量很多的时候,是如何传参的

参数数量很多的时候

实验代码如下

func main() {
	a, b, c, d, e, f, g, h, i, j, k, l, m, n := test(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
	print(a, b, c, d, e, f, g, h, i, j, k, l, m, n)
}

func test(a, b, c, d, e, f, g, h, i, j, k, l, m, n int) (int, int, int, int, int, int, int, int, int, int, int, int, int, int) {
	return a, b, c, d, e, f, g, h, i, j, k, l, m, n
}

代码中test函数共有14个参数,14个返回值,编译出汇编代码如下,删除了一些无关代码

// main函数汇编

// 1. 申请栈空间
0x0012 00018 (main1.go:3)       SUBQ    $392, SP
0x0019 00025 (main1.go:3)       MOVQ    BP, 384(SP)
0x0021 00033 (main1.go:3)       LEAQ    384(SP), BP

// 2. 传递参数
0x0029 00041 (main1.go:4)       MOVQ    $10, (SP)	// 第10个参数
0x0031 00049 (main1.go:4)       MOVQ    $11, 8(SP)	// 第11个参数
0x003a 00058 (main1.go:4)       MOVQ    $12, 16(SP)	// 第12个参数
0x0043 00067 (main1.go:4)       MOVQ    $13, 24(SP)	// 第13个参数
0x004c 00076 (main1.go:4)       MOVQ    $14, 32(SP)	// 第14个参数
0x0055 00085 (main1.go:4)       MOVL    $1, AX		// 第1个参数
0x005a 00090 (main1.go:4)       MOVL    $2, BX		// 第2个参数
0x005f 00095 (main1.go:4)       MOVL    $3, CX		
0x0064 00100 (main1.go:4)       MOVL    $4, DI
0x0069 00105 (main1.go:4)       MOVL    $5, SI
0x006e 00110 (main1.go:4)       MOVL    $6, R8
0x0074 00116 (main1.go:4)       MOVL    $7, R9
0x007a 00122 (main1.go:4)       MOVL    $8, R10
0x0080 00128 (main1.go:4)       MOVL    $9, R11		// 第9个参数
0x0086 00134 (main1.go:4)       CALL    "".test(SB)	// 调用test函数

// 3. 处理返回值
// 40(SP)到72(SP)存储的是test函数最后5个返回值
0x008b 00139 (main1.go:4)       MOVQ    40(SP), DX
0x0090 00144 (main1.go:4)       MOVQ    48(SP), R12
0x0095 00149 (main1.go:4)       MOVQ    56(SP), R13
0x009a 00154 (main1.go:4)       MOVQ    64(SP), R15
0x009f 00159 (main1.go:4)       MOVQ    R15, ""..autotmp_28+264(SP)
0x00a7 00167 (main1.go:4)       MOVQ    72(SP), R15
// 下面的代码从寄存器中获取返回值,放到栈上
0x00ac 00172 (main1.go:4)       MOVQ    AX, ""..autotmp_14+376(SP)
0x00b4 00180 (main1.go:4)       MOVQ    BX, ""..autotmp_15+368(SP)
0x00bc 00188 (main1.go:4)       MOVQ    CX, ""..autotmp_16+360(SP)
0x00c4 00196 (main1.go:4)       MOVQ    DI, ""..autotmp_17+352(SP)
0x00cc 00204 (main1.go:4)       MOVQ    SI, ""..autotmp_18+344(SP)
0x00d4 00212 (main1.go:4)       MOVQ    R8, ""..autotmp_19+336(SP)
0x00dc 00220 (main1.go:4)       MOVQ    R9, ""..autotmp_20+328(SP)
0x00e4 00228 (main1.go:4)       MOVQ    R10, ""..autotmp_21+320(SP)
0x00ec 00236 (main1.go:4)       MOVQ    R11, ""..autotmp_22+312(SP)
0x00f4 00244 (main1.go:4)       MOVQ    DX, ""..autotmp_23+304(SP)
0x00fc 00252 (main1.go:4)       MOVQ    R12, ""..autotmp_24+296(SP)
0x0104 00260 (main1.go:4)       MOVQ    R13, ""..autotmp_25+288(SP)
0x010c 00268 (main1.go:4)       MOVQ    ""..autotmp_28+264(SP), DX
0x0114 00276 (main1.go:4)       MOVQ    DX, ""..autotmp_26+280(SP)
0x011c 00284 (main1.go:4)       MOVQ    R15, ""..autotmp_27+272(SP)
// 下面将参数放到对应的变量位置上
0x0124 00292 (main1.go:4)       MOVQ    ""..autotmp_14+376(SP), DX
0x012c 00300 (main1.go:4)       MOVQ    DX, "".a+256(SP)
0x0134 00308 (main1.go:4)       MOVQ    ""..autotmp_15+368(SP), DX
0x013c 00316 (main1.go:4)       MOVQ    DX, "".b+248(SP)
0x0144 00324 (main1.go:4)       MOVQ    ""..autotmp_16+360(SP), DX
0x014c 00332 (main1.go:4)       MOVQ    DX, "".c+240(SP)
0x0154 00340 (main1.go:4)       MOVQ    ""..autotmp_17+352(SP), DX
0x015c 00348 (main1.go:4)       MOVQ    DX, "".d+232(SP)
0x0164 00356 (main1.go:4)       MOVQ    ""..autotmp_18+344(SP), DX
0x016c 00364 (main1.go:4)       MOVQ    DX, "".e+224(SP)
0x0174 00372 (main1.go:4)       MOVQ    ""..autotmp_19+336(SP), DX
0x017c 00380 (main1.go:4)       MOVQ    DX, "".f+216(SP)
0x0184 00388 (main1.go:4)       MOVQ    ""..autotmp_20+328(SP), DX
0x018c 00396 (main1.go:4)       MOVQ    DX, "".g+208(SP)
0x0194 00404 (main1.go:4)       MOVQ    ""..autotmp_21+320(SP), DX
0x019c 00412 (main1.go:4)       MOVQ    DX, "".h+200(SP)
0x01a4 00420 (main1.go:4)       MOVQ    ""..autotmp_22+312(SP), DX
0x01ac 00428 (main1.go:4)       MOVQ    DX, "".i+192(SP)
0x01b4 00436 (main1.go:4)       MOVQ    ""..autotmp_23+304(SP), DX
0x01bc 00444 (main1.go:4)       MOVQ    DX, "".j+184(SP)
0x01c4 00452 (main1.go:4)       MOVQ    ""..autotmp_24+296(SP), DX
0x01cc 00460 (main1.go:4)       MOVQ    DX, "".k+176(SP)
0x01d4 00468 (main1.go:4)       MOVQ    ""..autotmp_25+288(SP), DX
0x01dc 00476 (main1.go:4)       MOVQ    DX, "".l+168(SP)
0x01e4 00484 (main1.go:4)       MOVQ    ""..autotmp_26+280(SP), DX
0x01ec 00492 (main1.go:4)       MOVQ    DX, "".m+160(SP)
0x01f4 00500 (main1.go:4)       MOVQ    ""..autotmp_27+272(SP), DX
0x01fc 00508 (main1.go:4)       MOVQ    DX, "".n+152(SP)
... // 这里是调用print的代码,省略
0x02cb 00715 (main1.go:6)       MOVQ    384(SP), BP
0x02d3 00723 (main1.go:6)       ADDQ    $392, SP
0x02da 00730 (main1.go:6)       RET
// test 函数

// 1. 申请栈空间
0x0000 00000 (main1.go:8)       SUBQ    $80, SP
0x0004 00004 (main1.go:8)       MOVQ    BP, 72(SP)
0x0009 00009 (main1.go:8)       LEAQ    72(SP), BP

// 2. 获取参数放到栈上
0x000e 00014 (main1.go:8)       MOVQ    AX, "".a+168(SP)
0x0016 00022 (main1.go:8)       MOVQ    BX, "".b+176(SP)
0x001e 00030 (main1.go:8)       MOVQ    CX, "".c+184(SP)
0x0026 00038 (main1.go:8)       MOVQ    DI, "".d+192(SP)
0x002e 00046 (main1.go:8)       MOVQ    SI, "".e+200(SP)
0x0036 00054 (main1.go:8)       MOVQ    R8, "".f+208(SP)
0x003e 00062 (main1.go:8)       MOVQ    R9, "".g+216(SP)
0x0046 00070 (main1.go:8)       MOVQ    R10, "".h+224(SP)
0x004e 00078 (main1.go:8)       MOVQ    R11, "".i+232(SP)
// 3. 这里是初始化返回值,总共14个返回值,在栈空间上
0x0056 00086 (main1.go:8)       MOVQ    $0, "".~r14+64(SP)
0x005f 00095 (main1.go:8)       MOVQ    $0, "".~r15+56(SP)
0x0068 00104 (main1.go:8)       MOVQ    $0, "".~r16+48(SP)
0x0071 00113 (main1.go:8)       MOVQ    $0, "".~r17+40(SP)
0x007a 00122 (main1.go:8)       MOVQ    $0, "".~r18+32(SP)
0x0083 00131 (main1.go:8)       MOVQ    $0, "".~r19+24(SP)
0x008c 00140 (main1.go:8)       MOVQ    $0, "".~r20+16(SP)
0x0095 00149 (main1.go:8)       MOVQ    $0, "".~r21+8(SP)
0x009e 00158 (main1.go:8)       MOVQ    $0, "".~r22(SP)
0x00a6 00166 (main1.go:8)       MOVQ    $0, "".~r23+128(SP)
0x00b2 00178 (main1.go:8)       MOVQ    $0, "".~r24+136(SP)
0x00be 00190 (main1.go:8)       MOVQ    $0, "".~r25+144(SP)
0x00ca 00202 (main1.go:8)       MOVQ    $0, "".~r26+152(SP)
0x00d6 00214 (main1.go:8)       MOVQ    $0, "".~r27+160(SP)

// 4. 将返回值放在对应的位置上
0x00e2 00226 (main1.go:9)       MOVQ    "".a+168(SP), DX
0x00ea 00234 (main1.go:9)       MOVQ    DX, "".~r14+64(SP)
0x00ef 00239 (main1.go:9)       MOVQ    "".b+176(SP), DX
0x00f7 00247 (main1.go:9)       MOVQ    DX, "".~r15+56(SP)
0x00fc 00252 (main1.go:9)       MOVQ    "".c+184(SP), DX
0x0104 00260 (main1.go:9)       MOVQ    DX, "".~r16+48(SP)
0x0109 00265 (main1.go:9)       MOVQ    "".d+192(SP), DX
0x0111 00273 (main1.go:9)       MOVQ    DX, "".~r17+40(SP)
0x0116 00278 (main1.go:9)       MOVQ    "".e+200(SP), DX
0x011e 00286 (main1.go:9)       MOVQ    DX, "".~r18+32(SP)
0x0123 00291 (main1.go:9)       MOVQ    "".f+208(SP), DX
0x012b 00299 (main1.go:9)       MOVQ    DX, "".~r19+24(SP)
0x0130 00304 (main1.go:9)       MOVQ    "".g+216(SP), DX
0x0138 00312 (main1.go:9)       MOVQ    DX, "".~r20+16(SP)
0x013d 00317 (main1.go:9)       MOVQ    "".h+224(SP), DX
0x0145 00325 (main1.go:9)       MOVQ    DX, "".~r21+8(SP)
0x014a 00330 (main1.go:9)       MOVQ    "".i+232(SP), DX
0x0152 00338 (main1.go:9)       MOVQ    DX, "".~r22(SP)
0x0156 00342 (main1.go:9)       MOVQ    "".j+88(SP), DX
0x015b 00347 (main1.go:9)       MOVQ    DX, "".~r23+128(SP)
0x0163 00355 (main1.go:9)       MOVQ    "".k+96(SP), DX
0x0168 00360 (main1.go:9)       MOVQ    DX, "".~r24+136(SP)
0x0170 00368 (main1.go:9)       MOVQ    "".l+104(SP), DX
0x0175 00373 (main1.go:9)       MOVQ    DX, "".~r25+144(SP)
0x017d 00381 (main1.go:9)       MOVQ    "".m+112(SP), DX
0x0182 00386 (main1.go:9)       MOVQ    DX, "".~r26+152(SP)
0x018a 00394 (main1.go:9)       MOVQ    "".n+120(SP), DX
0x018f 00399 (main1.go:9)       MOVQ    DX, "".~r27+160(SP)
// 这里可以看到,将前9个返回值放到了AX...R11寄存器上
0x0197 00407 (main1.go:9)       MOVQ    "".~r14+64(SP), AX
0x019c 00412 (main1.go:9)       MOVQ    "".~r15+56(SP), BX
0x01a1 00417 (main1.go:9)       MOVQ    "".~r16+48(SP), CX
0x01a6 00422 (main1.go:9)       MOVQ    "".~r17+40(SP), DI
0x01ab 00427 (main1.go:9)       MOVQ    "".~r18+32(SP), SI
0x01b0 00432 (main1.go:9)       MOVQ    "".~r19+24(SP), R8
0x01b5 00437 (main1.go:9)       MOVQ    "".~r20+16(SP), R9
0x01ba 00442 (main1.go:9)       MOVQ    "".~r21+8(SP), R10
0x01bf 00447 (main1.go:9)       MOVQ    "".~r22(SP), R11
0x01c3 00451 (main1.go:9)       MOVQ    72(SP), BP
0x01c8 00456 (main1.go:9)       ADDQ    $80, SP
0x01cc 00460 (main1.go:9)       RET

从上面的汇编代码可以看到,前9个参数依次放在了AX,BX,CX,DI,SI,R8,R9,R10,R11寄存器上,如果还有更多的参数,则会放在栈上。返回结果的时候也是如此

其他情况

既然寄存器个数为9,那如果参数为结构体,且大小不能被9整除的时候,会不会把参数分开呢,比如一个结构体大小为32个字节的时候
实验代码如下,这里我们定义了一个A结构体,大小为32字节,正好能被4个寄存器存放。同时定义了test函数,其参数为3个A结构体,看看会不会出现最后一个参数一部分在栈上,一部分在寄存器上

package main

func main() {
	test(A{10, 10, 10, 10}, A{20, 20, 20, 20}, A{30, 30, 30, 30})
}

func test(a, b, c A) (A, A, A) {
	return a, b, c
}

type A struct {
	a int
	b int
	c int
	d int
}

编译成汇编代码后,得到如下输出,这里就不展示全部的汇编代码了,只展示传参时候的代码,可以看到这里前两个参数都放在了寄存器上,最后一个参数放在了栈上

0x00ec 00236 (main1.go:4)       MOVQ    ""..autotmp_0+192(SP), AX
0x00f4 00244 (main1.go:4)       MOVQ    ""..autotmp_1+160(SP), SI
0x00fc 00252 (main1.go:4)       MOVQ    ""..autotmp_0+200(SP), BX
0x0104 00260 (main1.go:4)       MOVQ    ""..autotmp_1+168(SP), R8
0x010c 00268 (main1.go:4)       MOVQ    ""..autotmp_0+208(SP), CX
0x0114 00276 (main1.go:4)       MOVQ    ""..autotmp_1+176(SP), R9
0x011c 00284 (main1.go:4)       MOVQ    ""..autotmp_0+216(SP), DI
0x0124 00292 (main1.go:4)       MOVQ    ""..autotmp_1+184(SP), R10
0x012c 00300 (main1.go:4)       MOVQ    $30, (SP)
0x0134 00308 (main1.go:4)       MOVQ    $30, 8(SP)
0x013d 00317 (main1.go:4)       MOVQ    $30, 16(SP)
0x0146 00326 (main1.go:4)       MOVQ    $30, 24(SP)

可见,寄存器传参不会破坏结构体的完整性

参考资料
  1. 《Go语言设计与实现》4.1 函数调用
  2. 《Go语言高级编程》第3章 汇编语言