源自 Plan9 汇编实现。 保存在 .s 文件中,编译器自动编译、链接。
本文内容基于 amd64 架构。
指令
指令参数长度。
MOVB: 1-byte
MOVW: 2
MOVL: 4
MOVQ: 8
数据移动方向:从左往右。
ADD R1, R2 // R2 += R1
SUB R3, R4 // R4 -= R3
SUB R3, R4, R5 // R5 = R4 - R3
MUL $7, R6 // R6 *= 7
内存访问。
MOV (R1), R2 // R2 = *R1
MOV 8(R1), R2 // R2 = *(8 + R2)
MOV 16(R1)(R2*2), R3 // R3 = *(16 + R1 + R2*2)
MOV runtime·x(SB), R2 // R2 = *runtime·x
跳转指令。
JMP label // 跳转到标签。
JMP 2(PC) // 跳转到 PC + n 行。
JMP -2(PC)
数字常量以 $ 开头,十进制($10)和 十六进制($0x10)。 标签仅在函数內有效。
伪寄存器
伪寄存器(pseudo-register)由语言定义并使用,最终会被编译为硬件寄存器引用。
考虑到平台差异,编译后的机器代码,可能须保存 PC、BP、SP 等物理寄存器值。 在编写汇编代码时,很难事先计算好实际所需偏移量。为此,汇编语言用伪寄存器表示某个相对位置就很有必要。
- SB: Static Base Pointer(全局符号)
表示一个全局符号地址,通常应用于全局函数或数据。
例如
CALL add(SB)
表示对应符号名字为 add 的内存地址。 在名字后添加尖括号(add<>(SB)
),表示该符号名仅在当前文件內可见。 还可用偏移量表示基于某个符号名字的地址,例如add+8(SB)
。 - FP: Frame Pointer(参数地址)
指向由调用方提供的参数列表起始地址,通过偏移量指向不同参数或返回值。
通常在偏移量前包含参数名。例如
MOVQ size+16(FP), AX
- SP: Stack Pointer (栈局部变量内存地址)
伪 SP 寄存器表示栈帧內,用于本地局部变量操作的起始地址。
鉴于栈从底开始的操作方式,SP 实际是栈底位置(等同调整后的 BP 地址)。
使用该方式访问局部变量,须添加变量名,如
x-8(SP)
。如果省略变量名,则表示硬件寄存器。 - PC: Program Counter(指令地址)
可用来按指令行数条转。
比如
JMP 2(PC)
表示以当前位置为 0 基准,往下跳到第 2 行。
考虑到栈帧内存实际上分成局部变量(底)和调用参数(顶)两部分使用,所以用伪 SP 寄存器负值便宜访问局部变量是很自然的做法。 如此,物理寄存器 SP 用来操作调用参数入栈;而伪寄存器 SP 用来访问局部变量。 毕竟 BP 寄存器是可选的。
注意
x+0(FP)
和gobuf_pc(AX)
宏函数的区别。
CALLEE
lo SP +-----------+ ..........................
| | .
+-----------+ .
| | frame size(包括 caller BP)
BP (pseudo SP) +-----------+ .
| caller BP | .
+-----------+ ..........................
| caller PC |
FP +-----------+------------+ SP ..........
| arg0 | call arg0 | .
+-----------+------------+ .
| argn | call argn | argument size
+-----------+------------+ .
| return | call ret | .
hi +-----------+------------+ .............
| local var0 |
+------------+
| local varn |
+------------+ BP (pseudo SP)
CALLER
函数
函数定义。
参数及返回值大小
|
TEXT runtime·cgocallback(SB),NOSPLIT,$32-32
| | |
包名 函数名 栈帧大小(不包括参数及返回值)
当前包,可省略包名,直接以中心点开始。
由调用者(caller)负责分配目标函数(callee)参数和返回值内存。 调用者须自行保存相关寄存器状态。
示例
使用汇编代码编写一个简单的加法。
add.s
#include "textflag.h"
// add(x, y int) int
TEXT ·add(SB), NOSPLIT, $8-24
MOVQ $0, z-0x8(SP)
MOVQ x+0x0(FP), AX
MOVQ y+0x8(FP), BX
ADDQ AX, BX
MOVQ BX, z-0x8(SP)
MOVQ BX, ret+0x10(FP)
RET
main.go
package main
func add(x, y int) (z int) // 声明汇编函数原型
func main() {
z := add(0x100, 0x200)
println(z)
}
可以看到编译器插入栈帧调整,环境保存等指令。
$ go build -gcflags "-l"
$ go tool objdump -s "main\.add" test
TEXT main.add(SB) add.s
add.s:5 0x104bfe0 SUBQ $0x10, SP // 因为要保存 BP,所以栈帧大小调整到 0x10。
add.s:5 0x104bfe4 MOVQ BP, 0x8(SP)
add.s:5 0x104bfe9 LEAQ 0x8(SP), BP
add.s:6 0x104bfee MOVQ $0x0, 0(SP)
add.s:7 0x104bff6 MOVQ 0x18(SP), AX
add.s:8 0x104bffb MOVQ 0x20(SP), BX
add.s:9 0x104c000 ADDQ AX, BX
add.s:10 0x104c003 MOVQ BX, 0(SP)
add.s:11 0x104c007 MOVQ BX, 0x28(SP)
add.s:12 0x104c00c MOVQ 0x8(SP), BP
add.s:12 0x104c011 ADDQ $0x10, SP // 清除栈帧。
add.s:12 0x104c015 RET
使用
-gcflags -S
输出反汇编时,会有 FUNCDATA 和 PCDATA 信息。 它们是编译器引入,包含垃圾回收器要使用的信息。
有疑问加站长微信联系(非本文作者)