今天的学习由两个问题进入:

第一个问题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 语言中不支持多个返回值。

收工!