
学微机原理的时候FLAGS 寄存器是个躲不掉的老大难。很多同学能把 MOV、ADD、SUB 背得滚瓜烂熟但一碰到条件转移指令 JZ、JG 就开始懵问题往往就出在这个状态标志寄存器上。它就像一张“成绩单”CPU 每执行一次运算就把结果的性质用二进制位记下来你写的每一行 if、每一个 while 循环在汇编层面本质上都是在读这张成绩单。这篇文章专门把它拆开讲透从每个位的含义、硬件上为什么这么设计到具体指令怎么改它、模拟器里怎么调试尽量让零基础也能真正掌握。我见过太多人在“标志位读取和判断”这个环节翻车写了个 CMP 之后该跳转的不跳转调了半天以为是指令写错最后才发现是理解错了 OF 和 CF 的区别。所以这次不打算只罗列定义而是把“为什么这么设计”和“实际执行时是什么状态”一起讲清楚你学完不仅记得住还能举一反三自己去推别的指令的行为。1. 它到底解决什么问题——FLAGS 寄存器的整体设计思路CPU 内部干活的核心是 ALU算术逻辑单元它负责加法、减法、与、或、移位这些基础运算。但 ALU 运算完CPU 怎么知道结果是不是 0是不是负数有没有产生进位这些信息如果不保存下来控制器后续做判断就得把结果重新算一遍效率极低也没法实现条件分支。于是 8086/8088 在 CPU 内部专门设计了一个 16 位的 FLAGS 寄存器用来记录上次运算结果的性质和 CPU 当前的工作模式。它不是普通的内存单元而是一系列独立的触发器的集合每个触发器对应一个特定的含义硬件上可以直接被控制单元读取速度极快。这个 16 位寄存器里实际用到的是其中的 9 位剩下的位被保留Reserved在编程时不要依赖它们的值。这 9 位可以分成两大类状态标志位Status Flags和控制标志位Control Flags。状态标志位反映运算结果的特征比如结果是否为 0、是否有进位、是否溢出控制标志位则用来控制 CPU 的运行方式比如是否响应中断、串操作的方向、是否单步执行。位名称类型位名称类型15-12保留—7SF符号标志状态11OF溢出标志状态6ZF零标志状态10DF方向标志控制5AF辅助进位标志状态9IF中断标志控制4PF奇偶标志状态8TF陷阱标志控制3CF进位标志状态| 0-2 | 保留 | — |我当年学的时候总是把状态标志和控制标志混在一起背结果做存储器分段、中断向量表这些题时老出错。后来才想明白状态标志是“结果说了算”每次运算可能变程序无法直接置位或清零控制标志是“程序说了算”需要用专门指令如 STC、CLI、CLD去改。把这两类分开记逻辑一下就清晰了。2. 九个标志位逐个拆解状态标志与条件判断这部分是全文的难点也是考试的重点。我会按位从低到高拆把每个标志的“触发条件”“为什么设计它”“常见的误区”一次说清。2.1 CF进位标志——无符号运算的边界感CF 全称 Carry Flag位 3是最简单也最容易让人掉坑的一个。它的定义是当无符号数运算结果超出它所能表示的范围时CF 被置 1。直白点说它记录的是“最高位有没有进位或借位”这件事。举个例子字节操作里0xFF 0x01 0x00但真实结果是 0x100已经超过一个字节能表示的 0x00~0xFF 范围了这时 CF1。同理做减法时如果被减数小于减数最高位会产生借位CF 也会置 1。为什么需要它因为无符号数没法直接表示负数进位和借位是判断“结果是否有效”的唯一方式。比如做多字节加法要用 ADC 指令把上一字节的进位链到下一字节CF 就是这条“进位链”的传递通道。此外循环移位指令也把移出的位放到 CF 里所以 CF 还承担了“临时存储位”的作用。2.2 PF奇偶标志——只看低 8 位的“粗心鬼”PF 全称 Parity Flag位 4意思是结果实际上是结果的低 8 位中“1”的个数是偶数时置 1否则清 0。很多初学者会困惑为什么有时候算出的结果明明是奇数个 1PF 却显示为 0答案是这个标志只管低 8 位。这个设计是有历史原因的。早期通信中大量使用奇偶校验来检查数据传输错误8086 保留了这一功能用于异步通信场景。它只检查低 8 位是因为串口通信一次大多只传一个字节没必要检查整个 16 位结果。使用时要特别小心如果你做的是 16 位运算想判断整字中 1 的个数不能直接用 JP/JNP 判断 PF你得先把高字节和低字节进行异或之后再检查。这是很多汇编老手都会犯的低级错误。2.3 AF辅助进位标志——BCD 运算的专属信号AF 全称 Auxiliary carry Flag位 5记录低 4 位向高 4 位的进位或借位。它只有在 BCD二进制编码的十进制数运算时才有实际意义专门配合 DAA十进制加法调整和 DAS十进制减法调整指令使用。举个例子ADD AL, BL 若产生的结果中低四位向高四位进位即 bit3 向 bit4 进 1则 AF1。为什么这么设计因为 BCD 数每个字节表示两位十进制数低位和高位各占 4 位运算结果超过 9 时就需要调整规则。CPU 通过 AF 告诉你“低位发生了进位”DAA 才知道要不要加上 6 来修正。编程时 AF 是最容易被忽略的一个标志因为普通条件转移指令没有直接检查 AF 的它只在 DAA/DAS 内部被硬件使用。写十进制调整代码时你要记得先做加法再立即调 DAA中间千万不能插入别的指令否则 AF 被后续操作覆盖调整结果就错了。2.4 ZF零标志——结果是不是全零ZF 全称 Zero Flag位 6当运算结果的所有位都是 0 时置 1否则为 0。它是最直观的一个标志也是条件转移中用得最多的标志之一。注意一个细节ZF 判断的是整个结果的宽度。如果是字节运算结果 8 位全 0ZF1如果是字运算结果 16 位全 0ZF1。如果有人把 0x00 和 0xFF 进行 XOR结果是 0xFFZF 肯定是 0但如果是 MOV AX, 0 然后执行 CMP AX, BX当 AX 和 BX 相等时相减结果为 0ZF1。所以 CMP 指令配合 JE/JZ 能实现“判断两数是否相等”本质就是检查相减结果是否为零。2.5 SF符号标志——最高位就是原罪SF 全称 Sign Flag位 7它的值就是运算结果的最高位即符号位。结果最高位是 1 则 SF1表示按有符号数解释是个负数最高位是 0 则 SF0表示非负数。这里要强调的是CPU 本身不知道你这个数是有符号还是无符号它只是机械地把最高位复制到 SF。同一段二进制数据你既可以把它看作无符号数也可以看作有符号数SF 只是帮你看出“最高位长什么样”。例如字节操作时 AL0x80SF1如果按无符号看是 128按有符号看是 -128全看你怎么解释。正因如此条件转移指令才被分成了“无符号跳转”和“有符号跳转”两套体系无符号看 CF有符号看 SF 和 OF 的组合。这一点一定要牢牢记住否则程序逻辑会完全颠倒。2.6 OF溢出标志——比 CF 难懂得多的“坑王”OF 全称 Overflow Flag位 11它是有符号数运算中标志结果是否溢出的核心位。很多人问的第一个问题是OF 和 CF 有什么区别CF 处理无符号数的界外OF 处理有符号数的界外。有符号数的溢出意味着结果的符号位被“破坏”了即两个正数相加得到负数或两个负数相加得到正数。例如 0x7F 0x01 0x80按有符号数看是 1271 128但 128 在字节表示里其实是 -128这个结果显然是错的OF1。硬件上判断溢出的标准是最高有效位bit6即符号位的前一位的进位与符号位bit7的进位不一致时视为溢出。回到 0x7F 0x01 这个例子计算过程中 bit6 向 bit7 发生了进位因为 bit6 是 1加上前面累加的进位后变 0 并产生 carry而 bit7 向更高位没有进位两者不一致于是 OF1。如果两次进位相同则 OF0。还有另一个绝对值特别大的例子0x80 0x80 0x00按有符号解释是 -128 -128 -256在 8 位有符号范围内也溢出了。此时 bit6 和 bit7 都产生了进位一致所以 OF 反而为 0。这里最容易犯糊涂我建议你自己在纸上用二进制加一遍然后把这个结果记下来以后遇到 OF 的判断题就不会再错。2.7 TF陷阱标志——调试器的灵魂TF 全称 Trap Flag位 8是控制标志位之一。当 TF1 时CPU 每执行一条指令就会自动产生一次类型 1 中断即单步中断。这个机制是硬件调试的基础DEBUG 工具里经典的 T 命令单步跟踪就是利用 TF 实现的。执行单步中断前TF 会被自动清零避免中断处理程序自身陷入无限的单步中断。实际编程中TF 通常由调试工具设置和维护应用程序一般不需要直接操作它。但是在学习中断和异常处理时理解 TF 的工作机制有助于你明白为什么单步跟踪能一条指令一条指令地停下来。2.8 IF中断标志——可屏蔽中断的总开关IF 全称 Interrupt Flag位 9控制 CPU 是否响应可屏蔽中断INTR 引脚上的中断请求。IF1 时允许响应IF0 时禁止。它不影响不可屏蔽中断NMI和内部中断如除法错、单步中断这部分中断永远有效。用 STI 和 CLI 两条指令可以分别置位和清零 IF。在编写需要保护临界资源的代码时先 CLI 关中断处理完关键事务后立刻 STI 开中断这个小操作在很多硬件驱动和操作系统代码里都能见到。学裸机单片机时这个位尤其重要开错中断顺序会导致死锁或数据竞争。2.9 DF方向标志——决定串操作往哪走DF 全称 Direction Flag位 10控制串操作指令如 MOVS、CMPS、SCAS、LODS、STOS中 SI 和 DI 指针的自动调整方向。DF0 时SI/DI 自动递增处理内存从低地址向高地址进行DF1 时SI/DI 自动递减从高地址向低地址进行。设置和清除 DF 分别用 STD 和 CLD 指令。这里有个常见坑很多实模式程序在开头不初始化 DF如果某个库函数把 DF 设置了忘了恢复后续的串操作就会反向跑导致内存复制出现严重错误。所以正规的程序里使用串操作前最好先用 CLD 显式清零方向标志或者用 PUSHF/POPF 把它保存和恢复。3. 实际执行中的表现指令如何改变 FLAGS理解了九位标志的定义接下来就要看它们在真实指令执行时是怎么被改变的。这是做题、看别人代码、自己写汇编的硬核能力。3.1 算术和逻辑指令的效果对照不同指令对 FLAGS 的影响差异巨大有些指令根本不改某些标志而有些指令则强制把某些位置位或清零。我整理了一张常用指令的影响表你可以打印出来贴在显示器旁边。指令CFOFSFZFAFPFADD / ADC / SUB / SBB / CMP************INC / DEC—**********NEG************MUL / IMUL****————DIV / IDIV——————AND / OR / XOR / TEST00****—**SHL / SHR / SAR********—**PUSH / POP / MOV——————STC / CLC / CMC**—————说明“**”表示该标志会按规则更新“0”表示强制清零“—”表示该指令不影响这个标志。要注意的是上表中的空白不代表标志保持不变而是“结果不可预测”或者“不影响”不同教科书表述不同。这里最容易被考到的三个点INC/DEC 不影响 CF逻辑运算AND/OR/XOR/TEST会清零 CF 和 OFMUL/IMUL 只影响 CF 和 OF。3.2 用二进制把“为什么”算给你看光看表还是容易记混我拿几个典型的计算过程手算一遍你跟着走一遍就会清楚很多。第一个例子ADD AL, BL设 AL0x3CBL0x25。计算 0x3C 0x25 0x61即 0011 1100 0010 0101 0110 0001。结果非 0ZF0最高位为 0SF0无进位CF0无溢出加两个正数结果为 0x61不超正数上限 127OF0PD 检查低 8 位 0x61 中 1 的个数是 3 个奇数所以 PF0低 4 位运算 0xC 0x5 0x11向高 4 位进位AF1。这个结果非常典型可以自己多试几组。第二个例子SUB AX, BX设 AX0x0000BX0xFFFF。0x0000 - 0xFFFF 0x0001但无符号运算中产生了最高位的借位CF1结果非 0ZF0最高位是 0SF0OF0因为虽然减法和加法机制略有不同计算机中减法是通过补码相加实现的0x0000 0x0001 没有符号溢出OF0AF 同样因为低 4 位的借位置 1。这个例子说明 CF 对于无符号判断“不够减”非常重要。第三个例子0x7F 0x01 的字节加法前面讲过 OF1、CF0。这个对比特别说明一个问题同一加法CF 看无符号跨界0x7F0x010x80 没超过 255所以 CF0OF 看有符号溢出1271128 超过了 127所以 OF1。还有个比较冷门但常考的例子NEG AL若 AL0则 0-00CF0若 AL0x80则 0 - 0x80 0x80这里按无符号看有借位 CF1按有符号看 -128 取负结果还是 -128溢出OF1。这个特性在写取绝对值算法时经常要特别处理。3.3 多字节运算和 BCD 运算中的标志位在实际编程中单字节运算往往不够用比如要做 32 位加法就需要把操作数拆成两个 16 位并用 ADC带进位加串联。第一次 16 位加法完成后CF 表示是否产生进位第二次 16 位加法用 ADC它会把上一次的 CF 自动加到结果里。这样一圈下来低 16 位存结果CF 存最高位的进位就完成了 32 位加法。BCD 运算则依赖 AF。例如 09H 01H 得到 0AH这不是合法的 BCD 数AF 会因为 4 位结果产生进位而置 1接着执行 DAACPU 读到 AF1就把 AL 加上 06H得到 10H正好对应十进制 10 的 BCD 编码。整个过程如果中间夹了 MOV 指令AF 就保不住了所以 DAA/DAS 必须紧跟算术指令。这是汇编代码里最容易被“优化”掉却又不能优化的关键顺序。4. 在汇编代码里怎么用读取、判断与调试掌握定义和指令影响后就到了最实用的一关怎么用这些标志写条件分支怎么直接操作标志怎么在调试器里读标志。4.1 条件转移指令背后的“标志组合”条件转移指令本质上就是读取 FLAGS 特定位的组合满足条件就跳转否则继续顺序执行。它们分成无符号和有符号两套体系这是精简指令还是复杂指令都必须有的核心逻辑。指令条件检查的标志JE / JZ相等 / 零ZF1JNE / JNZ不等 / 非零ZF0JB / JC / JNAE无符号低于 / 进位CF1JAE / JNC / JNB无符号高于等于 / 无进位CF0JA / JNBE无符号高于ZF0 且 CF0JBE / JNA无符号低于等于ZF1 或 CF1JS负数SF1JNS非负数SF0JO有符号溢出OF1JNO无符号溢出OF0JG / JNLE有符号大于ZF0 且 SFOFJGE / JNL有符号大于等于SFOFJL / JNGE有符号小于SF≠OFJLE / JNG有符号小于等于ZF1 或 SF≠OFJP / JPE低 8 位偶校验PF1JNP / JPO低 8 位奇校验PF0这里最经典的坑用比较两个有符号数时你不能用 JA/JB那是给无符号数用的必须用 JG/JL。比如两个数一个看成无符号的 0xF0240、一个看成有符号的 -16如果代码里写当前无符号逻辑跳转就会结果颠倒。初学者最容易犯的错就是把 CMP 之后紧跟着的转移指令选错类型。4.2 不用算指令直接操作标志位除了让运算结果自动设置标志你还可以用专门指令去显式控制某些标志位这主要应用于控制标志位 CF 和后半部分的 IF、DF、TF。CLCCF0STCCF1CMCCF 取反。这三个指令在做多字节算术、位旋转时特别有用比如你要把 CF 作为“第 17 位”参与计算就得先手工清它。CLIIF0STIIF1。用于开启和关闭可屏蔽中断。CLDDF0STDDF1。控制串操作方向。LAHF把低 8 位标志SF、ZF、AF、PF、CF装入 AHSAHF反过来用 AH 设置这些标志。这个机制能把标志位快速搬到寄存器实现更灵活的判断。PUSHF / POPF把整个 FLAGS 压栈或弹出PUSHFQ / POPFQ 则用于 64 位模式。这些指令在编写中断服务程序时必不可少因为中断会隐藏式压入标志服务程序退出前 IRET 会自动恢复它们但如果你要在中断里临时修改标志就得用 PUSHF 保存现场。4.3 汇编和高级语言里的实际应用在高级语言编译器生成的汇编代码里FLAGS 每天都在工作。例如 C 语言里if (a b)在 x86 下会被翻译成大致这样的指令序列先 MOV 取 a 和 b然后 CMP a, b接着 JG 跳到条件为真的分支。CMP 不像 SUB 会保存差值它只做减法并更新 FLAGS差值本身丢弃这正好说明 FLAGS 就是用来给后续转移指令当“通信信使”的。写汇编时我个人的习惯是把 CMP 和条件转移写得很靠近中间不插任何会修改 FLAGS 的指令。因为指令影响表里有不少“—”但更安全的做法是在需要保留标志变量的场景中直接把 FLAGS 压栈保存。比如在循环里既要判断某次运算结果又要继续累加就会出现前后两次运算的标志互相覆盖的问题。这种情况下我会用 PUSHF 保存再执行另一段运算最后 POPF 恢复确保判断逻辑不被破坏。4.4 用模拟器或调试器观察 FLAGS 的实时变化当年学 8086 时调试手段远不如现在丰富最常见的工具是 DOS 下的 DEBUG。用 DEBUG 的 R 命令就能看到所有寄存器的值FLAGS 寄存器会以字母形式显示CF、PF、AF、ZF、SF、OF 等用两位字母表示比如显示 CY进位、NC无进位。T 命令单步执行时每一步之后你都能看到标志的变化这是理解指令如何影响标志的最佳途径。现在有更好的选择比如各类网页版 8086 模拟器、emu8086、以及 Dosbox 里的 Turbo Debugger。我特别推荐做法是拿一个指令列表自己构造不同的初始值一步步走每执行一条指令就把 9 个标志位的变化抄下来和指令影响表对照。你会发现很多在纸面上感觉抽象的行为一旦看到寄存器值在眼前跳动马上就能记住。比如 INC AL 从不改 CF但 SUB AL, BL 的 CF 会随借位变化这件事光看很难有印象但实操两三次就彻底忘不掉。5. 常见误区、坑点与排查技巧这个部分是我不想让你走我旧路的地方。下面这些坑都是我在实际学习和写代码时亲身踩过的每条都能对应一段抓狂的调试经历。5.1 常见误区速查表误区真相后果把 OF 当成无符号溢出的标志OF 专门管有符号数无符号用 CF补码运算判断错误循环跳转方向相反以为 INC/DEC 会改 CF这两条指令明确不改 CF多字节加法链断裂结果错得莫名其妙以为 CMP 会改变操作数CMP 只改 FLAGS不改操作数调试时检查操作数被改怀疑指令有问题用无符号跳转指令判断有符号数比较JA/JB 属于无符号JG/JL 属于有符号比较结果完全相反逻辑出现难以察觉的 bug认为 PF 是整个结果的奇偶性PF 只看结果低 8 位16 位运算的奇偶校验算错忽略了 16 位运算中 AF 的意义BCD 调整时若无 AF 就全乱了DAA/DAS 结果不正确在 CMP 和条件跳转之间插入 MOV 等指令MOV 不影响标志但若插的是 ADD/SUB 就会覆盖条件跳转基于错误的标志程序行为不可预测忘记用 CLD 初始化方向标志DF 若为 1串操作反向内存复制方向反了缓冲内容全错以为所有指令都改所有标志有的指令干脆不带任何标志位变化依赖无关标志做判断结果是猜测出来的5.2 我排查标志相关 bug 的固定套路如果你调程序发现跳转异常第一步不是盯代码而是查标志。我自己的流程一般是这样的先用调试器单步到 CMP 指令之前记录当前内存和寄存器值执行 CMP 后立刻查看 FLAGS 变化确认它跟手工计算的一致再查看条件跳转指令确认它读到的位组合是否符合预期最后检查有没有其他指令夹在 CMP 和跳转之间把它删掉。用这个流程我解决过不少看起来很玄学的跳转问题。另外有个小技巧可以用逻辑运算获取标志后的可复制值。例如用 LAHF 拿到低 8 位标志然后 PUSH AX 存进栈再打印出来能很方便地分析断点处的标志状态。比起直接看调试器那一排 AT 符号自己打印出来的二位数数组更直观。5.3 最后分享一个实用的记忆方法个人经验里最有效的记忆方式不是背定义而是给每个标志编一个“生活类比”CF 是“袋子漏了没有”无符号的进位借位OF 是“身高量错了没有”有符号的溢出因为正负搞反实在离谱ZF 是“饭盘空没空”SF 是“心情消极不消极”AF 是“小零钱有没有找开”低位 4 位的进位PF 是“队伍单不单数”DF 是“从前往后还是从后往前”IF 是“大门开没开”TF 是“螃蟹会不会一格一格走”。这套类比帮我考场上 30 秒内判断出标志状态也希望能帮你把那张“成绩单”真正读进心里。你只要把这张表自己抄一遍再对着模拟器跑五个典型指令基本就不会再忘。