想象你是一位仓库管理员,面前有一排排整齐的储物柜。你的日常工作就是三件事:把货物从一处搬运到另一处、对货物的数量做加减运算、以及检查两个柜子里的货物是否相同。这套流程看似简单,却是整个仓库高效运转的核心。在计算机的世界里,CPU 就是那位勤奋的仓库管理员,而 MOV、ADD、CMP 就是它最得心应手的三大工具。MOV 负责搬运数据,ADD 负责算术运算,CMP 则负责比较判断。理解这三个基础指令如何协同工作,你就掌握了理解整个计算机运行逻辑的钥匙。

岗位关联

掌握 x86 基础指令是理解 CPU 执行模型的基石,对从事编译器开发、逆向工程、性能优化等 AI 基础设施工作的从业者而言,这是必经之路。

目录
  1. 可视化演示
  2. 寄存器与寻址
  3. MOV数据搬运
  4. ADD算术运算
  5. CMP比较判断
  6. JMP流程控制
  7. 指令组合实战
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示MOV数据传送流程、ADD与CMP标志位设置、JE条件跳转分支执行三阶段交互探索。

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

MOV数据传送指令 源操作数复制到目的操作数 支持寄存器到寄存器传送 不改变源操作数值 MOV dst, src MOV是x86最基本的数据传送指令

寄存器与寻址

仓库管理员的第一课,是认识仓库的格局。CPU 内部那些小巧而高速的寄存器,就像是管理员工作台上随手可及的便签本;而内存则像是远处巨大的货架区。每次从便签本上读取信息只需一瞬,但跑到货架区翻找就要慢得多。这便签本上的每一页,在 x86 世界里都有一个名字:AX、BX、CX、DX,以及专用的栈指针 SP、指令指针 IP 等等。

寻址方式,就是告诉 CPU “到哪儿去取货物” 的路径描述。最简单的方式,是把数字直接写进指令里,这叫立即数寻址——好比管理员在便签上直接写 “5“,不用去任何地方取货。直接寻址则像是给出 ”3 号货架第 2 格” 这样的门牌号,CPU 按地址去内存中取数。而寄存器寻址最为常用,因为数据就在手边的便签本里,取用最快。

在 x86 汇编中,一条指令通常写成 操作码 目标, 源 的形式。操作码就是动作命令(MOV、ADD、CMP),逗号左边是目的地,右边是来源。这种 “从左到右” 的阅读方式,与自然语言中 “把 A 给 B” 的习惯恰好相反,初学者极易搞混。

就像仓库管理员需要先熟悉便签本和货架的布局才能高效工作,理解寄存器与寻址方式,是掌握所有 x86 指令的第一步。

寻址方式∈{立即数,直接,寄存器,寄存器间接,变址}\text{寻址方式} \in \{ \text{立即数}, \text{直接}, \text{寄存器}, \text{寄存器间接}, \text{变址} \}
这个公式表明寻址方式是一个集合,不同的寻址方式决定了 CPU 从何处获取数据——就像管理员可以选择从便签本、货架或通过便签上的记号线去找货物。
  1. 寻址方式集合寻址方式是 CPU 获取操作数(即参与运算的数据)的途径,不同方式开销不同、灵活性不同。
    寻址方式∈{立即数,直接,寄存器}\text{寻址方式} \in \{ \text{立即数}, \text{直接}, \text{寄存器} \}
  2. 立即数寻址操作数直接编码在指令中,无需额外访问内存,执行速度最快,但数值是写死的。
    MOV AX,5⇒AX=5\text{MOV AX}, 5 \quad \Rightarrow \quad AX = 5
  3. 寄存器寻址操作数存放在寄存器中,执行速度快,且值可以动态改变,是最常用的寻址方式。
    MOV AX,BX⇒AX=BX\text{MOV AX}, BX \quad \Rightarrow \quad AX = BX

MOV数据搬运

