想象你是银行里的一位资深交易员,面前摆着一排排整齐的“现金抽屉”——这就是CPU里的寄存器(Register),每个抽屉能存放一个数字。银行给你发了一本《操作手册》,里面全是让你对抽屉里的钱做处理的命令:把两个抽屉的钱加起来(ADD)、从一个抽屉里减去另一个的钱(SUB)、把某个抽屉的钱翻倍再翻倍(SHL)、检查两个抽屉的钱是否一样(CMP)……这本手册,就是x86指令集里的“算术与逻辑指令体系”。 你可能会问:我只是个AI工程师,天天写Python,为什么要搞懂这些“底层账本”?因为无论是训练神经网络时累加梯度(ADD)、计算注意力得分(MUL),还是判断两个向量是否匹配(CMP后条件跳转),最终都会编译成这些基础指令在CPU上执行。当你理解了这些指令,你就能读懂性能剖析器的输出,明白为什么某段代码快、某段代码慢,甚至能写出对CPU缓存和流水线更友好的高效代码——这在优化大模型推理、数据处理管道时,价值无法估量。

岗位关联

掌握x86算术与逻辑指令,等于拿到了读懂CPU底层“每一笔账目”的钥匙。对AI从业者而言,这是调优高性能计算、理解编译器行为和定位系统瓶颈的必备内功。

目录
  1. 可视化演示
  2. 加减法基础算术
  3. 乘除法宽积商余
  4. 按位逻辑与或非
  5. 移位乘除2幂
  6. 比较指令设标志
  7. 算术标志位详解
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示ADD/SUB算术指令ALU执行与EFLAGS标志位设置、AND/OR/XOR位运算与SHL/SHR移位操作、MUL/DIV隐式操作数乘除法流程,揭示x86指令体系核心机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

ADD/SUB算术指令执行 ALU执行:操作数经ALU执行加减法运算输出结果 EFLAGS:CF/ZF/SF/OF标志位根据运算结果设置 ADC/SBB:带进位加减法利用CF实现多精度运算 result = op1 + op2; EFLAGS = compute_flags(result) ADD执行加法并更新标志位驱动条件跳转

加减法基础算术

交易员每天最频繁的操作是什么?清点现金、合并账户、支付款项。在银行柜台,加法和减法就是最基本的“入账”和“出账”。而在 CPU 的世界里,ADD 和 SUB 指令扮演着同样的角色——它们是构成一切复杂运算的地基。当你写 total = total + price 时,编译器最终会生成一条 ADD 指令;当你写 balance = balance - pay 时,背后就是一条 SUB。

从交易映射到技术,ADD 指令的运作逻辑极其直白:它从左边操作数(目标)和右边操作数(源)中取出数值,执行加法,然后把结果“回写”到目标操作数所在的位置。比如 ADD AX, BX 的意思就是:AX = AX + BX。SUB 同理,执行 AX = AX - BX。在汇编语言中,操作数可以是寄存器、内存单元或立即数(即直接写在指令里的常数)。需要注意的是,x86 指令集中绝不允许两个操作数都来自内存——交易员不能同时从两个远处仓库直接调货,必须有一个放到最近的抽屉(寄存器)里。

这些基础算术指令会自动更新标志寄存器 EFLAGS 中的几个关键位:如果结果为零,零标志位 ZF 被置 1;如果最高位发生了进位或借位,进位标志 CF 被置 1;如果结果超出了目标类型的取值范围,溢出标志 OF 被置 1。此外还有 INC(加一)和 DEC(减一)指令,它们是专为计数器优化的“快捷操作”——比如遍历数组时用 INC 递增索引、递减循环次数时用 DEC,它们在底层比 ADD AX, 1 更节省编码空间。

从交易员场景回看:ADD 和 SUB 之所以是基石,是因为现代 CPU 内部的一切复杂运算——乘法、除法、地址计算——最终都能分解为一系列加减法。而作为 AI 工程师,理解 ZF、CF 这些“账本标记”,是读懂后续条件跳转(JE、JNE)的前提。

