0%

《Golang》Plan9 汇编学习

Plan9 汇编语言是 Plan9 操作系统的汇编器支持的汇编语言

Plan9 操作系统是大名鼎鼎的贝尔实验室设计开发的,虽然它具有很多的优点,但由于诸多原因,它并不是一款成功的操作系统,可能生不逢时吧

Golang 的开发团队和 Plan9 操作系统团队基本一致,Golang 选择了 Plan9 汇编就在情理之中了

函数声明

看下面函数:

1
2
3
4
5
6
7
TEXT ·Print(SB), NOSPLIT, $16-16
MOVQ strp+0(FP), AX
MOVQ AX, 0(SP)
MOVQ size+8(FP), BX
MOVQ BX, 8(SP)
CALL ·Println(SB)
RET
  1. TEXT 是函数声明,类似 func
  2. 奇怪的符号 · ,就是 . 的含义
  3. Print(SB) 中 Print 是函数名
  4. NOSPLIT 使编译器不要进行内联优化
  5. $16-16 表示这个函数栈帧大小是 16 ,有 16 个字节的参数数据处于 caller 栈帧中

全局变量声明

1
2
3
DATA  msg<>+0x00(SB)/8, $"Hello, W"  // 初始化变量。可以参考本文中 SB 寄存器的含义
DATA msg<>+0x08(SB)/8, $"orld!\n"
GLOBL msg<>(SB),NOPTR,$16 // 将上面的变量声明为 global,后面需要跟两个参数,flag 和变量的大小

8(SP) 含义

8(SP) 是指 SP 寄存器中的值加上 8

这里就是指栈顶所指的地址 +8 之后的地址,也就是栈顶第二个元素

其他的比如 0(CX) 也是类似的含义,表示 CX 中的值加上 0

-8(AX) 表示 AX 中的值减去 8

汇编中不允许调用内建函数

汇编中不允许调用内建函数。但是有替代方案

bin/main/main.go

1
2
3
4
5
6
7
8
9
package main

import _ "fmt"

func Print(delta string)

func main() {
Print("hello")
}

bin/main/asm.s

1
2
3
4
5
#include "textflag.h"

TEXT ·Print(SB), NOSPLIT, $8
CALL fmt·Println(SB)
RET

运行上面代码会报错:main.Print: relocation target fmt.Println not defined for ABI0 (but is defined for ABIInternal)

可以改成下面方式

bin/main/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func Print(str string)

func main() {
Print("hello")
}

func Println(str string) {
fmt.Println(str)
}

bin/main/asm.s

1
2
3
4
5
6
7
8
9
10
#include "textflag.h"

TEXT ·Print(SB), NOSPLIT, $16-16
MOVQ strp+0(FP), AX
MOVQ AX, 0(SP) // 第一个参数:数据的开始指针
MOVQ size+8(FP), BX
MOVQ BX, 8(SP) // 第二个参数:string 的大小。改成 MOVQ $100, 8(SP) 试试,会发现打印了其他地方的数据
CALL ·Println(SB)
RET
// 这里一定要有换行,否则编译报错

bin/main/asm.s 中传递参数可能有些疑惑,main.Println 函数明明只有一个参数,为什么放了两个参数?

实际上,在 golang 中,struct 结构不被认为是一个数据单元,struct 中有几个成员,就有几个数据单元,因此 struct 传递时就会传递几个参数

而 golang 中的 string 其实是一个 struct ,定义如下:

1
2
3
4
type stringStruct struct {
str unsafe.Pointer // 数据的开始指针
len int // string的大小
}

所以上面需要传递两个参数

实例

获取 Goroutine Id

Goroutine Id 跟线程 id 一样,其实是有的,只是 Golang 开发人员没有暴露出来,是故意为之的,避免开发人员滥用

但是,如果真的有需求,可以通过汇编取到,看 git 仓库 https://github.com/pefish/go-hack

原子锁

bin/main/main.go

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func Xadd(addr *int32, delta int32) int32

func main() {
var a int32 = 0
fmt.Println(Xadd(&a, 1))
fmt.Println(a)
}

bin/main/asm.s

1
2
3
4
5
6
7
8
9
10
11
#include "textflag.h"

TEXT ·Xadd(SB), NOSPLIT, $0-20
MOVQ ptr+0(FP), BX // 第一个参数放到BX,是个指针
MOVL delta+8(FP), AX // 第二个参数放到AX
MOVL AX, CX // 第二个参数又放到CX
LOCK // 锁总线,多CPU排他执行指令。下一个指令将被锁住,执行完自动释放(CPU特性)
XADDL AX, 0(BX) // 交换并相加。指针中的值与AX的值交换,然后两者相加,结果放到指针指向的值中
ADDL CX, AX // 指针中原来的值加上第二个参数,结果放入AX
MOVL AX, ret+16(FP) // 返回AX
RET

