go语言函数如何传递数组变量

这篇文章以实践的方式验证go语言函数之间是如何传递数组类型变量的。
和slice相比,go对于array传参是传递整个array内容的,而不是引用,即把原array内容做一个拷贝,然后把拷贝后的内容值作为参数给被调用者使用。

以如下go语言程序为例子:

package main

const SIZE = 16

func main() {
  var ss [SIZE]int64

  ss[0]      = 0x1111
  ss[SIZE-1] = 0x2222

  useArray(ss)
}

func useArray(ss [SIZE]int64) {
  ss[0]      = 0x3333
  ss[SIZE-1] = 0x4444
}

逐段分析生成的哦汇编代码,先看数组是如何定义的:

  var ss [SIZE]int64
  44d682:   48 8d bc 24 80 00 00    lea    0x80(%rsp),%rdi
  44d689:   00
  44d68a:   0f 57 c0                xorps  %xmm0,%xmm0
  44d68d:   48 89 6c 24 f0          mov    %rbp,-0x10(%rsp)
  44d692:   48 8d 6c 24 f0          lea    -0x10(%rsp),%rbp
  44d697:   e8 ee ab ff ff          callq  44828a <runtime.duffzero+0x10a>
  44d69c:   48 8b 6d 00             mov    0x0(%rbp),%rbp

在main函数里面变量ss被分配在栈中,位置是用%rsp+0x80开始的 16*8字节,所以

  &ss[0] = %rsp+0x80
  &ss[1] = %rsp+0x88
  ...
  &ss[15] = %rsp+0xf8

因为我们看两条赋值语句的代码:
ss[0]对应的是%rsp + 0x80
ss[15]对应的是%rsp + 0xf8

  ss[0]      = 0x1111
  44d6a0:   48 c7 84 24 80 00 00    movq   $0x1111,0x80(%rsp)
  44d6a7:   00 11 11 00 00
  ss[SIZE-1] = 0x2222
  44d6ac:   48 c7 84 24 f8 00 00    movq   $0x2222,0xf8(%rsp)
  44d6b3:   00 22 22 00 00

同时我们也能看到数组被定义的同时,也进行了初始化操作,调用runtime.duffzero函数,duffzero函数接收参数%rdi,把从%rdi地址开始的内存空间(在这个例子中就是数组ss的首地址)填满%xmm0的值,这个函数的设计很巧妙,后面我们介绍它。

再看函数useArray的调用语句

  useArray(ss)
  44d6b8:   48 89 e7                mov    %rsp,%rdi
  44d6bb:   48 8d b4 24 80 00 00    lea    0x80(%rsp),%rsi
  44d6c2:   00
  44d6c3:   48 89 6c 24 f0          mov    %rbp,-0x10(%rsp)
  44d6c8:   48 8d 6c 24 f0          lea    -0x10(%rsp),%rbp
  44d6cd:   e8 fe ae ff ff          callq  4485d0 <runtime.duffcopy+0x310>
  44d6d2:   48 8b 6d 00             mov    0x0(%rbp),%rbp
  44d6d6:   e8 25 00 00 00          callq  44d700 <main.useArray>

从这段代码可以看到,main函数把ss的内容做了一个完整拷贝,函数runtime.duffcopy用来拷贝内存从%rsi(即ss的首地址)拷贝到%rdi(即当前栈顶),细心的读者会发现这个函数和前面的runtime.duffzero函数一样有一个问题,即没有指定内存的大小,不知道该拷贝填充多大的内存,虽然指定了内存地址的开始地址,但是没有指定结束地址。这其实就是这两个函数设计的巧妙之处,后面我们再介绍。

接着再看函数useArray的实现语句

func useArray(ss [SIZE]int64) {
  ss[0]      = 0x3333
  44d700:   48 c7 44 24 08 33 33    movq   $0x3333,0x8(%rsp)
  44d707:   00 00
  ss[SIZE-1] = 0x4444
  44d709:   48 c7 84 24 80 00 00    movq   $0x4444,0x80(%rsp)
  44d710:   00 44 44 00 00
}

