背景
在现代高级语言中,目前在云计算领域最为火热的当属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个指令对应:
- 将帧指针(frame-pointer)下降一个栈帧(stack-frame)的大小(就是“向下”一级)
- 将栈收缩 24 个字节,回收之前分配的栈空间
- 请求Go汇编器插入子过程返回相关的指令
Go的汇编初识介绍结束,凑合着看!
参考:go-internals-cn/go-internals