Golang语言基础—函数调用

1. C语言的函数调用惯例

所谓“调用惯例(calling convention)”是调用方和被调用方对于函数调用的一个明确的约定,包括:函数参数与返回值的传递方式、传递顺序。只有双方都遵守同样的约定,函数才能被正确地调用和执行。如果不遵守这个约定,函数将无法正确执行。
C语言中,一般使用gcc将C语言编译成汇编代码是分析函数调用的最常见方式,比如以下的代码:

int my_function(int arg1, int arg2) {
    return arg1 + arg2;
}

int main() {
    int i = my_function(1, 2);
}

通过gcc -S main.c指令生成main.s:

        .file   "main.c"
        .text
        .globl  my_function
        .type   my_function, @function
my_function:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    %edi, -4(%rbp)    // 取出第一个参数放到栈上
        movl    %esi, -8(%rbp)    // 取出第二个参数放到栈上
        movl    -4(%rbp), %edx    // 设置edx = edi = 1
        movl    -8(%rbp), %eax    // 设置eax = esi = 2
        addl    %edx, %eax        // 返回值放在eax,eax = eax + edx = 3
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   my_function, .-my_function
        .globl  main
        .type   main, @function
main:
.LFB1:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        movl    $2, %esi    // 设置第二个参数
        movl    $1, %edi    // 设置第一个参数
        call    my_function
        movl    %eax, -4(%rbp)
        movl    $0, %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE1:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
        .section        .note.GNU-stack,"",@progbits

可以看到:

在调用my_function函数前,main函数将两个参数分别存到edi和esi两个寄存器中;
在调用时,最后通过edx和eax接收到入参,并计算值存入eax寄存器(C语言的返回值都是存储在eax寄存器的),然后返回;

如果参数过多会怎么样呢?我们试着将入参拓展到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;
}

