第一个问题Golang是怎么做到可以返回多个参数的?
第二个问题是为什么其他的语言不可以返回多个参数?
那么我们现在直接进入第一个问题,新写一些简单明了的代码:
package main
func main() {
myFunction(1, 2)
}
func myFunction(a int, b int) (int, int) {
return a + b, a - b
}
上述代码的目的很明确,我们就是要看看它是如何运作的。
使用命令生成并查看汇编代码:
GOOS=linux GOARCH=amd64 go tool compile -S -N -l main.go
需要注意如果不加上 -N -l 的参数,编译器会对汇编代码进行优化,编译结果会跟这里的差别非常大。
main函数的汇编:
"".main STEXT size=68 args=0x0 locals=0x28
0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $40-0
0x0000 00000 (main.go:3) MOVQ (TLS), CX
0x0009 00009 (main.go:3) CMPQ SP, 16(CX)
0x000d 00013 (main.go:3) JLS 61
0x000f 00015 (main.go:3) SUBQ $40, SP // 分配 40 字节栈空间
0x0013 00019 (main.go:3) MOVQ BP, 32(SP) // 将基址指针存储到栈上
0x0018 00024 (main.go:3) LEAQ 32(SP), BP
0x001d 00029 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:3) FUNCDATA $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:4) PCDATA $2, $0
0x001d 00029 (main.go:4) PCDATA $0, $0
0x001d 00029 (main.go:4) MOVQ $1, (SP) // 第一个参数
0x0025 00037 (main.go:4) MOVQ $2, 8(SP) // 第二个参数
0x002e 00046 (main.go:4) CALL "".myFunction(SB)
0x0033 00051 (main.go:5) MOVQ 32(SP), BP
0x0038 00056 (main.go:5) ADDQ $40, SP
0x003c 00060 (main.go:5) RET
0x003d 00061 (main.go:5) NOP
0x003d 00061 (main.go:3) PCDATA $0, $-1
0x003d 00061 (main.go:3) PCDATA $2, $-1
0x003d 00061 (main.go:3) CALL runtime.morestack_noctxt(SB)
0x0042 00066 (main.go:3) JMP 0
0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 2e 48 dH..%....H;a.v.H
0x0010 83 ec 28 48 89 6c 24 20 48 8d 6c 24 20 48 c7 04 ..(H.l$ H.l$ H..
0x0020 24 01 00 00 00 48 c7 44 24 08 02 00 00 00 e8 00 $....H.D$.......
0x0030 00 00 00 48 8b 6c 24 20 48 83 c4 28 c3 e8 00 00 ...H.l$ H..(....
0x0040 00 00 eb bc ....
rel 5+4 t=16 TLS+0
rel 47+4 t=8 "".myFunction+0
rel 62+4 t=8 runtime.morestack_noctxt+0
function汇编:
"".myFunction STEXT nosplit size=49 args=0x20 locals=0x0
0x0000 00000 (main.go:7) TEXT "".myFunction(SB), NOSPLIT|ABIInternal, $0-32
0x0000 00000 (main.go:7) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:7) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:7) FUNCDATA $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:7) PCDATA $2, $0
0x0000 00000 (main.go:7) PCDATA $0, $0
0x0000 00000 (main.go:7) MOVQ $0, "".~r2+24(SP) // 初始化第一个返回值
0x0009 00009 (main.go:7) MOVQ $0, "".~r3+32(SP) // 初始化第二个返回值
0x0012 00018 (main.go:8) MOVQ "".a+8(SP), AX // AX = 1
0x0017 00023 (main.go:8) ADDQ "".b+16(SP), AX // AX = AX + 2 = 3
0x001c 00028 (main.go:8) MOVQ AX, "".~r2+24(SP) // (24)SP = AX = 3
0x0021 00033 (main.go:8) MOVQ "".a+8(SP), AX // AX = 1
0x0026 00038 (main.go:8) SUBQ "".b+16(SP), AX // AX = AX - 2 = -1
0x002b 00043 (main.go:8) MOVQ AX, "".~r3+32(SP) // (32)SP = AX = -1
0x0030 00048 (main.go:8) RET
0x0000 48 c7 44 24 18 00 00 00 00 48 c7 44 24 20 00 00 H.D$.....H.D$ ..
0x0010 00 00 48 8b 44 24 08 48 03 44 24 10 48 89 44 24 ..H.D$.H.D$.H.D$
0x0020 18 48 8b 44 24 08 48 2b 44 24 10 48 89 44 24 20 .H.D$.H+D$.H.D$
0x0030 c3
可以看出Golang 两个参数以及两个返回值都是通过SP来保存的,那么也就是说是通过堆栈寄存器来保存的。所以可以做到返回多个,因为它跟参数传递使用的是一种东西啊!
那么我们来看看第二个问题,其他语言为什么不可以返回多个值:
这里我们选用c语言:
int main() {
my_function(1, 2);
}
int my_function(int arg1, int arg2) {
return arg1 + arg2;
}
main方法:
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $1, %edi // 处理第一个参数
movl $2, %esi // 处理第二个参数
callq _my_function
xorl %esi, %esi //传递参数
movl %eax, -4(%rbp) ## 4-byte Spill
movl %esi, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.globl _my_function ## -- Begin function my_function
.p2align 4, 0x90
function:
_my_function: ## @my_function
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %esi
addl -8(%rbp), %esi //arg1+arg2
movl %esi, %eax
popq %rbp
retq
.cfi_endproc
## -- End function
.subsections_via_symbols
我们可以看到在调用 my_function 函数之前,上述代码将该函数需要的两个参数分别存到了 edi 和 esi 寄存器中;在 my_function 执行时,它会先从寄存器中取出数据并放置到堆栈上,随后通过汇编指令在 eax 寄存器上进行计算,最后的结果其实是通过另一个寄存器 esi 返回的,main 函数在 my_function 返回之后将返回值存储到堆栈上的 i 变量中。
这里我们需要知道esi,edi,分别是16位寄存器DI和SI的32位扩展,这也是为什么不能传递多个变量的原因。
扩展问题,既然返回值不能返回多个,那参数也是使用单值寄存器,如何实现变量可传递N多个?难道使用N多个单值寄存器吗?
那么我们只需要修改代码在看一下汇编代码就可以了:
int main() {
my_function(1, 2, 3, 4, 5, 6, 7, 8);
}
int my_function(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8) {
return arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8;
}
main:
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
movl $1, %edi //参数1
movl $2, %esi //参数2
movl $3, %edx //参数3
movl $4, %ecx //参数4
movl $5, %r8d //参数5
movl $6, %r9d //参数6
movl $7, (%rsp) //参数7
movl $8, 8(%rsp) //参数8
callq _my_function
xorl %ecx, %ecx
movl %eax, -4(%rbp) ## 4-byte Spill
movl %ecx, %eax
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.globl _my_function ## -- Begin function my_function
.p2align 4, 0x90
可以看出前6个参数是通过:edi、esi、edx、ecx、r8d、r9b 而 第7和8个参数是通过rsp来传递的.这里的寄存器的使用顺序也是有讲究的,作为调用惯例,传递参数时第一个参数一定会放在 edi 寄存器中,第二个参数放在 esi 寄存器,以此类推;最后的第七和第八两个参数都通过栈进行传递
function:
_my_function: ## @my_function
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movl 24(%rbp), %eax
movl 16(%rbp), %r10d
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl %edx, -12(%rbp)
movl %ecx, -16(%rbp)
movl %r8d, -20(%rbp)
movl %r9d, -24(%rbp)
movl -4(%rbp), %ecx
addl -8(%rbp), %ecx
addl -12(%rbp), %ecx
addl -16(%rbp), %ecx
addl -20(%rbp), %ecx
addl -24(%rbp), %ecx
addl 16(%rbp), %ecx
addl 24(%rbp), %ecx
movl %eax, -28(%rbp) ## 4-byte Spill
movl %ecx, %eax
movl %r10d, -32(%rbp) ## 4-byte Spill
popq %rbp
retq
.cfi_endproc
简单总结一下,如果我们在 C 语言中调用一个函数,函数的参数是通过寄存器和栈传递的,在 x86_64 的机器上,6 个以下(含 6 个)的参数会按照顺序分别使用 edi、esi、edx、ecx、r8d 和 r9d 六个寄存器传递,超过 6 个的剩余参数会通过栈进行传递;函数的返回值是通过 eax 寄存器进行传递的,这也就是为什么 C 语言中不支持多个返回值。
收工!