Golang|plan9汇编
当我们使用go tool compile -S -N -l xxx.go
命令,查看go语言对应的汇编代码时,生成的一行行汇编指令可能让人困惑,由于Go 使用了plan9 汇编,因此这篇文章接下来记录一下基础的plan9汇编知识。
基础指令
栈调整
plan9 的栈的调整大多是通过对硬件 SP 寄存器进行运算来实现的
|
|
数据常量
常数在 plan9 汇编用 $num
表示,可以为负数,默认情况下为十进制,可以用 $0x123 的形式来表示十六进制数。
搬运指令
搬运的长度是由 MOV 的后缀决定的,操作方向上左边是数据源,右边是要搬运的寄存器
|
|
计算指令
类似数据搬运指令,同样可以通过修改指令的后缀来对应不同长度的操作数。例如 ADDQ/ADDW/ADDL/ADDB。
|
|
跳转指令
|
|
寄存器
通用寄存器
plan9汇编里可使用的通用寄存器
AX、BX、CX、DX、DI、SI、BP、SP、R8、R9、R10、R11、R12、R13、R14、PC
伪寄存器
- FP: 使用形如
symbol+offset(FP)
的方式,引用函数的输入参数。例如arg0+0(FP)
,arg1+8(FP)
,使用 FP 不加symbol
时,无法通过编译,在汇编层面来讲,symbol
并没有什么用,加symbol
主要是为了提升代码可读性。 - PC: 实际上就是在体系结构的知识中常见的 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
。
变量声明
在汇编里所谓的变量,一般是存储在 .rodata 或者 .data 段中的只读值。对应到应用层的话,就是已初始化过的全局的 const、var、static 变量/常量。
使用 DATA 结合 GLOBL 来定义一个变量。
DATA 的用法为: DATA symbol+offset(SB)/width, value
, offset 需要稍微注意。其含义是该值相对于符号 symbol 的偏移。
GLOBL 的用法为: GLOBL divtab(SB), RODATA, $64
,GLOBL 必须跟在 DATA 指令之后。
下面是一个定义了多个 readonly 的全局变量的完整例子:
|
|
函数
一个典型的 plan9 的汇编函数的定义,在 plan9 中 TEXT 是一个指令,用来定义一个函数。
|
|
栈结构
|
|
从原理上来讲,如果当前函数调用了其它函数,那么 return addr 也是在 caller 的栈上的,不过往栈上插 return addr 的过程是由 CALL 指令完成的,在 RET 时,SP 又会恢复到图上位置。我们在计算 SP 和参数相对位置时,可以认为硬件 SP 指向的就是图上的位置。
调用流程
函数调用过程中栈帧发生了哪些变化?
- 执行 CALL 指令,调用
- 入栈函数调用后的返回地址 return address(callee 返回到 caller 后,执行的 caller 的后续指令的地址 addr)
- 跳转到 PC 寄存器指向的指令地址
- 分配好 callee 栈帧
- 函数调用头部,编译器会插入 3 指令
- 第一条:SUBQ $16, SP 分配 callee 的栈帧空间,将 sp 向下移动 16 字节,这个就是 callee 的栈顶
- 第二条:MOVQ BP, 8(SP) 将 caller 的 BP 栈基备份到 return address 下面(低地址处)
- 第三条:LEAQ 8(SP), BP 将 BP 寄存器指向 callee 的栈基(对 0x8(SP) 取地址,这个位置就是 callee 的栈基)
- 函数调用头部,编译器会插入 3 指令
- 执行完 callee 函数代码段 TEXT 后续的指令
- 恢复 caller 的栈帧
- 函数返回前,需要恢复 caller 的栈帧,编译器会插入 2 条指令到 函数的尾部
- 第一条:MOVQ 8(SP), BP 恢复 caller 的 BP 栈基地址
- 第二条:ADDQ $16, SP 释放 callee 的栈空间,SP 寄存器自然就恢复到了 caller 当初的位置
- 函数返回前,需要恢复 caller 的栈帧,编译器会插入 2 条指令到 函数的尾部
- 执行 RET 指令,返回
- 出栈返回地址 return address
- PC 寄存器跳转到 return address
- 执行 return address 处的指令,caller 得以恢复执行