MOV 指令是 CPU 世界里的“搬运工”。它就像管理员把货物从一个便签移到另一个便签、或从便签移到货架指定位置。注意,MOV 本质是“复制”而非“剪切”——源位置的数据不会消失,正如你用复印机复制文件,原件依然完好。

MOV 的语法非常直观:MOV 目标, 源。目标通常是寄存器或内存位置,源可以是立即数、寄存器或内存。但 x86 规定,两个操作数不能同时来自内存——这就像管理员不能直接从货架 A 搬到货架 B,而必须先把货物搬到手边的工作台(寄存器)上,再放到货架 B。

这里的 [1000H] 表示内存地址,加方括号是告诉 CPU “按这个地址去存取”,而不是把这个数字本身当作数据。这正是指令中 “RS“ 语义的关键:方括号是”解引用”的标志。

MOV 就像是仓库管理员的搬运准则:从哪儿拿到哪儿、什么能直接搬、什么必须经手工作台。掌握了它,你就理解了数据流动的基本法则。

MOV D,S→Val(D)←Val(S)\text{MOV } D, S \quad \rightarrow \quad \text{Val}(D) \leftarrow \text{Val}(S)
该公式表示 MOV 指令将源操作数 S 的值复制到目标操作数 D 中,源值保持不变,目标旧值被覆盖。
  1. 目标赋值箭头方向指示数据流动方向,目标位置收到源位置的值的副本。
    Val(D)←Val(S)\text{Val}(D) \leftarrow \text{Val}(S)
  2. 源不动等号右端的源值在操作后仍然保留,这是复制而非移动。
    Val(S) 保持不变\text{Val}(S) \text{ 保持不变}
  3. 寻址约束目标与源不能同时为内存操作数,必须通过寄存器中转。
    ¬(Mem(D)∧Mem(S))\neg (\text{Mem}(D) \land \text{Mem}(S))
text3 行
MOV AX, 5        ; 立即数 5 送入 AX(在便签本写数字 5)MOV BX, AX       ; AX 的值复制到 BX(便签内容抄一份到另一页)MOV [1000H], AX  ; AX 的值存入地址 1000H 的内存单元(放入货架指定格)

ADD算术运算

如果说 MOV 是搬运工,那 ADD 就是计算器。ADD 指令执行加法运算,并把结果存回目标位置——这好比管理员在便签上写下 “当前数量 + 新增数量 = 最新数量”,然后直接把便签上的旧数字擦掉,改成新数字。

ADD 的语法与 MOV 完全一致:ADD 目标, 源,效果是把目标与源相加,结果写回目标。这条指令会自动更新标志寄存器中的相关标志位,尤其是进位标志 CF 和溢出标志 OF。标志位就像是便签旁边的“状态指示灯”,告诉你刚才的计算有没有产生特殊情况。

注意最后一个例子:0x3000 + 0xE000 = 0x11000,超过了 16 位寄存器能表示的最大值 0xFFFF,于是高位的 1 溢出到进位标志 CF。同时,如果从有符号数角度看,结果还可能触发溢出标志 OF。

ADD 的运行规律就像管理员的心算:不仅算出结果,还会留意“灯”的变化。这些标志位看似不起眼,却是后续 CMP 与条件跳转的基础。

ADD D,S→D←D+S,更新 ZF, SF, CF, OF\text{ADD } D, S \quad \rightarrow \quad D \leftarrow D + S,\quad \text{更新 ZF, SF, CF, OF}
公式说明 ADD 将目标与源相加后写回目标,并根据结果设置零标志、符号标志、进位标志和溢出标志。
  1. 加法核心操作目标寄存器的值与被加的源值做二进制加法。
    D←D+SD \leftarrow D + S
  2. 零标志 ZF如果结果为 0,则 ZF = 1,表示结果是“零”;否则为 0。
    ZF=1  ⟺  (D+S)=0ZF = 1 \iff (D + S) = 0
  3. 进位标志 CF用于无符号数运算,最高位产生进位时 CF = 1。
    CF=1  ⟺  (D+S)>2n−1CF = 1 \iff (D + S) > 2^{n} - 1
  4. 溢出标志 OF用于有符号数运算,当结果超出有符号数表示范围时 OF = 1。
    OF=1  ⟺  (D+S)∉[−2n−1,2n−1−1]OF = 1 \iff (D + S) \notin [-2^{n-1}, 2^{n-1}-1]
