本文翻译自 https://github.com/teh-cmc/go-internals/tree/master/chapter1_assembly_primer
先看一个简单的 code
// go tool compile -N -l -S once.go
// go build -gcflags -S once.go
package main
//go:noinline
func add(a, b int32) (int32, bool) { return a + b, true }
func main() { add(10, 32) }
生成汇编
GOOS=linux GOARCH=amd64 go tool compile -S x.go
在我的机器 Ubuntu kernel 5.4.0, Go version go1.18.3 amd64 上出来的结果与原文中还是有些差异的,但为了文章通顺,下面还是用的原文的结果。
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
- 0x0000 : 表示当前指令相对于函数的偏移量
- TEXT “”.add : 定义函数,在链接阶段会被链接器替换为
main.add
- (SB): SB 是 static base pointer,代表程序地址空间的起始地址。
"".add(SB)
代表 add 在程序地址空间的一个固定 offset。比如像下面这样
$ objdump -j .text -t binary | grep 'main.add'
000000000044d980 g F .text 000000000000000f main.add
FP 和 SB 的作用可以参见 《Go 语言汇编 一》
- NOSPLIT: 告诉编译器不要插入 stack split,这是 Go 运行时决定是否需要对函数栈进行扩容。
- $0-16 : $0 表示 add 函数在 stack frame 上没有分配局部变量,所以是 0。 $16 表示 caller 传入的函数参数时 16 字节。注意:在我自己机器上,这个值是 8,并且 add 的两个参数是由 AX 和 BX 传递的。
PCDATA 和 FUNCDATA
Go 语言作为一门静态编译型语言,在执行时每个函数的地址都是固定的,函数的每条指令也是固定的。如果针对每个函数和函数的每个指令生成一个地址表格(也叫 PC 表格),那么在运行时我们就可以根据 PC 寄存器的值轻松查询到指令当时对应的函数和位置信息。
PCDATA 用于生成 PC 表格,PCDATA 的指令用法为:PCDATA tableid, tableoffset
。PCDATA 有个两个参数,第一个参数为表格的类型,第二个是表格的地址,PCDATA_StackMapIndex
和 PCDATA_InlTreeIndex
FUNC 表格用于记录函数的参数、局部变量的指针信息。FUNCDATA 指令和 PCDATA 的格式类似:FUNCDATA tableid, tableoffset
,第一个参数为表格的类型,第二个是表格的地址。
#define FUNCDATA_ArgsPointerMaps 0 /* garbage collector blocks */
#define FUNCDATA_LocalsPointerMaps 1
#define FUNCDATA_InlTree 2
#define NO_LOCAL_POINTERS FUNCDATA $FUNCDATA_LocalsPointerMaps, runtime·no_pointers_stackmap(SB)
RET
取出存在 0(SP)
的返回地址,然后返回主函数。
下面把分散的注释放到一起
;; Declare global function symbol "".add (actually main.add once linked)
;; Do not insert stack-split preamble
;; 0 bytes of stack-frame, 16 bytes of arguments passed in
;; func add(a, b int32) (int32, bool)
0x0000 TEXT "".add(SB), NOSPLIT, $0-16
;; ...omitted FUNCDATA stuff...
0x0000 MOVL "".b+12(SP), AX ;; move second Long-word (4B) argument from caller's stack-frame into AX
0x0004 MOVL "".a+8(SP), CX ;; move first Long-word (4B) argument from caller's stack-frame into CX
0x0008 ADDL CX, AX ;; compute AX=CX+AX
0x000a MOVL AX, "".~r2+16(SP) ;; move addition result (AX) into caller's stack-frame
0x000e MOVB $1, "".~r3+20(SP) ;; move `true` boolean (constant) into caller's stack-frame
0x0013 RET ;; jump to return address stored at 0(SP)
程序的虚拟地址空间如下所示
| +-------------------------+ <-- 32(SP)
| | |
G | | |
R | | |
O | | main.main's saved |
W | | frame-pointer (BP) |
S | |-------------------------| <-- 24(SP)
| | [alignment] |
D | | "".~r3 (bool) = 1/true | <-- 21(SP)
O | |-------------------------| <-- 20(SP)
W | | |
N | | "".~r2 (int32) = 42 |
W | |-------------------------| <-- 16(SP)
A | | |
R | | "".b (int32) = 32 |
D | |-------------------------| <-- 12(SP)
S | | |
| | "".a (int32) = 10 |
| |-------------------------| <-- 8(SP)
| | |
| | |
| | |
\ | / | return address to |
\|/ | main.main + 0x30 |
- +-------------------------+ <-- 0(SP) (TOP OF STACK)
(diagram made with https://textik.com)
main 函数
在回顾一下 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...
在 call 函数之前,所有的参数通过栈传递。
0x000f SUBQ $24, SP // 栈从高地址往低地址增长,因此是 sub
0x0013 MOVQ BP, 16(SP) // 把 BP 的值存到 16(SP)
0x0018 LEAQ 16(SP), BP
其中 24 字节是怎么算出来的呢 ?
- 8 bytes
16(SP)-24(SP)
,用于存当前 frame-pointer BP 值,to allow for stack-unwinding and facilitate debugging - 1+3 bytes
12(SP)-16(SP)
,用于第二个 bool 返回值,3 字节用于对齐,没有实际用处。 - 4 bytes
8(SP)-12(SP)
, 第一个返回值。 - 4 bytes
4(SP)-8(SP)
,用于第二个参数。 - 4 bytes
0(SP)-4(SP)
用于第一个参数。
LEAQ
指令的作用是计算frame pointer 的新地址,并把它存到 BP。
继续往下看:
0x001d MOVQ $137438953482, AX
0x0027 MOVQ AX, (SP)
这里的常量 137438953482 是怎么来的呢? 其实它是 a,b 两个 4 字节参数合成一个 8 字节数字的结果。
$ echo 'obase=2;137438953482' | bc
10000000000000000000000000000000001010
\____/\______________________________/
32 10
MOVQ 是 Quad Word, b 字节。 这里也算是也个编译器优化的结果吧,导致人很难看懂。
接下来就是 CALL
add 函数了。 注意:CALL 会把 8 字节的返回地址 push 到栈上,所以在 add 函数內部使用 SP 的时候,他已经默认增加了 8 字节的 offset。比如 "".a
不在 0(SP) 而是在 8(SP)。
最后三条语句
0x0030 MOVQ 16(SP), BP
0x0035 ADDQ $24, SP
0x0039 RET
- 首先展开 frame pointer (似乎在这个程序里 BP 的值并没有什么用 ? )
- 恢复栈,加上之前减去的 24 。
- 返回。
goroutine 和 栈 split
Goroutine 有自己的栈空间,初始时分配了 2KB,当不够用时 runtime 会自动重新 2 倍的空间,这个过程就叫做 stack split。 联想到我们之前看到的 NOSPLIT 汇编代码,可以猜到,这是编译器告诉 runtime 这个函数不需要 stack split。
看以下代码,注意注释部分的 prologue 和 epilogue
0x0000 TEXT "".main(SB), $24-0
;; stack-split prologue
0x0000 MOVQ (TLS), CX
0x0009 CMPQ SP, 16(CX)
0x000d JLS 58
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
;; stack-split epilogue
0x003a NOP
;; ...omitted PCDATA stuff...
0x003a CALL runtime.morestack_noctxt(SB)
0x003f JMP 0
- 当进入函数时,首先检查是否需要扩展栈空间。
- 如果需要,则跳到 epilogue 部分,调用 runtime 扩展栈,然后 JMP 0 返回函数开始的地方。
Prologue
0x0000 MOVQ (TLS), CX ;; store current *g in CX
0x0009 CMPQ SP, 16(CX) ;; compare SP and g.stackguard0
0x000d JLS 58 ;; jumps to 0x3a if SP <= g.stackguard0
TLS 是一个由 runtime 维护的虚拟寄存器,它指向当前的 g
, g 是管理所有 goroutine 状态的一个数据结构。下面是 g 的结构体:
type g struct {
stack stack // 16 bytes
// stackguard0 is the stack pointer compared in the Go stack growth prologue.
// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
stackguard0 uintptr
stackguard1 uintptr
// ...omitted dozens of fields...
}
可见,汇编中的 16(CX) 指的就是 g.stackguard0
,它的值是由 runtime 维护的。当 SP 的值小于 stackguard0 时,说明栈已经超出了 stackgurad0,所以需要 JLS 到 58 行进行扩容。
Epilogue 部分的三行代码很清晰,就不解释了。
最后附上同样 go code 在我的机器上生成的汇编代码
"".add STEXT nosplit size=8 args=0x8 locals=0x0 funcid=0x0 align=0x0
0x0000 00000 (x.go:4) TEXT "".add(SB), NOSPLIT|ABIInternal, $0-8
0x0000 00000 (x.go:4) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (x.go:4) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (x.go:4) FUNCDATA $5, "".add.arginfo1(SB)
0x0000 00000 (x.go:4) FUNCDATA $6, "".add.argliveinfo(SB)
0x0000 00000 (x.go:4) PCDATA $3, $1
0x0000 00000 (x.go:4) ADDL BX, AX
0x0002 00002 (x.go:4) MOVL $1, BX
0x0007 00007 (x.go:4) RET
0x0000 01 d8 bb 01 00 00 00 c3 ........
"".main STEXT size=54 args=0x0 locals=0x10 funcid=0x0 align=0x0
0x0000 00000 (x.go:6) TEXT "".main(SB), ABIInternal, $16-0
0x0000 00000 (x.go:6) CMPQ SP, 16(R14)
0x0004 00004 (x.go:6) PCDATA $0, $-2
0x0004 00004 (x.go:6) JLS 47
0x0006 00006 (x.go:6) PCDATA $0, $-1
0x0006 00006 (x.go:6) SUBQ $16, SP
0x000a 00010 (x.go:6) MOVQ BP, 8(SP)
0x000f 00015 (x.go:6) LEAQ 8(SP), BP
0x0014 00020 (x.go:6) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0014 00020 (x.go:6) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0014 00020 (x.go:6) MOVL $10, AX
0x0019 00025 (x.go:6) MOVL $32, BX
0x001e 00030 (x.go:6) PCDATA $1, $0
0x001e 00030 (x.go:6) NOP
0x0020 00032 (x.go:6) CALL "".add(SB)
0x0025 00037 (x.go:6) MOVQ 8(SP), BP
0x002a 00042 (x.go:6) ADDQ $16, SP
0x002e 00046 (x.go:6) RET
0x002f 00047 (x.go:6) NOP
0x002f 00047 (x.go:6) PCDATA $1, $-1
0x002f 00047 (x.go:6) PCDATA $0, $-2
0x002f 00047 (x.go:6) CALL runtime.morestack_noctxt(SB)
0x0034 00052 (x.go:6) PCDATA $0, $-1
0x0034 00052 (x.go:6) JMP 0
0x0000 49 3b 66 10 76 29 48 83 ec 10 48 89 6c 24 08 48 I;f.v)H...H.l$.H
0x0010 8d 6c 24 08 b8 0a 00 00 00 bb 20 00 00 00 66 90 .l$....... ...f.
0x0020 e8 00 00 00 00 48 8b 6c 24 08 48 83 c4 10 c3 e8 .....H.l$.H.....
0x0030 00 00 00 00 eb ca ......
rel 33+4 t=7 "".add+0
rel 48+4 t=7 runtime.morestack_noctxt+0