Golang 汇编介绍

在阅读 Golang 源代码时,总是被其中的汇编代码卡住,读起来不流畅。今天来简要了解下 Golang 中的汇编语言。

汇编分类

按指令集架构分类(针对 CPU)

  1. x86汇编(32bit):这种架构常被称为i386, x86
  2. x86汇编(64bit), 这种架构常被称为 AMD64, Intel64, x86-64, x64, 它是 AMD 设计的, 是 x86 架构的 64 位扩展, 后来公开
  3. ARM汇编, ARM处理器由于高性能, 低耗电, 常用于嵌入式, 移动设备.
  4. ...

按汇编格式分类(针对人的阅读习惯)

  1. Intel 格式
  2. AT&T 格式

平时我们说 golang 中汇编属于 plan9 风格,是按第二种方式分类的,其阅读风格(符号)与 Intel 与 AT&T 都有不同。plan9 汇编作者是 unix 操作系统的同一批人,bell 实验室所开发的。

Go汇编语言是基于 plan9 汇编,但是现实世界还有这么多不同架构的 CPU 在这。所以 golang 汇编在 plan9 风格下,同一个方法还有不同指令集架构的多种实现。

在哪能看到 Golang 汇编代码

  1. Golang 源代码中,如src/runtime/asm_amd64.ssrc/math/big/ ...
  2. go tool compile -S main.go,把自己编写的代码编译成汇编代码。如:在我的 Mac Intel 机器上,amd64的架构,汇编代码生成如下:
$ cat main.go 
package main

func main() {
        a, b := 0, 0
        println(a + b)
}
$ go tool compile -S main.go 
"".main STEXT size=66 args=0x0 locals=0x10 funcid=0x0
        0x0000 00000 (main.go:3)        TEXT    "".main(SB), ABIInternal, $16-0
        0x0000 00000 (main.go:3)        CMPQ    SP, 16(R14)
        0x0004 00004 (main.go:3)        PCDATA  $0, $-2
        0x0004 00004 (main.go:3)        JLS     57
        0x0006 00006 (main.go:3)        PCDATA  $0, $-1
        0x0006 00006 (main.go:3)        SUBQ    $16, SP
        0x000a 00010 (main.go:3)        MOVQ    BP, 8(SP)
        0x000f 00015 (main.go:3)        LEAQ    8(SP), BP
        0x0014 00020 (main.go:3)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0014 00020 (main.go:3)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0014 00020 (main.go:5)        PCDATA  $1, $0
        0x0014 00020 (main.go:5)        CALL    runtime.printlock(SB)
        0x0019 00025 (main.go:5)        XORL    AX, AX
...

Go 汇编基础语法

1. 寄存器

通用寄存器

寄存器与物理机架构相关, 不同的架构有不同的物理寄存器。

amd64 架构上提供了 16 个通用寄存器给用户使用

plan9 汇编语言提供了如下映射,在汇编语言中直接引用就可使用物理寄存器了。

amd64 rax rbx rcx rdx rdi rsi rbp rsp r8 r9 r10 r11 r12 r13 r14 rip
Plan9 AX BX CX DX DI SI BP SP R8 R9 R10 R11 R12 R13 R14 PC

如上文的例子中使用到了:SP,AX,R14,BP

虚拟寄存器

Go 汇编引入了 4 个 虚拟寄存器

  • FP: Frame pointer: arguments and locals. 帧指针,快速访问函数的参数和返回值
  • PC: Program counter: jumps and branches. 程序计数器,指向下一条指令的地址。在 amd64 其实就是 rip 寄存器
  • SB: Static base pointer: global symbols. 静态基址指针,全局符号。
  • SP: Stack pointer: the highest address within the local stack frame. 栈指针, 指向局部变量

用法

  • FP:0(FP) 表示第一个参数8(FP) 表示第二个参数(AMD64 架构)。first_arg+0(FP) 表示把第一个参数地址绑定到符号 first_arg
  • SP:localvar0-8(SP) 在 plan9 中表示函数中第一个局部变量。物理寄存器中也有 SP,硬件 SP 才是真正表示 栈顶位置。所以为了区分 SP 到底是指硬件 SP 还是指虚拟寄存器。plan9 代码中需要以特定的格式来区分。eg:symbol+offset(SP) 表示虚拟寄存器 SP。offset(SP) 则表示硬件 SP。如上述例子中的 8(SP) 指的是硬件 SP
  • PC:除个别跳转治理,一般用不到
  • SB:表示全局内存起点。foo(SB) 表示符号 foo 作为内存地址使用。这种形式用于声明全局函数、数据。foo+4(SB)表示 foo 往后 4 字节的地址。<> 限制符号只能在当前源文件使用

从网上偷的图:

Golang 汇编介绍_第1张图片

2. 指令

1. 变量声明

格式: 使用 DATAGLOBL 来声明一个全局变量

DATA symbol+offset(SB)/width, value
GLOBL symbol(SB), flag, $size

表示意义

  • DATA 部分: 对 symbol 变量中的字节赋值,把 offsetoffset + width 位置的字节赋值为 value
  • GLOBL 部分:必须在 DATA 后,表示声明了一个大小为size 的全局变量symbolflag代表变量一些属性,如 RODATA指只读。在 GLOBL 中加入 <>, 如 GLOBL bio<>(SB), RODATA, $16 也是表示这个全局变量只在本文件中生效。

实际例子:

