什么是 plan9 汇编

我们知道,CPU 是只认二进制指令的,也就是一串的 0101;人类无法记住这些二进制码,于是发明了汇编语言。汇编语言实际上是二进制指令的文本形式,它与指令可以一一对应。

每一种 CPU 指令都是不一样的,因此对应的汇编语言也就不一样。人类写完汇编语言后,把它转换成二进制码,就可以被机器执行了。转换的动作由编译器完成。

Go 语言的编译器和汇编器都带了一个 - S 参数,可以查看生成的最终目标代码。通过对比目标代码和原始的 Go 语言或 Go 汇编语言代码的差异可以加深对底层实现的理解。

Go 汇编语言实际上来源于 plan9 汇编语言,而 plan9 汇编语言最初来源于 Go 语言作者之一的 Ken Thompson 为 plan9 系统所写的 C 语言编译器输出的汇编伪代码。这里强烈推荐一下春晖大神的新书《Go 语言高级编程》,即将上市,电子版的点击阅读原文可以看到地址,书中有一整个章节讲 Go 的汇编语言,非常精彩!

理解 Go 的汇编语言,哪怕只是一点点,都能对 Go 的运行机制有更深入的理解。比如我们以前讲的 defer,如果从 Go 源码编译后的汇编代码来看,就能深刻地掌握它的底层原理。再比如,很多文章都会分析 Go 的函数参数传递都是值传递,如果把汇编代码秀出来,很容易就能得出结论。

汇编角度看函数调用及返回过程

假设我们有一个这样年幼无知的例子,求两个 int 的和,Go 源码如下:

使用如下命令得到汇编代码:

go tool compile-S

我们现在只关心 add 函数的汇编代码:

看不懂没关系,我目前也不是全部都懂,但是对于理解一个函数调用的整体过程而言,足够了。

add$0-24024

再看中间这四行:

bAXaCXabAX

(SP) 指栈顶,b+16(SP) 表示裸骑 1 的位置,从 SP 往上增加 16 个字节,注意,前面的 b 仅表示一个标号;同样,a+8(SP) 表示实参 0;~r2+24(SP) 则表示返回值的位置。

具体可以看下面的图:



上面 add 函数的栈帧大小为 0,其实更一般的调用者与被调用者的栈帧示意图如下:



RETadd返回地址rip返回地址mainadd
mainadd

这样,main 函数完成了函数调用,也拿到了返回值,完美。

汇编角度看 slice

slice
f

通过上面的汇编代码,我们画出函数调用的栈帧图:



我们可以清晰地看到,一个 slice 本质上是用一个数据首地址,一个长度 Len,一个容量 Cap。所以在参数是 slice 的函数里,对 slice 的操作会影响到实参的 slice。