ADD R1, R2⇒R1⇐R1+R2若结果为 0, ZF⇐1\text{ADD}\ R1,\ R2 \Rightarrow R1 \Leftarrow R1 + R2 \qquad \text{若结果为 } 0,\ \text{ZF}\Leftarrow 1
公式通俗说明:这像交易员把抽屉 R1 和 R2 里的钱合并,全部放回抽屉 R1;如果合并后抽屉恰好空了(结果为 0),就在账本上盖一个“零”章(ZF=1)。
  1. 目标回写指令执行后,结果存回第一个操作数(目标),类似于交易员把合并后的现金放回原抽屉。
    ADD R1, R2⇒R1⇐R1+R2\text{ADD}\ R1,\ R2 \Rightarrow R1 \Leftarrow R1 + R2
  2. 零标志更新运算后 CPU 检查结果,若为 0,则将 ZF 标志置 1,等价于账本上标记“此账户已清零”。
    若 R1+R2=0, ZF⇐1\text{若 } R1 + R2 = 0,\ \text{ZF} \Leftarrow 1
  3. 进位标志更新若加法产生最高位进位(无符号溢出),CF 被置 1,如同收到了“超出预期”的额外一摞现金。
    若产生进位, CF⇐1\text{若产生进位},\ \text{CF} \Leftarrow 1

乘除法宽积商余

如果说加减法是日常进出账,那乘除法就是“批量结算”。交易员月底要按 6 周每周 7 单的节奏计算总订单量,直接做 6+7 显然不对,需要一张“乘法表”。CPU 的 MUL 和 DIV 就是内置的乘法表,但它们有一个非常独特的规则:结果比想象中宽。

从交易规则说起:MUL(无符号乘法)有一个“双倍宽度”的隐藏约定。当你执行 MUL BX 时,被乘数默认放在 AX 中,而乘积的高 16 位被强制存入 DX,低 16 位存入 AX。这就像交易员不仅把总额放回原抽屉,还发现结果太大、一个抽屉装不下,于是把多出来的部分放进了相邻的备用抽屉(DX)。如果是 MUL ECX(32 位),那么结果用 EDX:EAX 这对 64 位组合来存放。“双倍宽度”保证了乘法永远不会因为空间不足而溢出。

除法 DIV 正好是乘法的逆运算:它把 DX:AX 组成的宽值作为被除数,除以操作数,商放回 AX,余数放入 DX。比如 DIV BX 就表示 AX = (DX:AX) / BX、DX = (DX:AX) % BX。有符号版本的 IMUL 和 IDIV 与之一一对应,只不过数值按补码解释。接下来看核心公式:

回到交易员视角:为何要如此大费周章?因为 CPU 的寄存器宽度是固定的,如果乘法结果不扩大到双倍宽度,进位信息就会永久丢失而无法察觉。对 AI 工程师来说,这解释了为什么在 C/C++ 中做 32 位整数乘法时,编译器有时会生成 imul 指令而不是 mul——有符号版本更灵活,且允许两个操作数不同。理解 MUL/DIV 的“宽结果”约定,能帮你避免在底层编码时因为溢出而写出“账实不符”的 bug。

MUL BX⇒DX:AX⇐AX×BX\text{MUL}\ BX \Rightarrow DX:AX \Leftarrow AX \times BX
DIV BX⇒AX⇐⌊(DX:AX)/BX⌋,DX⇐(DX:AX) mod BX\text{DIV}\ BX \Rightarrow AX \Leftarrow \lfloor (DX:AX) / BX \rfloor, \quad DX \Leftarrow (DX:AX) \bmod BX
公式通俗说明:MUL 像一张能展开的巨型收据——一个抽屉装不下乘积时,自动扩展到 DX 和 AX 组合的“加长收据”;DIV 则像拆包裹:商放进主抽屉,余数放进副抽屉,各归其位。
  1. 乘积双倍存放MUL 将乘积的高位存入 DX、低位存入 AX,保证不丢失信息。
    MUL BX⇒DX:AX⇐AX×BX\text{MUL}\ BX \Rightarrow DX:AX \Leftarrow AX \times BX
  2. 商与余数的分配DIV 用 32 位(DX:AX)整体做除法,商放到 AX,余数放到 DX。
    AX⇐⌊(DX:AX)/BX⌋,DX⇐(DX:AX) mod BXAX \Leftarrow \lfloor (DX:AX) / BX \rfloor,\quad DX \Leftarrow (DX:AX) \bmod BX

按位逻辑与或非

