原文:http://tianyalong.icu/content.html?id=5
Go汇编
实现和声明
Go汇编语言并不是一个独立的语言,因为Go汇编程序无法独立使用
。Go汇编代码必须以Go包
的方式组织,同时包中至少
要有一个Go
语言文件用于指明当前包名等基本包信息。如果Go汇编代码中定义的变量和函数要被其它Go语言代码引用,还需要通过Go语言代码将汇编中定义的符号声明
出来。用于变量的定义和函数的定义Go汇编文件类似于C语言中的.c文件,而用于导出汇编中定义符号的Go源文件类似于C语言的.h文件。
查看Go汇编
pkg.go
package pkg
var Id = 9527
go tool compile -S pkg.go
- -S
:表示输出汇编格式
- NOPTR
:表示没有指针类型数据
- 0x0000 37 25 00 00 00 00 00 00
:表示十六进制的9527
- Id
:代表变量的名字
- size
:代表内存的大小
go.cuinfo.packagename. SDWARFCUINFO dupok size=0
0x0000 70 6b 67 pkg
"".Id SNOPTRDATA size=8
0x0000 37 25 00 00 00 00 00 00 7%......
DATA
命令用于初始化包变量symbol
为变量在汇编中的表示符offset
代表的是偏移量width
初始化内存的大小·
代表这是一个全局变量<>
代表是一个私有变量
DATA ·symbol+offset(SB)/width, value
GLOBL file_private<>(SB),$1
GLOBL
命令用于将符号导出
GLOBL ·Id, $8
- string类型的go汇编
SRODATA
:标志表示这个数据在只读内存段dupok
:表示出现多个相同标识符的数据时只保留一个就可以了- 字符串变量只有十六个字节,对应
reflect.StringHeader
的Data
指针和Len
长度
package pkg
var Id = "gopher"
go.cuinfo.packagename. SDWARFCUINFO dupok size=0
0x0000 70 6b 67 pkg
go.string."gopher" SRODATA dupok size=6
0x0000 67 6f 70 68 65 72 gopher
"".Id SDATA size=16
0x0000 00 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00 ................
rel 0+8 t=1 go.string."gopher"+0
- 定义main函数
$16-0
表示main函数的栈帧大小是16字节,0代表函数没有返回值runtime·printstring(SB)
用来打印字符串runtime·printnl
打印换行符
package main
var helloworld = "你好,世界"
func main()
TEXT ·main(SB), $16-0
MOVQ ·helloworld+0(SB), AX; MOVQ AX, 0(SP)
MOVQ ·helloworld+8(SB), BX; MOVQ BX, 8(SP)
CALL runtime·printstring(SB)
CALL runtime·printnl(SB)
RET
X86-64体系结构
X86其实是是80X86的简称(后面三个字母),包括Intel 8086、80286、80386以及80486等指令集合,因此其架构被称为x86架构
- text
一般对应代码段
,用于存储要执行的指令
数据,一般是只读
的
- rodata
和data
为数据段,一般存放全局的数据,rodata
为只读的
- heap
段用于管理动态数据
- stack
用于管理每个函数调用时相关的数据
- rip
下个指令的地址
- rsp
stack point,栈顶指针,函数调用隐式的影响rps的值
- rbp
base point,当前栈帧起始位置,和栈相关指令隐式影响rsp的值
Go汇编中的伪寄存器
Go汇编为了简化汇编代码的编写,引入了PC
、FP
、SP
、SB
四个伪寄存器
- PC(program counter)
就是rip
寄存机的别名
- FP(stack frame pointer)
对应函数的栈指针,一般用来访问函数参数和返回值
- SP(virtual stack pointer)
对应当前函数栈帧的底部,一般用于定位局部变量。真的
SP寄存器对应的是栈的顶部,一般用于定位调用其他函数参数和返回值。
- SB(Static base pointer)
寄存器对应代码段
- 区分伪汇编和真汇编寄存器一般通过是否有前缀进行区分,例如(SP)
、+8(SP)
为真的,a(SP)
、b+8(SP)
为假的。
- 不同的字节宽度用B
一个字节、W
两个字节、L
4个字节、Q
8个字节
- JL
、JLZ
、JE
、JNE
、JG
、JGE
等指令,对应小于
、小于等于
、等于
、不等于
、大于
和大于等于
等条件时跳转。JMP
指令则对应无条件
跳转
- LEA
指令将标准参数格式中的内存地址
加载到寄存器
- PUSH
和POP
分别是压栈和出栈指令
常量与全局变量
常量是以$为前缀,常量的类型有整数常量、浮点数常量、字符常量和字符串常量
等几种类型。
$1 // 十进制
$0xf4f8fcff // 十六进制
$1.5 // 浮点数
$'a' // 字符
$"abcd" // 字符串
汇编函数
函数标识符通过TEXT
汇编指令定义,表示该行开始的指令定义在TEXT
内存段。
TEXT symbol(SB), [flags,] $framesize[-argsize]
- TEXT指令、函数名、可选的flags标志、函数帧大小(局部变量)和可选的函数参数(函数参数和返回值)大小。
- flags中常见的
NOSPLIT
主要用于指示叶子函数不进行栈分裂;WRAPPER
标志则表示这个是一个包装函数
,在panic
或runtime.caller
等某些处理函数帧的地方不会增加函数帧计数。NEEDCTXT
表示需要一个上下文参数,一般用于闭包函数
。
func Swap(a, b int) (int, int)
上述函数对应两种汇编形式
TEXT ·Swap(SB), NOSPLIT, $0-32
TEXT ·Swap(SB), NOSPLIT, $0
- 调用函数时,函数的
参数
和返回值
都存在当前栈中,而被调用函数的局部变量在被调用函数的栈帧中。 - 伪SP寄存器指向被调用调用栈的底部,真SP指向被调用函数的顶部。
- 伪FP寄存器表示函数当前帧的地址,也就是第一个参数的地址。
- 局部变量,先定义的离伪SP寄存器会越远
if/goto
goto
语句是有严格限制的:它无法跨越代码块,并且在被跨越的代码中不能含有变量定义的语句。
CMPQ CX, $0 // test ok
JZ L // if ok == 0, goto L
MOVQ AX, ret+24(FP) // return a
RET
L:
MOVQ BX, ret+24(FP) // return b
RET
call/ret
首先是调用函数前准备的输入参数和返回值空间。然后CALL
指令将首先触发返回地址入栈
操作。在进入到被调用函数内之后,汇编器自动插入了BP
寄存器相关的指令,因此BP
寄存器和返回地址
是紧挨着的。再下面就是当前函数的局部变量
的空间,包含再次调用其它函数需要准备的调用参数空间。被调用的函数执行RET
返回指令时,先从栈恢复BP
和SP
寄存器,接着取出的返回地址跳转到对应的指令执行。
- NO_LOCAL_POINTERS该语句表示函数没有局部指针变量
系统调用
系统调用(syscall.Syscall)的前6个参数直接由DI
、SI
、DX
、R10
、R8
和R9
寄存器传输,结果由AX
和DX
寄存器返回。