ALU 标志位:一次运算怎么同时告诉 CPU 正负、零、进位、溢出
更新时间:2026-09-24。本文是「集成电路大类 · 芯片设计」入门层(beginner) 数字逻辑基础的一篇。写高级语言时一句
if (a > b)看起来天经地义,但 CPU 并不懂「大于」——它只会算减法。那它凭什么知道结果是正是负、是不是零、有没有越界?答案是 加法器 在做运算的同时,还额外吐出几个只有 1 比特的「副产物」。比较器里反复出现的「看标志判断」,看的就是这四个标志。这一篇把 N/Z/C/V 一次讲透。
本文要回答的问题
- N(负)、Z(零)、C(进位/借位)、V(溢出)各自由结果的哪些位算出来?
- 进位 C 和溢出 V 到底什么区别?为什么
0xFF+1有 C 但没有 V? - 为什么有符号比较不能只看结果符号,而要看
N⊕V? - 减法的「借位」和加法的「进位」怎么用同一个加法器统一?
- 这些标志怎么驱动
beq、blt这类条件分支?RISC-V 又为什么不设标志寄存器?
一、标志位:一次运算免费搭车的副产物
程序里的判断最终都要落成硬件动作。高级语言的 if、循环的终止条件、switch 的跳转表选择,编译后几乎都是同一个套路:先做一次减法或比较,再根据结果决定下一条指令从哪里取。可减法器给出的只是一个 n 位的二进制数,控制器不可能去逐位读它再临时「思考」。于是 ALU(算术逻辑单元)在设计时就规定:每次运算除了在输出端给出结果 R,还在旁边同时算出几根 1 比特的状态线,分别回答四个问题——结果是不是负的、是不是零、无符号意义下有没有超出 n 位、有符号意义下有没有超出表示范围。
这四根线就是 ALU 标志位(status flags / condition codes):N、Z、C、V。顺带解释一个冷知识:溢出标志写作 V 很好理解,英文却叫 oVerflow——因为 O、I 这类字母在早期汇编和纸面文档里容易与数字 0、1 混淆,于是挑了单词里的 V 作为缩写,和 x86 里奇偶标志 P、辅助进位 A 的命名思路一致。
这套设计可以追溯到 1970 年代的小型机与早期微处理器:运算器面积金贵,不可能为每条分支单独做比较,把四个 1 比特标志集中存放、让所有分支共享,是当时最省晶体管的方案。此后几乎所有经典 ISA(8080、6800、8051、x86、ARM)都沿用了条件码寄存器,差别只在标志极性和分支助记符。它们不是事后软件分析出来的,而是和结果在同一个时钟周期里由组合逻辑算好的,几乎不额外花时间。习惯上它们会被保存在一个专门的寄存器里,叫法随架构而不同:程序状态字 PSW、状态寄存器 CCR、保存程序状态寄存器 SPSR,统称「条件码」。
理解 ALU 标志位要抓住一个前提:同一串二进制比特,用无符号视角读和用补码视角读,是两套互不相干的解释。11111111 既可以是无符号的 255,也可以是补码的 −1。标志位的分工正是沿着这条裂缝切开的:C 只服务无符号视角,N 和 V 服务补码视角,Z 则对两边都成立。
打个生活化的比方:标志位就像快递面单上那几个小勾选项——「易碎」「到付」「已签收」。包裹(运算结果)本身不变,但这几个勾选位让下游分拣环节(控制器)不用拆箱,扫一眼勾选就知道该走哪条流水线。硬件设计追求的正是这种「不看内容、只看标签」的快速决策。
二、四个标志的精确定义
一次 n 位运算产生结果 R[n-1:0],加法器内部每一位还有进位输出 c[i](c[-1] 即进位输入 cin)。四个标志的生成逻辑全部是组合电路:
| 标志 | 回答的问题 | 精确逻辑 |
|---|---|---|
| N(Negative) | 按补码结果为负? | N = R[n-1],直接取最高位(符号位) |
| Z(Zero) | 结果恰好为 0? | Z = ~(R[n-1] + … + R[0]),各位归约或非 |
| C(Carry) | 无符号结果超出 n 位? | 最高位向外的进位 c[n-1](减法时极性按 ISA 约定处理) |
| V(oVerflow) | 有符号结果超出范围? | V = c[n-1] ⊕ c[n-2],符号位进位与最高数值位进位异或 |
N 最便宜,就是从结果总线上引一根符号位,零个门。Z 稍复杂:只要有任何一位是 1,结果就不是零,所以把所有位送进一棵或归约树(或异或/或归约树,二者对「是否存在 1」等价),再整体取反。位宽 64 时这棵树一般折成几级两输入门,深度只有几级逻辑。C 直接取自加法器最高位的进位输出,本来就是行波/先行进位加法器的现成信号。V 则只需要一个异或门,把进入符号位的进位 c[n-2] 和离开符号位的进位 c[n-1] 比一比是否一致。
这张图就是 ALU 标志位的生成全景:结果总线流向 N 和 Z,加法器内部的进位链流向 C 和 V。注意 N、Z 只依赖最终结果,C、V 还依赖运算过程中的进位——这也解释了为什么逻辑运算(AND、OR)通常只更新 N、Z,而 C、V 会被清零或保持:它们根本没有进位过程可看。
为了把定义落到实处,不妨亲手算一笔 8 位加法:0x6D + 0x4A。先展开成二进制逐位相加:
0110 1101 (0x6D = 109)
+ 0100 1010 (0x4A = 74)
-----------
1011 0111 (结果 0xB7,cout = 0)结果 0xB7 = 10110111,按四个标志逐个核对:最高位是 1,所以 N=1;结果显然非零,Z=0;最高位没有向外进位,C=0;进入符号位的进位 c[6]=1(低七位相加 0x6D+0x4A 的低段 109+74=183 已超过 127),离开符号位的进位 c[7]=0,二者不一致,V=1。含义完全对得上:无符号 109+74=183 在 8 位内(C=0),补码读法则是 (+109)+(+74)=+183 超出 +127,硬件结果 0xB7 被读成 −73(V=1,N=1)。这个例子同时演示了一条常用排查口诀:N、Z 看结果,C 看最高进位,V 看符号位两侧进位是否一致。
| 运算类型 | N | Z | C | V |
|---|---|---|---|---|
| 加法 ADD | 更新 | 更新 | 进位 | 有符号溢出 |
| 减法 SUB/CMP | 更新 | 更新 | 借位语义 | 有符号溢出 |
| 逻辑 AND/OR/XOR | 更新 | 更新 | 通常清零 | 通常清零 |
| 数据传送 MOV/装入 | 多数 ISA 不更新 | 不更新 | 不更新 | 不更新 |
这张表也提醒:不是每条指令都碰标志。mov、装入/存储这类「搬数据」的指令一般不动标志位,标志只描述最近一次算术或逻辑运算——这正是下一节讨论「相关性」的伏笔。
三、C 与 V:两套「越界」语言,互不相关
初学者最常把 C 和 V 混为一谈,觉得都是「算溢出了」。实际上它们回答的是两个视角下各自独立的问题,可能的四种组合都会出现。先看两个经典例子:
0xFF + 0x01:按位相加得0x00,最高位产生进位。无符号视角是 255+1=256,8 位装不下,C=1;补码视角是 (−1)+(+1)=0,完全正确,V=0。0x7F + 0x01:按位相加得0x80,没有进位输出。无符号视角是 127+1=128,装得下,C=0;补码视角是 (+127)+(+1),结果10000000被读成 −128,正数加正数得出负数,V=1。
图里把同一笔运算拆成两条解读路径,C 与 V 各走各的,互不引用。减法也一样:0x00 − 0x01 在无符号看来是 0−1 必须借位(C 报告借位),在补码看来却是 0+(−1)=−1,完全合法。这就是为什么 C 语言里无符号减法回绕(unsigned 下 0−1 等于最大值)和有符号溢出未定义,在硬件上本就由不同标志各管一段。
V 还有一个不用看进位的直观判据:两个同号数相加得到异号结果——正+正得负,或负+负得正,必是溢出;异号数相加永远不会溢出,因为结果只会落在两者之间。电路上之所以用 V = c[n-1] ⊕ c[n-2],只是因为这个式子门最少:正常相加时进入符号位和离开符号位的进位必然相同,一旦二者不一致,说明符号位被「顶翻」了。
| 运算(8 位) | C | V | 各自含义 |
|---|---|---|---|
0xFF+0x01 | 1 | 0 | 无符号回绕,补码正确 |
0x7F+0x01 | 0 | 1 | 无符号正常,补码溢出 |
0x80+0x80 | 1 | 1 | (−128)+(−128)=−256,两边都越界 |
0x01+0x02 | 0 | 0 | 两边都正确 |
四、减法、借位,以及「有符号比较看 N⊕V」
减法 A−B 在硬件上并不需要单独的减法器,而是复用补码恒等式:A − B = A + (~B) + 1,即把 B 逐位取反、进位输入 cin 置 1(见加减法器)。于是「借位」也不是新信号,而是对加法器最高进位的不同取用方式。这里有个架构约定的坑:不同 ISA 对减法时 C 的极性规定相反。x86、RISC-V 的无符号分支语义里,A−B 不够减(有借位)时等价于 C=0;ARM 则直接规定减法的 C=1 表示「没有借位」。初学查表时觉得矛盾,本质只是把最高进位取反与否,硬件判断的内容完全相同。
更关键的结论是:有符号大小比较不能只看结果符号 N。道理在第三节已经埋下——一旦 V=1,结果的符号位被溢出顶反了,N 说的是假话。比如 (+127)−(−1),真实结果 +128 超出 8 位补码,硬件给出的结果是 0x80,N=1 看起来像「结果为负」,但显然 A 远大于 B。把符号修正回来的办法就是异或:
N ⊕ V = 0:结果真实为正(或零);N ⊕ V = 1:结果真实为负。
可以用穷举想明白这个式子为什么成立。V=0(没溢出)时,结果符号位说真话,N⊕0 = N,直接采信符号;V=1(溢出)时,真实大小关系与符号位必然相反——要么两个大正数相加被读成负(N=1 但真实为正),要么两个大负数相加被读成正(N=0 但真实为负),异或上 1 正好把结论翻回来。减法侧的对应例子是 0x80 − 0x01:(−128)−(+1)=−129 超出范围,硬件结果是 0x7F,N=0 看起来是正数,V=1,N⊕V=1 才如实报告「结果为负」。这条「先查 V、再决定信不信 N」的规则,是所有有符号比较指令在硬件上共同的核心。
于是同一个减法器,配不同的标志组合,就把所有比较分支都覆盖了:
| 比较(做 A−B) | 无符号判据 | 有符号判据 |
|---|---|---|
| A == B | Z=1 | Z=1 |
| A != B | Z=0 | Z=0 |
| A > B | C=1 且 Z=0 | (N⊕V)=0 且 Z=0 |
| A < B | C=0(有借位) | (N⊕V)=1 |
| A ≥ B | C=1 | (N⊕V)=0 或 Z=1 |
| A ≤ B | C=0 或 Z=1 | (N⊕V)=1 或 Z=1 |
这正好补上数值比较器篇留下的那句话:所谓有符号比较器,就是在同一个减法/比较电路后面,把判断逻辑从「看 C」换成「看 N⊕V、再看 Z」。实际电路里 (N⊕V) 也只多一个异或门。
五、状态寄存器、条件分支,以及标志的相关性
标志算出后被写入状态寄存器,分支指令再按指令类型挑对应标志:beq 看 Z、bne 看 ~Z、无符号小于看借位、有符号小于看 N⊕V……控制单元里只需要一小片组合逻辑,把指令操作码译码出的选择信号和四个标志拼成一根 1 比特的「分支成立」线;成立时就把目标地址写入 PC,不成立则顺序执行。ARM 还更进一步,几乎每条指令都能带条件后缀(addeq、movlt),复用的也是同一套标志。
但标志寄存器带来一个必须正视的副作用:标志是「上一条会写标志的运算」留下的共享状态。于是指令之间除了数据相关,还多了标志相关——cmp 和它后面的 blt 之间隔着别的会改标志的指令,判断就会错。这种错和数组越界一样隐蔽:程序大部分时候跑对,只在中间插入的指令恰好改写标志时翻车。手编汇编的纪律是比较指令与条件分支成对出现、中间不插算术指令;编译器则在指令调度时把不改标志的指令(装入、传送)挪进这个间隙。在流水线和乱序执行核里,标志要像普通数据一样做寄存器重命名、前递和冒险检测,否则分支会读到旧标志,详见 RISC-V 与处理器核。
从软件侧看,这个机制可以直接对照编译产物。下面这段 C 代码:
int x, y;
if (x < y)
x = x + 1;典型编译结果(忽略具体语法细节)就是三步:sub/cmp 做 x−y 并更新标志 → bge 之类的有符号分支在 (N⊕V)=0 时跳过 if 体 → 否则执行加法。整个过程没有任何指令去「读」减法结果的数值,分支只消费标志。如果把变量声明成 unsigned,编译器生成的分支就从看 N⊕V 换成看 C——同一份源码、同一套减法硬件,只因为类型不同就换了消费的标志,这是 C/V 分野在编译器层面的直接体现。
C 标志还有一个分支之外的重要用途:多精度算术。8 位机做 16 位加法时,先加低字节、再用「带进位加」ADC(add with carry)加高字节——高字节相加时自动把低字节产生的 C 当作 cin 加进去;多字节减法对应「带借位减」SBB。大数库、加密算法里的几百位整数运算,靠的就是 C 在字节块之间逐位传递。V 没有类似的链式指令,因为有符号溢出无法跨块累积,这再次说明 C 和 V 不是同一种东西。
RISC-V、MIPS 等精简指令集则干脆不设全局 ALU 标志位:比较由 slt/slti 这类指令直接把 0/1 写进通用寄存器,分支只判断寄存器是否为零。少一个共享的状态寄存器,指令间的隐性耦合就少一种,流水线更干净;代价是比较结果要占用一个通用寄存器。要注意的是,这不代表前面的硬件判据失效——slti 指令内部做的仍是减法加 N⊕V 判断,只是结果不落进专门的标志寄存器而已。浮点侧也有类似的一组标志(无效操作、溢出、下溢等),那是 IEEE 754 的异常标志,见 浮点表示与 IEEE 754。顺带一提,x86 的 EFLAGS 里还塞了奇偶标志 P、BCD 用的辅助进位 A 等历史标志,现代代码基本不再使用,初学时可以只抓 N/Z/C/V 这四个主角,其余随用随查。
这一篇在知识体系里的位置
本篇属于芯片设计入门层的数字逻辑基础,回答「CPU 怎么做判断」。向前读,标志的物理来源是 加法器与算术电路,N/Z 的归约结构和大小判断见 数值比较器;平行读,标志如何接入控制器改写 PC 属于 组合数据通路 的话题,溢出后不回绕而钳到边界的另一种策略见 饱和运算;向后读,标志相关在流水线里的处理见 RISC-V 与处理器核,浮点的异常标志见 浮点表示与 IEEE 754。
总结
ALU 每次运算在给出结果的同时,用组合逻辑免费搭车算出四个状态标志:N 直接取符号位、Z 是结果各位归约或非、C 是最高位向外的进位、V 是符号位进位与最高数值位进位异或;C 服务无符号视角、V 服务补码视角,二者互不相干,所以 0xFF+1 有 C 无 V、0x7F+1 无 C 有 V;减法靠 A+~B+1 复用加法器,借位只是对最高进位的不同取用约定;有符号比较因溢出会反转符号而必须看 N⊕V,再配 Z 即可区分相等与大小;这些标志写入状态寄存器后由一小片组合逻辑映射成 beq/blt 是否成立,标志本身也是一种需要在流水线里处理相关性的共享状态,而 RISC-V 取消标志寄存器、改用 slt 把判断结果写进通用寄存器,依据的硬件逻辑并无二致。