银行柜员每天要核验凭证:检查一张支票的某些位是否被盖章、把几张凭证的标记合并、或者核验某个开关是否处于“关”状态。计算机里也有类似的操作,不过对象变成了一片“二进制位”。这就是 AND、OR、XOR、NOT 四位“位级操作员”。

以交易场景来说,AND 像“保险箱双重锁校验”:只有当两把钥匙(两个操作数)在同一位置都为 1 时,该位置才被点亮(结果为 1)。这在编程中常用于“清零掩码”——比如只保留一个整数的低 8 位,可以用 AND AX, 0x00FF,高 8 位被全部屏蔽。OR 像“合并员工工牌权限”:任一操作数在某位为 1,结果该位即为 1,用于把多个开关标志合入一个状态字。XOR 像“核对账目是否一致”:两位相同时为 0,不同时为 1,因此 XOR AX, AX 可以快速把寄存器清零——这是编译器最喜欢的惯用法,比 MOV AX, 0 更短。NOT 则简单粗暴地翻转每一位,相当于把账本上的记录全部黑白颠倒。

回扣交易员视角:按位逻辑在 AI 领域到处都是——位掩码(bitmask)用来压缩存储多个布尔状态、二进制特征向量做 AND 就是特征的交集判断、用 XOR 比较两个哈希是否匹配(密码学中的 “异或校验”)。最重要的是,理解 XOR AX, AX 这种“清零惯用法”,能让你的汇编阅读能力上一个台阶。

A AND B: 位=1⇔(ai=1∧bi=1)A \ \text{AND}\ B:\ \text{位}=1 \Leftrightarrow (a_i=1 \wedge b_i=1)
A XOR B: 位=1⇔(ai≠bi)A \ \text{XOR}\ B:\ \text{位}=1 \Leftrightarrow (a_i \ne b_i)
公式通俗说明:AND 像两个人同时在支票的同一栏签名才生效;XOR 像“找不同”游戏——两处不一样才标红,完全一样则显示为 0。
  1. 与运算按位求解对 A 和 B 的每个二进制位分别执行“两者皆 1 才为 1”的规则。
    AND: 1∧1=1, 1∧0=0\text{AND}:\ 1 \wedge 1 = 1,\ 1 \wedge 0 = 0
  2. 异或运算按位求解对每个位执行“不同则为 1、相同则为 0”的规则。
    XOR: 1⊕0=1, 1⊕1=0\text{XOR}:\ 1 \oplus 0 = 1,\ 1 \oplus 1 = 0

移位乘除2幂

交易员发现一个快捷技巧:当需要对某个数字“乘以 10 的整数倍”时,不需要逐笔计算,只需要把账本上的数字往左平移、并在末尾补零就行。比如数字 3 在十进制的账本上左移一位变成 30,相当于乘以 10。计算机的二进制世界同样如此,而且更彻底——因为二进制的每一位都代表 2 的幂,所以左移一位等价于乘以 2、右移一位等价于除以 2。

SHL(Shift Logical Left)指令把目标操作数整体向左移动指定的位数,右边腾出的空位一律补 0。SHR(Shift Logical Right)则向右移,左边补 0。比如 SHL AX, 1 相当于 AX = AX * 2,SHL AX, 3 相当于 AX = AX * 8。这在性能上远比调用 MUL 指令快得多,因为移位是 CPU 的单周期操作,而乘法需要更多周期。SAL(Shift Arithmetic Left)与 SHL 在左移时完全等价;但 SAR(Shift Arithmetic Right)与 SHR 不同——算术右移会保留符号位,对负数右移时左边补的是 1 而不是 0,从而保证结果仍然是正确的负数。

在真实的 AI 工程中,编译器经常把 x * 2 优化成 SHL x, 1、把 x / 8 优化成 SAR x, 3(对于有符号正数场景)或 SHR x, 3(对无符号)。当你用 size_t index = base + offset 时,CPython 的 PyLong 对象内部也会大量使用移位来快速乘 2。理解了这一节,你就能在阅读编译产物时一眼认出这些“隐藏的快捷操作”。

