2020.01学习笔记
go汇编学习笔记。
go汇编
参考资料:
go汇编最关键的是理解四个伪寄存器 PC, SB, FP, SP。四个伪寄存器和AMD64的寄存器的关系如下图:
编译过程
go语言代码 => go汇编代码 => 平台代码
PC
PC寄存器与IP寄存器是等价的,也就是说在 go汇编代码 => 平台代码 过程中,PC寄存器直接转换成IP寄存器。
SB
我们都知道,在linux下,虚拟内存被划分成 文本区,数据区(大概可以分为全局数据区,只读数据区,bss区),栈区,堆区。SB寄存器在go汇编中就是用来定义和读取 文本区,数据区的。
定义和读取数据区
定义数据的格式:
GlOBL symbol(SB), width
DATA symbol+offset(SB)/width, value
//pkg.s
//DATA ·Id+0(SB)/8, $18 填充Id的数据,offset=>第0个字节开始,width=>填充宽度8
//GLOBL ·Id(SB), $8 导出8个字节的变量Id,前面的·是格式,加上就好
//pkg.go
//var Id int 声明Id为整形
offset 和 width也可以是别的值,操作的内存不要超出声明的8个字节就行了。
还可以给数据加上 NOPTR(非指针),RODATA(只读) 等标志,但是这很麻烦,我们一般不用go汇编声明数据,直接用go语言(var Id = 18)就好了。
但是用汇编读取数据区的数据是经常要做的,读取数据格式:
symbol+offset(SB)
//MOVQ ·Id+0(SB), AX 从Id变量的第0个字节开始读取,MOVQ指令决定了读取宽度为8个字节
定义和读取文本区
文本区也叫做代码区,代码是可运行的数据。
定义文本区基本就是定义函数。
定义函数的格式,先记住格式,$framesize-argsize跟函数调用栈有关,搞清楚之后也很容易确定:
TEXT symbol(SB), $framesize-argsize
//pkg.s
//TEXT ·ReadData(SB), $0-8 $0-8表示函数的帧为0字节,参数和返回值总大小为8字节
// MOVQ ·Id+0(SB), AX
// MOVQ AX, r0+0(FP)
// RET
//
//pkg.go
//func ReadData() int 声明函数
读取文本区就是调用函数,跳转到函数定义的地址。
symbol(SB)
// CALL ·ReadData(SB)
上面只是说了定义函数和调用函数的格式,那原理是什么?这跟 FP,SP 寄存器相关。
FP 和 SP 和 真SP 和 BP
这是三个密切相关的寄存器,我们硬件也有一个栈顶寄存器SP,接下来把它叫做 真SP。
在C中,CALL指令会把当前IP压栈,RET指令相当于 POP IP,然后在被调用函数内一般还会将BP寄存器压栈,用BP寄存器访问局部变量。如果不记得了,可以先复习一下C函数调用栈。
在go中,CALL指令会把当前PC压栈,RET指令相当于 POP PC,BP寄存器会根据被调用函数的framsize是否大于0来确定是否压栈。
通过寄存器来引用一个值的格式,symbol只是一个助记符,没有其他意义,也不需要和传入的参数名字一样:
symbol+offset(FP)
//arg1+8(FP) 表示FP的地址向前偏移8个字节的地址中的数据
要注意的是,在go汇编中 offset(SP) 的SP表示真SP,symbol+offset(SP)的SP表示伪寄存器。
我们定义一个函数:
// 想实现一个函数 func(a int, b int) int { return a*a + b*b }
//
//pkg.s
//TEXT ·Sum(SB), $0-24
// MOVQ a+8(SP), AX
// IMULQ AX, AX
// MOVQ b+16(SP), CX
// IMULQ CX, CX
// ADDQ CX, AX
// MOVQ AX, r1+24(SP)
// RET
//
//pkg.go
//func Sum(a int, b int) int
这个函数的 framsize 为 0, argsize 为 24。
假设调用 Sum(1, 2),它的函数帧栈为:
因为 framsize 等于 0,所以 BP 寄存器不压栈,真SP和伪SP都指向返回地址,FP指向函数的第一个参数。
所以,我们可以通过三种方式 arg1+0(FP), arg1+8(SP), 8(SP)来获取第一个参数的值。
go的返回值也是通过栈传递的,在这里 r1+16(FP), r1+24(SP), 24(SP)都可以表示返回值。
再定义一个函数:
// 想实现一个函数 func(a int, b int) int { return a*a + b*b }
//
//pkg.s
//TEXT ·Sum(SB), $16-24
// MOVQ a+0(FP), AX
// IMULQ AX, AX
// MOVQ b+8(FP), CX
// IMULQ CX, CX
// ADDQ CX, AX
// MOVQ AX, r1+16(FP)
// RET
//
//pkg.go
//func Sum(a int, b int) int
不同之处在于 framsize 为 16,通过 FP 来使用参数和设置返回值,函数帧栈为:
因为 framsize 大于 0 ,BP寄存器会压栈。同时 真SP 向下偏移framsize字节,预留空间。
go汇编 => 平台汇编 的过程中编译器会根据framsize会通过插入额外的压栈指令处理,手写汇编的时候不用自己判断,只需要知道寄存器指向哪里就好了。
为什么要预留空间呢?我们手写汇编时不应该使用PUSHQ,POPQ指令改变真SP的值,只能使用framsize规划好的内存,因为在运行这些指令时,我们没法知道协程栈会不会爆掉。如果提前规划好,调用前就可以比较栈够不够大。
而且,FP,SP只是真SP的快捷键,当真SP改变时,FP,SP也会改变。
在BP寄存器压栈时,它们的关系为:
FP = 真SP + framsize + 16
SP = 真SP + framsize
那framsize怎么确定,其实framsize不需要十分精确,但是要保证足够用来调用函数,比如如果想调用一个声明为 func (int, int)int 的函数,那自己的framsize至少得为24吧,否则调用函数的空间都没有。如果要保存局部变量,那还得更大一点。总之,framsize不用十分精确,至少要够用,大一点也无所谓。
再用一个函数验证FP,SP,真SP之间的关系:
// func(int) (int, int, int)
TEXT ·Output(SB), $8-48
// 通过FP获取
MOVQ arg1+0(FP), AX
MOVQ AX, ret1+8(FP)
// 通过伪SP获取
MOVQ arg1+16(SP), BX // 当前framsize大小为8 > 0,所以 FP 在 SP 的上方 16 字节处
MOVQ BX, ret2+16(FP)
// 通过真SP获取
MOVQ 24(SP), DX
MOVQ DX, ret3+24(FP)
RET
调用:
a, b, c := Output(100)
println(a, b, c) // 100 100 100
混合类型
func (a bool, b int16, c []int)
这种类型只要考虑清楚内存对齐,并不比全是 int 复杂多少,这个图很好地说明了问题: