golang汇编基础知识
Table of Contents
1 golang汇编基础知识
1.1 前言
golang的汇编基于plan9汇编,是一个中间汇编方式。这样可以忽略底层不同架构之间的一些差别。汇编主要了解各种寄存器的使用跟寻址方式。根据汇编我们能够一探golang的底层实现。比如内存如何分配,栈如何扩张。接口如何转变。
1.2 寄存器
各种伪计数器:
- FP: Frame pointer: arguments and locals.(指向当前栈帧)
- PC: Program counter: jumps and branches.(指向指令地址)
- SB: Static base pointer: global symbols.(指向全局符号表)
- SP: Stack pointer: top of stack.(指向当前栈顶部)
注意: 栈是向下整长 golang的汇编是调用者维护参数返回值跟返回地址。所以FP的值小于参数跟返回值。
1.3 跟例子学汇编
1.4 第一个例子
package main //go:noinline func add(a, b int32) (int32, bool) { return a + b, true } func main() { add(10, 32) }
如上的这段代码会被编译为以下汇编:
"".add STEXT nosplit size=20 args=0x10 locals=0x0 0x0000 00000 (call.go:4) TEXT "".add(SB), NOSPLIT, $0-16 0x0000 00000 (call.go:4) FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB) 0x0000 00000 (call.go:4) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (call.go:4) MOVL "".b+12(SP), AX 0x0004 00004 (call.go:4) MOVL "".a+8(SP), CX 0x0008 00008 (call.go:5) ADDL CX, AX 0x000a 00010 (call.go:5) MOVL AX, "".~r2+16(SP) 0x000e 00014 (call.go:5) MOVB $1, "".~r3+20(SP) 0x0013 00019 (call.go:5) RET 0x0000 8b 44 24 0c 8b 4c 24 08 01 c8 89 44 24 10 c6 44 .D$..L$....D$..D 0x0010 24 14 01 c3 $...
TEXT "".add(SB), NOSPLIT, $0-16
这行代码声明了一个add函数,NOSPLIT告诉编译器不要插入分裂栈检查点。这里可以不用检查栈的大小是因为这里add函数的栈大小为0. $0 表示栈大小为0, $16 表示参数大小为16个字节。
FUNCDATA
跟垃圾回收有关,暂时不用理会
SP 指向当前栈顶部 .a+8(SP) 之前的.a表示助记符,没有特别含义。8(SP)表示地址SP+8 这里是第一参数a, 因为当前栈为0且SP+0存储了函数返回地址。同理12(SP) 表示第二个参数b。16(SP)表示第一个返回值,20(SP)表示第二个返回值。
1.5 第二个例子
package main //go:noinline func add(a, b int32) (int32, bool) { return a + b, true } //go:noinline func callAdd() int32 { a, _ := add(10, 20) return a } func main() { callAdd() }
"".callAdd STEXT size=73 args=0x8 locals=0x18 0x0000 00000 (call.go:9) TEXT "".callAdd(SB), $24-8 ; 这三行代码检查是否需要扩展栈, 需要的话,跳到0x0042 0x0000 00000 (call.go:9) MOVQ (TLS), CX 0x0009 00009 (call.go:9) CMPQ SP, 16(CX) 0x000d 00013 (call.go:9) JLS 66 0x000f 00015 (call.go:9) SUBQ $24, SP 0x0013 00019 (call.go:9) MOVQ BP, 16(SP) 0x0018 00024 (call.go:9) LEAQ 16(SP), BP 0x001d 00029 (call.go:9) FUNCDATA $0, gclocals·2a5305abe05176240e61b8620e19a815(SB) 0x001d 00029 (call.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (call.go:10) MOVQ $85899345930, AX 0x0027 00039 (call.go:10) MOVQ AX, (SP) 0x002b 00043 (call.go:10) PCDATA $0, $0 0x002b 00043 (call.go:10) CALL "".add(SB) 0x0030 00048 (call.go:10) MOVL 8(SP), AX 0x0034 00052 (call.go:11) MOVL AX, "".~r0+32(SP) 0x0038 00056 (call.go:11) MOVQ 16(SP), BP 0x003d 00061 (call.go:11) ADDQ $24, SP 0x0041 00065 (call.go:11) RET ; 扩展栈, 扩展完了后跳到最开始 0x0042 00066 (call.go:11) NOP 0x0042 00066 (call.go:9) PCDATA $0, $-1 0x0042 00066 (call.go:9) CALL runtime.morestack_noctxt(SB) 0x0047 00071 (call.go:9) JMP 0
现在我们添加一层函数调用,看看生成后的样子, 主要关注callAdd函数。在这个例子中我们看到这里的栈大小是24byte(2个参数2个返回值(bool 对齐到4字节, 4 * 4 = 16 byte),再加上一个8byte的bp),参数是8byte(对齐到8byte)。 我们从0x000f说起,前面那段代码主要用于分裂栈检查。 0x000f 把栈减了24个字节。增大了栈空间。(向下增长) 0x0013-0x0018保存老的bp设置新的bp。这里的bp是真实的寄存器,这几行代码属于惯用法。在汇编中可以用bp来引用参数跟本地变量。 0x001d 是10跟20合起来的(八字节)二进制表示。本来需要两次move,现在只需要一次move,也算个小优化了吧。 接下来就是调用add方法,移动返回值。虽然我们没有使用add的第二个返回值,但是我们也要为他分配内存。 0x0038恢复BP寄存器,0x003d缩减栈空间. RET指令加载返回地址,跳到返回地址。