SHL AX,n⇒AX⇐AX×2n\text{SHL}\ AX, n \Rightarrow AX \Leftarrow AX \times 2^n
SAR AX,n⇒AX⇐⌊AX/2n⌋(保留符号位)\text{SAR}\ AX, n \Rightarrow AX \Leftarrow \lfloor AX / 2^n \rfloor \quad (\text{保留符号位})
公式通俗说明:SHL 就像在现金抽屉里把每张钞票的面额都翻一倍(1 变成 10、10 变成 100),总金额正好乘以 2^n;SAR 则像把每张钞票面额缩小 2^n 倍,但要保留它是“正钞票”还是“欠条”(符号位)的属性。
  1. 逻辑左移补零所有位向左移动 n 位,右边空出的位全部填入 0。
    SHL AX,n:biti⇐biti−n, bit0..n−1⇐0\text{SHL AX}, n:\quad bit_i \Leftarrow bit_{i-n},\ bit_{0..n-1}\Leftarrow 0
  2. 算术右移保号所有位向右移动 n 位,左边空出的位填入原符号位(正数补 0、负数补 1)。
    SAR AX,n:biti⇐biti+n, 高位⇐符号位\text{SAR AX}, n:\quad bit_i \Leftarrow bit_{i+n},\ 高位\Leftarrow 符号位

比较指令设标志

交易员最重要的职责之一:在付款前核对两个账目是否相等、某个账户余额是否充足。但请注意——他不需要真的把钱从抽屉里拿出来合并,只需要在账本上“比划一下”。CPU 的 CMP(Compare)和 TEST 指令正是这个“只比划不动钱”的动作。

CMP 指令执行的操作其实是减法:CMP AX, BX 在内部计算 AX - BX,但结果不会被写回到 AX——它只更新标志位。如果两者相等,结果为 0,于是 ZF=1;如果 AX < BX(无符号视角),减法会产生借位,CF=1;如果 AX ≥ BX,则 Z=0 且 CF=0。TEST 指令执行的是“内部相与”:TEST AX, BX 计算 AX & BX 但不回写,只用来检查某些位是否为零——如果结果全 0,ZF=1。它的经典用途是检测某个标志位是否被设置:TEST AL, 0x01 后,若 ZF=1 说明最低位为 0。

这些标志位驱动着后续的“条件跳转”指令(Conditional Jump):JE(相等则跳)检查 ZF、JNE(不等则跳)检查 ZF=0、JB 检查 CF=1、JA 检查 CF=0 且 ZF=0。在高级语言中,if (a == b) 编译后会变成 CMP + JE 的组合。

回扣比喻:CMP 是“比划一下”,TEST 是“检索一下”。在 AI 推理引擎中,条件跳转几乎无处不在——判断循环边界、检测内存对齐、验证缓存命中。读汇编时,看到 CMP 后紧跟 JE/JNE/JB/JA,你就能准确推断出源码里对应的 if/else 和循环条件。

\text{CMP}\ AX, BX:\quad \text{内部计算 } AX - BX,\ \text{仅更新} \\begin{cases}ZF=1, & AX = BX \\CF=1, & AX < BX\end{cases}
公式通俗说明:CMP 像柜员在两张支票上比划一下差值,但不实际动抽屉里的钱;如果发现金额相等,就在账本上盖个“相等”章(ZF),然后让条件跳转指令决定下一步走向哪里。
  1. 执行隐藏减法CMP 在 CPU 内部执行减法,但目的操作数的值保持不变。
    CMP AX,BX⇒内部临时值⇐AX−BX\text{CMP}\ AX, BX \Rightarrow \text{内部临时值} \Leftarrow AX - BX
  2. 更新零标志若结果为 0,则 ZF 置 1,表示“相等”。
    若 AX=BX,ZF⇐1\text{若 } AX = BX,\quad ZF \Leftarrow 1
  3. 更新进位标志若结果为负(无符号视角的借位),则 CF 置 1,表示 AX < BX。
    若 AX<BX,CF⇐1\text{若 } AX < BX,\quad CF \Leftarrow 1

算术标志位详解

交易员每天都会在账本上留下各类“记号”(flag):账目是否清零(Z)、是否超出抽屉容量而多出进位(C)、数额是否因过大而算错符号(O)、当前是入账还是出账(S)。CPU 的 EFLAGS 寄存器正是这样一本“账本记号簿”,里面每一类记号都有精确的含义。