text3 行
MOV AX, 1000H   ; AX = 0x1000(便签写上初始数量 4096)ADD AX, 2000H   ; AX = 0x3000(加上 8192,得 12288)ADD AX, 0E000H  ; AX = 0x1000,CF=1(超过 16 位上限,进位灯亮起)

CMP比较判断

CMP 指令是 CPU 的“比较器”。它执行一次减法操作 A - B,但不保存相减的结果——只更新标志位。就像管理员把两个便签上的数字默默对比一下,然后只在心里记下“谁大谁小”的结论,而不在便签上留下任何运算痕迹。

CMP 的比较结果体现在三个标志位上。如果 A 等于 B,相减结果为 0,零标志 ZF = 1;如果 A 小于 B(有符号视角),符号标志 SF 会反映结果的符号;如果 A 小于 B(无符号视角),则会产生借位,进位标志 CF = 1。

CMP 通常不单独使用,而是与条件跳转指令(如 JE、JNE、JG、JL)配合。条件跳转指令读取这些标志位,决定程序的下一步走向。这正是指令中 "RS" 语义的延伸:通过标志位间接地传递比较结果。

CMP 就如同天平上的一次快速称量——不移动任何货物,只在刻度盘上留下指针位置。接下来读表(标志位)的人,就是条件跳转指令。

CMP A,B→A−B,丢弃结果,更新 ZF, SF, CF, OF\text{CMP } A, B \quad \rightarrow \quad A - B,\quad \text{丢弃结果,更新 ZF, SF, CF, OF}
CMP 执行减法运算但只更新标志位,不改变任何通用寄存器的值。后续条件跳转指令通过读取标志位决定是否跳转。
  1. 减法操作在 CPU 内部执行 A - B 的运算,但结果不写回任何寄存器。
    Result=Val(A)−Val(B)\text{Result} = \text{Val}(A) - \text{Val}(B)
  2. 零判断若结果为 0,则 ZF 置 1,说明 A 等于 B。
    ZF=1  ⟺  A=BZF = 1 \iff A = B
  3. 借位判断无符号视角下,若 A < B 则产生借位,CF 置 1。
    CF=1  ⟺  unsigned(A)<unsigned(B)CF = 1 \iff \text{unsigned}(A) < \text{unsigned}(B)
  4. 符号判断有符号视角下,结果最高位为 1 时 SF 置 1,表示结果为负。
    SF=1  ⟺  A−B<0SF = 1 \iff A - B < 0
text4 行
MOV AX, 10      ; AX = 10MOV BX, 20      ; BX = 20CMP AX, BX      ; 计算 10 - 20 = -10,不保存结果                ; SF = 1(结果为负),CF = 1(无符号借位),ZF = 0

JMP流程控制

程序默认是顺序执行的,就像管理员按照清单逐项处理任务。但有时需要根据条件跳转,比如“如果库存不足,就去补货”。JMP 指令正是这条“跳转路径”的载体。

无条件跳转 JMP label 直接改变程序执行流,跳转到指定标签处继续执行——好比管理员直接翻到手册第 10 页而不管前面的内容。更有用的是条件跳转:JE(相等则跳)、JNE(不等则跳)、JG(大于则跳)、JL(小于则跳)等,它们配合 CMP 实现分支逻辑。

这段代码展示了分支结构:CMP 设置标志位,JE 根据 ZF 决定是否跳转。如果 AX 等于 BX(ZF = 1),程序跳到 equal 标签处执行 MOV CX, 20;否则顺序执行 MOV CX, 10。循环结构则通过条件跳转回指令序列的开头实现。

JMP 与条件跳转让程序拥有了“决策能力”。仓库管理员不再只是机械地照单执行,而能根据库存状态(标志位)灵活调整行动路线。

