在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
在深入阅读runtime和标准库的源码时候,发现底层有大片代码都会与汇编打交道,所以这篇文章主要是介绍golang使用到的汇编。 go汇编语言是一个不可忽视的技术。因为哪怕只懂一点点汇编,也便于更好地理解计算机原理,也更容易理解Go语言中动态栈/接口等高级特性的实现原理。 本文涉及到计算机架构体系相关的情况时,请假设我们是运行在 linux/amd64 平台上。 伪汇编Go 编译器会输出一种抽象可移植的汇编代码,这种汇编并不对应某种真实的硬件架构。之后 Go 的汇编器使用这种伪汇编,为目标硬件生成具体的机器指令。 伪汇编这一个额外层可以带来很多好处,最主要的一点是方便将 Go 移植到新的架构上。相关的信息可以参考文后列出的 Rob Pike 的 The Design of the Go Assembler。 go 汇编语言的一个简单实例 package main 注意这里的 //go:noinline 编译器指令。不要省略掉这部分,我们使用如下命令生成相应的汇编输出:go tool compile -S test1.go,会输出包含如下的输出: "".add STEXT nosplit size=20 args=0x10 locals=0x0 0x0000 00000 (test1.go:5) TEXT "".add(SB), NOSPLIT|ABIInternal, $0-16 0x0000 00000 (test1.go:5) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (test1.go:5) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (test1.go:5) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (test1.go:6) PCDATA $0, $0 0x0000 00000 (test1.go:6) PCDATA $1, $0 0x0000 00000 (test1.go:6) MOVL "".b+12(SP), AX 0x0004 00004 (test1.go:6) MOVL "".a+8(SP), CX 0x0008 00008 (test1.go:6) ADDL CX, AX 0x000a 00010 (test1.go:6) MOVL AX, "".~r2+16(SP) 0x000e 00014 (test1.go:6) MOVB $1, "".~r3+20(SP) 0x0013 00019 (test1.go:6) 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 $... "".main STEXT size=65 args=0x0 locals=0x18 0x0000 00000 (test1.go:9) TEXT "".main(SB), ABIInternal, $24-0 0x0000 00000 (test1.go:9) MOVQ (TLS), CX 0x0009 00009 (test1.go:9) CMPQ SP, 16(CX) 0x000d 00013 (test1.go:9) JLS 58 0x000f 00015 (test1.go:9) SUBQ $24, SP 0x0013 00019 (test1.go:9) MOVQ BP, 16(SP) 0x0018 00024 (test1.go:9) LEAQ 16(SP), BP 0x001d 00029 (test1.go:9) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (test1.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (test1.go:9) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (test1.go:10) PCDATA $0, $0 0x001d 00029 (test1.go:10) PCDATA $1, $0 0x001d 00029 (test1.go:10) MOVQ $137438953482, AX 0x0027 00039 (test1.go:10) MOVQ AX, (SP) 0x002b 00043 (test1.go:10) CALL "".add(SB) 0x0030 00048 (test1.go:11) MOVQ 16(SP), BP 0x0035 00053 (test1.go:11) ADDQ $24, SP 0x0039 00057 (test1.go:11) RET 0x003a 00058 (test1.go:11) NOP 0x003a 00058 (test1.go:9) PCDATA $1, $-1 0x003a 00058 (test1.go:9) PCDATA $0, $-1 0x003a 00058 (test1.go:9) CALL runtime.morestack_noctxt(SB) 0x003f 00063 (test1.go:9) JMP 0 0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 2b 48 dH..%....H;a.v+H 0x0010 83 ec 18 48 89 6c 24 10 48 8d 6c 24 10 48 b8 0a ...H.l$.H.l$.H.. 0x0020 00 00 00 20 00 00 00 48 89 04 24 e8 00 00 00 00 ... ...H..$..... 0x0030 48 8b 6c 24 10 48 83 c4 18 c3 e8 00 00 00 00 eb H.l$.H.......... 0x0040 bf . rel 5+4 t=16 TLS+0 rel 44+4 t=8 "".add+0 rel 59+4 t=8 runtime.morestack_noctxt+0 接下来我们会就上面的输出进行解析。 函数add部分0x0000 00000 (test1.go:5) TEXT "".add(SB), NOSPLIT|ABIInternal, $0-16
Go 编译器没有 PUSH/POP 族的指令: 栈的增长和收缩是通过在栈指针寄存器 SP 上分别执行减法和加法指令来实现的与大多数最近的编译器做法一样,Go 工具链总是在其生成的代码中,使用相对栈指针(stack-pointer)的偏移量来引用参数和局部变量。这样使得我们可以用帧指针(frame-pointer)来作为一个额外的通用寄存器,这一点即使是在那些寄存器数量较少的平台上也是一样的(例如 x86)。 “”.b+12(SP) 和 “”.a+8(SP) 分别指向栈的低 12 字节和低 8 字节位置(记住: 栈是向低位地址方向增长的!)。.a 和 .b 是分配给引用地址的任意别名;尽管 它们没有任何语义上的含义 ,但在使用虚拟寄存器和相对地址时,这种别名是需要强制使用的。 最后,有两个重点需要指出:
stacks 和 splitsstacks由于 Go 程序中的 goroutine 数目是不可确定的,并且实际场景可能会有百万级别的 goroutine,runtime 必须使用保守的思路来给 goroutine 分配空间以避免吃掉所有的可用内存。也由于此,每个新的 goroutine 会被 runtime 分配初始为 2KB 大小的栈空间(Go 的栈在底层实际上是分配在堆空间上的)。随着一个 goroutine 进行自己的工作,可能会超出最初分配的栈空间限制(就是栈溢出的意思)。为了防止这种情况发生,runtime 确保 goroutine 在超出栈范围时,会创建一个相当于原来两倍大小的新栈,并将原来栈的上下文拷贝到新栈上。这个过程被称为 栈分裂(stack-split),这样使得 goroutine 栈能够动态调整大小。 splits 为了使栈分裂正常工作,编译器会在每一个函数的开头和结束位置插入指令来防止 goroutine 爆栈。像我们本章早些看到的一样,为了避免不必要的开销,一定不会爆栈的函数会被标记上 NOSPLIT 来提示编译器不要在这些函数的开头和结束部分插入这些检查指令。
寄存器 通用寄存器 应用代码层面会用到的通用寄存器主要是: rax, rbx, rcx, rdx, rdi, rsi, r8~r15 这 14 个寄存器,虽然 rbp 和 rsp 也可以用,不过 bp 和 sp 会被用来管理栈顶和栈底,最好不要拿来进行运算。 plan9 中使用寄存器不需要带 r 或 e 的前缀,例如 rax,只要写 AX 即可
伪寄存器 Go 的汇编还引入了 4 个伪寄存器,援引官方文档的描述: FP: Frame pointer: arguments and locals. 官方的描述稍微有一些问题,我们对这些说明进行一点扩充:
我们这里对容易混淆的几点简单进行说明:
栈结构结构图: ----------------- current func arg0 ----------------- <----------- FP(pseudo FP) caller ret addr +---------------+ | caller BP(*) | ----------------- <----------- SP(pseudo SP,实际上是当前栈帧的 BP 位置) | Local Var0 | ----------------- | Local Var1 | ----------------- | Local Var2 | ----------------- - | ........ | ----------------- | Local VarN | ----------------- | | | | | temporarily | | unused space | | | | | ----------------- | call retn | ----------------- | call ret(n-1)| ----------------- | .......... | ----------------- | call ret1 | ----------------- | call argn | ----------------- | ..... | ----------------- | call arg3 | ----------------- | call arg2 | |---------------| | call arg1 | ----------------- <------------ hardware SP 位置 | return addr | +---------------+ 图上的 caller BP,指的是 caller 的 BP 寄存器值,有些人把 caller BP 叫作 caller 的 frame pointer,实际上这个习惯是从 x86 架构沿袭来的。Go 的 asm 文档中把伪寄存器 FP 也称为 frame pointer,但是这两个 frame pointer 根本不是一回事。 此外需要注意的是,caller BP 是在编译期由编译器插入的,用户手写代码时,计算 frame size 时是不包括这个 caller BP 部分的。是否插入 caller BP 的主要判断依据是:
如果编译器在最终的汇编结果中没有插入 caller BP(源代码中所称的 frame pointer)的情况下,伪 SP 和伪 FP 之间只有 8 个字节的 caller 的 return address,而插入了 BP 的话,就会多出额外的 8 字节。也就说伪 SP 和伪 FP 的相对位置是不固定的,有可能是间隔 8 个字节,也有可能间隔 16 个字节。并且判断依据会根据平台和 Go 的版本有所不同。 图上可以看到,FP 伪寄存器指向函数的传入参数的开始位置,因为栈是朝低地址方向增长,为了通过寄存器引用参数时方便,所以参数的摆放方向和栈的增长方向是相反的,即: FP high ----------------------> low argN, ... arg3, arg2, arg1, arg0 假设所有参数均为 8 字节,这样我们就可以用 symname+0(FP) 访问第一个 参数,symname+8(FP) 访问第二个参数,以此类推。用伪 SP 来引用局部变量,原理上来讲差不多,不过因为伪 SP 指向的是局部变量的底部,所以 symname-8(SP) 表示的是第一个局部变量,symname-16(SP)表示第二个,以此类推。当然,这里假设局部变量都占用 8 个字节。 图的最上部的 caller return address 和 current func arg0 都是由 caller 来分配空间的。不算在当前的栈帧内。 因为官方文档本身较模糊,我们来一个函数调用的全景图,来看一下这些真假 SP/FP/BP 到底是个什么关系: caller +------------------+ | | +----------------------> -------------------- | | | | | caller parent BP | | BP(pseudo SP) -------------------- | | | | | Local Var0 | | -------------------- | | | | | ....... | | -------------------- | | | | | Local VarN | -------------------- caller stack frame | | | callee arg2 | | |------------------| | | | | | callee arg1 | | |------------------| | | | | | callee arg0 | | ----------------------------------------------+ FP(virtual register) | | | | | | return addr | parent return address | +----------------------> +------------------+--------------------------- <-------------------------------+ | caller BP | | | (caller frame pointer) | | BP(pseudo SP) ---------------------------- | | | | | Local Var0 | | ---------------------------- | | | | Local Var1 | ---------------------------- callee stack frame | | | ..... | ---------------------------- | | | | | Local VarN | | SP(Real Register) ---------------------------- | | | | | | | | | | | | | | | | +--------------------------+ <-------------------------------+ callee
|
请发表评论