在浏览 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 mainfunc 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 汇编语言提供了如下映射,在汇编语言中间接援用就可应用物理寄存器了。

amd64raxrbxrcxrdxrdirsirbprspr8r9r10r11r12r13r14rip
Plan9AXBXCXDXDISIBPSPR8R9R10R11R12R13R14PC

如上文的例子中应用到了: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 字节的地址。<> 限度符号只能在以后源文件应用

从网上偷的图:

2. 指令

1. 变量申明

格局: 应用 DATAGLOBL 来申明一个全局变量

DATA symbol+offset(SB)/width, valueGLOBL 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, $0GLOBL _rt0_amd64_lib_argc<>(SB),NOPTR, $8DATA _rt0_amd64_lib_argv<>(SB)/8, $0GLOBL _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.gopackage mainfunc 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=1MOVW $0x10, BX       // 2 bytes, BX=10MOVD $1, DX          // 4 bytes, DX=1MOVQ $-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 += AXSUBQ  AX, BX   // BX -= AXIMULQ 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...