amd64 架构下的常用汇编指令

  • PUSH:进栈指令,PUSH 指令执行时会先将 ESP 减 4 ,接着将内容写入 ESP 指向的栈内存。
  • POP:出栈指令,POP 指令执行时先将 ESP 指向的栈内存的一个字长的内容读出,接着将 ESP 加 4 。注意:用 PUSH 指令和 POP 指令时只能按字访问栈,不能按字节访问栈。
  • CALL:调用函数指令,将返回地址( call 指令的下一条指令)压栈,接着跳转到函数入口。
  • RET:返回指令,将栈顶返回地址弹出到 EIP ,接着根据 EIP 继续执行。
  • LEAVE:等价于 mov esp,ebp; pop ebp;
  • MOVL:在内存与寄存器、寄存器与寄存器之间转移值
  • LEAL:用来将一个内存地址赋给目标。比如 LEAQ 0x17(SP), AX 是将指向 SP+0x17 位置的指针赋给 AX ,而 MOVQ 0x20(SP), AX 则是将 SP+0x20 处的值赋给 AX ,注意区分。注意:8 位指令后缀是 B、16 位是 S(字)、32 位是 L(双字)、64 位是 Q(四字)
  • ADD:ADDL CX, AX,保存在 AX 和 CX 寄存器中的值进行相加,然后再保存进 AX 寄存器中
  • XADD:交换并相加
  • NEG:求补指令,就是取相反数
  • SAR(shift arithmetic right):算数右移指令。右移时保留操作数的符号
  • SHR(shift logical right):逻辑右移指令。右移时不保留操作数的符号,用 0 代替
  • SAL:算数左移指令。最低位用 0 填充
  • SHL:逻辑左移指令。与 SAL 效果一样
  • XORPS:源操作数(第二个操作数)与目标操作数(第一个操作数)进行异或。结果保存到目标操作数
  • MOVUPS:与 MOV 一样,操作对象的类型不一样,这里是包含四个压缩单精度浮点值的双四字
  • SYSCALL:执行系统调用,系统调用号需要放入 AX 中,参数要分别放入 di, si, dx, r10, r8, r9 寄存器中。MOVL $SYS_clone, AX; SYSCALL 就是执行 clone 系统调用,结果保存到 AX
  • AH:是 ax 的高 32 位
  • AL:是 ax 的低 32 位
  • XCHG: 交换两个操作数内容,自带 LOCK 总线锁属性
  • JL: 全名 jump less,意为小于跳转,数比较类似的还有还有 jg、ja、jb 等

常见寄存器含义

  • BP: 栈基,栈帧(函数的栈叫栈帧)的开始位置
  • SP: 栈顶,栈帧的结束位置
  • PC: 就是 IP 寄存器,存放 CPU 下一个执行指令的位置地址
  • TLS: 虚拟寄存器。表示的是 thread-local storage ,Golang 中存放了当前正在执行的 g 的结构体
  • FS: 真实寄存器。32 位 Windows 中 FS 寄存器存放线程结构体 TIB 段的段号,TLS 数组存放在线程结构体中,可以通过 FS:[0x2C] 访问到 TLS 数组
  • GS: 与 FS 一样的功能,不过 64 位 Windows 使用的不是 FS 而是 GS
  • FP: 用来标识函数参数、返回值。其通过 symbol+offset(FP) 的方式进行使用。例如 arg0+0(FP) 表示函数第一个参数其实的位置( amd64 平台),arg1+8(FP) 表示函数参数偏移 8byte 的另一个参数。arg0/arg1 用于助记,但是必须存在,否则无法通过编译。至于这两个参数是输入参数还是返回值,得对应其函数声明的函数个数、位置才能知道
  • SB: 可以认为是内存的开始位置。foo(SB) 表示 foo 是 SB 偏移 0 处的地址,与 foo+0(SB) 一个意思。变量名后面加个 <> , foo<>+0x00(SB)/4 则表示 &foo~&foo+0x04 一段区间。DATA divtab<>+0x00(SB)/4 , $0xf4f8fcff 就是声明一个 4 字节常量

参考文档

Plan9 操作系统:https://plan9.io/plan9/

Plan9 汇编器手册:http://doc.cat-v.org/plan_9/4th_edition/papers/asm




微信关注我,及时接收最新技术文章