@Golang汇编快速入门
plan9基本指令常数
常数在 plan9 汇编用 $num 表示,可以为负数,默认情况下为十进制。可以用 $0x123 的形式来表示十六进制数。
栈调整
intel 或 AT&T 汇编提供了 push 和 pop 指令族,plan9 中没有 push 和 pop,栈的调整是通过对硬件 SP 寄存器进行运算来实现的,例如:
SUBQ $20, SP // 对 SP 做减法,为函数分配函数栈帧
ADDQ $20, SP // 对 SP 做加法,清除函数栈帧
数据搬运
搬运的长度是由 MOV 的后缀决定的,操作数的方向是和 intel 汇编相反的,将左值赋值给右值(一般情况下)
MOVB $1, AX // 1 byte
MOVW $1, AX // 2 bytes
MOVD $1, AX // 4 bytes
MOVQ $1, AX // 8 bytes
地址运算
地址运算也是用 lea 指令,amd64 平台地址都是8个字节,所以直接就用LEAQ
LEAQ 4(AX), AX
其他的不做太多介绍
Golang与plan9,必看Go 的汇编还引入了 4 个伪寄存器,援引官方文档的描述:
- FP: Frame pointer: arguments and locals.
- PC: Program counter: jumpsand branches.
- SB: Static base pointer: global symbols.
- SP: Stack pointer: top of stack.
对看golang的汇编代码无多大影响,这里不做过多介绍,之所以提出来是因为这里有坑
对于编译输出(go tool compile -S / go tool objdump)的代码来讲,目前所有的 SP 都是硬件寄存器 SP,因此伪SP,基本上没有啥影响。编译输出的汇编代码只有BP和SP
在这里插入代码片
package main
func main() {
a1:= add(3, 2)
b1:= add(6,4)
a1=a1+b1
}
func add(a, b int) int {
return a+b
}
go tool compile -S -N .\test.go
"".main STEXT nosplit size=131 args=0x0 locals=0x58 funcid=0x0
0x0000 00000 (.\test.go:3) TEXT "".main(SB), NOSPLIT|ABIInternal, $88-0 // 定义函数
0x0000 00000 (.\test.go:3) SUBQ $88, SP // 移动SP,分配栈空间
0x0004 00004 (.\test.go:3) MOVQ BP, 80(SP) // 保存调用者BP
0x0009 00009 (.\test.go:3) LEAQ 80(SP), BP // 移动BP
0x000e 00014 (.\test.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (.\test.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (.\test.go:4) MOVQ $3, "".a+48(SP) // 对第一次add()的传入参数a赋值
0x0017 00023 (.\test.go:4) MOVQ $2, "".b+24(SP) // 同上
0x0020 00032 (<unknown line number>) NOP
0x0020 00032 (.\test.go:10) MOVQ "".a+48(SP), AX // 这里事实上是在add()里的计算过程
0x0025 00037 (.\test.go:10) ADDQ $2, AX // 将计算结果保存到AX里
0x0029 00041 (.\test.go:4) MOVQ AX, ""..autotmp_8+72(SP) // 临时变量,目前还为理解意义在哪
0x002e 00046 (.\test.go:4) MOVQ AX, "".~R0+8(SP) // 将结果传给返回值 也就是 add() int 的int
0x0033 00051 (.\test.go:4) JMP 53 //跳到53
0x0035 00053 (.\test.go:4) MOVQ AX, "".a1+40(SP) // 第二次add()调用 不做解释
0x003a 00058 (.\test.go:5) MOVQ $6, "".a+56(SP)
0x0043 00067 (.\test.go:5) MOVQ $4, "".b+32(SP)
0x004c 00076 (<unknown line number>) NOP
0x004c 00076 (.\test.go:10) MOVQ "".a+56(SP), AX
0x0051 00081 (.\test.go:10) LEAQ 4(AX), CX // 这里是利用LEQ实现计算
0x0055 00085 (.\test.go:5) MOVQ CX, ""..autotmp_9+64(SP)
0x005a 00090 (.\test.go:5) MOVQ CX, "".~R0(SP)
0x005e 00094 (.\test.go:5) NOP
0x0060 00096 (.\test.go:5) JMP 98
0x0062 00098 (.\test.go:5) MOVQ CX, "".b1+16(SP) // a1= a1 + b1的计算过程
0x0067 00103 (.\test.go:6) MOVQ "".a1+40(SP), CX
0x006c 00108 (.\test.go:6) LEAQ (AX)(CX*1), AX
0x0070 00112 (.\test.go:6) LEAQ 4(AX), AX
0x0074 00116 (.\test.go:6) MOVQ AX, "".a1+40(SP) // 将计算结果赋值给a1
0x0079 00121 (.\test.go:7) MOVQ 80(SP), BP // 移动BP
0x007e 00126 (.\test.go:7) ADDQ $88, SP // 缩减栈
0x0082 00130 (.\test.go:7) RET // 返回
"".add STEXT nosplit size=25 args=0x18 locals=0x0 funcid=0x0
0x0000 00000 (.\test.go:9) TEXT "".add(SB), NOSPLIT|ABIInternal, $0-24
0x0000 00000 (.\test.go:9) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (.\test.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (.\test.go:9) MOVQ $0, "".~r2+24(SP) // 给返回值赋初值
0x0009 00009 (.\test.go:10) MOVQ "".a+8(SP), AX
0x000e 00014 (.\test.go:10) ADDQ "".b+16(SP), AX
0x0013 00019 (.\test.go:10) MOVQ AX, "".~r2+24(SP) // 将结果赋值给返回值
0x0018 00024 (.\test.go:10) RET // 返回
TEXT "".main(SB), NOSPLIT|ABIInternal, $88-0
TEXT symbol(SB), [flags,] $framesize[-argsize]
TEXT 用于定义函数符号
symbol 函数名符号,由包名和函数名构成,包名可以省略,SB 表示是函数名符号相对于SB伪寄存器的偏移量,二者组合在一起最终是绝对地址
flags 用于指示函数的一些特殊行为 常见的NOSPLIT主要用于指示叶子函数不进行栈分裂
framesize 表示函数的局部变量需要多少栈空间,其中包含调用其他函数时准备调用参数的隐式栈空间
argsize 表示函数参数的大小,包括传入参数和返回值
栈帧分析
mian()
说句实话main()的栈帧我没太明白,可能是函数调用的原因导致的,autotmp,~R0这些东西究竟有何意义,等我那天懂了,在补充一下吧,原谅我是个小菜鸡
add()
TEXT "".add(SB), NOSPLIT|ABIInternal, $0-24
add()不存在局部变量所以framesize大小为0,存在3个参数所以argsize大小为24
栈结构
下面是一个典型的函数的栈结构图:
args:参数部分的内存排布的方式是后出现变量的在高地址,即从左至右对应的从上往下
Caller BP:存放的是调用函数的BP
frame: 存放局部变量,局部变量采取的内存排布方式是按照变量名升序从上往下拍
return addr: 返回地址,即调用位置