“ 看书犯困,那是梦开始的地方。”
Plan9汇编语言选择的是Plan9操作系统的汇编器支持的汇编语言
Plan9操作系统是大名鼎鼎的贝尔实验室设计开发的,虽然它具有很多的优点,但由于诸多原因,它并不是一款成功的操作系统,可能生不逢时吧
Golang的开发团队和Plan9操作系统团队基本一致,Golang选择了Plan9汇编就在情理之中了
函数声明
看下面函数:
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
- TEXT是函数声明,类似func
- 奇怪的符号·,就是.的含义
- Print(SB)中Print是函数名
- NOSPLIT使编译器不要进行内联优化
- $16-16表示这个函数栈帧大小是16,有16个字节的参数数据处于caller栈帧中
全局变量声明
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)是指栈顶所指的地址+8之后的地址,也就是栈顶第二个元素
其他的比如0(CX)也是类似的含义,括号前面表示偏移,负数则表示负向偏移
汇编中不允许调用内建函数
汇编中不允许调用内建函数。但是有替代方案
bin/main/main.go
package main
import _ "fmt"
func Print(delta string)
func main() {
Print("hello")
}
bin/main/asm.s
#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
package main
import (
"fmt"
)
func Print(str string)
func main() {
Print("hello")
}
func Println(str string) {
fmt.Println(str)
}
bin/main/asm.s
#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,定义如下:
type stringStruct struct {
str unsafe.Pointer // 数据的开始指针
len int // string的大小
}
所以上面需要传递两个参数
实例
获取Goroutine Id
Goroutine Id跟线程id一样,其实是有的,只是Golang开发人员没有暴露出来,是故意为之的,避免开发人员滥用
但是,如果真的有需求,可以通过汇编取到,看代码
bin/main/main.go
package main
import (
"fmt"
)
func getgid() int64
func main() {
fmt.Println(getgid())
}
bin/main/asm.s
#include "textflag.h"
TEXT ·getgid(SB), NOSPLIT, $0-8
MOVL TLS, CX // 取出TLS
MOVQ 0(CX)(TLS*1), AX // 取出当前g
MOVQ AX, ret+0(FP) // 返回g结构的第一个元素,正是goroutine id
RET
原子锁
bin/main/main.go
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
#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
常用汇编指令
- 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:用来将一个内存地址直接赋给目的操作数 注意:8位指令后缀是B、16位是S、32位是L、64位是Q
- ADD:ADDL CX, AX,保存在 AX 和 CX 寄存器中的值进行相加,然后再保存进 AX 寄存器中
- XADD:交换并相加
常见寄存器含义
- BP: 栈帧(函数的栈叫栈帧)的开始位置
- SP: 栈帧的结束位置
- PC: 就是IP寄存器,存放CPU下一个执行指令的位置地址
- TLS: 表示的是thread-local storage,它存放了g结构体
- 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
戳↙【阅读原文】