JE label  ⟺  ZF=1⇒PC←label\text{JE label} \iff ZF = 1 \quad \Rightarrow \quad \text{PC} \leftarrow \text{label}
该公式表达条件跳转的语义:当零标志 ZF 为 1 时,程序计数器 PC 被更新为标签所对应的地址,实现跳转。
  1. 条件判断JE 指令首先检查零标志 ZF 的值,决定是否跳转。
    Condition: ZF=1\text{Condition: } ZF = 1
  2. PC 更新若条件成立,则程序计数器 PC 跳转到目标标签的地址。
    PC←address(label)\text{PC} \leftarrow \text{address(label)}
  3. 顺序不成立若条件不成立,PC 继续指向下一条指令,程序正常往下执行。
    PC←PC+1\text{PC} \leftarrow \text{PC} + 1
text8 行
MOV AX, 5MOV BX, 5CMP AX, BX       ; 比较 AX 与 BXJE  equal        ; 若相等则跳转到 equalMOV CX, 10       ; 不等时执行JMP doneequal: MOV CX, 20 ; 相等时执行done: ...

指令组合实战

单独的指令就像单字,组合起来才成为语言。真实程序正是由成千上万条 MOV、ADD、CMP、JMP 组合而成。以最常见的 “变量交换” 为例,不能直接用一条指令交换两个寄存器的值——那就像让两个人同时坐到对方椅子上一样不可能。必须引入第三个临时寄存器暂存。

再看循环累加的例子,它组合了 MOV、ADD、CMP、JMP 四种指令:

这段代码计算 0+1+2+……+9 的和。MOV 负责初始化,ADD 负责任务递增和计数,CMP 与 JNE 配合形成循环出口。四个指令各司其职,组合出完整的循环逻辑。

从交换数据到循环累加,四个基础指令的组合就像管理员的一套标准操作流程:搬运、计算、比较、决策。掌握了这些组合模式,你就能读懂任何汇编程序。

∑i=09i=9×102=45\sum_{i=0}^{9} i = \frac{9 \times 10}{2} = 45
上述循环执行完成后 AX 中的值应为 45。通过 CMP 与条件跳转的组合,实现循环控制,这正是高级语言中 for/while 循环的底层实现方式。
  1. 初始化阶段MOV 指令为循环变量和累加器设置初值。
    CX←0,AX←0\text{CX} \leftarrow 0,\quad \text{AX} \leftarrow 0
  2. 累加操作ADD 指令将当前计数器的值加到累加器。
    AX←AX+CX\text{AX} \leftarrow \text{AX} + \text{CX}
  3. 计数递增计数器在每次循环末尾加 1,确保循环能推进。
    CX←CX+1\text{CX} \leftarrow \text{CX} + 1
  4. 边界判断CMP 与 JNE 配合,当计数器不等于 10 时跳回循环起点。
    CX≠10⇒PC←loop_start\text{CX} \ne 10 \Rightarrow \text{PC} \leftarrow \text{loop\_start}
text4 行
; 交换 AX 与 BX 的值MOV CX, AX    ; CX = AX(把 A 的值暂存到临时位)MOV AX, BX    ; AX = BX(B 坐到了 A 的位置)MOV BX, CX    ; BX = 原 AX(临时值放到 B 的位置)
text7 行
MOV CX, 0     ; 计数器清零MOV AX, 0     ; 累加器清零loop_start:ADD AX, CX    ; 累加ADD CX, 1     ; 计数器加 1CMP CX, 10    ; 比较是否达到 10JNE loop_start ; 未达到则继续循环

AI 应用场景

编译器后端指令生成

阶段:代码生成/优化

解决的问题:编译器后端需要把高层语言翻译成目标机器的汇编指令。如果没有对 MOV、ADD、CMP 等指令的深刻理解,编译器生成的代码会冗长低效。AI 辅助编译器通过模式匹配和成本模型,选择最优的指令序列来完成同样的逻辑。

