go1.17调用规约的改动

1,003 阅读2分钟

go 1.17,函数调用规约有更新,会使用九个寄存次存放参数和返回值(因为寄存器速度更快),而在go1.17之前,函数调用时的参数与返回值都会放在栈上,所以分别用go1.17go1.16.4来验证下 测试代码如下:

package main

func main() {
	callee(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
}

func callee(a, b, c, d, e, f, g, h, i, j int) (int, int, int, int, int, int, int, int, int, int) {
	localVar := 1  // 为了让接下来画callee的栈让读者更好理解,这里故意加了一个本地变量
	_ = localVar
	return 1, 2, 3, 4, 5, 6, 7, 8, 9, localVar
}

1.16.4

执行 go tool compile -S -N -l main.go
输出如下(删除了不重要的细节):

"".main STEXT size=170 args=0x0 locals=0xa8 funcid=0x0
        (...)
        
        0x002f 00047 (main.go:4)        MOVQ    $1, (SP)   // 传递给callee的第一个参数
        0x0037 00055 (main.go:4)        MOVQ    $2, 8(SP)
        0x0040 00064 (main.go:4)        MOVQ    $3, 16(SP)
        0x0049 00073 (main.go:4)        MOVQ    $4, 24(SP)
        0x0052 00082 (main.go:4)        MOVQ    $5, 32(SP)
        0x005b 00091 (main.go:4)        MOVQ    $6, 40(SP)
        0x0064 00100 (main.go:4)        MOVQ    $7, 48(SP)
        0x006d 00109 (main.go:4)        MOVQ    $8, 56(SP)
        0x0076 00118 (main.go:4)        MOVQ    $9, 64(SP)
        0x007f 00127 (main.go:4)        MOVQ    $10, 72(SP) // 传递给callee的最后一个参数
        
        (...)
        
"".callee STEXT nosplit size=254 args=0xa0 locals=0x10 funcid=0x0
        (...)
        
        0x007d 00125 (main.go:8)        MOVQ    $1, "".localVar(SP)
        0x0085 00133 (main.go:10)       MOVQ    $1, "".~r10+104(SP)  // 本地变量
        0x008e 00142 (main.go:10)       MOVQ    $2, "".~r11+112(SP)  // 第一个返回值
        0x0097 00151 (main.go:10)       MOVQ    $3, "".~r12+120(SP)
        0x00a0 00160 (main.go:10)       MOVQ    $4, "".~r13+128(SP)
        0x00ac 00172 (main.go:10)       MOVQ    $5, "".~r14+136(SP)
        0x00b8 00184 (main.go:10)       MOVQ    $6, "".~r15+144(SP)
        0x00c4 00196 (main.go:10)       MOVQ    $7, "".~r16+152(SP)
        0x00d0 00208 (main.go:10)       MOVQ    $8, "".~r17+160(SP)
        0x00dc 00220 (main.go:10)       MOVQ    $9, "".~r18+168(SP) // 最后一个返回值
        0x00e8 00232 (main.go:10)       MOVQ    "".localVar(SP), AX
        
        (...)

main和callee的栈布局如下:

image.png 可以看出,1,16版本的go是通过栈传递所有的参数和返回值的

1.17

执行 go tool compile -S -l main.go 这里没有加上-N

"".main STEXT size=103 args=0x0 locals=0x60 funcid=0x0
        (...)
        
        0x0014 00020 (main.go:4)        MOVQ    $10, (SP)  // 第10个参数
        0x001c 00028 (main.go:4)        MOVL    $1, AX     // 第1个参数
        0x0021 00033 (main.go:4)        MOVL    $2, BX
        0x0026 00038 (main.go:4)        MOVL    $3, CX
        0x002b 00043 (main.go:4)        MOVL    $4, DI
        0x0030 00048 (main.go:4)        MOVL    $5, SI
        0x0035 00053 (main.go:4)        MOVL    $6, R8
        0x003b 00059 (main.go:4)        MOVL    $7, R9
        0x0041 00065 (main.go:4)        MOVL    $8, R10
        0x0047 00071 (main.go:4)        MOVL    $9, R11    // 第9个参数
        (...)
"".callee STEXT nosplit size=59 args=0x58 locals=0x0 funcid=0x0
        (...)
        
        0x0000 00000 (main.go:10)       MOVQ    $10, "".~r19+16(SP)  // 第10个返回值
        0x0009 00009 (main.go:10)       MOVL    $1, AX      // 第1个返回值
        0x000e 00014 (main.go:10)       MOVL    $2, BX
        0x0013 00019 (main.go:10)       MOVL    $3, CX
        0x0018 00024 (main.go:10)       MOVL    $4, DI
        0x001d 00029 (main.go:10)       MOVL    $5, SI
        0x0022 00034 (main.go:10)       MOVL    $6, R8
        0x0028 00040 (main.go:10)       MOVL    $7, R9
        0x002e 00046 (main.go:10)       MOVL    $8, R10
        0x0034 00052 (main.go:10)       MOVL    $9, R11     // 第9个返回值
        0x003a 00058 (main.go:10)       RET
        
        (...)

可以看到,官方只使用了 9 个通用寄存器,依次是 AX,BX,CX,DI,SI,R8,R9,R10,R11,超出部分,按顺序放在栈上。