零标志 ZF(Zero Flag)是使用频率最高的一位:当算术或逻辑运算的结果为 0 时被置 1。比如 XOR AX, AX 会将 AX 清零并同时把 ZF 置 1。进位标志 CF(Carry Flag)记录无符号运算的“越界”:最高位产生的进位(加法)或借位(减法)会让 CF=1,这有点像交易员发现现金超过了抽屉的物理容量,溢出的那一摞要么转存进相邻抽屉(多精度算术),要么报告异常。溢出标志 OF(Overflow Flag)则记录有符号运算的“符号错乱”:当两个正数相加得到负数、或两个负数相加得到正数时,OF=1——这意味着结果从数学上讲已经不正确了。符号标志 SF(Sign Flag)最简单:直接反映结果的最高位(符号位),结果为负则 SF=1。

在实际的 AI 高性能优化中,这些标志位是底层循环优化的核心。比如在 SIMD 向量化的归约(reduction)代码里,靠检查 ZF 来判断累加和是否为零、提前终止循环;在自定义内存池中,用 CF 检测分配大小的越界。看懂了 EFLAGS 这四枚“印章”,你就学会了阅读 CPU 的“账本审阅报告”。

ZF=1⇔结果为 0CF=1⇔无符号运算产生进位/借位OF=1⇔有符号运算结果溢出SF=1⇔结果最高位为 1 (负数)\begin{array}{l}ZF = 1 \Leftrightarrow \text{结果为 } 0\\CF = 1 \Leftrightarrow \text{无符号运算产生进位/借位}\\OF = 1 \Leftrightarrow \text{有符号运算结果溢出}\\SF = 1 \Leftrightarrow \text{结果最高位为 } 1\ (\text{负数})\end{array}
公式通俗说明:这四项标记分别是账本上的“为零章”、“超限章”、“错签章”和“欠款章”。每次算完账,CPU 就自动盖上这几枚章,后续指令通过查看这些章决定是否跳转。
  1. 零标志判定运算结果的所有位都为 0 时,ZF 置 1,常用于判断相等或是否为零值。
    result=0⇒ZF=1\text{result} = 0 \Rightarrow ZF = 1
  2. 进位标志判定无符号加法从最高位产生进位(或减法产生借位)时,CF 置 1。
    carry-out=1⇒CF=1\text{carry-out} = 1 \Rightarrow CF = 1
  3. 溢出标志判定有符号加/减结果超出可表示范围时置 1,例如最大正数加 1 变负数。
    result>2n−1−1∨result<−2n−1⇒OF=1\text{result} > 2^{n-1}-1 \lor \text{result} < -2^{n-1} \Rightarrow OF = 1
  4. 符号标志判定结果最高位为 1 时置 1,等价于结果为负(有符号视角)。
    MSB=1⇒SF=1\text{MSB} = 1 \Rightarrow SF = 1

AI 应用场景

向量化前检查数据对齐

阶段:数据预处理 / 性能优化

解决的问题:在 GPU 和 CPU 上做向量化计算前,需要确认数据在内存中的地址是否对齐(如 16 字节或 32 字节对齐)。传统方式用 % 取模或者多次判断地址,效率很低;用 TEST 指令一行就能完成:TEST EAX, 0xF 后检查 ZF 是否置 1,如果地址低 4 位全为 0,就是 16 字节对齐。

具体案例:某 AI 推理框架在加载权重文件时,需要把张量数据搬运到对齐的缓冲区中。优化师用 TEST 指令快速判断每个 chunk 的起始地址是否满足 AVX-512 的 64 字节对齐要求,不满足则用汇编写一个微小的对齐拷贝例程,整体加载速度提升了 18%。

快速哈希计算中的逻辑运算

阶段:特征工程 / 数据索引

解决的问题:在构建大规模向量索引或特征哈希表时,需要快速计算哈希值的散列位。传统 hash % n 的取模运算很慢,而用 AND 指令配合掩码 hash & (n-1)(要求 n 为 2 的幂)可以一步完成等价运算。

具体案例:某推荐系统团队实现用户特征哈希时,将哈希表长度定为 2 的 20 次方(1048576),用 AND 指令代替取模后,在线特征查询的 P99 延迟从 3.2ms 降到 2.1ms。底层正是 x86 的 AND 指令在高效执行位掩码运算。

循环计数器的 DEC 优化

阶段:模型推理 / 算子实现

