golang汇编使用的是plan9汇编,这相当于是一个帮助文档,帮助理解golang底层汇编代码的实现。
由于汇编不具备跨平台,所以这里使用的是linux amd64平台。
寄存器
通用寄存器
下面是通用通用寄存器的名字在 IA64 和 plan9 中的对应关系:
| IA64 | 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 |
寄存器分类图
graph TB
subgraph "通用寄存器 (64位)"
A1[AX - 累加器]
A2[BX - 基址寄存器]
A3[CX - 计数寄存器]
A4[DX - 数据寄存器]
A5[DI - 目标索引]
A6[SI - 源索引]
A7[BP - 基址指针]
A8[SP - 栈指针]
A9[R8-R14 - 扩展寄存器]
end
subgraph "特殊寄存器"
B1[PC - 程序计数器
对应 IA64 的 RIP]
end
style A1 fill:#ffcccc
style A8 fill:#ccffcc
style B1 fill:#ccccff
常用寄存器用途:
- AX (RAX):累加器,常用于算术运算和函数返回值
- BX (RBX):基址寄存器,常用于存储基址
- CX (RCX):计数寄存器,常用于循环计数
- DX (RDX):数据寄存器,常用于存储数据
- DI (RDI):目标索引,函数调用时存储第一个参数
- SI (RSI):源索引,函数调用时存储第二个参数
- SP (RSP):栈指针,指向当前栈顶
- BP (RBP):基址指针,指向当前栈帧基址
- PC (RIP):程序计数器,指向下一条要执行的指令
伪寄存器
汇编中还引入了4个伪寄存器:
FP(Frame pointer):指向第一个参数(即参数列表最下面),通过其加上偏移量可以获取函数入参和返回值。FP使用形式是symbol+offset(FP),例如arg0+0(FP),arg1+8(FP),使用 FP 不加 symbol 时,无法通过编译,在汇编层面来讲,symbol 并没有什么用,加 symbol 主要是为了提升代码可读性。PC(Program counter):指向当前指令,用于跳转指令或分支。SB(Static base pointer):指向全局基底,可通过其引用全局变量或声明函数。SP(Stack pointer):指向第一个局部变量(即本地变量最上面),通过其减偏移量可以获取局部变量。SP使用形如symbol+offset(SP)的方式,引用函数的局部变量。offset的合法取值是[-framesize, 0),注意是个左闭右开的区间。
FP指向内容是有低地址往高地址增长,SP指向内容是由高地址往低地址增长,所以对于offset,FP是正数,SP是负数。
如果SP前面的偏移量是正数,表示这里SP是硬件SP寄存器,且硬件SP寄存器没有symbol。当使用反汇编工具后显示的汇编代码中的SP都是硬件SP寄存器。
伪寄存器指向关系图
graph TB
subgraph "内存布局(从低地址到高地址)"
direction TB
A1["全局变量/函数
(SB 指向的全局基底)"]
A2["调用者栈帧"]
A3["参数区域
(FP 指向第一个参数)"]
A4["返回地址"]
A5["局部变量区域
(SP 指向第一个局部变量)"]
A6["当前指令
(PC 指向)"]
end
B1[SB
静态基址指针] -.->|指向| A1
B2[FP
帧指针] -.->|指向| A3
B3[SP
栈指针] -.->|指向| A5
B4[PC
程序计数器] -.->|指向| A6
A1 --> A2
A2 --> A3
A3 --> A4
A4 --> A5
A5 --> A6
style B1 fill:#ffcccc
style B2 fill:#ccffcc
style B3 fill:#ccccff
style B4 fill:#ffffcc
伪寄存器说明:
- SB (Static Base):指向全局内存区域的基址,所有全局变量和函数都通过
symbol(SB)的形式引用 - FP (Frame Pointer):指向函数参数区域的起始位置,通过
symbol+offset(FP)访问参数和返回值 - SP (Stack Pointer):指向局部变量区域的起始位置,通过
symbol+offset(SP)访问局部变量(offset 为负数) - PC (Program Counter):指向当前执行的指令,用于跳转和分支控制
数据
变量声明
常量
常量以$为前缀。常量的类型有整数常量、浮点数常量、字符常量和字符串常量等几种类型。以下是几种类型常量的例子:
1 | $1 // 十进制 |
Go汇编语言中的常量其实不仅仅只有编译时常量,还包含运行时常量。比如包中全局的变量和全局函数在运行时地址也是固定不变的,这里地址不会改变的包变量和函数的地址也是一种汇编常量。
下面是本章第一节用汇编定义的字符串代码:
1 | GLOBL ·NameData(SB),$8 |
其中$·NameData(SB)也是以$美元符号为前缀,因此也可以将它看作是一个常量,它对应的是NameData包变量的地址。在汇编指令中,我们也可以通过LEA指令来获取NameData变量的地址。
全局变量
要定义全局变量,首先要声明一个变量对应的符号,以及变量对应的内存大小。导出变量符号的语法如下:
1 | GLOBL symbol(SB), width |
GLOBL汇编指令用于定义名为symbol的变量,变量对应的内存宽度为width,内存宽度部分必须用常量初始化。下面的代码通过汇编定义一个int32类型的count变量:
1 | GLOBL ·count(SB),$4 |
其中符号·count以中点开头表示是当前包的变量,最终符号名为被展开为path/to/pkg.count。count变量的大小是4个字节,常量必须以$美元符号开头。内存的宽度必须是2的指数倍,编译器最终会保证变量的真实地址对齐到机器字倍数。需要注意的是,在Go汇编中我们无法为count变量指定具体的类型。在汇编中定义全局变量时,我们只关心变量的名字和内存大小,变量最终的类型只能在Go语言中声明。
变量定义之后,我们可以通过DATA汇编指令指定对应内存中的数据,语法如下:
1 | DATA symbol+offset(SB)/width, value |
具体的含义是从symbol+offset偏移量开始,width宽度的内存,用value常量对应的值初始化。DATA初始化内存时,width必须是1、2、4、8几个宽度之一,因为再大的内存无法一次性用一个uint64大小的值表示。
变量寻址
寻址的方式有很多很迷惑的行为。
利用MOV来说明问题
1 | MOVQ $10, AX // Ax = 10 |
寻址方式可视化
graph LR
subgraph "立即数寻址"
A1["MOVQ $10, AX
AX = 10
直接赋值常量"]
end
subgraph "寄存器寻址"
A2["MOVQ BX, AX
AX = BX
寄存器间传递"]
end
subgraph "间接寻址"
A3["MOVQ (BX), AX
AX = *BX
通过寄存器指向的内存"]
end
subgraph "基址偏移寻址"
A4["MOVQ 8(BX), AX
AX = *(BX + 8)
基址 + 偏移量"]
end
subgraph "索引寻址"
A5["MOVQ 16(CX)(BX*1), AX
AX = *(16 + CX + BX*1)
基址 + 索引*比例 + 偏移"]
end
subgraph "全局变量寻址"
A6["MOVQ ·myvar(SB), AX
AX = *myvar
通过 SB 访问全局变量"]
A7["MOVQ $·myvar(SB), AX
AX = &myvar
获取全局变量地址"]
end
style A1 fill:#ffcccc
style A2 fill:#ccffcc
style A3 fill:#ccccff
style A4 fill:#ffffcc
style A5 fill:#ffccff
style A6 fill:#ccffff
style A7 fill:#ffcccc
寻址方式说明:
- 立即数:
$前缀表示常量值 - 寄存器:直接使用寄存器名
- 间接:
(寄存器)表示寄存器指向的内存地址 - 基址偏移:
offset(基址)表示基址 + offset 的内存地址 - 索引寻址:
offset(基址)(索引*比例)用于数组访问 - 全局变量:
symbol(SB)访问全局变量,$symbol(SB)获取地址
函数声明
1 | 静态基地址(static-base) 指针 |
TEXT,定义函数标识pkgname,函数包名,可以不写,也可以""替代·,在程序链接后会转换为.<>,表示堆栈结构是否符合GOABI,也可以写作<ABIInternal>(SB),让SB认识这个函数,即生成全局符号,有用绝对地址TAG,标签,表示函数某些特殊功能,多个标签可以通过|连接,常用标签NOSPLIT,向编译器表明,不应该插入stack-split的用来检查栈需要扩张的前导指令,减少开销,一般用于叶子节点函数(函数内部不调用其他函数)NOFRAME,不分配函数堆栈,函数必须是叶子节点函数,且以0标记堆栈函数,没有保存帧指针(或link寄存器架构上的返回地址)
$16-24,16表示函数栈帧大小,24表示入参和返回大小
预处理
#include,引入头文件#define,宏定义#undef,取消宏定义#ifdef,判断是否有宏定义#ifndef,判断是否没有宏定义#else,if的另一个情况#endif,结束if
例如在启动函数runtime·rt0_go中,会根据不同平台执行响应指令:
1 | #ifndef GOOS_windows |
标签声明
标签用于JMP跳转,用symbol:表示。
函数栈帧
调用者caller,被调用者callee,以被调用callee的角度理解函数栈帧,并显示伪寄存器指向的位置
函数栈帧布局(Frame 函数)
graph TB
subgraph "调用者栈帧 (Caller Frame)"
direction TB
C1[调用者局部变量]
C2[调用者参数]
end
subgraph "被调用者栈帧 (Callee Frame) - 从高地址到低地址"
direction TB
A1["参数 3
(高地址)"]
A2["参数 2"]
A3["参数 1
(FP 指向这里)"]
A4["返回地址"]
A5["调用者 BP
(保存的帧指针)"]
A6["局部变量 1
(SP 指向这里)"]
A7["局部变量 2"]
A8["局部变量 N
(低地址)"]
end
C2 --> A1
A3 --> A4
A4 --> A5
A5 --> A6
A6 --> A7
A7 --> A8
B1[FP
帧指针] -.->|指向第一个参数| A3
B2[SP
栈指针] -.->|指向第一个局部变量| A6
style A3 fill:#ffcccc
style A6 fill:#ccffcc
style B1 fill:#ff9999
style B2 fill:#99ff99
说明:
- FP (Frame Pointer):指向第一个参数(参数列表最下面,高地址)
- SP (Stack Pointer):指向第一个局部变量(局部变量最上面,低地址)
- 参数访问:通过
arg+offset(FP)访问,offset 为正数(向高地址增长) - 局部变量访问:通过
var-offset(SP)访问,offset 为负数(向低地址增长)
函数栈帧布局(NOFRAME 函数)
graph TB
subgraph "调用者栈帧"
C1[调用者局部变量]
end
subgraph "被调用者栈帧 (NOFRAME) - 从高地址到低地址"
direction TB
A1["参数 3
(高地址)"]
A2["参数 2"]
A3["参数 1
(FP 指向这里)"]
A4["返回地址
(SP 指向这里)"]
A5["局部变量 1"]
A6["局部变量 2"]
A7["局部变量 N
(低地址)"]
end
C1 --> A1
A3 --> A4
A4 --> A5
A5 --> A6
A6 --> A7
B1[FP
帧指针] -.->|指向第一个参数| A3
B2[SP
栈指针] -.->|指向返回地址| A4
style A3 fill:#ffcccc
style A4 fill:#ccccff
style B1 fill:#ff9999
style B2 fill:#9999ff
NOFRAME 特点:
- 不保存调用者的 BP(帧指针)
- SP 直接指向返回地址
- 函数必须是叶子节点函数(不调用其他函数)
- 栈帧更小,性能更好
常见指令
与数据相关的指令结尾会跟上BWDQ,分别表示操作字节数1248,常见的MOVQ、ADDQ等都是对8字节数进行操作。下面指令皆以Q结尾即8字节操作数。
1 | MOVB $1, DI // 1 byte |
栈操作
由于golang栈帧是固定大小,没有提供压栈弹栈操作,但这些操作可以通过伪指针加偏移量来模拟。
移动操作
MOV允许赋值,移动数据,解引用,地址偏移等操作
1 | MOVQ $10, AX // Ax = 10 |
运算操作
数值运算
- ADDQ,加法运算
- SUBQ,减法运算
- IMULQ,乘法运算
示例:
1 | ADDQ AX, BX // BX += AX |
逻辑运算
ANDQ:按位与运算,ANDQ AX, BX表示BX = BX & AXORQ:按位或运算,ORQ AX, BX表示BX = BX | AXXORQ:按位异或运算,XORQ AX, BX表示BX = BX ^ AXNOTQ:按位取反,NOTQ AX表示AX = ~AX
示例:
1 | ANDQ AX, BX // BX = BX & AX |
移位运算
- SALQ,有符号左移
- SARQ,有符号右移
- SHLQ,无符号左移
- SHRQ,无符号右移
条件操作
条件操作即为处理当前操作并设置标志寄存器相关标志位的值,常见的标志位有:
CF (Carry Flag),进位标志位,运算过程是否产生进位或借位PF (Parity Flag),奇偶标志位,运算结果中1的个数是奇数还是偶数ZF (Zero Flag),零标志位,运算结果是否为0SF (Sign Flag),符号标志位,运算结果的最高位OF (Overflow Flag),溢出标志位,运算结果超过运算数表示范围
标志寄存器结构
graph LR
subgraph "标志寄存器 (FLAGS)"
F1[CF
进位标志]
F2[PF
奇偶标志]
F3[ZF
零标志]
F4[SF
符号标志]
F5[OF
溢出标志]
end
style F1 fill:#ffcccc
style F2 fill:#ccffcc
style F3 fill:#ccccff
style F4 fill:#ffffcc
style F5 fill:#ffccff
条件指令:
TESTQ,源操作数和目标操作数按位逻辑与,结果不置目标操作数,根据响应的结果设置SF、ZF、和PF标志位,并将CF和OF标志位清零。常见TESTQ AX AX配合ZF即可得出AX是否为0。CMP,前操作数减去后操作数,结果不置目标操作数,根据结果设置标志寄存器标志位。- 等于,ZF为1
- 不等于,ZF为0
- 小于,CF为1
- 小于等于,CF为1或ZF为1
- 大于等于,CF为0
- 大于,CF为0并且ZF为0
条件判断流程图
flowchart TD
A[执行 CMP 或 TEST] --> B{检查标志位}
B -->|ZF=1| C[结果为零/相等]
B -->|ZF=0| D[结果不为零/不等]
B -->|CF=1| E[有进位/小于]
B -->|CF=0| F[无进位/大于等于]
B -->|SF=1| G[结果为负]
B -->|SF=0| H[结果为正或零]
B -->|OF=1| I[发生溢出]
B -->|OF=0| J[无溢出]
C --> K[使用 JE/JZ 跳转]
D --> L[使用 JNE/JNZ 跳转]
E --> M[使用 JB/JC 跳转]
F --> N[使用 JAE/JNC 跳转]
style C fill:#ffcccc
style D fill:#ccffcc
style E fill:#ccccff
style F fill:#ffffcc
跳转操作
跳转操作即为检测标志寄存器相关的标志位,进而处理逻辑。
JMP (JuMP),无条件跳转,可以跳转标签,跳转函数,跳过指定个数的指令。
例如,如果是2(PC)则是跳过两条指令,也可以为负数-2(PC)向上跳2个指令
1 | JMP 2(PC) // ---- |
跳转指令流程图
flowchart TD
A[执行条件指令
CMP/TEST] --> B{检查标志位}
B --> C{跳转条件}
C -->|ZF=1| D1[JE/JZ
等于/为零]
C -->|ZF=0| D2[JNE/JNZ
不等于/不为零]
C -->|CF=1| D3[JB/JC
小于/有进位]
C -->|CF=0| D4[JAE/JNC
大于等于/无进位]
C -->|SF=1| D5[JL
有符号小于]
C -->|SF=0| D6[JGE
有符号大于等于]
D1 --> E[跳转到目标地址]
D2 --> E
D3 --> E
D4 --> E
D5 --> E
D6 --> E
F[JMP
无条件跳转] --> E
style D1 fill:#ffcccc
style D2 fill:#ccffcc
style D3 fill:#ccccff
style D4 fill:#ffffcc
style F fill:#ffccff
JA (Jump if Above),无符号大于就跳转JAE (Jump if Above or Equal),无符号大于等于就跳转JB (Jump if Below),无符号小于就跳转JBE (Jump if Below or Equal),无符号小于等于就跳转JC (Jump if Carry),如果有进位就跳转JNC (Jump if No Carry),没有进位就跳转JE (Jump if Equal),相等就跳转JNE (Jump if Not Equal),不等就跳转JL (Jump if Less),有符号小于就跳转JLE (Jump if Less or Equal),有符号小于等于就跳转JG (Jump if Greater),有符号大于就跳转JGE (Jump if Greater or Equal),有符号大于等于就跳转JHI (Jump if HIgher),无符号大于就跳转JHS (Jump if Higher or Same),无符号大于就跳转或等于就跳转,同JCJLO (Jump if LOwer),无符号小于就跳转,同JNCJLS (Jump if Lower or Same),无符号小于或等于就跳转JN (Jump if Negative),为负就跳转JZ (Jump if equal to Zero),等于零值就跳转JNZ (Jump if Not equal to Zero),不等于零值就跳转
其他操作
LEAQ:加载有效地址(Load Effective Address),计算地址但不访问内存,常用于地址计算CALL:调用函数,也可以理解为跳转到函数,会自动保存返回地址RET:退出函数,从栈中恢复返回地址并跳转NOP:空操作,不执行任何操作,常用于对齐或占位INT:中断,触发软件中断
函数调用流程
sequenceDiagram
participant Caller as 调用者
participant Stack as 栈
participant Callee as 被调用者
Caller->>Stack: 1. 压入参数(从右到左)
Caller->>Stack: 2. 压入返回地址
Caller->>Callee: 3. CALL 函数
Note over Callee: 4. 保存调用者 BP
Callee->>Stack: 5. 分配局部变量空间
Note over Callee: 6. 执行函数体
Callee->>Stack: 7. 清理局部变量
Callee->>Stack: 8. 恢复调用者 BP
Callee->>Caller: 9. RET(弹出返回地址)
Caller->>Stack: 10. 清理参数
LEAQ 地址计算示例
graph LR
A["LEAQ 8(BX)(CX*2), AX"] --> B["计算: 8 + BX + CX*2"]
B --> C["AX = 地址值
不访问内存"]
D["MOVQ 8(BX)(CX*2), AX"] --> E["计算: 8 + BX + CX*2"]
E --> F["访问该地址的内存
AX = 内存值"]
style A fill:#ffcccc
style D fill:#ccffcc
style C fill:#ccccff
style F fill:#ffffcc
LEAQ vs MOVQ:
LEAQ:只计算地址,不访问内存,常用于地址计算和算术运算优化MOVQ:计算地址并访问该地址的内存内容
反汇编
对于写好的 go 源码,生成对应的 Go 汇编,大概有下面几种
- 方法 1 先使用 go build -gcflags “-N -l” main.go 生成对应的可执行二进制文件 再使用 go tool objdump -s “main.“ main 反编译获取对应的汇编
反编译时”main.“ 表示只输出 main 包中相关的汇编”main.main” 则表示只输出 main 包中 main 方法相关的汇编 - 方法 2 使用 go tool compile -S -N -l main.go 这种方式直接输出汇编
- 方法 3 使用go build -gcflags=”-N -l -S” main.go 直接输出汇编
注意:在使用这些命令时,加上对应的 flag,否则某些逻辑会被编译器优化掉,而看不到对应完整的汇编代码
-l 禁止内联 -N 编译时,禁止优化 -S 输出汇编代码
反汇编流程图
flowchart TD
A[Go 源码] --> B{选择反汇编方法}
B -->|方法1| C1[go build -gcflags -N -l]
C1 --> C2[生成可执行文件]
C2 --> C3[go tool objdump -s main]
C3 --> D[输出汇编代码]
B -->|方法2| E1[go tool compile -S -N -l]
E1 --> D
B -->|方法3| F1[go build -gcflags -N -l -S]
F1 --> D
D --> G[分析汇编代码]
style C1 fill:#ffcccc
style E1 fill:#ccffcc
style F1 fill:#ccccff
style D fill:#ffffcc
命令说明:
- 方法1:
go build -gcflags "-N -l" main.go生成可执行文件,然后go tool objdump -s "main\." main反编译 - 方法2:
go tool compile -S -N -l main.go直接输出汇编 - 方法3:
go build -gcflags="-N -l -S" main.go直接输出汇编
编译选项说明:
-l:禁止内联,保留函数调用-N:禁止优化,保留所有逻辑-S:输出汇编代码
参考文献
Go 系列文章3 :plan9 汇编入门
状态标志寄存器
Go functions in assembly language