plan9 assembly 完全解析

众所周知,Go 使用了 Unix 老古董(误 们发明的 plan9 汇编。就算你对 x86 汇编有所了解,在 plan9 里还是有些许区别。说不定你在看代码的时候,偶然发现代码里的 SP 看起来是 SP,但它实际上不是 SP 的时候就抓狂了哈哈哈。

本文将对 plan9 汇编进行全面的介绍,同时解答你在接触 plan9 汇编时可能遇到的大部分问题。

本文所使用的平台是 linux amd64,因为不同的平台指令集和寄存器都不一样,所以没有办法共同探讨。这也是由汇编本身的性质决定的。

基本指令

栈调整

intel 或 AT&T 汇编提供了 push 和 pop 指令族,plan9 中没有 push 和 pop,栈的调整是通过对硬件 SP 寄存器进行运算来实现的,例如:

SUBQ $0x18, SP // 对 SP 做减法,为函数分配函数栈帧
...               // 省略无用代码
ADDQ $0x18, SP // 对 SP 做加法,清除函数栈帧

通用的指令和 IA64 平台差不多,下面分节详述。

数据搬运

常数在 plan9 汇编用 $num 表示,可以为负数,默认情况下为十进制。可以用 $0x123 的形式来表示十六进制数。

MOVB $1, DI      // 1 byte
MOVW $0x10, BX   // 2 bytes
MOVD $1, DX      // 4 bytes
MOVQ $-10, AX     // 8 bytes

可以看到,搬运的长度是由 MOV 的后缀决定的,这一点与 intel 汇编稍有不同,看看类似的 IA64 汇编:

mov rax, 0x1   // 8 bytes
mov eax, 0x100 // 4 bytes
mov ax, 0x22   // 2 bytes
mov ah, 0x33   // 1 byte
mov al, 0x44   // 1 byte

plan9 的汇编的操作数的方向是和 intel 汇编相反的,与 AT&T 类似。

MOVQ $0x10, AX ===== mov rax, 0x10
       |    |------------|      |
       |------------------------|

不过凡事总有例外,如果想了解这种意外,可以参见参考资料中的 [1]。

常见计算指令

ADDQ  AX, BX   // BX += AX
SUBQ  AX, BX   // BX -= AX
IMULQ AX, BX   // BX *= AX

类似数据搬运指令,同样可以通过修改指令的后缀来对应不同长度的操作数。例如 ADDQ/ADDW/ADDL/ADDB。

条件跳转/无条件跳转

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

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

指令集

可以参考源代码的 arch 部分。

额外提一句,Go 1.10 添加了大量的 SIMD 指令支持,所以在该版本以上的话,不像之前写那样痛苦了,也就是不用人肉填 byte 了。

寄存器

通用寄存器

amd64 的通用寄存器:

(lldb) reg read
General Purpose Registers:
       rax = 0x0000000000000005
       rbx = 0x000000c420088000
       rcx = 0x0000000000000000
       rdx = 0x0000000000000000
       rdi = 0x000000c420088008
       rsi = 0x0000000000000000
       rbp = 0x000000c420047f78
       rsp = 0x000000c420047ed8
        r8 = 0x0000000000000004
        r9 = 0x0000000000000000
       r10 = 0x000000c420020001
       r11 = 0x0000000000000202
       r12 = 0x0000000000000000
       r13 = 0x00000000000000f1
       r14 = 0x0000000000000011
       r15 = 0x0000000000000001
       rip = 0x000000000108ef85  int`main.main + 213 at int.go:19
    rflags = 0x0000000000000212
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

在 plan9 汇编里都是可以使用的,应用代码层面会用到的通用寄存器主要是: rax, rbx, rcx, rdx, rdi, rsi, r8~r15 这 14 个寄存器,虽然 rbp 和 rsp 也可以用,不过 bp 和 sp 会被用来管理栈顶和栈底,最好不要拿来进行运算。

plan9 中使用寄存器不需要带 r 或 e 的前缀,例如 rax,只要写 AX 即可:

MOVQ $101, AX = mov rax, 101

下面是通用通用寄存器的名字在 IA64 和 plan9 中的对应关系:

IA64 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

伪寄存器

Go 的汇编还引入了 4 个伪寄存器,援引官方文档的描述:

  • FP: Frame pointer: arguments and locals.
  • PC: Program counter: jumps and branches.
  • SB: Static base pointer: global symbols.
  • SP: Stack pointer: top of stack.

官方的描述稍微有一些问题,我们对这些说明进行一点扩充:

  • FP: 使用形如 symbol+offset(FP) 的方式,引用函数的输入参数。例如 arg0+0(FP)arg1+8(FP),使用 FP 不加 symbol 时,无法通过编译,在汇编层面来讲,symbol 并没有什么用,加 symbol 主要是为了提升代码可读性。另外,官方文档虽然将伪寄存器 FP 称之为 frame pointer,实际上它根本不是 frame pointer,按照传统的 x86 的习惯来讲,frame pointer 是指向整个 stack frame 底部的 BP 寄存器。假如当前的 callee 函数是 add,在 add 的代码中引用 FP,该 FP 指向的位置不在 callee 的 stack frame 之内,而是在 caller 的 stack frame 上。具体可参见之后的 栈结构 一章。
  • PC: 实际上就是在体系结构的知识中常见的 pc 寄存器,在 x86 平台下对应 ip 寄存器,amd64 上则是 rip。除了个别跳转之外,手写 plan9 代码与 PC 寄存器打交道的情况较少。
  • SB: 全局静态基指针,一般用来声明函数或全局变量,在之后的函数知识和示例部分会看到具体用法。
  • SP: plan9 的这个 SP 寄存器指向当前栈帧的局部变量的开始位置,使用形如 symbol+offset(SP) 的方式,引用函数的局部变量。offset 的合法取值是 [-framesize, 0),注意是个左闭右开的区间。假如局部变量都是 8 字节,那么第一个局部变量就可以用 localvar0-8(SP) 来表示。这也是一个词不表意的寄存器。与硬件寄存器 SP 是两个不同的东西,在栈帧 size 为 0 的情况下,伪寄存器 SP 和硬件寄存器 SP 指向同一位置。手写汇编代码时,如果是 symbol+offset(SP) 形式,则表示伪寄存器 SP。如果是 offset(SP) 则表示硬件寄存器 SP。务必注意。对于编译输出(go tool compile -S / go tool objdump)的代码来讲,目前所有的 SP 都是硬件寄存器 SP,无论是否带 symbol。

我们这里对容易混淆的几点简单进行说明:

  1. 伪 SP 和硬件 SP 不是一回事,在手写代码时,伪 SP 和硬件 SP 的区分方法是看该 SP 前是否有 symbol。如果有 symbol,那么即为伪寄存器,如果没有,那么说明是硬件 SP 寄存器。
  2. SP 和 FP 的相对位置是会变的,所以不应该尝试用伪 SP 寄存器去找那些用 FP + offset 来引用的值,例如函数的入参和返回值。
  3. 官方文档中说的伪 SP 指向 stack 的 top,是有问题的。其指向的局部变量位置实际上是整个栈的栈底(除 caller BP 之外),所以说 bottom 更合适一些。
  4. 在 go tool objdump/go tool compile -S 输出的代码中,是没有伪 SP 和 FP 寄存器的,我们上面说的区分伪 SP 和硬件 SP 寄存器的方法,对于上述两个命令的输出结果是没法使用的。在编译和反汇编的结果中,只有真实的 SP 寄存器。
  5. FP 和 Go 的官方源代码里的 framepointer 不是一回事,源代码里的 framepointer 指的是 caller BP 寄存器的值,在这里和 caller 的伪 SP 是值是相等的。

以上说明看不懂也没关系,在熟悉了函数的栈结构之后再反复回来查看应该就可以明白了。个人意见,这些是 Go 官方挖的坑。。

变量声明

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

使用 DATA 结合 GLOBL 来定义一个变量。DATA 的用法为:

DATA    symbol+offset(SB
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值