具体案例:Clang/LLVM 的后端在生成 x86 代码时,会将 a = b + c 这样的中间表示翻译为 MOV 加载变量到寄存器、ADD 执行加法、再 MOV 存回内存的指令序列。AI 优化模块会分析标志位使用情况,比如发现 CMP 之后紧跟的条件跳转可以合并为一条带条件码的指令,从而减少指令数。

程序分析中的指令语义识别

阶段:特征提取/模式识别

解决的问题:安全分析中,恶意代码检测需要理解程序的行为逻辑。传统特征码匹配无法应对变种恶意代码,而基于 AI 的程序分析通过识别 MOV/ADD/CMP 组成的“语义模式”(如解密循环、反调试检测)来判定程序类别。

具体案例:在 objdump 工具反汇编出的指令流中,AI 模型识别出连续多条 ADD 和 CMP 指令构成的模式,判断这是一个嵌入式勒索软件的加密密钥生成逻辑。通过将这些模式作为特征输入分类器,能够在新变种出现时快速捕获。

程序语义嵌入与检索

阶段:语义嵌入/比对

解决的问题:跨架构的二进制代码相似性检测是软件供应链安全的核心挑战。AI 模型需要理解指令的语义(如“把值加载到寄存器并比较”),而不仅仅是文本层面的操作码。

具体案例:训练一个基于指令嵌入的 AI 模型时,将每条汇编指令看作“操作码+操作数类型”的组合。模型通过对比学习,让 MOV AX, BX 与 MOV ECX, EDX 在语义空间中相近(都是寄存器复制),而 ADD AX, 1 则与它们距离较远。这样在跨编译器、跨优化等级的代码匹配中,准确率大幅提升。

Python 演示

python90 行
#!/usr/bin/env python3"""Simple x86 instruction simulator for MOV/ADD/CMP/JMP.""" class Simulator:    def __init__(self):        self.regs = {'AX': 0, 'BX': 0, 'CX': 0, 'DX': 0}        self.flags = {'ZF': 0, 'SF': 0, 'CF': 0, 'OF': 0}        self.memory = {}        self.pc = 0        self.pc_count = 0     def get_value(self, op):        if op in self.regs:            return self.regs[op]        if op.startswith('[') and op.endswith(']'):            addr = int(op[1:-1], 16)            return self.memory.get(addr, 0)        return int(op, 16)     def set_value(self, target, value):        if target in self.regs:            self.regs[target] = value & 0xFFFF        elif target.startswith('[') and target.endswith(']'):            addr = int(target[1:-1], 16)            self.memory[addr] = value & 0xFFFF     def update_flags(self, result):        self.flags['ZF'] = 1 if result == 0 else 0        self.flags['SF'] = 1 if (result & 0x8000) else 0        self.flags['CF'] = 1 if result > 0xFFFF else 0        self.flags['OF'] = 1 if result > 0x7FFF or result < -0x8000 else 0     def run(self, instructions):        self.pc_count = 0        labels = {}        for i, ins in enumerate(instructions):            if ins.endswith(':'):                labels[ins[:-1]] = i        while self.pc < len(instructions):            ins = instructions[self.pc]            self.pc_count += 1            parts = ins.replace(',', ' ').split()            if not parts:                self.pc += 1                continue            op = parts[0].upper()            if op == 'MOV':                self.set_value(parts[1], self.get_value(parts[2]))            elif op == 'ADD':                dst, src = parts[1], self.get_value(parts[2])                result = self.get_value(dst) + src                self.set_value(dst, result)                self.update_flags(result)            elif op == 'CMP':                a, b = self.get_value(parts[1]), self.get_value(parts[2])                self.update_flags(a - b)            elif op == 'JE' and self.flags['ZF'] == 1:                self.pc = labels[parts[1]]                continue            elif op == 'JNE' and self.flags['ZF'] == 0:                self.pc = labels[parts[1]]                continue            elif op == 'JMP':                self.pc = labels[parts[1]]                continue            self.pc += 1        return self.regs, self.flags, self.pc_count  def main():    sim = Simulator()    program = [        'MOV AX, 0',        'MOV CX, 0',        'loop_start:',        'ADD AX, CX',        'ADD CX, 1',        'CMP CX, 10',        'JNE loop_start'    ]    regs, flags, steps = sim.run(program)    print('Final registers:', regs)    print('Final flags:', flags)    print('Total steps:', steps)    assert regs['AX'] == 45, 'Expected 45 (sum of 0..9)'    print('PASS: sum 0+1+...+9 =', regs['AX'])  if __name__ == '__main__':    main()
目的:用 Python 实现了一个微型 x86 指令模拟器,执行 MOV/ADD/CMP/JMP 组合的汇编程序,验证循环累加结果为 45。
  1. class Simulator:定义模拟器类,封装寄存器、标志位、内存和程序计数器。
  2. self.regs = {'AX': 0, ...}用字典模拟 16 位寄存器组,属性加 _ 后缀避免与方法同名。
  3. def get_value(self, op):解析操作数:寄存器查表、内存解引用、数字直接转换。
  4. self.flags['ZF'] = 1 if result == 0 else 0根据运算结果更新零标志位。
  5. if ins.endswith(':'):预处理阶段扫描标签,记录跳转目标地址。
  6. op == 'MOV'执行数据传送,调用 set_value 写回目标位置。
  7. self.update_flags(a - b)CMP 的核心:做减法但不保存结果,只更新标志位。
  8. elif op == 'JE' and self.flags['ZF'] == 1:条件跳转:检查零标志并决定是否更新 PC。
  9. assert regs['AX'] == 45最终结果断言,确保循环累加逻辑的正确性。

