在浏览 Golang 源代码时,总是被其中的汇编代码卡住,读起来不晦涩。明天来简要理解下 Golang 中的汇编语言。
汇编分类
按指令集架构分类(针对 CPU)
- x86 汇编(32bit): 这种架构常被称为
i386
,x86
- x86 汇编(64bit), 这种架构常被称为
AMD64
,Intel64
,x86-64
,x64
, 它是 AMD 设计的, 是 x86 架构的 64 位扩大, 起初公开 - ARM 汇编, ARM 处理器因为高性能, 低耗电, 罕用于嵌入式, 挪动设施.
- …
按汇编格局分类(针对人的浏览习惯)
- Intel 格局
- AT&T 格局
平时咱们说 golang 中汇编属于 plan9 格调,是按第二种形式分类的,其浏览格调(符号)与 Intel 与 AT&T 都有不同。plan9 汇编作者是 unix 操作系统的同一批人,bell 实验室所开发的。
Go 汇编语言是基于 plan9 汇编,然而事实世界还有这么多不同架构的 CPU 在这。所以 golang 汇编在 plan9 格调下,同一个办法还有不同指令集架构的多种实现。
在哪能看到 Golang 汇编代码
- Golang 源代码中,如
src/runtime/asm_amd64.s
,src/math/big/
… go tool compile -S main.go
, 把本人编写的代码编译成汇编代码。如:在我的 Mac Intel 机器上,amd64
的架构,汇编代码生成如下:
$ cat main.go
package main
func main() {
a, b := 0, 0
println(a + b)
}
$ go tool compile -S main.go
"".main STEXT size=66 args=0x0 locals=0x10 funcid=0x0
0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $16-0
0x0000 00000 (main.go:3) CMPQ SP, 16(R14)
0x0004 00004 (main.go:3) PCDATA $0, $-2
0x0004 00004 (main.go:3) JLS 57
0x0006 00006 (main.go:3) PCDATA $0, $-1
0x0006 00006 (main.go:3) SUBQ $16, SP
0x000a 00010 (main.go:3) MOVQ BP, 8(SP)
0x000f 00015 (main.go:3) LEAQ 8(SP), BP
0x0014 00020 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0014 00020 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0014 00020 (main.go:5) PCDATA $1, $0
0x0014 00020 (main.go:5) CALL runtime.printlock(SB)
0x0019 00025 (main.go:5) XORL AX, AX
...
Go 汇编根底语法
1. 寄存器
通用寄存器
寄存器与物理机架构相干, 不同的架构有不同的物理寄存器。
在 amd64
架构上提供了 16 个通用寄存器给用户应用
plan9 汇编语言提供了如下映射,在汇编语言中间接援用就可应用物理寄存器了。
amd64 | rax | rbx | rcx | rdx | rdi | rsi | rbp | rsp | r8 | r9 | r10 | r11 | r12 | r13 | r14 | rip |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Plan9 | AX | BX | CX | DX | DI | SI | BP | SP | R8 | R9 | R10 | R11 | R12 | R13 | R14 | PC |
如上文的例子中应用到了:SP
,AX
,R14
,BP
虚构寄存器
Go 汇编引入了 4 个 虚构寄存器
FP
: Frame pointer: arguments and locals. 帧指针,快速访问函数的参数和返回值PC
: Program counter: jumps and branches. 程序计数器,指向下一条指令的地址。在amd64
其实就是 rip 寄存器SB
: Static base pointer: global symbols. 动态基址指针,全局符号。SP
: Stack pointer: the highest address within the local stack frame. 栈指针, 指向局部变量
用法:
- FP:
0(FP)
示意第一个 参数,8(FP)
示意第二个参数(AMD64 架构)。first_arg+0(FP)
示意把第一个参数地址绑定到符号 first_arg - SP:
localvar0-8(SP)
在 plan9 中示意函数中第一个 局部变量 。物理寄存器中也有 SP,硬件 SP 才是真正示意 栈顶地位。所以为了 辨别 SP 到底是指硬件 SP 还是指虚构寄存器。plan9 代码中须要以特定的格局来辨别。eg:symbol+offset(SP)
示意虚构寄存器 SP。offset(SP)
则示意硬件 SP。如上述例子中的8(SP)
指的是硬件 SP - PC:除个别跳转治理,个别用不到
- SB:示意全局内存终点。
foo(SB)
示意符号 foo 作为内存地址应用。这种模式用于申明全局函数、数据。foo+4(SB)
示意 foo 往后 4 字节的地址。<>
限度符号只能在以后源文件应用
从网上偷的图:
2. 指令
1. 变量申明
格局: 应用 DATA
与 GLOBL
来申明一个全局变量
DATA symbol+offset(SB)/width, value
GLOBL symbol(SB), flag, $size
示意意义:
- DATA 局部: 对
symbol
变量中的字节赋值,把offset
到offset + width
地位的字节赋值为value
。 - GLOBL 局部:必须在 DATA 后,示意申明了一个大小为
size
的全局变量symbol
。flag
代表变量一些属性,如RODATA
指只读。在 GLOBL 中退出<>
, 如GLOBL bio<>(SB), RODATA, $16
也是示意这个全局变量只在本文件中失效。
理论例子:
// src/runtime/asm_amd64.s, 这里申明的 argc,argv 是 Go 程序的入参
DATA _rt0_amd64_lib_argc<>(SB)/8, $0
GLOBL _rt0_amd64_lib_argc<>(SB),NOPTR, $8
DATA _rt0_amd64_lib_argv<>(SB)/8, $0
GLOBL _rt0_amd64_lib_argv<>(SB),NOPTR, $8
NOPTR
这个示意不是指针,不须要垃圾回收扫描
局部变量 :其在栈帧中,不须要申明。间接依附 offset
取出应用。例如0(FP)
代表函数第一个参数,localvar0-8(SP)
函数中第一个局部变量。
2. 函数申明
格局:
TEXT pkgname·funname(SB),flag,$framesize-argsize
示意意义:
pkgname
: 能够省略,最好省略。不然批改包名还要级联批改;
funname
: 申明的函数名
flag
: 标记位,如 NOSPLIT
,咱们晓得 Go Runtime 会追踪每个 stack 的应用状况,而后动静自增。而 NOSPLIT
标记位禁止查看,节俭开销,然而写程序的人要保障这个函数是平安的。
framesize
: 函数栈帧大小 = 局部变量 + 调用其它函数参数空间的总大小
argsize
: 一些参考资料说这里是 参数 + 返回值 大小,但在试验中已有些许差别。这个应该和 GO 1.7 的更新无关,GO1.7 基于寄存器的调用规约 GO 1.7 的优化
- GO 1.7 之前 参数 + 返回值都存在栈帧中
- GO 1.7 更新后,优先应用 9 个 通用寄存器传递参数与返回值,超出局部再存在栈中。并且寄存器中返回值会笼罩参数中的值
- 参数 + 返回值少于 9 个,argsize 值是参数的大小
- 返回值 > 9 个,argsize = 参数大小 + 返回值超出 9 个的局部
理论例子:
$ cat main.go
package main
func main() {}
func add(a int64, b int64) int64 {return a + b}
$ go tool compile -S main.go
"".main STEXT nosplit size=1 args=0x0 locals=0x0 funcid=0x0
...
"".add STEXT nosplit size=4 args=0x10 locals=0x0 funcid=0x0
0x0000 00000 (main.go:6) TEXT "".add(SB), NOSPLIT|ABIInternal, $0-16
0x0000 00000 (main.go:6) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:6) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:6) FUNCDATA $5, "".add.arginfo1(SB)
0x0000 00000 (main.go:7) ADDQ BX, AX
0x0003 00003 (main.go:7) RET
...
上述例子:函数内不需寄存局部变量,framesize = 0,两个 int64 的参数,argsize = 16
3. 常见操作指令
自主查问链接
上面介绍下常见的
1. 数据搬运
MOV
指令:其后缀示意搬运长度,$NUM
示意具体的数字,如上面例子
MOVB $1, DI // 1 byte, DI=1
MOVW $0x10, BX // 2 bytes, BX=10
MOVD $1, DX // 4 bytes, DX=1
MOVQ $-10, AX // 8 bytes, AX=-10
LEA
, 将无效地址加载到指定的地址寄存器中
// ret+24(FP) 这代表了第三个函数参数,是个地址
LEAQ ret+24(FP), AX // 把 ret+24(FP) 地址移到 AX 寄存器中
2. 计算指令
ADD
,SUB
,IMULQ
,如上面例子
ADDQ AX, BX // BX += AX
SUBQ AX, BX // BX -= AX
IMULQ AX, BX // BX *= AX
- 能够利用计算指令来调整栈空间,咱们晓得
SP
指向栈顶地位,调整SP
中的值即可。
// 栈空间: 高地址向低地址
SUBQ $0x18, SP // 对 SP 做减法,为函数调配函数栈帧
ADDQ $0x18, SP // 对 SP 做加法,革除函数栈帧
3. 条件跳转 / 无条件跳转
JMP
,JZ
,JLS
…
// 无条件跳转
JMP addr // 跳转到地址,地址可为代码中的地址,不过实际上手写不会呈现这种货色
JMP label // 跳转到标签,能够跳转到同一函数内的标签地位
JMP 2(PC) // 以以后指令为根底,向前 / 后跳转 x 行
JMP -2(PC) // 同上
// 有条件跳转
JZ target // 如果 zero flag 被 set 过,则跳转
JLS num // 如果上一行的比拟后果,右边小于左边则执行跳到 num 地址处
4. 其它
-
比拟:
CMP
, 与挑战指令搭配应用CMPQ BX, $0 // 比拟与 BX 与 0 的大小 JNE 3(PC) // 右边小于左边则执行跳到以后 PC 指令后第三条指令的地位
- 位运算:
AND
,OR
,XOR
总结
通过这篇文章,置信你曾经能大抵读懂一些简略的汇编程序了。这里举荐几个源代码的汇编浏览。
- Go 程序的终点:
src/runtime/asm_amd64.s
中的rt0_go(SB)
函数 - Go 原子包:
src/runtime/internal/atomic_amd64.s
中的Case
函数
参考
- https://segmentfault.com/a/11…
- https://go.dev/doc/asm
- https://medium.com/martinombu…
- https://xargin.com/go-and-pla…
- https://kcode.icu/posts/go/20…
- https://mioto.me/2021/01/plan…
- https://www.symbolcrash.com/2…