操作方向

plan9 汇编操作数方向 与 intel 汇编方向相反,plan9 是从左到右,intel 是从右到左。

1
2
3
4
//plan9 汇编
MOVQ $123, AX
//intel汇编
mov rax, 123

栈扩大、缩小

plan9 中栈操作并没有 push pop,而是采用 sub 和 add SP。

SP 是栈顶指针,对应 BP 栈底指针,一般只需要操作 SP 指针,即可完成入栈,出栈操作, 所以 BP 指针用的少。

1
2
SUBQ $0x18, SP // 对SP做减法 为函数分配函数栈帧
ADDQ $0x18, SP // 对SP做加法 清除函数栈帧

数据拷贝

MOV 开头的指令是用来移动数据的, 提供的命令比较丰富, 一次移动的数据量有所差异

1
2
3
4
MOVB $1, DI      // MOVB 可以一次移动 1 byte
MOVW $0x10, BX   // MOVW 可以一次移动 2 bytes
MOVD $1, DX      // MOVD 可以一次移动 4 bytes
MOVQ $-10, AX    // MOVQ 可以一次移动 8 bytes

计算指令

  • ADD 做加法
  • SUB 做减法
  • IMUL 做乘法
1
2
3
ADDQ AX, BX      // BX += AX
SUBQ AX, BX      // BX -= AX
IMULQ AX, BX     // BX *= AX

跳转

跳转指令是程序执行流程切换的关键

1
2
3
4
5
6
7
8
//无条件跳转
JMP label  // 跳转到标签 可以跳转到同一函数内的标签位置 (常用)
JMP addr   // 跳转到地址,地址可为代码中的地址 不过实际上手写不会出现这种东西
JMP 2(PC)  // 以当前 PC 为基础,向前/后跳转 n 行
JMP -2(PC) // 同上

//有条件跳转
JNZ target // 如果zero flag被set过,则跳转

常数定义

plan9 汇编中使用 num 表示常数,可以为负,默认情况为 十进制,可以使用 0x123 的形式表示 十六进制

变量声明

汇编中的变量一般是存储在 .rodata 或者 .data 段中的只读值。对应到应用层就是已经初始化的全局的 const、var 变量/常量。

  • DATA 可以声明和初始化一个变量
1
DATA symbol+offset(SB)/width,value

上面的语句初始化 symbol+offset(SB) 的数据中 width bytes, 赋值为 value。(相对于栈操作,SB 的操作都是增地址,栈是减 SP 地址)

  • GLOBL 声明一个全局变量

如果在 go package 下,GLOBL 可以导出 DATA 初始化的变量给外部使用

1
2
3
GLOBL runtime·tlsoffset(SB), NOPTR, $4
// 声明一个全局变量tlsoffset,4byte,没有DATA部分,因其值为0。
// NOPTR 表示这个变量数据中不存在指针,GC不需要扫描。

寄存器

Go 汇编为了简化汇编代码的编写,引入了 PC、FP、SP、SB 4 个伪寄存器,加其它的通用寄存器就是 Go 汇编语言对 CPU 的重新抽象。

以 AMD64 环境为例,各个寄存器的用途说明

  • 伪PC寄存器:

    • 含义:IP寄存器的别名,指向指令地址
    • 用途:用来指示下一条指令的地址(逻辑地址即偏移量),一般情况下,系统指示对其进行加1操作,当遇到转移指令,如JMP、CALL、LOOP等时系统就会将跳转到的指令地址保存在PC中
    • 使用频率:除了个别跳转之外,手写代码与PC寄存器打交道的情况较少
  • 伪SB寄存器:

    • 含义:可以理解为原始内存,指向全局符号表
    • 用途:一般用来声明函数或全局变量
    • 使用:foo(SB)的意思是用foo来代表内存中的一个地址。foo(SB)可以用来定义全局的function和数据,foo<>(SB)表示foo只在当前文件可见,跟C中的static效果类似。此外可以在引用上加偏移量,如foo+4(SB)表示foo+4bytes的地址
    • 使用频率:常用
  • 真BP寄存器:

    • 含义:表示已给调用栈的 起始栈底 (栈的方向从大到小,SP表示栈顶),记录当前函数栈帧的 结束位置
    • 用途:保存在进入函数前的栈基址,配合SP使用,维护调用栈关系
    • 使用频率:一般用的不多,若需要做手动维护调用栈关系,需要用到BP寄存器,手动split调用栈。
    • 细节:函数调用相关的指令会隐式地影响BP的值
  • SP寄存器

    • 含义:表示已给调用栈的 结束栈顶 (栈的方向从大到小,BP表示栈底),记录当前函数栈帧的 结束位置
    • 用途:伪SP一般用于定位局部变量
    • 使用:
      • 通过symbol+offset(SP)的方式使用。SP指向local stack frame的栈顶,所以使用时需要使用 负偏移量,取值范围为[-framesize,0)
      • foo-8(SP)表示foo的栈第8byte
    • 使用频率:常用
    • SP有伪SP和硬件SP的区分,两个 SP 寄存器的区别和关系:
      • 区别:
        • 伪SP寄存器:本地变量最高起始地址
        • 真SP寄存器:函数栈真实栈顶地址
      • 关系:
        • 若没有本地变量: 伪SP=硬件SP+8
        • 若有本地变量:伪SP=硬件SP+16+本地变量空间大小
      • 如何区分:
        • 伪寄存器一般需要一个标识符和偏移量为前缀,如果没有 标识符前缀 则是真寄存器。比如(SP)、+8(SP)没有标识符前缀为真SP寄存器,而a(SP)、b+8(SP)有标识符为前缀表示伪寄存器。
  • 伪FP寄存器:

    • 含义:编译器 维护了基于FP偏移的栈上参数指针,标识参数
    • 用途:一般用来标识和访问函数的参数和返回值
    • 使用:要访问具体function的参数,编译器强制要求必须使用 标识符前缀 来访问FP,比如 foo+0(FP)获取foo的第一个参数,foo+8(FP)获取第二个参数,64位系统加上偏移量就可以访问更多的参数
    • 使用频率:常用
    • 与伪SP寄存器的关系:
      • 若本地变量或者栈调用存严格split关系(无NOSPLIT),伪FP=伪SP+16,否则 伪FP=伪SP+8
      • FP是访问入参、出参的基址,一般用 正向偏移 来寻址
      • SP是访问本地变量的起始基址,一般用 负向偏移 来寻址
    • 注意: 修改硬件SP,会引起伪SP、FP同步变化
  • 真通用寄存器:AX、BX、CX、DX、SI、DI、R8-R15

  • R0-R7:并不是通用寄存器,它们只是X87开始引入的MMX指令专有的寄存器

  • TLS 伪寄存器

    • 该寄存器存储当前 goroutine g 结构地址

参考资料: