Plan 9 汇编语言指令集相对较小,但覆盖了基本的操作。下面是 Plan 9 汇编中的一些常见指令以及它们的用法和解释。
数据传输指令
MOV 指令
- MOVB src, dst: 移动字节
- MOVW src, dst: 移动字
- MOVL src, dst: 移动双字
- MOVQ src, dst: 移动四字
MOVQ $10, AX // 将立即数 10 移动到 AX 寄存器
MOVQ AX, 0x1000 // 将 AX 寄存器的值移动到内存地址 0x1000
MOVQ 0x1000, BX // 将内存地址 0x1000 的值移动到 BX 寄存器算术运算指令
ADD 指令
- ADDB src, dst: 字节加法
- ADDW src, dst: 字加法
- ADDL src, dst: 双字加法
- ADDQ src, dst: 四字加法
ADDQ AX, BX // 将 AX 和 BX 的值相加,结果存储在 BXSUB 指令
- SUBB src, dst: 字节减法
- SUBW src, dst: 字减法
- SUBL src, dst: 双字减法
- SUBQ src, dst: 四字减法
SUBQ AX, BX // 将 BX 减去 AX 的值,结果存储在 BXMUL 指令
- MULB src: 字节乘法
- MULW src: 字乘法
- MULL src: 双字乘法
- MULQ src: 四字乘法
MULQ AX // 将 AX 和 AX 相乘,结果存储在 AXDIV 指令
- DIVB src: 字节除法
- DIVW src: 字除法
- DIVL src: 双字除法
- DIVQ src: 四字除法
DIVQ AX // 将 DX:AX 除以 src,结果存储在 AX(商)和 DX(余数)逻辑运算指令
AND 指令
- ANDB src, dst: 字节与
- ANDW src, dst: 字与
- ANDL src, dst: 双字与
- ANDQ src, dst: 四字与
ANDQ AX, BX // 将 AX 和 BX 按位与,结果存储在 BXOR 指令
- ORB src, dst: 字节或
- ORW src, dst: 字或
- ORL src, dst: 双字或
- ORQ src, dst: 四字或
ORQ AX, BX // 将 AX 和 BX 按位或,结果存储在 BXXOR 指令
- XORB src, dst: 字节异或
- XORW src, dst: 字异或
- XORL src, dst: 双字异或
- XORQ src, dst: 四字异或
XORQ AX, BX // 将 AX 和 BX 按位异或,结果存储在 BXNOT 指令
- NOTB dst: 字节取反
- NOTW dst: 字取反
- NOTL dst: 双字取反
- NOTQ dst: 四字取反
NOTQ AX // 将 AX 的每个位取反数据比较指令
CMP 指令
- CMPB src, dst: 字节比较
- CMPW src, dst: 字比较
- CMPL src, dst: 双字比较
- CMPQ src, dst: 四字比较
CMPQ AX, BX // 比较 AX 和 BX,结果影响标志寄存器控制流指令
JMP 指令
- JMP addr: 无条件跳转
JMP 0x1000 // 跳转到地址 0x1000条件跳转指令
- JE addr: 如果相等则跳转
- JNE addr: 如果不相等则跳转
- JG addr: 如果大于则跳转
- JGE addr: 如果大于等于则跳转
- JL addr: 如果小于则跳转
- JLE addr: 如果小于等于则跳转
CMPQ AX, BX // 比较 AX 和 BX
JE 0x1000 // 如果 AX == BX,跳转到 0x1000栈操作指令
PUSH 指令
- PUSHB src: 压入字节
- PUSHW src: 压入字
- PUSHL src: 压入双字
- PUSHQ src: 压入四字
PUSHQ AX // 将 AX 压入栈POP 指令
- POPB dst: 弹出字节
- POPW dst: 弹出字
- POPL dst: 弹出双字
- POPQ dst: 弹出四字
POPQ AX // 从栈顶弹出一个四字到 AX函数调用指令
CALL 指令
- CALL addr: 调用函数
CALL runtime·newproc(SB) // 调用 runtime 的 newproc 函数RET 指令
- RET: 返回
RET // 从函数返回特殊指令
NOP 指令
- NOP: 空操作
NOP // 空操作,通常用于对齐或延迟HLT 指令
- HLT: 停止处理器
HLT // 停止处理器,通常用于调试其他
LEA 指令
- LEAQ src, dst: 计算内存地址
LEAQ 8(SP), BP // 将 SP 加 8 的结果存储在 BPPCDATA 和 FUNCDATA 指令
- PCDATA 和 FUNCDATA: 用于调试和栈跟踪
PCDATA $0, $-2 // 标记调试信息
FUNCDATA $1, info // 存储函数数据这些指令构成了 Plan 9 汇编的基础,熟悉这些指令后,读 go build -gcflags="-S" 的输出会更顺手。
用反汇编看 Go 的内部机制
读 Go 的底层汇编,能看清函数调用、栈帧、并发和垃圾回收在机器层面长什么样。下面用几个小例子,编译后反汇编来验证。
函数调用与栈帧
在 Go 中,每个函数调用都会创建一个新的栈帧,用于保存函数的参数、局部变量和返回地址。栈帧的管理是由编译器和运行时系统自动处理的。
示例代码
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
result := add(3, 4)
fmt.Println(result)
}编译并反汇编以上代码,可以看到函数调用和栈帧的管理:
go build -gcflags="-S" main.go生成的汇编代码(部分,Go 1.27 / amd64,不同版本输出会有差异)如下:
0x0000 00000 (main.go:5) TEXT main.add(SB), NOSPLIT|NOFRAME|ABIInternal, $0-16
0x0000 00000 (main.go:5) FUNCDATA $0, gclocals·g5+hNtRBP6YXNjfog7aZjQ==(SB)
0x0000 00000 (main.go:5) FUNCDATA $1, gclocals·g5+hNtRBP6YXNjfog7aZjQ==(SB)
0x0000 00000 (main.go:5) FUNCDATA $5, main.add.arginfo1(SB)
0x0000 00000 (main.go:5) FUNCDATA $6, main.add.argliveinfo(SB)
0x0000 00000 (main.go:6) ADDQ BX, AX
0x0003 00003 (main.go:6) RET可以看到,自 Go 1.17 起函数参数和返回值通过寄存器(amd64 上是 AX、BX 等)传递,参数在栈上的开销为 $0-16,函数体只剩一条 ADDQ BX, AX;早期版本(Go 1.16 及之前)则会看到通过 SP 偏移在栈上存取参数的代码。
并发机制
Go 的并发模型主要靠 goroutine 和 channel。goroutine 是轻量级线程,由 Go 运行时管理。
Goroutine 的创建
go func() {
fmt.Println("Hello from goroutine")
}()编译并反汇编后,可以看到 go 语句最终通过调用运行时的 runtime.newproc 创建 goroutine(用户代码侧的调用点):
0x0015 00021 (main.go:6) CALL runtime.newproc(SB)runtime.newproc 的实现在 runtime/proc.go 中,它负责分配 g 结构体、初始化栈并把它加入运行队列。可用 go tool objdump -s 'runtime.newproc' 可执行文件 查看其内部实现。
垃圾回收
Go 语言的垃圾回收机制是自动化的,采用的是非分代的标记-清除算法。垃圾回收器会在后台线程中运行,标记不再使用的对象,并释放它们占用的内存。
垃圾回收的触发
runtime.GC()编译并反汇编后,可以看到 runtime.GC() 调用最终落在运行时的 runtime.GC 入口上(用户代码侧的调用点):
0x000a 00010 (main.go:6) CALL runtime.GC(SB)GC 的核心逻辑(标记调度 gcStart、并发标记、清除等)位于 runtime/mgc.go 中,可用 go tool objdump -s 'runtime\.GC$' 可执行文件 或阅读源码进一步查看。
总结
函数调用在 amd64 上编译成寄存器传参加一条 ADDQ 加 RET,go 语句落到 runtime.newproc,runtime.GC() 落到 runtime.GC 入口。想看得更深,用 go tool objdump 配合 runtime/proc.go、runtime/mgc.go 的源码继续往下追。