1. FLAGS 到底是什么它和你一直在用的 AX、BX 有什么本质区别很多同学刚开始学 8086/8088 汇编时总是把 FLAGS 当作一个普普通通的“寄存器”来记——既然是寄存器那大概就是像 AX、BX 一样存个数字只不过存的东西比较特殊。这个理解大方向没错但它会给你后续学习埋下很多麻烦。实际上FLAGS 和通用寄存器最大的区别在于AX、BX、CX、DX 存的是“你给的数”而 FLAGS 存的是“CPU 自己算出来的状态”。你可以把它理解成一台洗衣机的运行指示灯——灯亮了不代表洗衣机里存了什么东西只代表刚才那个洗涤程序跑完之后的状况。CPU 也一样执行完一条指令之后结果是什么样FLAGS 里相关的位就会自动置 1 或清零不需要你手动去改也通常不应该由程序随意写入。8086/8088 的 FLAGS 是 16 位的寄存器但 16 位里并不是每一位都有意义。真正被定义了功能的位集中在中间靠后的位置剩下的位要么保留不用要么根本没有物理实现。下文会逐个拆解。你可以先记住一个判断技巧当你在调试器里看到 FLAGS 的值是一个十六进制数比如 0002H 或者 0046H你无法直接看出 CPU 现在处于什么状态必须一位一位解开看而当你理解了这个数是怎么来的反推标志位的对应关系就会变得非常自然。顺便说一下8088 和 8086 在标志寄存器上完全一致两者的区别主要在外部数据总线宽度8088 是 8 位8086 是 16 位指令集和内部寄存器结构相同。所以你学任何一份资料里面的 FLAGS 内容都能同时用在两种 CPU 上。2. 六个状态标志位真正决定“结果怎么样”的关键FLAGS 里最核心的是六个状态标志位它们由算术逻辑运算指令比如加法、减法、比较、逻辑运算在执行完之后自动更新。这六位是CF进位/借位标志、PF奇偶标志、AF辅助进位标志、ZF零标志、SF符号标志、OF溢出标志。2.1 ZF零标志结果是不是 0ZF 是最好理解的一个位。当一条指令的运算结果为 0 时ZF 置 1结果不为 0 时ZF 清 0。举个例子MOV AX, 5 SUB AX, 5这条 SUB 指令执行完之后AX 里的值是 0于是 ZF 就被置为 1。又比如MOV AX, 0FFFFH ADD AX, 1这里 AX 加 1 之后变成 0000H——结果是 0ZF 同样置 1。注意这个加法同时还会影响 CF 和 OF后面细说。很多初学者容易混淆“结果是 0”和“标志位 ZF 是 0”这两个说法。ZF 这个位置 1 代表“是的结果为 0”ZF 为 0 代表“不是结果不为 0”。这和位本身的数值 1/0 正好相反。你写条件跳转指令时经常会看到 JZJump if Zero结果为 0 时跳转它的跳转条件是 ZF1JNZ 的条件是 ZF0。所以每条指令条件跳转前建议先在草稿上演算一下指令执行后的值再对照标志位的定义。2.2 CF进位/借位标志无符号数运算的“越界”信号CF 在中文教材里通常叫“进位标志”或“借位标志”。它记录的是无符号数运算时最高位有没有产生进位加法或借位减法。判断方法很简单。做加法时把两个数当成无符号数来看如果结果超过了 16 位能表示的范围0~65535最高位就会有进位出去CF1反之 CF0。做减法时如果被减数小于减数最高位就会发生借位CF1反之 CF0。比如MOV AX, 0FFFFH MOV BX, 1 ADD AX, BX这里的 0FFFFH 就是十进制的 65535加上 1 之后是 65536超出了 16 位无符号数的表示范围。ALU 执行加法时最高位产生了进位这个进位就被记录在 CF 里CF1。实际上 AX 里的值会变成 0000H而那个“多出来”的 1 就体现在 CF 上。如果两个无符号数相加没有超出范围比如 1000H 加 2000HCF 就是 0。减法情况类似MOV AX, 1 MOV BX, 2 SUB AX, BX1 减 2 不够减需要向高位借位所以 CF1。很多教材上说“减法时 CF 是借位标志”本质是一回事——无符号数减法中被减数小于减数时必然发生借位CF 被置 1。2.3 OF溢出标志有符号数运算的“越界”信号CF 和 OF 是初学者最容易混淆的一对因为中文里一个叫“进位标志”一个叫“溢出标志”听起来很像。实际上它们服务的对象完全不同CF 服务于无符号数运算OF 服务于有符号数运算。OF 记录的是把操作数当成有符号数补码表示进行运算时结果是否超出了 8 位或 16 位有符号数的表示范围。16 位有符号数能表示的范围是 -32768~32767。如果两个正数相加得到负数或者两个负数相加得到正数说明结果明显“溢出”了OF1。经典例子MOV AX, 7FFFH ; 32767有符号数里的最大正数 MOV BX, 1 ADD AX, BX32767 加 1 等于 32768但 16 位有符号数最多只能表示到 32767结果变成 8000H在补码表示里这是 -32768。一个正数加上正数得到负数OF1。再看一个 CF 和 OF 同时为 1 的例子MOV AX, 0FFFFH MOV BX, 1 ADD AX, BX刚才我们讨论过这行指令执行之后 CF1无符号数 655351 超出范围。但如果我们把这行代码里的操作数看作有符号数0FFFFH 是 -1-110结果正确没有溢出OF0。所以上例中ADD 0FFFFH, 1 执行后 CF1、OF0。真实的 CPU 确实这么做——同一时刻FLAGS 里可以出现 CF1 而 OF0 的组合。这就是为什么 8086 设计了两组不同的条件跳转指令无符号数的跳转JA、JB 等看 CF有符号数的跳转JG、JL 等看 OF 和 SF。一个数到底是无符号还是有符号取决于你怎么解释它CPU 不关心它只负责把两个标志都算出来。我一直建议同学们亲手在 DEBUG 里做一下上面这个 0FFFFH1 实验亲眼看到 AX0000H、CF1、OF0 三个现象同时出现——比背十遍书都有用。2.4 SF符号标志结果的正负性SF 标志记录的是运算结果的最高位第 15 位是 0 还是 1。最高位是 0说明结果为正数补码最高位为 0SF0最高位是 1说明结果为负数补码最高位为 1SF1。严格来说SF 是在结果按有符号数解释时的符号位。它和 OF 经常被放在一起使用因为判断“两个有符号数比较大小时到底谁大谁小”不仅看符号还要看有没有溢出。比如MOV AX, 8000H ADD AX, 0FFFFH8000H 的二进制最高位是 1所以执行完后 SF1。如果把它按有符号数看这个结果是负数。但若是把这两个数都当作无符号数SF 就没什么参考价值它只反映了结果最高位是 1 而已。SF 还有一个容易混淆的地方它只反映结果的最高位不反映整个结果的“数学正负性”。举个例子无符号数 0FFFFH 的最高位是 1SF1但你如果用无符号数的眼光看65535 是正数。所以严格说法应该是“SF 表示结果的最高有效位”而“结果的符号”取决于你采用的数制。教材上为了方便通常说 SF1 表示负数这在有符号数语境下才能成立。2.5 PF奇偶标志低 8 位里 1 的个数是奇还是偶PF 标志统计的是运算结果低 8 位中“1”的个数。如果 1 的个数是偶数PF1如果是奇数PF0。注意两个细节第一只看低 8 位不是整个 16 位的结果第二它关心的是二进制 1 出现的次数不是结果本身是奇数还是偶数。虽然这俩经常对应低 8 位中 1 的个数为偶数的结果往往也是偶数但并没有因果关系你不要用“结果是不是偶数”来推断 PF。举个例子MOV AX, 0FFH AND AX, 0FFH0FFH 低 8 位是 11111111B其中 1 的个数是 8是偶数所以 PF1。再比如MOV AX, 3 AND AX, 33 的二进制是 0011B1 的个数是 2 个是偶数PF1。PF 标志在实际编程里主要用于串行通信和某些校验算法的实现。比如你从外部设备读入一个字节可以通过 PF 快速检查这个字节的奇偶性是否和约定一致。另外在 DEBUG 调试时看 PF也可以反向验证你算出来的结果二进制形式有没有写错。2.6 AF辅助进位标志BCD 码运算的小助手AFAuxiliary Carry Flag在六个状态标志里最容易被忽略但它在 BCD 码运算中不可替代。AF 记录的是运算结果低 4 位即 D3 位是否产生了进位加法或借位减法。简单来说如果低 4 位做加法时向高 4 位进位了或者做减法时从高 4 位借位了AF1否则 AF0。例子MOV AL, 09H ADD AL, 01H09H 01H 0AH低 4 位从 1001 变成 1010实际没有向高 4 位进位所以 AF0。再看这个MOV AL, 19H ADD AL, 01H19H 01H 1AH。表面上看起来没有进位但低 4 位 91 已经超过了 15不对9110十进制的 10 在十六进制里是 0AH依然在低 4 位范围内。不过等等真实 ALU 是按二进制整体加的19H 01H 1AH低 4 位是 A这个过程中低 4 位 91 10十进制 0AH依然没有超过 15哦不对0AH 就是十进制的 10没有超过 15所以没有进位AF0。真正 AF1 的例子是 0AH 06H 10H低 4 位 AF不对0AH 06H 10HA616十进制超过了 15产生进位所以 AF1。再比如MOV AL, 0DH ADD AL, 05H0DH 05H 12H低 4 位 D5 135 18十进制超过 15产生进位AF1。AF 在什么时候真正有用呢在十进制 BCD 码运算时。假设你要计算两个十进制数 39 28把每位十进制数用 BCD 码表示39 是 39H28 是 28H。MOV AL, 39H ADD AL, 28HAL 39H 28H 61H这是一个非法 BCD 码因为 39H 的低 4 位 9 加 8 得到 17低 4 位变成了 1同时向高 4 位进位。但 CPU 只会置 AF1因为它发现低 4 位产生了进位。接下来你执行 DAA十进制加法调整指令时就会根据 AF1 把结果调整成合法的 BCD 码 67H即十进制的 67。整个过程你完全不需要自己去判断低 4 位是否溢出AF 已经替你记好了。所以说AF 是 BCD 运算和 BCD 调整指令DAA、DAS、AAA、AAS之间沟通的桥梁。如果没有 AF你维护 BCD 运算就得手动从结果里拆位分析麻烦得多。3. 三个控制标志位CPU 行为的开关除了上述六个状态标志FLAGS 里还有三个控制标志位IF中断允许标志、DF方向标志、TF陷阱标志。它们不由算术运算结果决定而是由程序员用指令如 STI、CLI、STD、CLD显式设置用来控制 CPU 的某些工作模式。3.1 IF中断开关IFInterrupt Flag控制 CPU 是否响应可屏蔽中断INTR 引脚上的请求。IF1 时CPU 允许响应可屏蔽中断IF0 时CPU 忽略可屏蔽中断请求。你可以在程序里用 STISet Interrupt Flag指令把 IF 置 1用 CLIClear Interrupt Flag指令把 IF 清 0。典型场景是在操作系统的临界区保护中进入临界区前 CLID 关中断避免在执行关键操作时被打断退出临界区后 STI 重新开中断。在 DOS 时代的实模式编程里如果你要处理串口、定时器等外设中断通常必须先 STI 确保中断能进来而在修改中断向量表这类关键操作时必须先 CLI 防止中途被中断。IF 对不可屏蔽中断NMI不起作用也就是说NMI 来了IF 是 1 是 0 都得响应。这一点教材里容易忽略面试也常拿来做区分点。3.2 DF串操作方向开关DFDirection Flag控制串操作指令中指针SI、DI的移动方向。DF0 时串操作指令执行后 SI、DI 自动递增方向从低地址往高地址移DF1 时SI、DI 自动递减方向从高地址往低地址移。使用 STDSet Direction Flag把 DF 置 1使用 CLDClear Direction Flag把 DF 清 0。一个非常常见的坑是你在写串操作程序比如用 MOVSB 复制字符串之前忘记 CLD而在此之前有些代码把 DF 置 1 了结果字符串复制方向从头到尾反着来轻则输出结果错位重则覆盖错误内存区域导致程序崩溃。很多教材里的程序默认 DF0但你没有权利假设程序一开始 DF 一定是 0——之前的代码可能修改过它。所以每次使用串操作指令前第一件事就是根据你的需求 CLD 或 STD给出明确的方向。顺带一提串操作指令大多还配合 REP、REPZ 等前缀使用。DEC 指令在循环里修改 CX然后 REP MOVSB 重复执行到 CX0。这类代码在 DEBUG 单步执行时尤其容易出错因为 DF 的初始状态不好肉眼判断。3.3 TF单步执行开关TFTrap Flag是三个控制标志位里最特殊的一个它和调试紧密相关。TF1 时CPU 每执行完一条指令就会产生一次类型为 1 的中断单步中断。你可以把这个中断理解为“每条指令执行完都停下来让你有机会检查现场”——这就是所有单步调试工具的基础。DEBUG 里常用的 T 命令单步跟踪本质上就是先置 TF1再让程序运行每执行一条指令后 CPU 自动进入中断服务程序调试器借此显示寄存器状态并等待下一个命令。TF0 时CPU 正常工作不会产生单步中断。由于 TF 没有直接的置位指令你没法用一条普通的 MOV 指令去改 FLAGS通常是通过 PUSHF 把 FLAGS 压栈、修改对应位、再用 POPF 弹回的方式来实现。PUSHF MOV BP, SP OR WORD PTR [BP], 0100H ; 把 TF 位置 1 POPF这段代码的思想是先把 FLAGS 存到栈上然后修改栈里保存的 FLAGS 副本中 TF 对应的位第 8 位最后用 POPF 把修改后的值写回 FLAGS。注意 TF 是第 8 位对应 0100H。写完后程序每执行一条指令就产生一次单步中断调试器就能一条条地显示寄存器状态。这种通过栈间接修改 FLAGS的技巧并不只是理论实际在调试器、反调试代码中经常用到。你可以自己试试在 DEBUG 里输入R F命令直接修改标志位效果更直观但如果想在汇编程序里动态修改某个标志位PUSHF/POPF 是唯一方法。4. 指令对 FLAGS 的影响哪些指令动标志位哪些纹丝不动这部分可以说是最“陷阱密集”的考点因为教材里的说法往往很简略真正做题时会发现一堆特例。4.1 算术逻辑指令全面更新状态标志ADD、SUB、ADC、SBB、INC、DEC、NEG、CMP、AND、OR、XOR、TEST 这组指令都会影响六个状态标志位。但每个指令对各个标志的影响方式并不完全相同。ADD/SUB/ADC/SBB/CMP正常计算结果CF、PF、AF、ZF、SF、OF 全部按结果更新。INC/DEC不影响 CF。为什么设计者认为 INC/DEC 本来就是单操作数加/减 1如果也去更新 CF那 CF 很容易被这类操作干扰。因此你写循环用 INC 或 DEC 控制计数时循环条件基于 ZF 判断完全没问题但如果你依赖 CF 判断进位就要小心——INC 之后 CF 还保留着之前指令设置的旧值。NEG结果若为 0CF0否则 CF1因为 NEG 本质是 0 - 操作数只要结果不是 0就发生了借位。NEG 对 OF、SF、ZF、AF、PF 也都有更新。AND/OR/XOR/TEST强制清 CF 和 OF然后更新 ZF、SF、PF、AF。为什么 AND 要清 CF因为逻辑运算没有进位的概念CF 和 OF 这些与“进位/溢出”相关的位在没有意义的情况下应归零。这个“某些指令会清 CF”的技巧在编程里很有用。比如你想从一个逻辑运算后再比较两个数又要保证 CF 记录的是之前某次减法产生的借位那你就不能用 AND 或 OR因为 AND 会把 CF 清掉。反过来如果你不关心 CF只是想在逻辑运算之后保留一个干净的状态AND 清 CF 反而省事。4.2 移位指令CF 是“被移出去的那一位”8086 的移位指令SHL、SHR、SAL、SAR、ROL、ROR、RCL、RCR对标志位的影响比普通算术指令更特殊。对于 SHL/SHR 这类非循环移位CF 记录的是最后移出最高位或最低位的那一位的值。比如MOV AL, 80H SHL AL, 180H 左移一位后 AL00H最高位移出去的那一位是 1CF1。循环移位 ROL、ROR 也会置 CF更新的方式和循环出的那一位有关。而 RCL、RCR 这类带 CF 的循环移位还会把原来的 CF 值移入寄存器从而参与整个移位过程。循环移位里有个常见考点RCL/RCR 是 9 位循环把 CF 当作第 9 位参与循环而 ROL/ROR 是 8 位循环。这种细微差别在密码学算法和某些高精度运算中经常用到。要注意的是8086 的移位指令中移位次数为 1 时OF 才会被有意义地更新移位次数大于 1 时OF 的值不定实质上是未定义。考试中如果问移位多次后 OF 是多少直接写“不确定”才能拿分。4.3 数据传送指令一般不碰标志位MOV、PUSH、POP、LEA、XCHG 这些指令执行之后所有状态标志位保持不变。这一点显得很“佛系”也正是它容易踩坑的地方。比如你写MOV AX, 1234H SUB AX, 1234H执行完 SUB 后 ZF1然后你又执行了一条 MOV BX, 0此时 ZF 仍然是 1不会因为 MOV 而清除。这个特性在写循环时非常重要。很多人以为“只要指令运行过标志位就会被更新”但实际上数据传送指令不会。如果你依赖 MOV 来“清标志位”那是做不到的标志位只能通过算术/逻辑运算或者显式的标志操作指令如 CLC、STC来修改。4.4 标志操作指令直接动手改标志位CPU 还提供一组专门修改个别标志位的指令CLCCF 清零STCCF 置 1CMCCF 取反CLD/STDDF 清零/置 1CLI/STIIF 清零/置 1LAHF/SAHF把 FLAGS 的低 8 位送入 AH或把 AH 送入 FLAGS 低 8 位PUSHF/POPF把整个 FLAGS 压栈或弹栈CLC/STC/CMC 看起来很简单但在多字节运算里不可或缺。比如你要实现一个 32 位加法8086 一次只能加 16 位你要分两步走先把低 16 位相加然后把 CF 带到高 16 位一起加。这时你就需要先用 CLC 确保初始 CF0再用 ADC带进位加法把低 16 位产生的进位传到高 16 位。; 实现 DX:AX DX:AX BX:CX必须放在 16 位模式下考虑 CLC ADD AX, CX ADC DX, BX第一次 ADD 之前先用 CLC 把 CF 清 0确保低 16 位相加没有遗留进位然后 ADC 会把 ADD 产生的进位加到高 16 位里。初学者经常忘记最前面的 CLC导致如果之前某条指令留下 CF1低 16 位就会被错误进位程序结果差 1。5. 条件跳转指令是怎样读取 FLAGS 的从 JZ 到 JC 到 JPE标志位本身只是状态真正让它们发挥作用的是条件跳转指令。8086 提供了非常多的条件跳转指令每条指令检测一个或几个标志位的组合决定是否跳转。这部分是考试和实际编程的交叉点也是许多人包括我当年容易绕晕的地方。5.1 单标志跳转最简单的条件跳转只检查一个标志位指令跳转条件说明JZ/JEZF1结果为零跳转JNZ/JNEZF0结果不为零跳转JSSF1结果为负跳转JNSSF0结果为正跳转JOOF1有溢出跳转JNOOF0无溢出跳转JCCF1有进位/借位跳转JNCCF0无进位/借位跳转JP/JPEPF1偶校验跳转JNP/JPOPF0奇校验跳转这里面最容易混的是 JZ 和 JE。其实就是同一条指令的两个名字结果为零时两个操作数相等因为减法结果为 0所以 JZ 就是 JE。你在反汇编文件里有时会看到 JE有时会看到 JZ取决于编译器或汇编器的输出偏好。5.2 无符号数与有符号数的大小比较跳转这是整个 FLAGS 使用中最容易出错的环节。CMP 指令执行完后需要根据比较结果跳转。关键问题是怎么判断“谁大谁小”如果是无符号数比较那么“大于”的标准很简单A B 当且仅当 A - B 没有发生借位CF0且不为 0ZF0。因此JA/JNBECF0 且 ZF0无符号数大于JAE/JNBCF0无符号数大于等于JB/JNAECF1无符号数小于JBE/JNACF1 或 ZF1无符号数小于等于如果是有符号数比较情形复杂得多。判断 A B 要看符号标志 SF 和溢出标志 OF只有当不存在溢出且差为正数或者存在溢出且差为负数这种情况实际是结果被“翻”过去了真实大小仍然可辨才能判断大于。于是有了JG/JNLEZF0 且 SFOF有符号数大于JGE/JNLSFOF有符号数大于等于JL/JNGESF≠OF有符号数小于JLE/JNGZF1 或 SF≠OF有符号数小于等于这里面的 SFOF 这个条件可以这样理解如果减法没有溢出OF0结果的正负就是真实的正负SF0 表示 A≥BSF1 表示 AB如果发生溢出OF1结果符号被翻转需要反过来解释。所以 SF 和 OF 相等时说明没有因溢出造成困惑可以放心按 SF 判断大小SF 和 OF 不等时说明发生了溢出按 SF 反着来。把这两种情况统一成“SFOF 则左操作数 ≥ 右操作数”就得到了 JGE/JL 的条件。很多人考试时背这些条件背得头大我建议你自己手动推一遍。假设 AX7FFFH、BX8000H用 CMP AX, BX 比较7FFFH - 8000H 0FFFFH有符号数是 -1。这个减法有没有溢出7FFFH 327678000H -3276832767 - (-32768) 65535 超出了 16 位有符号范围OF1结果为负SF1。SF≠OF按 JL 跳转条件来说 AX BX从数学上也确实成立32767 -32768等一下这里要小心。这里我必须停一下理清逻辑。CMP AX, BX 是 AX - BX结果 0FFFFH。按有符号数解释结果是 -1实际上 32767 - (-32768) 65535但 16 位有符号范围装不下 65535结果截断为 -1。这个结果是负数SF1。产生溢出OF1。SFOF所以 JGE 条件成立即 AX ≥ BX。数学上 32767 ≥ -32768正确。所以前一段我说“SF≠OF 所以 AXBX”是错的正确的应该是 SFOF 时左操作数大于等于右操作数这也正是 JGE 的定义。你可以自己再用 AX8000H、BX7FFFH 做一次8000H - 7FFFH 0001H结果为正SF0数学上 -32768 - 32767 -65535超出范围OF1。SF≠OFJLE 条件成立AX ≤ BX数学上 -32768 32767正确。这样推过一轮你对 SFOF 的理解就牢了。5.3 常见误区CMP 和 SUB 的结果去哪里了CMP 指令本质上是做一次减法它会更新所有状态标志位但结果不写回任何寄存器。所以如果你想知道“CMP 到底减没减出数来”直接看标志位即可。它和 SUB 的唯一区别就是“结果是否回写”。这带来了一个很常见的考试题执行 CMP AX, BX 之后AX 和 BX 的值变了吗答案是完全没变变的只有 FLAGS。实际项目中CMP 后面几乎总是跟着一条条件跳转比如CMP AX, 10 JAE label_large这组代码的意思就是如果 AX无符号数视角大于等于 10就跳到 label_large 去执行。写这段代码时有一个细节CMP 后不要做任何可能改变标志位的操作再跳转。比如你如果在 CMP 和 JAE 之间插入一条 MOV、INC、ADD 之类的指令标志位可能就被覆盖了跳转逻辑全错。这是汇编程序调试中非常常见的“隐蔽 bug”。6. 实机调试与经验用 DEBUG 观察 FLAGS、手算标志位以及多字节运算中的 CF 传递如果只看教科书上的定义你多半会觉得自己已经懂了但真到写代码调试时才会发现各种问题。这里分享一些我在 Windows 的 DEBUG 工具和 DOS 模拟器里摸爬滚打得到的经验。6.1 在 DEBUG 里直接观察 FLAGSDEBUG 里的 R 命令可以显示所有寄存器的当前值包括 FLAGS。比如你把以下指令输入 DEBUGMOV AX, 7FFFH MOV BX, 1 ADD AX, BX执行完这三条指令后你再输入 RDEBUG 会给你显示类似AX8000 BX0001 CX0000 DX0000 DSxxxx ESxxxx SSxxxx SPxxxx OV DN EI NG NZ AC PE CY注意看末尾那组缩写它们就是 FLAGS 状态的十六进制表示对应的字符串形式OV溢出 OF1 或 NV无溢出 OF0DN方向 DF1 或 UP方向 DF0EI中断 IF1 或 DI中断 IF0NG符号 SF1 或 PL符号 SF0NZ零 ZF0 或 ZR零 ZF1AC辅助进位 AF1 或 NA辅助进位 AF0PE奇偶 PF1 或 PO奇偶 PF0CY进位 CF1 或 NC进位 CF0在这个例子里执行 ADD AX, BX 后 AX8000H所以 SF1NG、OF1OVZF0NZ因为结果不是 0PF0PO因为 8000H 低 8 位是 00H1 的个数为 0是偶数这里要注意8000H 低 8 位是 00H二进制 00000000 中 1 的个数是 00 是偶数所以 PF 应该是 1显示 PE。我刚才随口写 PO 是错的这里需要仔细。实际上 8000H 低 8 位是 00H1 的个数为 0偶数PF1DEBUG 应显示 PE。为了验证你可以手动计算 8000H 的低 8 位8000H 1000 0000 0000 0000B低 8 位是 0000 0000B1 的个数 0PF1。所以 R 命令显示里应该有 PE。CF 呢7FFFH 0001H 8000H无符号数 32767 1 32768未超过 65535CF0显示 NC。如果你想在 DEBUG 里手动改变标志位状态、试验不同跳转条件可以用 R F 命令。DEBUG 会列出当前标志位的状态符并允许你输入新的标志位状态符号逐个切换。这是调试跳转逻辑非常好用的手段。6.2 手算标志位组合一个必须掌握的技能笔试和面试题经常要求你给出某条指令执行后 CF、ZF、SF、OF、PF、AF 的值你要学会手算。我这里给你一套标准流程第一明确操作数类型。如果指令里出现了立即数那默认按指令字长来。比如 ADD AX, 1234H操作数是 16 位全程按 16 位处理。如果 ADD AL, 12H操作数是 8 位只看 8 位结果。第二做二进制运算。把操作数展开成二进制逐位相加或相减。第三根据判断条件逐项看标志位ZF结果 16 位或 8 位全为 0 时ZF1CF加法看最高位有没有进位减法看最高位有没有借位OF把操作数看成补码根据符号判断结果有没有溢出两个正数相加得负、两个负数相加得正都算溢出SF结果最高位是 1 时SF1PF结果低 8 位中 1 的个数为偶数时PF1AF结果低 4 位向高 4 位进位/借位时AF1拿一个具体例子练一遍MOV AL, 83H ADD AL, 93H83H 1000 0011B93H 1001 0011B相加1000 0011 1001 0011 1 0001 0110B结果取低 8 位为 16H。ZF16H ≠ 0ZF0CF最高位相加产生了进位11 进位到第 9 位CF1OF83H 按有符号数是 -12593H 按有符号数是 -109−125 (−109) −234超出了 8 位有符号范围-128~127OF1SF16H 的最高位是 0SF0PF16H 低 8 位是 0001 0110B1 的个数为 3是奇数PF0AF低 4 位 0011 0011 0110没有进位336 没超过 15AF0如果你全部算对就能和 DEBUG 显示对应上。这种练习多算几道标志位就不再是死概念了。6.3 多字节运算与 CF 的传递20 世纪 80 年代到 90 年代初8086/8088 处理器是 16 位那时候做大整数运算比如 32 位、64 位加法必须手动把进位一级一级传下去。现在虽然有 32 位、64 位处理器了但这个思路在嵌入式开发和理解指令集时依然极有价值。32 位加法例子之前写过了这里给一个 16 位模式下计算两个 32 位数的减法; 设被减数 DX:AX减数 BX:CX结果在 DX:AX ; 先算低 16 位AX AX - CX SUB AX, CX ; 再算高 16 位DX DX - BX同时考虑低 16 位借位 SBB DX, BXSBB带借位减法会从结果里再减掉 CF 的值。SUB 低 16 位时如果发生借位CF1SBB 在执行 DX DX - BX - CF 时就能把借位一并处理。这套思路与 ADC 对应是高精度运算的基础。我在实际调这种代码时有一次印象深刻低 16 位的 SUB 执行完CF1但我在它和高 16 位的 SBB 之间插入了一条 PUSH 指令保存现场。结果 SBB 执行后DX 的结果凭空少了 1——因为 PUSH 虽然不改变标志位但它会把栈指针 SP 改掉如果你用来保存数据的寄存器恰好是 SP 相关的现象就会很奇怪。最后我排查了半天发现根本不是 SBB 的问题而是不该在标志位敏感的指令序列之间插入可能改变执行流或数据的操作。这个小教训我一直记到现在标志位是非常脆弱的上下文它不像寄存器那样能随意保存必须在使用前确保没有被额外改动。6.4 关于 8088/8086 与后续 CPU 的一个提示8086/8088 的标志寄存器在后续 x86 体系里被扩展成了 32 位EFLAGS和 64 位RFLAGS新增了 VM 标志、RF 标志、AC 标志等但低 16 位的定义基本保持了兼容。这意味着你如果迁移到现代 x86 汇编原来基于 FLAGS 的编程经验依然有效只是要留意额外的新标志位对某些指令的行为有影响比如地址对齐检查 AC 等。对初学者来说先把 8086/8088 这 16 位吃透后面的扩展就顺理成章了。7. 把 FLAGS 用起来的两个实战案例概念讲了一大堆最后看两个可运行的程序片段。我建议你在 DOSBox 或其他 8086 模拟环境里实际操作一下比空想有效得多。7.1 用状态标志判断两个数的大小关系下面这段程序从键盘读入两个操作数比较大小并输出判断结果。这里只贴核心判断部分因为输入输出子程序在不同环境下写法差异较大。; 假设 AL 存放第一个操作数BL 存放第二个操作数 CMP AL, BL ; 比较 AL 与 BL计算 AL - BL JA above_label ; 无符号大于跳转 JB below_label ; 无符号小于跳转 ; 若走到这里说明 AL BL如果改成有符号比较只需把 JA/JB 换成 JG/JLCMP AL, BL JG above_label ; 有符号大于 JL below_label ; 有符号小于 ; AL BL这个程序的灵感在于 CMP 和条件跳转的组合能把比较结果“延迟”到分支处使用。实际写的时候记住我之前说的CMP 之后别乱加指令。7.2 用进位链实现 32 位加法这个例子虽短但对理解 CF 传递非常关键; 计算 result A B其中 A 在 DX:AXB 在 BX:CX ; 结果存回 DX:AX CLC ; 清 CF保证低字加法不受之前遗留进位影响 ADD AX, CX ; 低字相加可能产生进位 ADC DX, BX ; 高字相加同时加上低字的进位如果你希望写出带进位的多字节加法循环还可以用 LOOP 配合 ADC 逐字节处理; 假设 SI 指向加数1DI 指向加数2CX 为字节个数 ; 结果写回 SI 指向的区域 CLC LOOP_ADD: MOV AL, [SI] ADC AL, [DI] MOV [SI], AL INC SI INC DI LOOP LOOP_ADD这里每次 ADC 都会把上一次加法产生的 CF 带进来实现真正的逐字节进位链。注意这个循环体里我用了 INC 而不是 ADD SI, 1因为 INC 不会改变 CF——这在多字节加法中是刻意为之的选择也是 INC/DEC 不影响 CF 这个特性的实际价值。如果你把上面的 INC 误写成 ADD SI, 1虽然功能一样但 ADD 会更新 CF从而破坏进位链导致后续 ADC 计算错误。这种 bug 在课本里很少提到但在实际编码中非常致命。我当年就在一个复制程序里犯过同样的错误调试了很久才发现是 INC/ADD 对 CF 的不同作用导致的。7.3 利用 TF 实现单步跟踪如果你想在真实机器上体验单步跟踪可以编写如下程序; 将 TF 置 1 PUSHF MOV BP, SP OR WORD PTR [BP], 0100H POPF ; 从这里开始每执行一条指令就会触发一次单步中断不过请注意在真实 DOS 环境下单步中断处理程序INT 1是系统提供的而你在 Windows 或 Linux 下的模拟器里运行这段代码效果可能不明显但在 DOSBox 的 DEBUG 里直接用 T 命令你就能看到每执行一条指令后的全部寄存器状态。观察标志位变化与指令执行之间的对应关系是我认为学习 FLAGS 最有用的方式之一。我在实际教学生时经常让他们把一段简单循环放进 DEBUG 里用 T 命令单步执行然后对照自己提前写好的“每步标志位预测表”。几乎每个人都会在某一步发现自己的预测和实际不符而排查这个不一致的过程恰恰是最佳学习过程。比如你会发现 CMP 之后标志位的更新方式和 ADD 完全一样但操作数本身没有变化你也会发现 MOVSB 串操作对 DF 的依赖有多么直接等等。8. 我踩过的几个坑希望你能避开讲完技术和实操最后分享几个我在学习和教学过程中反复看到的典型误区。第一个误区是把 OF 和 CF 混为一谈。我见过太多人写比较跳转时无符号比较用了 JG/JL结果遇到大数就出错。其实只要记住一句话CF 管无符号OF 管有符号。你只需在动手写代码前先问自己“这两个数在我的程序语义里是有符号还是无符号”再选对应的跳转指令。第二个误区是不理解 CMP 不保存减法结果。很多人以为 CMP 会改变某个寄存器的值于是写完 CMP 后直接用那个寄存器做后续运算结果全错。CMP 只是派出所调查员看看情况、记下状态但不把嫌疑犯带走。真正要保存结果的场景请用 SUB。第三个误区是忽略 INC/DEC 不动 CF 的特性。在循环里用 INC 控制计数器时这个特性是福音如果你误以为 INC 会置 CF就可能在依赖 CF 的代码中写出完全错误的多字节运算。反过来如果你在用 INC/DEC 的循环里又依赖 CF 来判断一个比较结果那你的 CF 可能还是最初那个值——这个“初始值”也许来自程序开头的某条指令因此调试时更容易懵。第四个误区是轻视 PF 和 AF。考试里 PF 和 AF 分值不高、实际用的人也不多但这个轻视会在你遇到 BCD 运算、串行通信校验、CRC 计算等场景时付出代价。尤其是 AF它和 DAA/DAS 指令配合是 BCD 运算的核心机制绕过它你将寸步难行。最后我自己始终的建议是不要孤立地背标志位定义要把 FLAGS 看成一条“指令执行之后的健康报告”。看到一条指令先问自己“这条指令会动哪些标志位”再问“这些标志位的变化会怎么影响后面的条件跳转”。这样你的汇编代码读起来会顺畅得多调试时也更有方向感。