常见误区

误区我总觉得 MOV AX, BX 会把 BX 里的数据“搬走”,就像把文件从 A 文件夹拖到 B 文件夹后 A 文件夹就空了,所以执行完后 BX 应该变成 0。

正解MOV 实际上是复制操作而非移动操作。执行 MOV AX, BX 后,BX 的值保持不变,只是 AX 被更新为 BX 的副本。这就像用复印机复印文件,原件和复印件同时存在。在汇编中,没有一条指令能让寄存器之间的数据“移动”后让源寄存器清零——如果需要清空源寄存器,必须再执行一条 MOV BX, 0 或 XOR BX, BX。

为何常见:“MOV“ 这个助记符本身就带有”移动”的含义,加上高级语言中赋值语句 a = b 让初学者容易混淆:高级语言的赋值其实也是复制语义,但人们通常只关心被赋值变量的值变化,忽略了源变量的状态。

误区ADD 和 CMP 执行完,只要结果是想要的值就没问题了,那些 ZF、CF、SF 标志位似乎无关紧要,反正我读结果时用不到它们。

正解标志位是 CPU 与后续指令通信的“便签”,会在指令执行后保留。一个常见的坑是:CMP AX, BX 之后,又执行了一条 MOV CX, AX,然后才用 JE 判断——此时 ZF 已经被 MOV 指令破坏了(MOV 不更新标志位,但在某些处理器上 MOV 之后标志位保持不变,而若中间插入 ADD 等会改变标志的指令,判断就失效了)。正确的做法是让条件跳转紧跟 CMP,或在跳转前不要穿插任何会修改标志位的指令。

为何常见:初学者往往把每条指令当作孤立操作,忽略指令间的“副作用”传递。汇编程序的 bug 常常就藏在这些被忽略的标志位变化中。

误区CMP 指令比较两个数的大小,直接看结果的正负号就行。`CMP AX, BX` 如果结果是负数那就说明 AX 小于 BX。

正解CMP 本身不区分有符号和无符号,区别在于你使用哪组条件跳转指令。无符号比较用 JA(高于则跳)、JB(低于则跳);有符号比较用 JG(大于则跳)、JL(小于则跳)。关键区别在于:对于字节值 0xFF(无符号 255 / 有符号 -1),如果与 0x01 比较,无符号视角下 255 > 1,但有符号视角下 -1 < 1。错误地混用跳转指令会得到完全相反的结果。