解决的问题:深度学习中很多算子(如 LayerNorm、Softmax)需要遍历张量的每个元素,传统 for (i = 0; i < n; i++) 循环在汇编层面通常被优化成 DEC 递减计数器的形式,这样可以同时利用 ZF 标志位判断循环是否结束,减少一次 CMP 比较指令。

具体案例:某大模型推理框架的 Softmax 算子中,开发者观察到编译器生成的循环体使用了 DEC R8 加 JNZ loop 的模式,于是手动调整指针递增方向,让循环在反向遍历时命中这种优化模式,端到端推理吞吐提升了约 5%。

Python 演示

python80 行
class X86ALU:    """模拟 x86 算术逻辑单元核心行为的教学工具"""     def __init__(self):        self.regs = {"AX": 0, "BX": 0, "CX": 0, "DX": 0}        self.flags = {"ZF": 0, "CF": 0, "OF": 0, "SF": 0}     def _update_flags(self, value):        v = value & 0xFFFF  # 模拟 16 位寄存器回写        self.flags["ZF"] = 1 if v == 0 else 0        self.flags["SF"] = 1 if (v >> 15) & 1 else 0        return v     def set_reg(self, name, value):        self.regs[name] = value & 0xFFFF     def add(self, dst, src):        a, b = self.regs[dst], self.regs[src]        s = a + b        self.flags["CF"] = 1 if s > 0xFFFF else 0        self.regs[dst] = self._update_flags(s)     def sub(self, dst, src):        a, b = self.regs[dst], self.regs[src]        r = a - b        self.flags["CF"] = 1 if r < 0 else 0        self.regs[dst] = self._update_flags(r)     def mul(self, src):        prod = self.regs["AX"] * self.regs[src]        self.regs["DX"] = (prod >> 16) & 0xFFFF  # 高 16 位入 DX        self.regs["AX"] = prod & 0xFFFF          # 低 16 位入 AX     def div(self, src):        dividend = (self.regs["DX"] << 16) | self.regs["AX"]        divisor = self.regs[src]        self.regs["AX"] = dividend // divisor        self.regs["DX"] = dividend % divisor     def band(self, dst, src):        self.regs[dst] = self._update_flags(self.regs[dst] & self.regs[src])     def bor(self, dst, src):        self.regs[dst] = self._update_flags(self.regs[dst] | self.regs[src])     def bxor(self, dst, src):        self.regs[dst] = self._update_flags(self.regs[dst] ^ self.regs[src])     def shl(self, dst, n):        self.regs[dst] = self._update_flags(self.regs[dst] << n)     def cmp(self, a, b):        diff = self.regs[a] - self.regs[b]        self.flags["ZF"] = 1 if diff == 0 else 0        self.flags["CF"] = 1 if diff < 0 else 0        self.flags["SF"] = 1 if (diff >> 15) & 1 else 0     def show(self, label):        regs = " ".join(f"{k}={self.regs[k]}" for k in self.regs)        flags = " ".join(f"{k}={v}" for k, v in self.flags.items())        print(f"{label:12s} | {regs} | {flags}")  if __name__ == "__main__":    alu = X86ALU()    alu.set_reg("AX", 35)    alu.set_reg("BX", 28)    alu.show("初始状态")    alu.add("AX", "BX")    alu.show("ADD AX,BX")    alu.mul("BX")    alu.show("MUL BX")    alu.sub("AX", "CX")    alu.show("SUB AX,CX")    alu.set_reg("AX", 0x00FF)    alu.set_reg("BX", 0x0F0F)    alu.band("AX", "BX")    alu.show("AND AX,BX")    alu.cmp("AX", "BX")    alu.show("CMP AX,BX")
目的:用 Python 类模拟 x86 汇编的算术/逻辑指令执行流程,直观展示双宽度乘法、标志位更新等核心行为,适合配合讲解与实验。
  1. def _update_flags(self, value)统一的标志位更新逻辑:结果为零则置 ZF,最高位为 1 则置 SF,模拟 CPU 的自动“盖印章”。
  2. self.flags["CF"] = 1 if s > 0xFFFF else 0加法超过 16 位最大表示范围时置进位标志,对应真实 CPU 的 CF 行为。
  3. self.regs["DX"] = (prod >> 16) & 0xFFFFMUL 的高半部分存入 DX,低半部分存入 AX,复刻 x86 双倍宽度乘积约定。
  4. dividend = (self.regs["DX"] << 16) | self.regs["AX"]DIV 前把 DX:AX 拼接成 32 位被除数,对应真实除法指令的输入形态。
  5. def cmp(self, a, b)CMP 只更新标志位、不回写寄存器,模拟“只比划不动钱”的行为。

