当前位置: 首页 > news >正文

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_startprocpushq   %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6movl    %edi, -4(%rbp)    // 取出第一个参数放到栈上movl    %esi, -8(%rbp)    // 取出第二个参数放到栈上movl    -4(%rbp), %edx    // 设置edx = edi = 1movl    -8(%rbp), %eax    // 设置eax = esi = 2addl    %edx, %eax        // 返回值放在eax,eax = eax + edx = 3popq    %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE0:.size   my_function, .-my_function.globl  main.type   main, @function
main:
.LFB1:.cfi_startprocpushq   %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6subq    $16, %rspmovl    $2, %esi    // 设置第二个参数movl    $1, %edi    // 设置第一个参数call    my_functionmovl    %eax, -4(%rbp)movl    $0, %eaxleave.cfi_def_cfa 7, 8ret.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_startprocpushq   %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6subq    $16, %rsppushq   $8pushq   $7movl    $6, %r9dmovl    $5, %r8dmovl    $4, %ecxmovl    $3, %edxmovl    $2, %esimovl    $1, %edicall    my_functionaddq    $16, %rspmovl    %eax, -4(%rbp)movl    $0, %eaxleave.cfi_def_cfa 7, 8ret.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 mainfunc 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=0x00x0000 00000 (main.go:7)        TEXT    "".main(SB), ABIInternal, $128-00x0000 00000 (main.go:7)        CMPQ    SP, 16(R14)0x0004 00004 (main.go:7)        PCDATA  $0, $-20x0004 00004 (main.go:7)        JLS     1110x0006 00006 (main.go:7)        PCDATA  $0, $-10x0006 00006 (main.go:7)        ADDQ    $-128, SP0x000a 00010 (main.go:7)        MOVQ    BP, 120(SP)0x000f 00015 (main.go:7)        LEAQ    120(SP), BP0x0014 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存入AX0x0033 00051 (main.go:8)        MOVL    $2, BX       // 2存入BX0x0038 00056 (main.go:8)        MOVL    $3, CX       // 3存入CX0x003d 00061 (main.go:8)        MOVL    $4, DI       // 4存入DI0x0042 00066 (main.go:8)        MOVL    $5, SI       // 5存入SI0x0047 00071 (main.go:8)        MOVL    $6, R8       // 6存入R80x004d 00077 (main.go:8)        MOVL    $7, R9       // 7存入R90x0053 00083 (main.go:8)        MOVL    $8, R10      // 8存入R100x0059 00089 (main.go:8)        MOVL    $9, R11      // 9存入R110x005f 00095 (main.go:8)        PCDATA  $1, $00x005f 00095 (main.go:8)        NOP0x0060 00096 (main.go:8)        CALL    "".myFunction(SB)0x0065 00101 (main.go:9)        MOVQ    120(SP), BP0x006a 00106 (main.go:9)        SUBQ    $-128, SP0x006e 00110 (main.go:9)        RET

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

"".myFunction STEXT nosplit size=399 args=0x78 locals=0x50 funcid=0x00x0000 00000 (main.go:3)   TEXT   "".myFunction(SB), NOSPLIT|ABIInternal, $80-1200x0000 00000 (main.go:3)   SUBQ   $80, SP // SP 先减去0x800x0004 00004 (main.go:3)   MOVQ   BP, 72(SP)0x0009 00009 (main.go:3)   LEAQ   72(SP), BP0x000e 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), DX0x00cc 00204 (main.go:4)   MOVQ   DX, "".~r12+64(SP)0x00d1 00209 (main.go:4)   MOVQ   "".b+144(SP), DX0x00d9 00217 (main.go:4)   MOVQ   DX, "".~r13+56(SP)0x00de 00222 (main.go:4)   MOVQ   "".c+152(SP), DX0x00e6 00230 (main.go:4)   MOVQ   DX, "".~r14+48(SP)0x00eb 00235 (main.go:4)   MOVQ   "".d+160(SP), DX0x00f3 00243 (main.go:4)   MOVQ   DX, "".~r15+40(SP)0x00f8 00248 (main.go:4)   MOVQ   "".e+168(SP), DX0x0100 00256 (main.go:4)   MOVQ   DX, "".~r16+32(SP)0x0105 00261 (main.go:4)   MOVQ   "".f+176(SP), DX0x010d 00269 (main.go:4)   MOVQ   DX, "".~r17+24(SP)0x0112 00274 (main.go:4)   MOVQ   "".g+184(SP), DX0x011a 00282 (main.go:4)   MOVQ   DX, "".~r18+16(SP)0x011f 00287 (main.go:4)   MOVQ   "".h+192(SP), DX0x0127 00295 (main.go:4)   MOVQ   DX, "".~r19+8(SP)0x012c 00300 (main.go:4)   MOVQ   "".i+200(SP), DX0x0134 00308 (main.go:4)   MOVQ   DX, "".~r20(SP)0x0138 00312 (main.go:4)   MOVQ   "".j+88(SP), DX0x013d 00317 (main.go:4)   MOVQ   DX, "".~r21+112(SP)// 第10个返回值0x0142 00322 (main.go:4)   MOVQ   "".k+96(SP), DX0x0147 00327 (main.go:4)   MOVQ   DX, "".~r22+120(SP)// 第11个返回值0x014c 00332 (main.go:4)   MOVQ   "".l+104(SP), DX0x0151 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), BP0x018a 00394 (main.go:4)   ADDQ   $80, SP0x018e 00398 (main.go:4)   RET

