解析 Go 中的函数调用

栏目: 编程语言 · 发布时间: 6年前

内容简介:解析 Go 中的函数调用

让我们来看一些简单的 Go 的函数,然后看看我们能否明白函数调用是怎么回事。我们将通过分析 Go 编译器根据函数生成的汇编来完成这件事。对于一个小小的博客来讲,这样的目标可能有点不切实际,但是别担心,汇编语言很简单。哪怕是 CPU 都能读懂。

解析 Go 中的函数调用

图片来自 Rob Baines https://github.com/telecoda/inktober-2016

这是我们的第一个函数。对,我们只是让两个数相加。

func add(a, b int) int {
        return a + b
}

我们编译的时候需要关闭优化,这样方便我们去理解生成的汇编代码。我们用 go build -gcflags 'N -l' 这个命令来完成上述操作。然后我们可以用 go tool objdump -s main.add func 输出我们函数的具体细节(这里的 func 是我们的包名,也就是我们刚刚用 go build 编译出的可执行文件)。

如果你之前没有学过汇编,那么恭喜你,你将接触到一个全新的事物。另外我会在 Mac 上完成这篇博客的代码,因此所生成的是 Intel 64-bit 汇编。

main.go:20 0x22c0 48c744241800000000 MOVQ $0x0, 0x18(SP)
 main.go:21 0x22c9 488b442408  MOVQ 0x8(SP), AX
 main.go:21 0x22ce 488b4c2410  MOVQ 0x10(SP), CX
 main.go:21 0x22d3 4801c8   ADDQ CX, AX
 main.go:21 0x22d6 4889442418  MOVQ AX, 0x18(SP)
 main.go:21 0x22db c3   RET

现在我们看到了什么?如下所示,每一行被分为了4部分:

  • 源文件的名称和行号(main.go:15)。这行的源代码会被转换为标有代码行号的说明。Go 的一行可能被转换成多行程序集。
  • 目标文件中的偏移量(例如 0x22C0)。
  • 机器码(例如 48c744241800000000)。这是 CPU 实际执行的二进制机器码。我们不需要看这个,几乎没有人看这玩意。
  • 机器码的汇编表示形式,这也是我们想要理解的部分。

让我们将注意力集中在最后一部分,汇编语言。

  • MOVQ,ADDQ 和 RET 是指令。它们告诉 CPU 需要执行的操作。后面的参数告诉 CPU 对什么执行该操作。
  • SP,AX 和 CX 是 CPU 寄存器。寄存器是 CPU 用于存储值的地方,CPU 有多个寄存器可以使用。
  • SP 是一个专用寄存器,用于存储当前堆栈指针。堆栈是记录局部变量,参数和函数调用的寄存器。每个 goroutine 都有一个堆栈。当一个函数调用另一个函数,然后另一个函数再调用其他函数,每个函数在堆栈上获得自己的存储区域。在函数调用期间创建存储区域,将 SP 的大小中减去所需的存储大小。
  • 0x8(SP)是指超过 SP 指向的存储单元的 8 个字节的存储单元。

因此,我们的工作的内容包含存储单元,CPU 寄存器,用于在存储器和寄存器之间移动值的指令以及寄存器上的操作。 这几乎就是一个 CPU 所完成的事情了。

现在让我们从第一条指令开始看每一条内容。别忘了我们需要从内存中加载两个参数 ab ,把它们相加,然后返回至调用函数。

  1. MOVQ $0x0, 0x18(SP) 将 0 置于存储单元 SP+0x18 中。 这句代码看起来有点抽象。
  2. MOVQ 0x8(SP), AX 将存储单元 SP+0x8 中的内容放到 CPU 寄存器 AX 中。也许这就是从内存中加载的我们所使用的参数之一?
  3. MOVQ 0x10(SP), CX 将存储单元 SP+0x10 的内容置于 CPU 寄存器 CX 中。 这可能就是我们所需的另一个参数。
  4. ADDQ CX, AX 将 CX 与 AX 相加,将结果存到 AX 中。好,现在已经把两个参数相加了。
  5. MOVQ AX, 0x18(sp) 将寄存器 AX 的内容存储在存储单元 SP+0x18 中。这就是在存储相加的结果。
  6. RET 将结果返回至调用函数。

记住我们的函数有两个参数 ab ,它计算了 a+b 并且返回了结果。 MOVQ 0x8(SP), AX 将参数 a 移到 AX 中,在 SP+0x8 的堆栈中 a 将被传给函数。 MOVQ 0x10(SP), CX 将参数 b 移到 CX 中,在 SP+0x10 的堆栈中 b 将被传给函数。 ADDQ CX, AX 使 ab 相加。 MOVQ AX, 0x18(SP) 将结果存储到 SP+0x18 中。 现在相加的结果被存储在 SP+0x18 的堆栈中,当函数返回调用函数时,可以从栈中读取结果。

我假设 a 是第一个参数, b 是第二个参数。我不确定是不是这样。我们需要花一点时间来完成这件事,但是这篇文章已经很长了。

那么有点神秘的第一行代码究竟是做什么用的? MOVQ $0X0, 0X18(SP) 将 0 存储至 SP+0x18 中,而 SP+0x18 是我们存储相加结果的地方。我们可以猜测,这是因为 Go 把没有初始化的值设置为 0 ,我们已经关闭了优化,即使没有必要,编译器也会执行这个操作。

