背景

在现代高级语言中,目前在云计算领域最为火热的当属Golang,Golang出生于2009年,背景是Google公司,相对一些其他类似C,Java,C++,Python,Ruby,Rust这些古老的高级语言而言则显得十分年轻。
Golang天生支持用户级对称协程(这个对称协程感觉也可以单独拉出来讨论),交叉编译,跨平台是跨的最爽的一个,Golang的强悍之处这里不再过多介绍。
我们知道高级语言最终还是要转换成机器语言交付给CPU执行,而汇编也只是一个中间状态,汇编指令相对于以上的高级语言而言则显得十分拗口。在大部分强类型的语言中,基本上代码在执行前会经历几个阶段:
语法分析--->词法分析--->目标码生成
一些语言存在链接器的过程,但是一些依赖于虚拟机运行的语言,比如Erlang,Java这种就没有链接器的过程,Java把自己整成bytecode被jvm解释执行,jdk大伙应该最熟悉不过了,而jvm上来先吃几百兆内存。

下面开始介绍一下Golang的汇编:

总体概括:Go的汇编器会使用一种伪汇编后再为目标硬件生成具体的机器指令。
既然是伪汇编,那么Go的汇编器则不是对底层机器的直接表示,即Go的汇编器没有直接使用目标机器的汇编指令。

那么Go的汇编是怎么样的?
Go汇编器所用的指令,一部分与目标机器的指令 一一对应,而另外一部分则不是。这是因为编译器套件不需要汇编器直接参与常规的编译过程。相反,编译器使用了一种半抽象的指令集,并且部分指令是在代码生成后才被选择的。汇编器基于这种半抽象的形式工作,所以虽然你看到的是一条MOV指令,但是工具链针对这条指令实际生成可能完全不是一个移动指令,也许会是清除或者加载。也有可能精确的对应目标平台上同名的指令。
由于这种汇编并不对应某种真实的硬件架构,Go编译器会输出一种抽象可移植的汇编代码。


假设有如下代码 main.go

package main 
​
//go:noinline
func add(a, b int32) (int32, bool) { 
  return a + b, true 
}
​
func main() { 
  add(10, 32) 
}
其中//go:noinline为编译器指令,不是注释,这里应该意为禁止内联,这部分在scan后形成ast树时也会scan到这个记录,在汇编的过程中会读取这个标记,从而控制一些汇编行为。

将这段代码编译到汇编:

$ GOOS=linux GOARCH=amd64 go tool compile -S main.go
0x0000 TEXT   "".add(SB), NOSPLIT, $0-16
  0x0000 FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
  0x0000 FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x0000 MOVL   "".b+12(SP), AX
  0x0004 MOVL   "".a+8(SP), CX
  0x0008 ADDL   CX, AX
  0x000a MOVL   AX, "".~r2+16(SP)
  0x000e MOVB   $1, "".~r3+20(SP)
  0x0013 RET
​
0x0000 TEXT   "".main(SB), $24-0
  ;; ...omitted stack-split prologue...
  0x000f SUBQ   $24, SP
  0x0013 MOVQ   BP, 16(SP)
  0x0018 LEAQ   16(SP), BP
  0x001d FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x001d FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x001d MOVQ   $137438953482, AX
  0x0027 MOVQ   AX, (SP)
  0x002b PCDATA   $0, $0
  0x002b CALL   "".add(SB)
  0x0030 MOVQ   16(SP), BP
  0x0035 ADDQ   $24, SP
  0x0039 RET
  ;; ...omitted stack-split epilogue...
add
0x0000 TEXT "".add(SB), NOSPLIT, $0-16
0x0000TEXT "".addTEXT"".add.text"""".addmain.add(SB)SB"".add(SB)NOSPLITaddadd$0-16$016
0x0000 FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
0x0000 FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA以及PCDATA指令包含有被gc回收所使用的信息,这些指令是被编译器加入的。


0x0000 MOVL "".b+12(SP), AX
0x0004 MOVL "".a+8(SP), CX
SP
"".b+12(SP)"".a+8(SP)

最后,有两个重点需要指出:

a0(SP)8(SP)CALL0(SP)
0x0008 ADDL CX, AX
0x000a MOVL AX, "".~r2+16(SP)
0x000e MOVB $1, "".~r3+20(SP)
ADDLAXCXAX"".~r2+16(SP)"".~r2


trueSP
0x0013 RET
RET0(SP)


main
0x0000 TEXT   "".main(SB), $24-0
  ;; ...omitted stack-split prologue...
  0x000f SUBQ   $24, SP
  0x0013 MOVQ   BP, 16(SP)
  0x0018 LEAQ   16(SP), BP
  ;; ...omitted FUNCDATA stuff...
  0x001d MOVQ   $137438953482, AX
  0x0027 MOVQ   AX, (SP)
  ;; ...omitted PCDATA stuff...
  0x002b CALL   "".add(SB)
  0x0030 MOVQ   16(SP), BP
  0x0035 ADDQ   $24, SP
  0x0039 RET
  ;; ...omitted stack-split epilogue...
  
  0x0000 TEXT "".main(SB), $24-0
"".mainmain.main.text
mainSUBQ
16(SP)24(SP)BP12(SP)16(SP)boolamd648(SP)12(SP)int324(SP)8(SP)b (int32)0(SP)4(SP)a (int32)
LEAQBP
0x001d MOVQ     $137438953482, AX
0x0027 MOVQ     AX, (SP)

调用方将被调用方需要的参数作为一个 Quad word(8 字节值,对应$137438953482)推到了刚刚增长的栈的栈顶。

1374389534821032
$ echo 'obase=2;137438953482' | bc
10000000000000000000000000000000001010
\_____/\_____________________________/
   32                             10
0x002b CALL     "".add(SB)
addCALL
CALLaddSP"".a0(SP)8(SP)
0x0030 MOVQ     16(SP), BP
0x0035 ADDQ     $24, SP
0x0039 RET

这3个指令对应:

  1. 将帧指针(frame-pointer)下降一个栈帧(stack-frame)的大小(就是“向下”一级)
  2. 将栈收缩 24 个字节,回收之前分配的栈空间
  3. 请求Go汇编器插入子过程返回相关的指令


Go的汇编初识介绍结束,凑合着看!

参考:go-internals-cn/go-internals