1. 数据移动:mov
mov dest, src
作用:把 src 的值复制到 dest。
常见形式:
mov rax, rdi ; 寄存器 -> 寄存器
mov rax, [rdi] ; 内存 -> 寄存器(load)
mov [rax], rdi ; 寄存器 -> 内存(store)
mov rax, 13 ; 立即数 -> 寄存器
mov rax, [rsp-8] ; 栈上取值
课程里要点
mov本质就是“拷贝值”。- 方括号
[]表示解引用地址,也就是访问内存。 mov [rax], [rdi]这种内存到内存形式在 x86 里不允许,必须借助中间寄存器。mov不会更新 flags(这点很重要,因为后面条件跳转依赖 flags)。
2. 算术:add sub imul
add dest, src
作用:dest = dest + src
add rax, rdi
add rdx, 13
除了改目标操作数,还会更新 RFLAGS。
sub dest, src
作用:dest = dest - src
sub rax, rdi
sub rdx, 13
也会更新 RFLAGS。
imul
课程里提到它属于“会更新 flags 的算术指令”那一类。 作用:有符号整数乘法。
在这门课的语境里,你只需要抓住:
- 它是整数乘法
- 它和
add/sub一样会影响 flags - 编译表达式乘法时会用到它
不需要记完整 ISA 的各种复杂变体。
3. 比较与 flags:cmp test
这门课里,条件控制流的核心不是“if 指令”,而是:
- 先执行某条会设置 flags 的指令
- 再执行条件跳转
jcc
cmp a, b
作用:像做 a - b 一样设置 flags,但不保存结果
cmp rax, rcx
你可以理解成:
- 不是为了得到减法结果
- 而是为了让
ZF/SF/OF变成后续je/jle/jg...能读的条件状态
常见用途
cmp rax, 0
je some_label
意思就是:如果 rax == 0,跳转。
test a, b
作用:像做 a & b 一样设置 flags,但不保存结果
test rax, 1
常用来检测某些 bit 是否被置位。 课程里它主要用于标签/tag 检查这类场景。
直觉
cmp更像“比较大小/相等”test更像“看某些位是不是开着”
4. 条件码与跳转:jmp jcc je jne jl jle jg jge
jmp loc
作用:无条件跳转
jmp done
效果就是直接把 RIP 改成 loc,下一条执行 loc 对应的指令。
这就是汇编里的“goto”。
jcc loc
这是一个家族,不是一条具体指令。
cc = condition code,表示“满足某个条件就跳”。
课程里明确讲到的有:
je:equaljne/jne的概念(讲义里重点写的是ne)jl:less thanjle:less or equaljg:greater thanjge:greater or equal
je loc
如果“相等”就跳。
本质上是检查 ZF = 1。
cmp rax, rcx
je equal_case
jne loc
如果“不相等”就跳。
本质上检查 ZF = 0。
jl loc
如果“小于”就跳。
这是有符号比较下的小于。
它不是简单看一个标志位,而是根据 SF 和 OF 组合判断。
jle loc
如果“小于等于”就跳。
jg loc
如果“大于”就跳。
jge loc
如果“大于等于”就跳。
这门课里你真正该记的模式
不是死记 flags 公式,而是记这个套路:
cmp a, b
jcc label
也就是:
cmp负责产生比较状态jcc负责根据状态选分支
这就是源语言里 if 的底层实现。
5. 位运算与布尔实现:and or setne
and dest, src
作用:按位与。
and rax, r10
课程里强调:
- 这是bitwise and
- 不是源语言层面的逻辑 and
但如果布尔值被规约成只有 0/1,那么按位与就和逻辑与等价。
课程里的用途
- 实现布尔运算
- 做 tag mask,比如检查最低几位标签
or dest, src
作用:按位或。
or rax, 0b11
课程里经常用它来:
- 给运行时值打 tag
- 构造布尔/数组等带标签的值
setne al
这是课上实现 intToBool 时出现的关键指令。
作用:
如果“not equal”条件成立,就把目标字节设成 1;否则设成 0。
cmp rax, 0
mov rax, 0
setne al
这个序列的意思是:
- 先比较
rax和 0 - 先把整个
rax清零 - 如果
rax != 0,就把低 8 位al设成 1
所以结果就是:
- 原值非 0 ->
rax = 1 - 原值等于 0 ->
rax = 0
这正好实现“把任意整数规约成布尔 0/1”。
为什么要先 mov rax, 0
因为 setne al 只写低 8 位 al,不清空高位。
先清零,才能保证最终整个 rax 是干净的 0 或 1。
6. 栈操作:push pop
push arg
作用:把一个值压栈。
语义上等价于:
sub rsp, 8
mov [rsp], arg
也就是:
- 栈顶往下挪 8 字节
- 把值写到新的栈顶
pop reg
作用:从栈顶弹出一个值。
语义上等价于:
mov reg, [rsp]
add rsp, 8
也就是:
- 先读出栈顶值
- 再把
rsp往上恢复 8 字节
课程里为什么这两个重要
因为后面的 call/ret 可以看成是:
call = push return_address + jmpret = pop return_address + jmp
7. 过程调用:call ret
call loc
作用:调用函数。
课程里的理解方式非常重要:
call loc
可以近似看成两步:
- 把“下一条指令地址”压栈
- 跳转到
loc
也就是“push return_address + jmp loc”。
ret
作用:从函数返回。
可以近似看成:
- 从栈顶弹出返回地址
- 跳到这个地址
也就是“pop addr + jmp addr”。
为什么 call/ret 是课程核心
因为这门课后半段开始做:
- 非尾调用
- extern function
- System V AMD64 调用约定
- 参数传递
- 栈对齐
所有这些都围绕 call/ret 展开。
8. 调用约定里反复出现、但不是“新指令”的东西
这部分容易混淆,所以单独说。
rsp
不是指令,是寄存器。 它是栈指针。
在这门课里有两种视角:
- base pointer 视角:拿它当当前栈帧基准,访问局部变量
- top-of-stack 视角:配合
push/pop/call/ret看成真正的栈顶
rax
通常是返回值寄存器。 也常被当 scratch register。
参数寄存器
SysV AMD64 下前六个参数放在:
rdirsirdxrcxr8r9
这不是指令,但你在阅读课程里的 assembly 时必须认识。
9. 课程里还“出现”但要小心区分的,不一定是 x86 指令
有些名字你会在讲义里看到,但它们不是原生 x86 指令,而是:
- SSA 操作
- 伪代码
- 运行时 helper
比如:
load(p, off)store(p, off, v)allocateArray(n)assertIntassertBoolassertArrayassertInBounds
这些在后端会被翻译成真正的 x86,例如:
load->mov dest, [addr]store->mov [addr], srcallocateArray->call某个 runtime 函数
所以它们不是 x86 指令本体。
10. flags
- OF (Overflow Flag):溢出标志。算术结果发生有符号溢出时为 1,否则为 0。
- SF (Sign Flag):符号标志。结果为负时为 1,否则为 0。
- ZF (Zero Flag):零标志。结果为 0 时为 1,否则为 0。
明确两点:
mov不影响 flags。add、sub、imul这类算术指令会更新 flags;cmp像sub一样设置 flags 但不写回结果;test像按位and一样设置 flags 但不写回结果。
另外,课程里条件跳转就是围绕这三个 flag 讲的,条件码公式也明确给了:
e:ZFne:~ZFl:OF ^ SFle:(OF ^ SF) | ZFg:~((OF ^ SF) | ZF)ge:~(OF ^ SF)