go-asm
0x00 Introduce
比较重要的一点那就是go的汇编不是对底层机器指令的最直接的标识。有些汇编指令直接映射到机器指令,有些则不是.
0x01 伪寄存器
- FP: 帧指针,参数和局部变量
对应x86汇编里面的函数帧指针,一般用于访问函数的参数和返回值(和传统的x86的帧指针不一样,它指向的位置是在caller’s stack里面, 在go里面callee的参数是在caller’s stack里面的)
- PC: 程序计数器,和x86汇编里面pc功能一样
对应x86汇编里面IP寄存器
- SB: 静态基地址指针
一般用来申明函数或者全局变量。
- SP: 栈指针(栈顶)
这个和真
sp寄存器不一样,它指向的是当前栈帧的局部变量开始的地方(这个其实是指向的栈底)
Calle的各个寄存器布局如下:
go汇编伪寄存器和x86寄存器的对应关系如下
SB伪寄存器可以被认为是内存最原始的位置(汇编地址, 标号可以认为是就是汇编地址), 例如foo(SB)就可以认为是foo在内存里面的地址(和在x86汇编里面的标号功能类似).这种形式通常用来表示全局函数和全局变量. 有时候可以看到形如foo<>(SB)这种形式的指令,<>这个符号表示这个标号只在当前汇编文件里面可见(功能和C语言里面static功能相似).在标号前面增加一个偏移量表示距离这个标号汇编地址偏移offset的地方, 因此foo+4(SB)代表的时候距离foo4个byte的地方。
FP伪寄存器是一个虚拟的帧指针(一般用来表示函数的参数).编译器维护了一个虚拟机的帧指针,用一个距离这个伪寄存器偏移量来代表栈上参数。因此0(FP)就是函数的第一个参数,8(FP)是函数的第二个参数….(这在64位平台上).当用这种方式来表示一个函数的参数的时候,在开头增加一个名字上很有必要,正如first_arg+0(FP)和second_arg(FP)(这意味着偏移量,具体帧指针的一偏移量,这个和使用SB寄存器上不太一样的 ,一个是相对FP寄存器的偏移量,另外一个相对标号的地方)。这种偏移格式是强制性的,例如0(FP) 这种是非法的。但是这个名字其实和具体的语意是无关的,这个主要用来表示这个作用是干嘛的(有点类似注释的感觉)。需要注意的时候go汇编里面的FP是一个伪寄存器,和硬件帧指针是两个不同的东西.
SP伪寄存器是一个虚拟的栈指针,一般用来指向函数帧内的局部变量和为接下来的函数调用准备的参数(值得注意的时候,在c/c++里面传参数是通过寄存器来传参数的,在go里面是通过栈来参的,并且参数是放在调用者的函数栈里。).它指向当前函数帧的栈顶。因此引用的时候偏移数值应该在[-framesize, 0)这个范围内,例如x-8(SP)……
在硬件级别SP和PC是寄存器的别名,在golang里面SP和PC被特殊的对待了,例如引用SP需要加一个标号,FP也是。为了访问硬件寄存器,需要使用R名称,在arm架构上为了访问SP和PC寄存器,就需要通过R13和R15两个寄存器来标识。
1 | ;分支跳转通常写成PC偏移量或者直接jump到标号 |
0x02 指令
汇编器会使用各种指令来绑定到标号名称上。例如下面是一个函数的完整定义。
1 | TEXT runtime.profileloop(SB), NOSPLIT, $8 |
`TEXT`指令用来声明`runtime.profileloop`, 这个指令和下面的部分构成了一个函数的body。`TEXT`指令块的最后必须是一些`jmp`指令,但是通常是一个`RET`的伪指令(如果既没有`jmp`指令又没有`ret`指令,那么链接器会在最后追加一条跳转自己的jmp指令).在这个标号后面,是参数的标志,和帧大小(这是一个const常量).
通常情况下,帧大小后面紧跟着一个参数的大小,通过是用一个减号分割开来的(这个不是在做减法,这是特殊的语法).例如帧大小$24-8表示这个函数拥有一个24byte大小帧,并且调用者传给他一个8byte大小的参数,这个参数是存放在调用者的函数栈上。如果NOSPLIT这个标号没有被指定的话,那么这个参数大小必须被指定。go的汇编语言里面,go vet将会检查函数参数的大小是否正确。
需要注意的是,symbol名称使用了一个.来分割成两个部分,他们作为静态基地址伪寄存器的一个偏移量。这个函数来自gopackage runtime里面的profileloop名称函数会被调用。
全局数据标号由一些列的DATA初始化指令,再紧跟一个GLOBL指令。每个DATA指令初始化一小部分对应的内存区域。没有被显示初始化的内存会被隐式的初始化为0., 一个通用的DATA指令如下面:DATA symbol+offset(SB)/width, valueDATA的偏移地址必须是增量的。
GLOBL指令来声明一个标号是全局性的。作为一个gloabel全局申明,它有两个可选的标识,还有数据的大小。并且data会被初始化为0,除非data已经被DATA指令显示的初始化过了。GLOBL指令必须有一系列的DATA指令。
例如:
1 | DATA divtab<>+0x00(SB)/4, $0xF4F8FCFF |
上面段代码初始化一段大小是64byte只读的divtab<>,里面包含了一对大小4byte的整型。 最后声明了一个4byte大小的全局变量runtime.tlsoffset,并且初始化为0,非指针类型。
在指令后面一般都有一个或者两个参数,如果是两个参数,那么第一个参数是用来表示设置标识(这部分可以被写成一个整数表达式),或者写成标识号(更方面查看)。下面有一堆预定义的flags及其对应的值
- NOPROF = 1
这个标识主要是针对
TEXT指令。不要剖析标记的函数(这个已经被废弃了) - DUPOK = 2
出现多个相同标识符的数据时只保留一个
- NOSPLIT = 4
和栈分裂相关的功能,如果设置这个flag,那么编译器不会检查是不是要进行栈分裂
- RODATA = 8
只读数据
- NOPTR = 16
非指针类型数据
- WRAPPER = 32
- NEEDCTXT = 64
- LOCAL = 128
- RLABSS = 256
- NOFRAME = 512
- TOPFRAME = 2048
0x03 变量
0x00 整型变量
1 | go.cuinfo.packagename. SDWARFCUINFO dupok size=0 |
整数是8个字节大小的数值
0x01 字符串类型
1 | ➜ pkg cat variable.go |
string由两部分组成,一部分是指向具体字符串的指针类型(这个部分占8个字节,并且SRODATA表面它存放在只读数据区域),string结构体如下:
1 | type stringStruct struct { |
因此在汇编的角度来看string就是一个大小16个字节的结构体类型,前8byte是一个指向底层具体字符串的指针,后8个字节是字符串的长度, 因此汇编语句如下
1 | #include "textflag.h" |
上面通过引入了额外的符号,将底层字符串数组和字符串头部存放在一起
1 | #include "textflag.h" |
0x03 函数
函数的定义如下TEXT package·functionname(SB), flags, $frame_size-argument_size
需要注意的时候如果flags没有被指定,那么argument_size必须要指定
1 | #include "textflag.h" |
1 | package main |
0x04 常量
在go汇编里面常量以$符号为前提
1 | $1 |
0x05 全局变量
全局变量的定义是通过globl指令来声明的上面已经提过,在golang里面内存是通过SB(Static base pointer)寄存器来定义的,所有的静态全局符号都可以通过一个SB+偏移地址来定义
其实这个和x86汇编里面的标号很类似,标号可以翻译成对应的汇编地址, 在go汇编里面sb+一个偏移可以得到内存地址(其实这个内存地址可以看成是汇编地址)。例如 foo(SB) 可以当成(sb+foo) 来看待
0x06 数组类型
在go里面数组是一种扁平数据结构的基础类型。
1 | ➜ go cat pkg/variable.go |
0x07 bool类型和int类型
example
1 | func Showstr(str string) { |
1 | #include "textflag.h" |
0x08 slice类型
slice结构和string非常像
1 | type reflect.SliceHeader struct { |
example
1 | package pkg |
1 | #include "textflag.h" |
引用
https://golang.org/doc/asm
https://blog.hackercat.ninja/2018/quick_intro_to_go_assembly
https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-01-basic.html