为何常见:高级语言中 int 和 unsigned int 的比较由编译器根据类型自动选择合适的跳转指令,初学者看不到这一层,因此容易忽略底层的符号差异。

误区ADD 指令就是做加法用的,想实现其他运算得用别的指令。如果我要把 AX 乘以 2,那肯定要找 MUL 指令来完成。

正解ADD 指令可以用来实现多种操作。比如 ADD AX, AX 实现 AX 翻倍,等同于左移一位。灵活运用 ADD、SUB(减法)和移位指令,可以在没有乘除法指令的早期 CPU 上高效实现乘法运算。此外,ADD AX, 1 就是最简单的计数器递增,这是循环中最常用的操作之一。

为何常见:按“功能名”理解指令是一种直觉习惯,但计算机底层是通用计算设备——一条指令的用途远不止助记符的字面含义,关键在于理解它能对数据产生什么样的变换。

误区`MOV AX, 1000` 和 `MOV AX, [1000]` 我觉得是一样的,反正最终 AX 里的值都是 1000。

正解两者完全不同!MOV AX, 1000 是立即数寻址,直接把数值 1000 赋给 AX。而 MOV AX, [1000] 是直接寻址,它去内存地址 1000 处取出那个单元里存的值再赋给 AX。如果地址 1000 处存储的值是 42,则第一条指令后 AX = 1000,第二条指令后 AX = 42。方括号是“解引用”的符号,表示按地址访问内存。

为何常见:高级语言中没有这种显式的地址解引用语法(C 语言的 *ptr 除外),初学者对“数值”与“地址处的值”的区分缺乏直觉,需要反复练习才能根深蒂固。

面试题

Q1. 执行 MOV AX, 5 之后 MOV BX, AX,以下描述正确的是?

单选题 · easy

  • A.AX = 5,BX = 0
  • B.AX = 0,BX = 5
  • C.AX = 5,BX = 5← 正确答案
  • D.AX = 0,BX = 0

答案:C。MOV 是复制操作。第二条指令 MOV BX, AX 把 AX 当前的值 5 复制到 BX 中,AX 本身保持不变。选择 A 和 B 都包含“某寄存器变为 0”的误解,D 则是完全没有执行任何有效操作。

Q2. ADD AX, 0FFFEH 在 16 位寄存器中执行后,进位标志 CF 下列说法正确的是?

单选题 · easy

  • A.只要结果大于 65535,CF 就为 1← 正确答案
  • B.CF 只在结果为 0 时才为 1
  • C.CF 与 ADD 指令无关
  • D.CF 表示结果是否为负数

答案:A。进位标志 CF 用于无符号数运算,当加法结果超出目标寄存器能表示的最大值(16 位即 65535)时,CF 被置 1。0xFFFF + 1 的结果是 0x10000,超出 16 位范围,因此 CF = 1。B 描述的是 ZF 的行为,C 和 D 是完全错误的认知。

Q3. 执行 CMP AX, BX 后零标志 ZF = 1,说明什么?

单选题 · mid

  • A.AX > BX
  • B.AX = BX← 正确答案
  • C.AX < BX
  • D.无法确定

答案:B。CMP 执行的是 AX - BX 的减法运算。当结果为 0 时,ZF 置 1。结果为 0 意味着 AX - BX = 0,即 AX = BX。选项 A 对应结果为正且 ZF = 0,选项 C 对应结果为负,D 不正确因为 ZF = 1 已经给出了明确的相等信息。

Q4. 以下哪条指令会导致程序计数器 PC 无条件跳转到指定标签?

单选题 · mid

  • A.CMP label
  • B.JE label
  • C.JMP label← 正确答案
  • D.MOV label

答案:C。JMP 是无条件跳转指令,执行后 PC 直接指向目标标签。CMP 是比较指令,不改变执行流。JE 是条件跳转,仅在 ZF = 1 时跳转。MOV 是数据传送指令,不能用于跳转。注意区分无条件跳转与条件跳转的使用场景。