在函数useArray内部参数ss占用的空间是%rsp+0x8到%rsp+0x80的范围,注意此%rsp和main函数的%rsp相差一个8字节位置,因为main函数调用到useArray函数时,%rsp指针发生了移动(保存了%rsp)。

我们看到在调用useArray前后栈的变化

in main             values                      in useArray
------------------+--------------------------|-------------------------
%rsp + 0xf8 -->   | ss[15]                   |
%rsp + 0xf0 -->   | ss[14]                   |
...               |                          |
%rsp + 0x88 -->   | ss[1]                    |
%rsp + 0x80 -->   | ss[0]                    |
%rsp + 0x78 -->   | reserved.param.ss[15]    |    <-- %rsp + 0x80
%rsp + 0x70 -->   | reserved.param.ss[14]    |    <-- %rsp + 0x78
...               |                          |
%rsp + 0x8  -->   | reserved.param.ss[1]     |    <-- %rsp + 0x10
%rsp + 0x0  -->   | reserved.param.ss[0]     |    <-- %rsp + 0x8
                  | (useArray return addr)   |    <-- %rsp + 0x0

最后我们来看下runtime.duffzero和runtime.duffcopy
这两个系统函数由go运行环境提供,功能类似于memset和memory,它是由汇编语言直接生成;代码如下

0000000000448180 <runtime.duffzero>:
  448180:   0f 11 07                movups %xmm0,(%rdi)
  448183:   0f 11 47 10             movups %xmm0,0x10(%rdi)
  448187:   0f 11 47 20             movups %xmm0,0x20(%rdi)
  44818b:   0f 11 47 30             movups %xmm0,0x30(%rdi)
  44818f:   48 83 c7 40             add    $0x40,%rdi

  448193:   0f 11 07                movups %xmm0,(%rdi)
  448196:   0f 11 47 10             movups %xmm0,0x10(%rdi)
  44819a:   0f 11 47 20             movups %xmm0,0x20(%rdi)
  44819e:   0f 11 47 30             movups %xmm0,0x30(%rdi)
  4481a2:   48 83 c7 40             add    $0x40,%rdi

  ...

  4482b0:   c3                      retq


00000000004482c0 <runtime.duffcopy>:
  4482c0:   0f 10 06                movups (%rsi),%xmm0
  4482c3:   48 83 c6 10             add    $0x10,%rsi
  4482c7:   0f 11 07                movups %xmm0,(%rdi)
  4482ca:   48 83 c7 10             add    $0x10,%rdi


  4482ce:   0f 10 06                movups (%rsi),%xmm0
  4482d1:   48 83 c6 10             add    $0x10,%rsi
  4482d5:   0f 11 07                movups %xmm0,(%rdi)
  4482d8:   48 83 c7 10             add    $0x10,%rdi

  ...
  
  448640:   c3                      retq  

可以看到这两个函数都非常整齐,就是由4条/5条指令组的重复,删除了所有的函数entry/exit的标准模板代码,然后在最后有一个RET指令。
每一条指令组使用%xmm0寄存器来一次拷贝/赋值16字节的内容,每一组由4条指令来拷贝/赋值64字节内容。

前面提到的runtime.duffzero和runtime.duffcopy函数调用缺少参数指定内存大小的问题怎么解决吗?在这里不由callee不负责,而是由caller负责,caller根据要操作内存的大小来决定call到这两个函数的具体哪一条指令,对比通常的函数调用都是跳转到函数的入口地址,而对这个函数是跳转到函数内部的某一条指令,直到运行到RET指令为止,这期间运行了多少条拷贝赋值指令,就是操作了多大内存空间。

  44d697:   e8 ee ab ff ff          callq  44828a <runtime.duffzero+0x10a>
...
  44d6cd:   e8 fe ae ff ff          callq  4485d0 <runtime.duffcopy+0x310>

看前面的代码,通常的函数调用都是

callq  0x...... <functionmame>

也就是直接调用函数的入口指令,而这两个函数都是带一个偏移量的,这也就只有像runtime.duffzero和runtime.duffcopy这种内部逻辑简单的函数可以这么调用。这实际上编译器处理了大量的工作。

    原文作者:CodingCode
    原文地址: https://www.jianshu.com/p/e6566f682b4e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