常见误区

误区我以为 `ADD AX, BX` 只是临时算一下 `AX + BX`,并不会改变 AX 的值,就像 C 语言里的 `a + b` 表达式不会修改 a 和 b 一样。

正解x86 的 ADD 指令是“两操作数”语法,第一个操作数既是输入也是输出,计算完成后结果会覆盖写回。这与高级语言中 a + b 不改变 a 的语义完全不同!更接近 a += b。如果不希望修改原值,需要先 MOV 复制一份,或使用 LEA 指令间接计算。

为何常见:初学者从 Python/C 的表达式求值模型迁移到汇编,很容易把“指令”当成“表达式”,而忽略了汇编指令是“就地修改”的。

误区我以为 `MUL BX` 和 `IMUL BX` 都只在 16 位内得到一个结果,溢出就直接丢弃了。

正解MUL 的单操作数形式会把乘积扩展到双倍宽度(AX×BX 的结果共 32 位,写入 DX:AX),因此永远不会因为溢出丢失高位。而 IMUL 有多种形式,双操作数形式 IMUL AX, BX 才是在 16 位内截断结果的。两指令的溢出处理逻辑不同,使用时必须区分。

为何常见:看到“乘法”就直觉认为结果和操作数同宽,忽略了 x86 为了不丢失进位信息而设计的扩展存储约定。

误区右移指令不就是把各位往右推、左边补 0 吗?SAR 和 SHR 应该没有差别。

正解对于无符号数两者确实等价;但对于有符号负数,SHR 在左边补 0 会把负数变成一个很大的正数,数学意义完全错误。SAR 会在左边补符号位,保证 -8 右移 1 位仍是 -4 而不是某个正数。编译器对 int x / 2 会生成 SAR,对 unsigned x / 2 才用 SHR。

为何常见:初学者做移位练习时通常用正数,没有接触负数场景,导致忽略了算术右移与逻辑右移的关键区别。

误区`CMP AX, BX` 既然做了减法,AX 的值应该会变成 `AX - BX` 吧?例如想保留原 AX 又想知道大小关系,就先备份 AX 再比较。

正解CMP 确实在内部执行减法,但结果只用于更新标志位(ZF、CF、SF、OF 等),不会写回任何通用寄存器。AX 和 BX 的值在执行前后完全不变。真正把减法结果写回的是 SUB;CMP 就是“为了比较而减、减完就扔”。

为何常见:名字里带“Compare“但没有明显的语义线索说明”内部减但不保存”,初学者容易套用 SUB 的行为模型。

面试题

Q1. 关于 x86 的 ADD AX, BX 指令,下列说法正确的是?

单选题 · easy

  • A.计算 AX+BX 后,结果仅保存在临时寄存器
  • B.计算 AX+BX 后,结果写回 AX,并更新标志位← 正确答案
  • C.计算 AX+BX 后,结果写回 BX,并更新标志位
  • D.该指令不更新任何标志位

答案:B。ADD 是两操作数指令,执行 AX = AX + BX,结果回写到目标操作数 AX,同时更新 ZF/CF/OF/SF 等标志位。A 错在“不保存”;C 错在写回目标;D 错在算术运算必然联动标志位。

Q2. 执行 SUB AX, BX 后,若 AX 与 BX 的值相等,则哪个标志位会被置 1?

单选题 · easy

  • A.CF
  • B.OF
  • C.ZF← 正确答案
  • D.SF

答案:C。两数相等时相减结果为 0,零标志 ZF 被置 1。CF 只在产生借位时置 1,OF 表示有符号溢出,SF 表示结果为负——A、B、D 均不符合相等相减为 0 的情形。

Q3. MUL BX 指令中,乘积的高 16 位存放在哪个寄存器?

单选题 · easy

  • A.AX
  • B.BX
  • C.DX← 正确答案
  • D.CX

