go-asm-基础
了解底层才能理解程序的运作方式,哪怕只懂一点点汇编,也便于更好地理解计算机底层原理。
基础
程序的编译阶段
从源码到最终可执行文件的整体流程如下:
源程序->编译预处理->编译->汇编->链接->可执行文件。
编译器
编译器即编译这一步干活的程序。
编译器粗略分为
词法分析,语法分析,类型检查,中间代码生成,代码优化,目标代码生成,目标代码优化。
以 中间代码生成 区分为前端/后端
指令集
目前市面上存在两种指令集架构类型:
- Reduced Instruction Set Computing (RISC) 精简指令集,比如ARM,MIPS等
- Complex Instruction Set Computing (CISC) 复杂指令集,比如Intel的X86及优化指令集SSE/AVX等
linux 可以 cat /proc/cpuinfo
查看支持指令集
macos 可以 sysctl machdep.cpu
查看支持指令集
go也可以通过cpu.X86查看X86当前支持的指令集
1 | package main |
Go汇编语言并没有支持全部的CPU指令。
完整的X86指令定义看这里。
同时Go汇编还正对一些指令定义了别名,具体可以参考这里。
寄存器
通用寄存器
rax(al,ah,ax,eax),rbx,rcx,rdx,rdi,rsi,r8-r15
Go汇编精简为
AX,BX,CX…..
指针寄存器
bp:指向栈底
sp:指向栈顶
段寄存器
cs——代码段寄存器(Code Segment Register),其值为代码段的段值
ds——数据段寄存器(Data Segment Register),其值为数据段的段值
es——附加段寄存器(Extra Segment Register),其值为附加数据段的段值
ss——堆栈段寄存器(Stack Segment Register),其值为堆栈段的段值
fs——附加段寄存器(Extra Segment Register),其值为附加数据段的段值
gs——附加段寄存器(Extra Segment Register),其值为附加数据段的段值
指令指针寄存器
rip: instruction pointer指令指针寄存器
cs:ip 一般也称为PC(programme counter程序计数器),指向下一条要执行的指令
伪寄存器
Go汇编为了简化汇编代码的编写,工具链维持出来了FP、PC、SB、SP四个伪寄存器
。
- FP: frame pointer, 帧指针,参数和局部变量
- PC: program counter: 程序计数器,跳转和分支
- SB: static base pointer: 静态基址指针,全局符号
- SP: stack pointer: 栈指针,栈的顶端
FP:寄存器指向函数参数。0(FP)是第一个参数,8(FP)是第二个参数(64-bit machine). first_arg+0(FP)表示把第一个参数地址绑定到符号 first_arg, 这个与SB的含义不同。
PC:程序计数器,指下一步要执行的程序,伪寄存器PC和硬件寄存器PC作用差不多。
SB:寄存器表示全局内存起点,foo(SB) 表示 符号foo作为内存地址使用。这种形式用于命名 全局函数,数据。<>限制符号只能在当前源文件使用,类似 C 中的 static。foo+4(SB)表示foo 往后 4字节的地址。
SP:寄存器表示栈指针,指向当前栈顶, 所以 offset 都是负数,范围在 [ -framesize, 0 ), 例如 x-8(SP). 对于硬件寄存器名称为SP的架构,x-8(SP) 表示虚拟栈指针寄存器(实际上是当前栈帧的 BP 位置), -8(SP) 表示硬件 SP 寄存器(与虚拟SP相隔当前栈帧大小).如果使用SP时有一个临时标识符前缀就是伪SP,否则就是真SP寄存器,所有生成的汇编代码都是真实SP寄存器,与手写汇编不一致
下方给个参考图示,1.17后会有区别见下方调用规约相关。
1 | 高地址位 |
其他
其他寄存器,比如cf,xmm等可以自己查看相关文献
汇编风格比较
Intel | AT&T | Go |
---|---|---|
mov eax, 1 |
movl $1, %eax |
MOVQ $1, AX |
mov rbx, 0ffh |
movl $0xff, %rbx |
MOVQ $0xff, BX |
mov ecx, [ebx+3] |
movl 3(%ebx), %ecx |
MOVQ 2(BX), CX |
整体风格与AT&T类似
- 以$符号表示常量,但是寄存器并没有%前缀
- 以命令来区分操作长度比如 MOVB MOVW MOVL MOVQ
Go汇编命令绝大大多与AT&T一致 op src dst
,比如 MOVQ $1, AX
表示把1赋值给AX,也有一部分是不一样的,比如CMP相关
程序段(section)
程序编译以后会分为不同的段,一般来说会分为
- .text 代码段 存放程序代码,通常是只读的
- .data 数据段 存放初始化了的全局变量和初始化了的静态变量
- .bss 全局变量段 存放未初始化的全局变量和未初始化的静态变量
名字都是历史定的,也没啥特殊硬记就完事。主要就是把数据按照不同的分类放在一起。也就是说可以自定义段信息来区分,能找到对应的数据就行。
Go汇编基础语法
symbol
为变量在汇编语言中对应的标识符,当前包中Go语言定义的符号symbol
,在汇编代码中对应·symbol
,在符号后的<>作用是限制数据在当前文件使用。
offset
是符号开始地址的偏移量
width
是要初始化内存的宽度大小
value
是要初始化的值
(SB)
表示符号相对于SB伪寄存器
的偏移量,二者组合在一起最终是绝对地址
DATA:用于初始化包变量
语法为: DATA symbol+offset(SB)/width, value
GLOBL:用于将符号导出
语法为: GLOBL symbol(SB), width
TEXT:用于定义符号(一般就是函数)
语法为: TEXT symbol(SB), [flags,] $framesize[-argsize]
其中函数名(symbol)中当前包的路径可以省略
flags
用于指示函数的一些特殊行为,标志在textlags.h文件中定义,常见的NOSPLIT
主要用于标记函数不进行栈分裂。
framesize
表示栈帧大小,其中包含局部变量+调用其它函数时准备调用参数的隐式栈空间。
argsize
参数及返回值大小,之所以可以省略是因为编译器可以从Go语言的函数声明中推导出函数参数的大小。
*PCDATA和FUNCDATA
Go里有一个函数叫做
runtime.Caller
,可以获取当前的PC寄存器值,以及文件和行号。然后根据PC寄存器表示的指令位置,通过runtime.FuncForPC
函数获取函数的基本信息,这种功能是如何实现的?Go语言作为一门静态编译型语言,在执行时每个函数的地址都是固定的,函数的每条指令也是固定的。如果针对每个函数和函数的每个指令生成一个地址表格(也叫PC表格),那么在运行时我们就可以根据PC寄存器的值轻松查询到指令当时对应的函数和位置信息。而Go语言也是采用类似的策略,只不过地址表格经过裁剪,舍弃了不必要的信息。因为要在运行时获取任意一个地址的位置,必然是要有一个函数调用,因此我们只需要为函数的开始和结束位置,以及每个函数调用位置生成地址表格就可以了。同时地址是有大小顺序的,在排序后可以通过只记录增量来减少数据的大小;在查询时可以通过二分法加快查找的速度。
这两命令就是标记地址表格和函数表格的指令
两者语法也一致,语法为: PCDATA/FUNCDATA tableid, tableoffset
tableid
: 表格的类型
PCDATA有两种类型PCDATA_StackMapIndex
,PCDATA_InlTreeIndex
,两种类型类似,后者为内联函数的表格
FUNCDATA有3种类型
1. `FUNCDATA_ArgsPointerMaps`: 函数参数的指针信息表
2. `FUNCDATA_LocalsPointerMaps`: 局部指针信息表
3. `FUNCDATA_InlTree`: 被内联展开的指针信息表
tableoffset
: 表格的地址
通过FUNC表格,Go语言的垃圾回收器可以跟踪全部指针的生命周期,同时根据指针指向的地址是否在被移动的栈范围来确定是否要进行指针移动。
一般来说都是由编译器自动生成,手动实现不太现实
手写汇编
一般来说,就看下汇编生成就行了,手写汇编意义不大,当然,真想写也不是不行。。。
简单计算
1 | //add.s |
变量定义
1 | //a.s |
获取goid
1 | //goid.s |
系统调用
1 | //a.s |
函数调用规约
传统的调用规约中,一般会区分为
- caller saved registers 调用方保存现场
- callee saved registers 被调方保存现场
1.16及以前
要求所有参数和返回值都通过从右至左压栈来传递
看一个示例
1 | func add(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 int) (r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11 int) { |
可以简单的看出来就是把a11-a1移动到了r1-r11上
1.17+
改成caller saved registers,通过寄存器来调用,但是与c不一样的是,使用9个寄存器来进行交互,依次是 AX,BX,CX,DI,SI,R8,R9,R10,R11,超出部分还是通过栈来传递
所以1.17在性能及内存使用上会提升很多
同样看示例
1 | func add(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 int) (r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11 int) { |
可以看到先将超出9个的BX(a2),AX(a1)移动到r10,r11上,然后交换对应的寄存器的值最后返回