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
总结
汇编太麻烦了,再也不想写了。
参考资料:
更多文章
- socks5 协议详解
- zerotier简明教程
- 搞定面试中的系统设计题
- frp 源码阅读与分析(一):流程和概念
- 用peewee代替SQLAlchemy
- Golang(Go语言)中实现典型的fork调用
- DNSCrypt简明教程
- 一个Gunicorn worker数量引发的血案
- Golang validator使用教程
- Docker组件介绍(一):runc和containerd
- Docker组件介绍(二):shim, docker-init和docker-proxy
- 使用Go语言实现一个异步任务框架
- 协程(coroutine)简介 - 什么是协程?
- SQLAlchemy简明教程
- Go Module 简明教程