Q5. 要实现“若 AX == BX 则跳转到 equal”,正确的指令组合是?

单选题 · mid

  • A.ADD AX, BX 后 JE equal
  • B.CMP AX, BX 后 JNE equal
  • C.CMP AX, BX 后 JE equal← 正确答案
  • D.MOV AX, BX 后 JE equal

答案:C。CMP 比较 AX 与 BX,若相等则 ZF = 1,JE 检查 ZF 并在其为 1 时跳转。选项 B 用的是 JNE(不等则跳),逻辑相反。选项 A 用 ADD 会改变 AX 的值且不能用 JE 直接判断相等。D 选项 MOV 会覆盖 AX 的值,完全错误。

Q6. 16 位寄存器中 ADD AX, 7FFFH(AX 初始为 1),下列关于 OF 的说法正确的是?

单选题 · hard

  • A.OF = 0,因为结果为正数
  • B.OF = 1,因为结果超出了有符号数 16 位范围← 正确答案
  • C.OF 与 CF 的值一定相同
  • D.OF 与有符号数无关

答案:B。1 + 0x7FFF = 0x8000,在有符号数视角下是 -32768,而两个正数(1 和 32767)相加不可能得到负数,说明产生了溢出,所以 OF = 1。注意 OF 针对有符号数,CF 针对无符号数,两者不一定相同——这是区分两个标志位的核心考点。

Q7. 关于 MOV 指令,下列说法正确的有哪些?

多选题 · mid

  • A.MOV 是复制操作,源数据不会被清除← 正确答案
  • B.MOV AX, BX 和 MOV BX, AX 效果完全相同
  • C.两个操作数不能同时为内存操作数← 正确答案
  • D.MOV AX, 5 是将数值 5 送入 AX 寄存器← 正确答案

答案:ACD。A 正确,MOV 是复制语义。C 正确,x86 架构规定 MOV 的两个操作数不能同时来自内存,必须通过寄存器中转。D 正确,立即数 5 直接赋给 AX。B 错误,两条指令的方向相反,前者把 BX 的值赋给 AX,后者把 AX 的值赋给 BX,执行后的寄存器状态完全不同。

Q8. 在执行 CMP AX, BX 后,以下哪些说法是正确的?

多选题 · mid

  • A.如果 AX = BX,则 ZF = 1← 正确答案
  • B.如果 AX < BX(无符号视角),则 CF = 1← 正确答案
  • C.AX 和 BX 的值都会被修改
  • D.如果 AX > BX(有符号视角),则 SF = 0 且 ZF = 0← 正确答案

答案:ABD。CMP 执行减法但不保存结果,C 错误,AX 和 BX 本身的值不会被修改。A 正确,相等时结果为 0,ZF = 1。B 正确,无符号视角下 AX < BX 说明发生了借位,CF = 1。D 正确,有符号视角下 AX > BX 意味着结果为正(SF = 0)且不为零(ZF = 0)。

Q9. 下列代码执行后,CX 的值是多少?

单选题 · hard

  • A.0
  • B.1← 正确答案
  • C.2
  • D.无限循环

答案:B。循环过程:第一次循环,CX 变为 1,比较 AX(1)与 CX(1),相等,ZF = 1,JNE 不跳转,循环结束,CX = 1。选项 B 正确。注意这里循环只执行了一次就退出,因为循环条件是在 CX 递增后立即生效的。

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

多选题 · hard

  • A.JMP 是无条件跳转,JE 是条件跳转← 正确答案
  • B.条件跳转通常与 CMP 搭配使用← 正确答案
  • C.JE 跳转的前提是 ZF = 0
  • D.JL 和 JB 都可以用于有符号比较

答案:AB。A 正确,JMP 无条件改变执行流,JE 根据标志位条件跳转。B 正确,CMP 设置标志位后,条件跳转指令根据标志位决定是否跳转,这是标准的比较-分支模式。C 错误,JE 的前提是 ZF = 1。D 错误,JB(低于则跳)用于无符号比较,有符号小于用 JL,混用会导致有符号数比较结果错误。