Golang ASM简明教程

这几天倒腾了一下Go的ASM,然后写了一个简单的汇编代码,记录一下以防忘记。首先要说明的是Go的ASM是一种中间码,或者说是 众多汇编语言的一种抽象体,但是呢,又不完全是抽象,总之,揉合了Go自定义的一部分,和真实汇编语言。这里主要记录的就是 Go自己定义的那部分。

首先如果你想查看一段Go代码产生的汇编码,可以这样:

$ cat main.go
package main

func add(int64, int64) int64

func main() {
	println(add(1, 2))
}
$ go build -gcflags -S .
# github.com/jiajunhuang/test
"".main STEXT size=107 args=0x0 locals=0x28
	0x0000 00000 (/home/jiajun/Code/test/main.go:5)	TEXT	"".main(SB), ABIInternal, $40-0
	0x0000 00000 (/home/jiajun/Code/test/main.go:5)	MOVQ	(TLS), CX
	0x0009 00009 (/home/jiajun/Code/test/main.go:5)	CMPQ	SP, 16(CX)
	0x000d 00013 (/home/jiajun/Code/test/main.go:5)	PCDATA	$0, $
    ...

首先Go定义了4个虚拟寄存器:

  • FP: Frame pointer: arguments and locals. 这个是用来指向当前frame的起始位置
  • PC: Program counter: jumps and branches. 这个用来指向下一条要执行的指令的位置
  • SB: Static base pointer: global symbols. 这个用来指向全局变量的位置,可以把它看作是程序起始地址
  • SP: Stack pointer: top of stack. 这个指向栈顶

然后Go说了:All user-defined symbols are written as offsets to the pseudo-registers FP (arguments and locals) and SB (globals).

也就是说,所有我们自定义的变量,都是基于FP和SB的一个偏移的位置。只不过呢,这个偏移的写法有点不同:foo+4(SB)。它的意思是:

  • 这个变量叫做foo(但是和实际变量不是同一个东西,它不能直接作为变量名使用,只是为了更容易看懂,但是一般会生成和变量名一样的)。
  • +4 表示相对SB的位置,偏移4byte
  • 如果有 <>,说明这个变量是局限于本文件的,相当于C语言里,在函数前加一个 static

对于FP的使用,也是一样的:first_arg+0(FP)。对于SP,略有不同,一般都是相对它为一个负数的offset:x-8(SP),而不是加号。

注意一点,对于有 SP 寄存器的硬件平台来说,SP前面是否有变量名,区分了它到底是指的Go自定义的虚拟寄存器还是真实硬件的SP寄存器。 比如 x-8(SP) 指的是相对于虚拟寄存器SP的偏移,而 -8(SP) 就是相对于真实寄存器的偏移了。

On architectures with a hardware register named SP, the name prefix distinguishes references to the virtual stack pointer from references to the architectural SP register. That is, x-8(SP) and -8(SP) are different memory locations: the first refers to the virtual stack pointer pseudo-register, while the second refers to the hardware’s SP register.

函数声明

Go ASM咋声明一个函数呢?这样:

TEXT ·add(SB), $0-24
    MOVQ arg1+0(FP), CX
    MOVQ arg2+8(FP), BP
    ADDQ CX, BP
    MOVQ BP, result+16(FP)
    RET

同时需要在一个 .go 文件里有这么一行生命它的签名:

func add(int64, int64) int64

把函数体留空,这样Go编译器就知道实现是在汇编里。

我们来看看上面 add 函数的汇编代码,首先要用一个TEXT来声明这是一个函数,它的格式是 TEXT package_name·function_name(SB),$frame_size-arguments_size。包名可以不写,表明这是当前包,然后一个圆点, 注意这个圆点是 · 而不是英文的 .,它的UTF8值是 U+00B7。接下来是函数名,然后 (SB),一个逗号,接着 $0-24, 前面的是帧的大小,我们这个add函数只用了寄存器,没有本地变量,所以是0,一个 - 后面的24,表明我们这个函数用了24个byte, 因为我们有两个 int64 的参数,这里就占用了16byte,那么剩下的8byte是啥呢?是返回结果,所以一共是24byte。

接下来的代码就好理解了:

  • 把第一个参数(位置在 FP+0 的偏移位置,写为 arg1+0(FP)) 的值复制到CX寄存器
  • 把第二个参数(位置在FP+8 的偏移位置,写为 arg2+8(FP)) 复制到 BP 寄存器里
  • 把 CX 和 BP 寄存器的值相加,结果存储在 BP 里
  • 把 BP 寄存器的值复制到 FP+16 的位置,写为 result+16(FP),也就是返回值的内存位置里
  • 调用 RET 返回

看到没,一个简单的 add 函数,汇编就这么复杂了,所以bill gates当年一个人拿纸和笔,用汇编写了一个Basic解释器,那是什么层次 的神才能做到的?我再也不想写汇编了。

变量声明及初始化

数据,分为全局变量和局部变量,咋初始化呢?

DATA	symbol+offset(SB)/width, value
GLOBL runtime·tlsoffset(SB), NOPTR, $4

不过它这个value得是一个内存地址,比如:

DATA divtab<>+0x00(SB)/4, $0xf4f8fcff
DATA divtab<>+0x04(SB)/4, $0xe6eaedf0
...
DATA divtab<>+0x3c(SB)/4, $0x81828384
GLOBL divtab<>(SB), RODATA, $64

GLOBL runtime·tlsoffset(SB), NOPTR, $4

另外一点,就是类似 NOSPLIT 这种,可以看下下面的代码,写明了咋用以及啥含义(位于 ./src/runtime/textflag.h):

// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This file defines flags attached to various functions
// and data objects. The compilers, assemblers, and linker must
// all agree on these values.
//
// Keep in sync with src/cmd/internal/obj/textflag.go.

// Don't profile the marked routine. This flag is deprecated.
#define NOPROF	1
// It is ok for the linker to get multiple of these symbols. It will
// pick one of the duplicates to use.
#define DUPOK	2
// Don't insert stack check preamble.
#define NOSPLIT	4
// Put this data in a read-only section.
#define RODATA	8
// This data contains no pointers.
#define NOPTR	16
// This is a wrapper function and should not count as disabling 'recover'.
#define WRAPPER 32
// This function uses its incoming context register.
#define NEEDCTXT 64
// Allocate a word of thread local storage and store the offset from the
// thread local base to the thread local storage in this variable.
#define TLSBSS	256
// Do not insert instructions to allocate a stack frame for this function.
// Only valid on functions that declare a frame size of 0.
// TODO(mwhudson): only implemented for ppc64x at present.
#define NOFRAME 512
// Function can call reflect.Type.Method or reflect.Type.MethodByName.
#define REFLECTMETHOD 1024
// Function is the top of the call stack. Call stack unwinders should stop
// at this function.
#define TOPFRAME 2048

总结

汇编太麻烦了,再也不想写了。


参考资料:


更多文章
  • gRPC-gateway 源码阅读与分析
  • 如何阅读源代码
  • 我心目中的配置中心应该怎么做?
  • 设计一个HTTP网关
  • 设计一个分布式块存储
  • Linux低电量自动关机
  • CGO简明教程
  • 求值策略:Applicative Order vs Normal Order
  • High Performance MySQL阅读笔记
  • MySQL EXPLAIN中的filesort是什么?
  • 数据库索引设计与优化
  • 如何调试?
  • Docker CE 18.03源码阅读与分析
  • 容器时代的日志处理
  • Golang和Thrift