x86 将庞大且不规则的指令集归纳为数据移动、算术运算、位操作、控制流和字符串处理等通用模式。操作数大小、地址大小、执行模式、前缀和 ISA 扩展都可能改变指令的编码方式或可用性。Intel 和 AMD 的架构手册仍然是权威资料;汇编成功只能证明某个工具能够对源代码进行编码。
数据移动与地址生成 #
MOV 将立即数或寄存器值复制到寄存器或内存,或者将内存复制到寄存器。普通 MOV 最多包含一个显式内存操作数。MOVZX 对较窄的整数进行零扩展,而 MOVSX 和 MOVSXD 进行符号扩展。在 64 位模式下,以 32 位通用寄存器为目标会隐式清除高 32 位。
CMOVcc 根据状态标志,有条件地复制寄存器或内存源,而不改变控制流。以内存为源的条件移动仍可能在选择结果之前读取源,因此不能用它来保护无效地址。SETcc 使用相同的条件族写入一个字节结果。
XCHG 交换两个操作数。其内存形式即使没有显式的 LOCK 前缀,也具有锁定语义,因此相对于其他处理器对所寻址内存是原子的。原子性并不能使复合算法自动正确;顺序、缓存行布局和重试行为仍然很重要。
LEA 计算有效地址并写入算术结果,但不会访问内存:
lea rax, [rdi + rdi*4] ; rax = rdi * 5编译器使用 LEA 进行指针计算,以及执行选定的加法或常数乘法。由于没有解引用内存操作数,因此不会应用段基址和分页。
栈操作 #
PUSH 递减栈指针并存储一个值;POP 加载一个值并递增栈指针。确切的操作数大小行为取决于模式。对齐、红区、影子空间、保存的寄存器以及哪些内存可作为有效栈存储空间,均由平台 ABI 而不是指令决定。
CALL 存储返回地址并转移控制,而近返回 RET 从栈中加载返回地址。因此,栈指针不匹配或返回地址损坏都会改变控制流。远调用、远返回、调用门和 IRET 还会处理段状态或特权状态,不应将它们视为普通的函数调用指令。
整数算术、比较与标志 #
ADD、ADC、SUB 和 SBB 实现普通算术和多精度算术。ADC 使用进位标志 CF,而 SBB 将其作为借位使用,因此从低位到高位的 limb 序列会传播进位或借位。INC 和 DEC 更新多个算术标志,但保留 CF。
CMP 只执行减法以设置标志。TEST 只执行按位 AND 以设置标志。条件分支、条件移动和字节设置指令随后解释这些标志:
- 相等使用
ZF。 - 无符号顺序使用
CF和ZF。 - 有符号顺序组合使用
SF、OF和ZF。
CF 记录无符号进位或借位,而 OF 记录有符号溢出。因此,同一个位模式需要使用与程序数据解释相匹配的有符号或无符号条件。
单操作数 MUL 和 IMUL 将累加器与显式操作数相乘,并在隐式的高位和低位寄存器中生成双宽度结果。双操作数和三操作数形式的 IMUL 将截断后的结果写入显式目标。DIV 和 IDIV 对隐式的双宽度被除数执行除法。当除数为零,或商无法装入目标时,它们会引发除法错误;软件必须在除法前正确准备高半部分。
逻辑、位测试、移位与旋转操作 #
AND、OR、XOR 和 NOT 按位操作。使用 xor eax, eax 清除寄存器,是当前乱序处理器上常见的解除依赖惯用法,但其架构结果就是零。
BT 将选定的位复制到 CF;BTS、BTR 和 BTC 还会分别设置、清除或取反该位。内存位索引可以寻址超出编码内存操作数的范围,因此边界分析非常重要。显式 LOCK 前缀可以使受支持的内存读-改-写形式具有原子性。
BSF 和 BSR 定位最低位或最高位的置位位。当源为零时,其目标未定义。如果存在相关扩展,TZCNT 和 LZCNT 会为零输入提供定义的结果;但在不支持该扩展的处理器上,它们的编码可能作为 BSF 或 BSR 执行。因此必须进行特性检测。
SHL 和 SAL 是等价的左移。SHR 移入零位,而 SAR 复制符号位。ROL 和 ROR 在操作数内部旋转;RCL 和 RCR 通过 CF 旋转。移位计数会根据操作数大小进行掩码处理,而大计数或零计数产生的标志结果需要查阅具体指令定义。
直接与间接控制流 #
近形式的 JMP、CALL 和条件 Jcc 指令可以使用相对位移,其测量基准是指令末尾。间接 JMP 和 CALL 从寄存器或内存中读取目标。RET 从栈中取得目标。间接目标会与控制流完整性、返回预测和推测执行缓解措施发生交互,因此外观等价的形式可能具有不同的安全性或性能特征。
短条件分支编码 8 位有符号位移。近形式在 32 位和 64 位模式下使用更宽的位移。x86 没有能够有条件地调用或返回的通用指令;代码通过条件分支组合出这些操作。
异常、软件中断和外部中断通过中断描述符表进行转移。INT、IRET 和基于门的转移具有超出普通分支的特权语义。SYSCALL/SYSRET 和 SYSENTER/SYSEXIT 使用特定于模型的配置和操作系统 ABI。
字符串指令与重复前缀 #
MOVS、CMPS、SCAS、LODS 和 STOS 使用隐式的源、目标、累加器和计数寄存器。字节、字、双字以及选定的四字形式会明确元素大小。方向标志(DF)决定索引寄存器递增还是递减。
REP 在计数不为零时重复执行操作。REPE/REPZ 和 REPNE/REPNZ 还会在比较或扫描指令中使用 ZF。当前处理器可以优化大型 REP MOVSB 和 REP STOSB 操作,但根据特性和大小进行库分派仍属于实现决策。
当目标起始位置位于源范围内部且高于源范围起始位置时,向前执行 rep movsb 是不安全的,因为较早的存储会覆盖尚未读取的字节。向后复制可以处理这种重叠:
std ; 递减索引
rep movsb
cld ; 恢复 ABI 要求的方向调用方必须将指针初始化为最后一个元素,以执行向后复制。大多数 x86 ABI 要求函数进入和返回时 DF=0,因此执行 STD 的代码即使在失败路径上也必须恢复该标志。生产环境中的 memmove 实现还会根据重叠情况、大小和处理器特性选择方向与优化后的指令序列。
CLI、STI、LGDT、LIDT、控制寄存器访问、端口 I/O 和 HLT 等特权操作受到当前模式和特权级别的限制。汇编器接受某条指令并不授予执行权限。
评论
在 GitHub 上阅读或评论(发表评论需要登录)