int main() {
    int i = my_function(1, 2, 3, 4, 5, 6, 7, 8);

然后查看汇编代码,可以发现前6个参数放到寄存器中,但是后面的参数会通过栈传递。

main:
.LFB1:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        pushq   $8
        pushq   $7
        movl    $6, %r9d
        movl    $5, %r8d
        movl    $4, %ecx
        movl    $3, %edx
        movl    $2, %esi
        movl    $1, %edi
        call    my_function
        addq    $16, %rsp
        movl    %eax, -4(%rbp)
        movl    $0, %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc

可以总结,在x86_64的机器上使用C语言调用函数时:

  • 6个及以下的参数会按照顺序分别使用edi、esi、edx、ecx、r8d和r9d这六个寄存器传递;
  • 6个以上的参数传递会使用寄存器+栈,前六个参数会按照以上顺序使用寄存器,后面的会按照从右到左的顺序入栈。

2. Go语言的函数调用惯例

在Go v1.17版本之前,Go语言的函数调用是通过栈来传递参数的。根据存储山结构,CPU从寄存器上取值要比从内存取快几百倍,即使局部性高,L1 Cache的缓存命中率高,那也会比寄存器中取值速度慢4倍左右,所以栈传参大大限制了Go语言函数调用的速度。基于栈传递参数和接收返回值的设计大大降低了实现的复杂度,但是牺牲了函数调用的性能,在Go v1.17版本之后引入了寄存器传递函数传参。
我们直接以下面的例子来看一下Go语言的调用惯例:

package main

func myFunction(a, b, c, d, e, f, g, h, i, j, k, l 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
}

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

通过go tool compile -S -N -l main.go >> main.s得到汇编代码,可以看到,函数传参,前9个参数都是通过寄存器传入,超过9个的以上通过栈传递参数。

"".main STEXT size=118 args=0x0 locals=0x80 funcid=0x0
        0x0000 00000 (main.go:7)        TEXT    "".main(SB), ABIInternal, $128-0
        0x0000 00000 (main.go:7)        CMPQ    SP, 16(R14)
        0x0004 00004 (main.go:7)        PCDATA  $0, $-2
        0x0004 00004 (main.go:7)        JLS     111
        0x0006 00006 (main.go:7)        PCDATA  $0, $-1
        0x0006 00006 (main.go:7)        ADDQ    $-128, SP
        0x000a 00010 (main.go:7)        MOVQ    BP, 120(SP)
        0x000f 00015 (main.go:7)        LEAQ    120(SP), BP
        0x0014 00020 (main.go:7)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0014 00020 (main.go:7)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0014 00020 (main.go:8)        MOVQ    $10, (SP)    // 10入栈
        0x001c 00028 (main.go:8)        MOVQ    $11, 8(SP)   // 11入栈
        0x0025 00037 (main.go:8)        MOVQ    $12, 16(SP)  // 12入栈
        0x002e 00046 (main.go:8)        MOVL    $1, AX       // 1存入AX
        0x0033 00051 (main.go:8)        MOVL    $2, BX       // 2存入BX
        0x0038 00056 (main.go:8)        MOVL    $3, CX       // 3存入CX
        0x003d 00061 (main.go:8)        MOVL    $4, DI       // 4存入DI
        0x0042 00066 (main.go:8)        MOVL    $5, SI       // 5存入SI
        0x0047 00071 (main.go:8)        MOVL    $6, R8       // 6存入R8
        0x004d 00077 (main.go:8)        MOVL    $7, R9       // 7存入R9
        0x0053 00083 (main.go:8)        MOVL    $8, R10      // 8存入R10
        0x0059 00089 (main.go:8)        MOVL    $9, R11      // 9存入R11
        0x005f 00095 (main.go:8)        PCDATA  $1, $0
        0x005f 00095 (main.go:8)        NOP
        0x0060 00096 (main.go:8)        CALL    "".myFunction(SB)
        0x0065 00101 (main.go:9)        MOVQ    120(SP), BP
        0x006a 00106 (main.go:9)        SUBQ    $-128, SP
        0x006e 00110 (main.go:9)        RET

再看返回值,可以发现返回值也是前9个利用相同的寄存器返回的,但是如果返回值超过9,剩下的也是用栈返回的,注意是在入参栈的下面再开辟栈,所以不会占据传参的栈。

"".myFunction STEXT nosplit size=399 args=0x78 locals=0x50 funcid=0x0
   0x0000 00000 (main.go:3)   TEXT   "".myFunction(SB), NOSPLIT|ABIInternal, $80-120
   0x0000 00000 (main.go:3)   SUBQ   $80, SP // SP 先减去0x80
   0x0004 00004 (main.go:3)   MOVQ   BP, 72(SP)
   0x0009 00009 (main.go:3)   LEAQ   72(SP), BP
   0x000e 00014 (main.go:3)   FUNCDATA   $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
   0x000e 00014 (main.go:3)   FUNCDATA   $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
   0x000e 00014 (main.go:3)   FUNCDATA   $5, "".myFunction.arginfo1(SB)
   0x000e 00014 (main.go:3)   MOVQ   AX, "".a+136(SP)
   0x0016 00022 (main.go:3)   MOVQ   BX, "".b+144(SP)
   0x001e 00030 (main.go:3)   MOVQ   CX, "".c+152(SP)
   0x0026 00038 (main.go:3)   MOVQ   DI, "".d+160(SP)
   0x002e 00046 (main.go:3)   MOVQ   SI, "".e+168(SP)
   0x0036 00054 (main.go:3)   MOVQ   R8, "".f+176(SP)
   0x003e 00062 (main.go:3)   MOVQ   R9, "".g+184(SP)
   0x0046 00070 (main.go:3)   MOVQ   R10, "".h+192(SP)
   0x004e 00078 (main.go:3)   MOVQ   R11, "".i+200(SP)
   0x0056 00086 (main.go:3)   MOVQ   $0, "".~r12+64(SP)
   0x005f 00095 (main.go:3)   MOVQ   $0, "".~r13+56(SP)
   0x0068 00104 (main.go:3)   MOVQ   $0, "".~r14+48(SP)
   0x0071 00113 (main.go:3)   MOVQ   $0, "".~r15+40(SP)
   0x007a 00122 (main.go:3)   MOVQ   $0, "".~r16+32(SP)
   0x0083 00131 (main.go:3)   MOVQ   $0, "".~r17+24(SP)
   0x008c 00140 (main.go:3)   MOVQ   $0, "".~r18+16(SP)
   0x0095 00149 (main.go:3)   MOVQ   $0, "".~r19+8(SP)
   0x009e 00158 (main.go:3)   MOVQ   $0, "".~r20(SP)
   0x00a6 00166 (main.go:3)   MOVQ   $0, "".~r21+112(SP)
   0x00af 00175 (main.go:3)   MOVQ   $0, "".~r22+120(SP)
   0x00b8 00184 (main.go:3)   MOVQ   $0, "".~r23+128(SP)
   0x00c4 00196 (main.go:4)   MOVQ   "".a+136(SP), DX
   0x00cc 00204 (main.go:4)   MOVQ   DX, "".~r12+64(SP)
   0x00d1 00209 (main.go:4)   MOVQ   "".b+144(SP), DX
   0x00d9 00217 (main.go:4)   MOVQ   DX, "".~r13+56(SP)
   0x00de 00222 (main.go:4)   MOVQ   "".c+152(SP), DX
   0x00e6 00230 (main.go:4)   MOVQ   DX, "".~r14+48(SP)
   0x00eb 00235 (main.go:4)   MOVQ   "".d+160(SP), DX
   0x00f3 00243 (main.go:4)   MOVQ   DX, "".~r15+40(SP)
   0x00f8 00248 (main.go:4)   MOVQ   "".e+168(SP), DX
   0x0100 00256 (main.go:4)   MOVQ   DX, "".~r16+32(SP)
   0x0105 00261 (main.go:4)   MOVQ   "".f+176(SP), DX
   0x010d 00269 (main.go:4)   MOVQ   DX, "".~r17+24(SP)
   0x0112 00274 (main.go:4)   MOVQ   "".g+184(SP), DX
   0x011a 00282 (main.go:4)   MOVQ   DX, "".~r18+16(SP)
   0x011f 00287 (main.go:4)   MOVQ   "".h+192(SP), DX
   0x0127 00295 (main.go:4)   MOVQ   DX, "".~r19+8(SP)
   0x012c 00300 (main.go:4)   MOVQ   "".i+200(SP), DX
   0x0134 00308 (main.go:4)   MOVQ   DX, "".~r20(SP)
   0x0138 00312 (main.go:4)   MOVQ   "".j+88(SP), DX
   0x013d 00317 (main.go:4)   MOVQ   DX, "".~r21+112(SP)// 第10个返回值
   0x0142 00322 (main.go:4)   MOVQ   "".k+96(SP), DX
   0x0147 00327 (main.go:4)   MOVQ   DX, "".~r22+120(SP)// 第11个返回值
   0x014c 00332 (main.go:4)   MOVQ   "".l+104(SP), DX
   0x0151 00337 (main.go:4)   MOVQ   DX, "".~r23+128(SP)// 第12个返回值
   0x0159 00345 (main.go:4)   MOVQ   "".~r12+64(SP), AX // 第1个返回值
   0x015e 00350 (main.go:4)   MOVQ   "".~r13+56(SP), BX // 第2个返回值
   0x0163 00355 (main.go:4)   MOVQ   "".~r14+48(SP), CX // 第3个返回值
   0x0168 00360 (main.go:4)   MOVQ   "".~r15+40(SP), DI // 第4个返回值
   0x016d 00365 (main.go:4)   MOVQ   "".~r16+32(SP), SI // 第5个返回值
   0x0172 00370 (main.go:4)   MOVQ   "".~r17+24(SP), R8 // 第6个返回值
   0x0177 00375 (main.go:4)   MOVQ   "".~r18+16(SP), R9 // 第7个返回值
   0x017c 00380 (main.go:4)   MOVQ   "".~r19+8(SP), R10 // 第8个返回值
   0x0181 00385 (main.go:4)   MOVQ   "".~r20(SP), R11   // 第9个返回值
   0x0185 00389 (main.go:4)   MOVQ   72(SP), BP
   0x018a 00394 (main.go:4)   ADDQ   $80, SP
   0x018e 00398 (main.go:4)   RET

其中,Go使用的是Plan9汇编,其和C语言直接使用的x86_64的寄存器对比如下表:
Golang语言基础—函数调用_第1张图片
总结如下:

  • 当Go语言的函数传参和返回值在9个及以下时,按顺序使用AX、BX、CX、DI、SI、R8、R9、R10和R11作为传递的寄存器,注意传参和返回值一致;
  • 当Go语言的函数传参和返回值大于9个时,多于9个的部分使用栈传递;

2.1 结构体参数如何传参

当结构体中的参数能够被寄存器装下时,则采用寄存器传递结构体中的参数。

如下代码:

package main

type Request struct {
   a, b, c, d, e, f, g, h, i int
}

type Response struct {
   a, b, c, d, e, f, g, h, i int
}

func myFunction(req Request) Response {
   return Response{
      a: req.a,
      b: req.b,
      c: req.c,
      d: req.d,
      e: req.e,
      f: req.f,
      g: req.g,
      h: req.h,
      i: req.i,
   }
}

func main() {
   myFunction(Request{
      a: 1,
      b: 2,
      c: 3,
      d: 4,
      e: 5,
      f: 6,
      g: 7,
      h: 8,
      i: 9,
   })
}

编译后的代码是:

"".main STEXT size=91 args=0x0 locals=0x50 funcid=0x0
        ...
        0x0014 00020 (main.go:26)       MOVL    $1, AX
        0x0019 00025 (main.go:26)       MOVL    $2, BX
        0x001e 00030 (main.go:26)       MOVL    $3, CX
        0x0023 00035 (main.go:26)       MOVL    $4, DI
        0x0028 00040 (main.go:26)       MOVL    $5, SI
        0x002d 00045 (main.go:26)       MOVL    $6, R8
        0x0033 00051 (main.go:26)       MOVL    $7, R9
        0x0039 00057 (main.go:26)       MOVL    $8, R10
        0x003f 00063 (main.go:26)       MOVL    $9, R11
        0x0045 00069 (main.go:26)       PCDATA  $1, $0
        0x0045 00069 (main.go:26)       CALL    "".myFunction(SB)

如果我们增加一个结构体参数,就会看到以下的传参,通过DUFFCOPY拷贝到栈中。

"".main STEXT size=73 args=0x0 locals=0x58 funcid=0x0
        0x0000 00000 (main.go:25)       TEXT    "".main(SB), ABIInternal, $88-0
        0x0000 00000 (main.go:25)       CMPQ    SP, 16(R14)
        0x0004 00004 (main.go:25)       PCDATA  $0, $-2
        0x0004 00004 (main.go:25)       JLS     66
        0x0006 00006 (main.go:25)       PCDATA  $0, $-1
        0x0006 00006 (main.go:25)       SUBQ    $88, SP
        0x000a 00010 (main.go:25)       MOVQ    BP, 80(SP)
        0x000f 00015 (main.go:25)       LEAQ    80(SP), BP
        0x0014 00020 (main.go:25)       FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0014 00020 (main.go:25)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0014 00020 (main.go:38)       MOVQ    SP, DI
        0x0017 00023 (main.go:26)       LEAQ    ""..stmp_1(SB), SI
        0x001e 00030 (main.go:26)       PCDATA  $0, $-2
        0x001e 00030 (main.go:26)       NOP
        0x0020 00032 (main.go:26)       DUFFCOPY        $826
        0x0033 00051 (main.go:26)       PCDATA  $0, $-1
        0x0033 00051 (main.go:26)       PCDATA  $1, $0
        0x0033 00051 (main.go:26)       CALL    "".myFunction(SB)

2.2 浮点型如何传参

浮点型参数。由于amd 64架构中,浮点型数据的编码与整形数据编码大不相同,而浮点数的运算会使用专用寄存器和指令。所以浮点数不会使用这9个通用寄存器来传递,而是使用这15个XMM寄存器来传递。这组XMM寄存器是随着多媒体相关的指令集一起引入的,go 语言使用它们来处理浮点数。前15个浮点型参数会依次使用x0到x14这15个寄存器来传递。
如果还有就要使用栈来传递了。

3. Go语言的参数传递

Go中只有传值调用,没有传引用调用!
至于为什么有些操作看起来就像传指针一样,需要明确的是:

切片复制,结构体的底层指针指向同一个地址,所以修改切片已有值会影响原切片底层数组的值,但是append操作不会;
字符串复制和切片复制类似,但是其底层数组值不可修改;
map本质上就是一个指针,所以看起来像传引用,实际上还是传值,只不过这个值是指针;
channel本质上也是个指针;
结构体传值时也会复制对象,所以太大的结构体最好采用指针传值调用。bi

4 闭包

闭包的本质是函数+引用环境,如下,incr函数返回一个匿名函数,其含有一个局部变量i,这个局部变量会发生逃逸。

package main

import "fmt"

func incr() func() int {
   var i int
   return func() int {
      i++
      return i
   }
}

func main() {
   incr1, incr2 := incr(), incr()

   fmt.Println(incr1())
   fmt.Println(incr1())
   fmt.Println(incr1())

   fmt.Println(incr2())
   fmt.Println(incr2())

   fmt.Println(incr())
   fmt.Println(incr()())
}

以上代码执行的结果是:

1
2
3
1
2
0x104400280
1

当执行incr1, incr2 := incr(), incr()时就会生成两个闭包,可以想象,闭包incr1和incr2保存这个一个对i的引用,可以理解为incr1有一个指向i的指针。
incr()是一个函数,打印的是一个函数地址;incr()()是这个函数执行,打印的是这个函数的执行结果。

你可能感兴趣的:(golang,开发语言,后端)