所以我们从中明白了什么:

  • 好,看起来参数都存在堆栈中,第一个参数存储在 SP+0x8 中,另一个在更高编号的地址中。
  • 并且看上去返回的结果存储在参数后边,一个更高编号的地址中。

现在让我们看另一个函数。这个函数有一个局部变量,不过我们依然会让它看起来很简单。

func add3(a int) int {
    b := 3
    return a + b
}

我们用和刚才一样的过程来获取程序集列表。

TEXT main.add3(SB) 
/Users/phil/go/src/github.com/philpearl/func/main.go
 main.go:15 0x2280 4883ec10  SUBQ $0x10, SP
 main.go:15 0x2284 48896c2408  MOVQ BP, 0x8(SP)
 main.go:15 0x2289 488d6c2408  LEAQ 0x8(SP), BP
 main.go:15 0x228e 48c744242000000000 MOVQ $0x0, 0x20(SP)

 main.go:16 0x2297 48c7042403000000 MOVQ $0x3, 0(SP)

 main.go:17 0x229f 488b442418  MOVQ 0x18(SP), AX
 main.go:17 0x22a4 4883c003  ADDQ $0x3, AX
 main.go:17 0x22a8 4889442420  MOVQ AX, 0x20(SP)
 main.go:17 0x22ad 488b6c2408  MOVQ 0x8(SP), BP
 main.go:17 0x22b2 4883c410  ADDQ $0x10, SP
 main.go:17 0x22b6 c3   RET

喔!看起来有点复杂。让我们来试试。

前4条指令是根据源代码中的第15行列出的。这行代码是这样的:

func add3(a int) int {

这一行代码似乎没有做什么。所以这可能是一种声明函数的方法。让我们分析一下。

  • SUBQ $0x10, SP 从 SP 减去 0x10=16。这个操作为我们释放了 16 字节的堆栈空间
  • MOVQ BP, 0x8(SP) 将寄存器 BP 中的值存储至 SP+8 中,然后 LEAQ 0x8(SP), BP 将地址 SP+8 中的内容加载到 BP 中。现在我们已经有空间可以存储 BP 中之前所存的内容,然后将 BP 中的内容存储至刚刚分配的存储空间中,这有助于建立堆栈区域链(或者堆栈框架)。这有点神秘,不过在这篇文章中我们恐怕不会解决这个问题。
  • 在这一部分的最后是 MOVQ $ 0x0, 0x20 (SP) ,它和我们刚刚分析的最后一句类似,就是将返回值初始化为0。

下一行对应的是源码中的 b := 3MOVQ $03x, 0(SP) 把 3 放到 SP+0 中。这解决了我们的一个疑惑。当我们从 SP 中减去 0x10 = 16 时,我们得到了可以存储两个 8 字节值的空间:我们的局部变量 b 存储在 SP+0 中,而 BP 之前的值存储在 SP+0x08 中。

接下来的 6 行程序集对应于 return a + b 。这需要从内存中加载 ab ,然后将它们相加,并且返回结果。让我们依次看看每一行。

  • MOVQ 0x18(SP), AX 将存储在 SP+0x18 的参数 a 移动到寄存器 AX 中
  • ADDQ $0x3, AX 将 3 加到 AX(由于某些原因,它不使用我们存储在 SP+0 的局部变量 b ,尽管编译时优化被关闭了)
  • MOVQ AX, 0x20(SP)a+b 的结果存储到 SP+0x20 中,也就是我们返回结果所存的地方。
  • 接下来我们得到的是 MOVQ 0x8(SP), BP 以及 ADDQ $0x10, SP ,这些将恢复BP的旧值,然后将 0x10 添加到 SP,将其设置为该函数开始时的值。
  • 最后我们得到了 RET ,将要返回给调用函数的。

所以我们从中学到了什么呢?

  • 调用函数在堆栈中为返回值和参数分配空间。返回值的存储地址比参数的存储地址高。
  • 如果被调用函数有局部变量,则通过减少堆栈指针 SP 的值为它们分配空间。它也和寄存器 BP 做了一些神秘的事情。
  • 当函数返回任何对 SP 和 BP 的操作都会相反。

让我们看看堆栈在 add3() 方法中如何使用:

SP+0x20: the return value


SP+0x18: the parameter a


SP+0x10: ??


SP+0x08: the old value of BP

SP+0x0: the local variable b

如果你觉得文章中没有提到 SP+0x10,所以不 知道 这是干什么用的。我可以告诉你,这是存储返回地址的地方。这是为了让 RET 指令知道返回到哪里去。

这篇文章已经足够了。 希望如果以前你不知道这些东西如何工作,但是现在你觉得你已经有了一些了解,或者如果你被汇编吓倒了,那么也许它不那么晦涩难懂了。 如果你想了解有关汇编的更多信息,请在评论中告诉我,我会考虑在之后的文章中写出来。

既然你已经看到这儿了,如果喜欢我的这篇文章或者可以从中学到一点什么的话,那么请给我点个赞这样这篇文章就可以被更多人看到了。

掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

HTTP Essentials

HTTP Essentials

Stephen A. Thomas、Stephen Thomas / Wiley / 2001-03-08 / USD 34.99

The first complete reference guide to the essential Web protocol As applications and services converge and Web technologies not only assume HTTP but require developers to manipulate it, it is be......一起来看看 《HTTP Essentials》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试