// src/runtime/asm_amd64.s, 这里声明的 argc,argv 是 Go 程序的入参
DATA _rt0_amd64_lib_argc<>(SB)/8, $0
GLOBL _rt0_amd64_lib_argc<>(SB),NOPTR, $8
DATA _rt0_amd64_lib_argv<>(SB)/8, $0
GLOBL _rt0_amd64_lib_argv<>(SB),NOPTR, $8

NOPTR 这个表示不是指针,不需要垃圾回收扫描

局部变量:其在栈帧中,不需要声明。直接依靠 offset取出使用。例如0(FP) 代表函数第一个参数,localvar0-8(SP) 函数中第一个局部变量。

2. 函数声明

格式:

TEXT pkgname·funname(SB),flag,$framesize-argsize

表示意义:

pkgname :可以省略,最好省略。不然修改包名还要级联修改;

funname: 声明的函数名

flag: 标志位,如 NOSPLIT,我们知道 Go Runtime 会追踪每个 stack 的使用情况,然后动态自增。而NOSPLIT 标志位禁止检查,节省开销,但是写程序的人要保证这个函数是安全的。

framesize: 函数栈帧大小 = 局部变量 + 调用其它函数参数空间的总大小

argsize: 一些参考资料说这里是 参数+返回值大小,但在实验中已有些许差异。这个应该和 GO 1.7 的更新有关,GO1.7 基于寄存器的调用规约 GO 1.7 的优化

  • GO 1.7 之前 参数+返回值都存在栈帧中
  • GO 1.7 更新后, 优先使用 9 个 通用寄存器传递参数与返回值,超出部分再存在栈中。并且寄存器中返回值会覆盖参数中的值
  • 参数 + 返回值少于 9 个,argsize 值是参数的大小
  • 返回值 > 9 个,argsize = 参数大小 + 返回值超出 9 个的部分

实际例子:

$ cat main.go
package main

func main() {
}

func add(a int64, b int64) int64 {
        return a + b
}

$ go tool compile -S main.go
"".main STEXT nosplit size=1 args=0x0 locals=0x0 funcid=0x0
...
"".add STEXT nosplit size=4 args=0x10 locals=0x0 funcid=0x0
        0x0000 00000 (main.go:6)        TEXT    "".add(SB), NOSPLIT|ABIInternal, $0-16
        0x0000 00000 (main.go:6)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (main.go:6)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (main.go:6)        FUNCDATA        $5, "".add.arginfo1(SB)
        0x0000 00000 (main.go:7)        ADDQ    BX, AX
        0x0003 00003 (main.go:7)        RET
        ...

上述例子:函数内不需存放局部变量,framesize = 0, 两个 int64 的参数,argsize = 16

3. 常见操作指令

自主查询链接

下面介绍下常见的

1. 数据搬运
  • MOV 指令:其后缀表示搬运长度, $NUM 表示具体的数字,如下面例子
MOVB $1, DI          // 1 byte,  DI=1
MOVW $0x10, BX       // 2 bytes, BX=10
MOVD $1, DX          // 4 bytes, DX=1
MOVQ $-10, AX     // 8 bytes, AX=-10
  • LEA, 将有效地址加载到指定的地址寄存器中
// ret+24(FP) 这代表了第三个函数参数,是个地址
LEAQ    ret+24(FP), AX    // 把 ret+24(FP) 地址移到 AX 寄存器中
2. 计算指令
  • ADDSUB,IMULQ,如下面例子
ADDQ  AX, BX   // BX += AX
SUBQ  AX, BX   // BX -= AX
IMULQ AX, BX   // BX *= AX
  • 可以利用计算指令来调整栈空间,我们知道 SP 指向栈顶位置,调整 SP中的值即可。
// 栈空间: 高地址向低地址
SUBQ $0x18, SP // 对 SP 做减法,为函数分配函数栈帧
ADDQ $0x18, SP // 对 SP 做加法,清除函数栈帧
3. 条件跳转/无条件跳转
  • JMPJZ,JLS ...
// 无条件跳转
JMP addr   // 跳转到地址,地址可为代码中的地址,不过实际上手写不会出现这种东西
JMP label  // 跳转到标签,可以跳转到同一函数内的标签位置
JMP 2(PC)  // 以当前指令为基础,向前/后跳转 x 行
JMP -2(PC) // 同上

// 有条件跳转
JZ target // 如果 zero flag 被 set 过,则跳转
JLS num        // 如果上一行的比较结果,左边小于右边则执行跳到 num 地址处
4. 其它
  • 比较:CMP, 与挑战指令搭配使用

    CMPQ    BX, $0    // 比较与 BX 与 0 的大小
    JNE    3(PC)            // 左边小于右边则执行跳到当前 PC 指令后第三条指令的位置
  • 位运算: AND,OR,XOR

总结

经过这篇文章,相信你已经能大致读懂一些简单的汇编程序了。这里推荐几个源代码的汇编阅读。

  • Go 程序的起点:src/runtime/asm_amd64.s 中的 rt0_go(SB) 函数
  • Go 原子包:src/runtime/internal/atomic_amd64.s 中的 Case 函数

参考

  1. https://segmentfault.com/a/11...
  2. https://go.dev/doc/asm
  3. https://medium.com/martinombu...
  4. https://xargin.com/go-and-pla...
  5. https://kcode.icu/posts/go/20...
  6. https://mioto.me/2021/01/plan...
  7. https://www.symbolcrash.com/2...

你可能感兴趣的