答案:C。单操作数 MUL 使用隐含的 AX 作为被乘数,乘积为 32 位:低 16 位回写 AX,高 16 位强制写入 DX,形成 DX:AX 对。BX 是乘数之一,CX 不参与,AX 只存低半部分。

Q4. 哪条指令可以用来快速判断一个寄存器的值是否为 0,且不改变该寄存器?

单选题 · mid

  • A.XOR AX, AX
  • B.CMP AX, 0← 正确答案
  • C.ADD AX, 0
  • D.SUB AX, AX

答案:B。CMP 做减法但不回写,用 CMP AX, 0 可以检测 AX 是否为 0 且不修改它。XOR AX, AX 会把 AX 清零,A 改变了寄存器;ADD AX, 0 不改变值但污染标志位且效率不如 CMP;SUB AX, AX 同样清零寄存器。

Q5. 在 x86 中,SAR AX, 1 与 SHR AX, 1 的区别是?

单选题 · mid

  • A.SAR 向左移,SHR 向右移
  • B.SAR 在左端补符号位,SHR 补 0← 正确答案
  • C.SAR 在左端补 0,SHR 补符号位
  • D.两者没有任何区别

答案:B。SAR(算术右移)在左端补入原符号位的值,以保持负数在右移后仍然为负数;SHR(逻辑右移)无条件补 0。A 方向描述错误;C 正好说反;D 对无符号数成立但对有符号数不成立。

Q6. 对于无符号整数 value,若想只保留其低 8 位,应该使用哪条指令?

单选题 · mid

  • A.AND value, 0xFF← 正确答案
  • B.OR value, 0xFF
  • C.XOR value, 0xFF
  • D.NOT value

答案:A。AND 配合掩码 0xFF 可将高 8 位清零、低 8 位原样保留。OR 是把低 8 位全置 1;XOR 会把低 8 位全取反;NOT 会将所有位取反,均无法实现“只保留低 8 位”。

Q7. TEST AX, 0x0001 指令的目的是?

单选题 · mid

  • A.把 AX 的低 1 位清零
  • B.测试 AX 的最低位是否为 1,且不修改 AX← 正确答案
  • C.把 AX 的最低位置 1
  • D.判断 AX 是否等于 1

答案:B。TEST 执行内部 AND 但不回写,仅更新标志位,用于测试 AX 与掩码按位与的结果是否为 0。若 AX 最低位为 1,则结果为非 0,ZF=0;为 0 则 ZF=1。它不修改 AX,也不做相等性判断,A/C/D 均错。

Q8. 执行 ADD AX, BX 后,CF=1 且 OF=1,关于这次运算说法正确的是?

单选题 · hard

  • A.无符号结果超出 16 位,有符号结果也在合法范围内
  • B.无符号结果未越界,但有符号结果发生了溢出
  • C.无符号结果超出 16 位,同时有符号结果也发生了溢出← 正确答案
  • D.CF 与 OF 不能同时为 1,该情况不可能出现

答案:C。CF=1 表示无符号加法发生了最高位进位(结果大于 0xFFFF);OF=1 表示有符号视角下结果符号错误(如正+正得负或负+负得正)。无符号“进位”和有符号“溢出”是两种独立的判定逻辑,完全可以同时触发。

Q9. 下列哪些指令不会将结果写回通用寄存器?

多选题 · mid

  • A.CMP AX, BX← 正确答案
  • B.TEST AX, 0xFF← 正确答案
  • C.SUB AX, BX
  • D.MUL BX

答案:AB。CMP 和 TEST 只更新标志位不写回任何寄存器,是“只比划不动钱”的指令。SUB 将差值写回 AX,MUL 将乘积写回 DX:AX,二者都会修改寄存器。

Q10. 关于 DIV 指令,以下说法正确的是?

多选题 · hard

  • A.商存入 AX,余数存入 DX← 正确答案
  • B.被除数默认来自 DX:AX 组合← 正确答案
  • C.若除数为 0 会导致除法错(CPU 异常)← 正确答案
  • D.无符号除法的结果会受到 CF 标志位影响

答案:ABC。DIV 用 DX:AX 拼接的宽值除以操作数,商入 AX、余入 DX,除数为 0 时 CPU 触发 #DE 异常。CF 是运算后更新的标志位之一,不会作为输入影响除法行为,D 错。