其中,Go使用的是Plan9汇编,其和C语言直接使用的x86_64的寄存器对比如下表:
在这里插入图片描述
总结如下:

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

2.1 结构体参数如何传参

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

如下代码:

package maintype 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, AX0x0019 00025 (main.go:26)       MOVL    $2, BX0x001e 00030 (main.go:26)       MOVL    $3, CX0x0023 00035 (main.go:26)       MOVL    $4, DI0x0028 00040 (main.go:26)       MOVL    $5, SI0x002d 00045 (main.go:26)       MOVL    $6, R80x0033 00051 (main.go:26)       MOVL    $7, R90x0039 00057 (main.go:26)       MOVL    $8, R100x003f 00063 (main.go:26)       MOVL    $9, R110x0045 00069 (main.go:26)       PCDATA  $1, $00x0045 00069 (main.go:26)       CALL    "".myFunction(SB)

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

"".main STEXT size=73 args=0x0 locals=0x58 funcid=0x00x0000 00000 (main.go:25)       TEXT    "".main(SB), ABIInternal, $88-00x0000 00000 (main.go:25)       CMPQ    SP, 16(R14)0x0004 00004 (main.go:25)       PCDATA  $0, $-20x0004 00004 (main.go:25)       JLS     660x0006 00006 (main.go:25)       PCDATA  $0, $-10x0006 00006 (main.go:25)       SUBQ    $88, SP0x000a 00010 (main.go:25)       MOVQ    BP, 80(SP)0x000f 00015 (main.go:25)       LEAQ    80(SP), BP0x0014 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, DI0x0017 00023 (main.go:26)       LEAQ    ""..stmp_1(SB), SI0x001e 00030 (main.go:26)       PCDATA  $0, $-20x001e 00030 (main.go:26)       NOP0x0020 00032 (main.go:26)       DUFFCOPY        $8260x0033 00051 (main.go:26)       PCDATA  $0, $-10x0033 00051 (main.go:26)       PCDATA  $1, $00x0033 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 mainimport "fmt"func incr() func() int {var i intreturn 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()()是这个函数执行,打印的是这个函数的执行结果。

http://www.xdnf.cn/news/1190809.html

相关文章:

  • Spring Cloud Gateway:微服务架构下的 API 网关详解
  • Java,八股,cv,算法——双非研0四修之路day16
  • PYTHON从入门到实践-16数据视图化展示
  • Docker的简单使用
  • 【C++】定义常量
  • 图片查重从设计到实现(5)Milvus可视化工具
  • 嵌入式硬件篇---zigbee无线串口通信问题
  • Python - 100天从新手到大师 - Day6
  • 【Redis】Linux 配置Redis
  • 从零开始的云计算生活——第三十六天,山雨欲来,Ansible入门
  • [Python 基础课程]注释
  • Flowable 实战落地核心:选型决策与坑点破解
  • uniapp 自定义tab栏切换
  • 全球化2.0 | 云轴科技ZStack亮相阿里云印尼国有企业CXO专家活动
  • 数据结构预备知识
  • JavaWeb01——基础标签及样式(黑马视频笔记)
  • 伟淼科技李志伟:破解二代接班传承困局,系统性方案破除三代魔咒
  • mysql查找数据库表中某几个连续的编号中中断的编号
  • 如何实现打印功能
  • Kafka——Java消费者是如何管理TCP连接的?
  • 两个USB-CAN-A收发测试
  • pytorch学习笔记-自定义卷积
  • 在C#中判断两个列表数据是否相同
  • Day04–链表–24. 两两交换链表中的节点,19. 删除链表的倒数第 N 个结点,面试题 02.07. 链表相交,142. 环形链表 II
  • # JsSIP 从入门到实战:构建你的第一个 Web 电话
  • VTK交互——ImageRegion
  • kali [DNS劫持] 实验(详细步骤)
  • python I 本地 html 文件读取方法及编码报错问题详解
  • Android 蓝牙学习
  • 在python3.8和pytorch1.8.1的基础上安装tensorflow