☰
汇编语言入门:加减指令与标志位实战解析
2026/10/18 11:14:01 网站建设 项目流程

汇编语言入门——1.3 常用演算指令(一):加减运算与标志位实战拆解

很多人学汇编,卡住的第一关往往不是寄存器、不是内存寻址,而是“指令明明看得懂,但一写程序就懵”。ADD、SUB这两条指令看着简单,无非是加法和减法,可一旦涉及标志位、溢出、借位、补码,事情立刻变得微妙起来。这套东西不搞清楚,后面学跳转指令、条件分支、循环结构,全都像踩在棉花上。

这篇文章把1.3节的内容拆开揉碎,围绕最基础的加法和减法指令展开,重点讲清楚ADD、SUB、ADC、SBB四条指令的行为,以及CF、OF、ZF、SF、AF这几个标志位到底在干什么。讲完指令之后,会给出一套完整的验证思路和一个小练习项目的建议。各位读者,尤其是刚接触汇编的朋友,建议手边备好调试器,一边看文章一边单步执行,光读不练,标志位的变化永远记不住。

1. 为什么先从加减法讲起

16位x86汇编的指令系统里,加减法指令属于最底层的砖瓦。访问内存、搬数据、跳转、压栈出栈,这些都是“搬运”操作,真正让数据发生变化的,往往是算术和逻辑指令。加减法又是算术指令里最核心的部分,因为它们牵动了标志寄存器(FLAGS)里的多个标志位,而这些标志位正是后续所有条件跳转、循环控制的判断依据。

简单概括:不会加减法,就写不出真正有逻辑的程序。像“统计一批数据里正数和负数的个数”“计算校验和”“把一个二进制数转成十进制显示”,底子全是ADD和SUB。理解了这两条指令以及它们对标志位的影响,也就理解了CPU如何“感知”计算结果。

这篇文章的叙述重点集中在数据处理本身,不涉及中断、系统调用、字符串操作这些外围内容。选用的汇编风格是16位实模式下的基本指令集,这种风格贴近教学场景,逻辑直观,寄存器和内存模型也相对容易理解。后续如果接触到32位、64位指令,虽然操作数宽度不同,但标志位行为几乎没有变化,学到的原理可以复用。

2. 加减指令的基础操作与操作数规则

2.1 指令的基本行为

ADD指令的语义很简单:把左边操作数和右边操作数相加,结果存回左边操作数。SUB同理,结果是左边减右边,结果存回左边。两条指令都支持寄存器与寄存器、寄存器与内存、寄存器与立即数等组合。

一个基础例子:

mov ax, 1000h mov bx, 2000h add ax, bx ; AX = 3000h

这条ADD执行完之后,AX里的值是3000h。寄存器里的内容没有发生“旧值消失”的问题,因为目标操作数同时充当了“被加数”和“结果存放处”。这一点和高级语言里的a = a + b写法是一回事,但在汇编里你必须明确目标操作数是谁。

SUB的例子同样直接:

mov ax, 3000h mov bx, 1000h sub ax, bx ; AX = 2000h

需要注意:SUB指令的语义是“目标操作数减去源操作数”,方向不能搞反。sub ax, bx和sub bx, ax的结果完全不同,这在初学阶段是最容易踩的坑之一。

2.2 “不改变操作数”的常用配合方式

实际写代码的时候,经常需要“比较两个数的大小但不改变原来的值”。SUB能做到这一点,但会把结果覆盖到目标操作数上,所以工程师一般会用CMP指令来做。CMP本质上就是SUB,唯一的区别是它不把差存回去,只更新标志位。这个特性在后续的跳转指令里几乎是命脉一样的存在,这里提前提一句,加深印象。

另一个常见配合是清空寄存器。mov ax, 0当然可以,但很多汇编代码里写sub ax, ax或者xor ax, ax,后者更常见,因为指令编码更短。SUB清空寄存器的思路就是“自己减自己”,结果必定为零,同时ZF标志位置1,这个副作用有时候也被利用来传递状态。

2.3 关于ADDS和操作数宽度的提醒

在16位模式下,ADD和SUB的操作数默认宽度是8位或16位,两种宽度由寄存器选择决定。比如:

add al, 05h ; 8位加法 add ax, 05h ; 16位加法

写代码时容易犯迷糊的是立即数。add al, 100h这类写法会直接报错,因为100h超出了8位立即数能表达的范围。汇编器会根据目标寄存器宽度自动检查常量合法性,编译阶段就会提示。别把立即数的宽度的判断交给运行时,那是调试器都难救的灾难。

还有一个细节:ADD和SUB可以操作内存操作数,但两个操作数不能同时是内存。比如add [bx], 10合法,而add [bx], [si]在经典16位指令集里是不允许的。原因是CPU的硬件设计里,一条指令最多只能携带一个内存操作数,多个内存操作数会让指令编码结构急剧复杂化,而且流水线加载的时序也变难处理。想完成“内存值加到内存值”这件事,必须先把其中一个值搬到寄存器里。

3. 标志位机制:为什么指令的“副作用”才是主角

单纯算出一个数值结果,那只是ADD和SUB的表面价值。真正让这两条指令有意思的,是它们同时更新了标志寄存器里的若干标志位。后续的条件跳转全靠这些标志,所以理解标志位的行为,比记住指令格式更重要。

3.1 ZF零标志位

ZF(Zero Flag)记录的是“计算结果是否为0”。如果结果为0,ZF=1;结果非0,ZF=0。这件事用一句话就能概括,但很多人忽略了一点:ZF是根据目标操作数的最终结果来判断的,不是根据“数学上是否相等”。比如:

mov ax, 1234h sub ax, 1234h

执行后AX=0,ZF=1。这个逻辑看起来平平无奇,但在循环计数里是核心判断依据。如果在一个循环里用dec cx递减计数器,然后立刻用JNZ判断是否跳转,背后的机制就是ZF的变化。

3.2 SF符号标志位

SF(Sign Flag)记录结果的最高位(MSB)是0还是1。在无符号数的视角下,MSB只是数值的一部分;在有符号数的视角下,MSB是符号位。SF的行为非常直白:结果最高位为1,SF=1;结果最高位为0,SF=0。

初学时容易产生误解:SF=1就代表结果是负数吗?答案是“在把结果解释为有符号数时,SF=1代表负数”。但CPU本身不区分有符号还是无符号,它只是忠实地复制了最高位。举个例子:

mov al, 0FFh add al, 01h

AL的结果是0,此时ZF=1。但若从有符号数角度理解,0FFh是-1,-1加1等于0,结果符号为正,SF应该为0。实际上,这条指令执行后SF=0,因为结果的最高位是0。如果再把AL的值当作无符号数255,那这次加法就产生了进位,CF会置1。同一个二进制位模式,两套解释逻辑并存,全靠处理器同时更新对应的标志位,这种设计恰恰是为了让后续程序自己选择“看哪个标志”。

3.3 CF进位/借位标志位

CF(Carry Flag)是无符号运算的溢出指示。加法运算中,如果最高位产生了进位,CF=1;减法运算中,如果最高位产生了借位,CF=1。这句描述是无数教材里反复出现的标准说法,但真正理解它需要拆开看。

加减法在CPU内部都走加法器。减法不是“独立的减法电路”,而是“被减数加减数的补码”。SUB在硬件层面这样做:

  • 取减数,按位取反;
  • 目标操作数加上取反后的减数;
  • 再加1。

这个过程里,如果发生了“最高位进位”,就说明减法产生了借位。一个具体的例子:

mov al, 00h mov bl, 01h sub al, bl

AL的结果是0FFh,因为没有进位,CF=1。从无符号数角度说,0减1不够减,必须向更高位借1,所以CF置1。

这个CF标志位是后续编写多字节加减法、BCD运算、多精度算术的枢纽。比如要做16位以上的大数运算,必须把低位字节的进位或借位传递到高位字节,这正是ADC和SBB指令存在的理由。

3.4 AF辅助进位标志位

AF(Auxiliary Carry Flag)只关注低4位(一个十六进制数字位)是否发生进位或借位。它的服务对象是BCD(二进制编码十进制)运算的调整指令,普通加减法里基本用不到,但调试时看到AF变化不要慌,那是正常现象。

具体规则:加法时低4位向高4位进位,AF=1;减法时低4位向高4位借位,AF=1。判断方式是看结果的低4位(即AL的低4位)和两个操作数的低4位之间的关系。

比如:

mov al, 09h add al, 01h

AL结果是0Ah。低4位从9变成0,同时向高4位进1,所以AF=1。类似的情况在ASCII码调整和BCD调整指令中非常关键,比如DAA和AAA指令就依赖AF来决定是否需要调整。这里先不做展开,但务必记住:AF只跟低4位有关,别拿它和CF混为一谈。

3.5 OF溢出标志位

OF(Overflow Flag)是有符号数运算的溢出指示。这是全文章最容易被混淆的一个标志。区分CF和OF,建议用这个思路:CF服务于“无符号数”视角,OF服务于“有符号数”视角。无符号数溢出意味着“装不下”,有符号数溢出意味着“符号错乱”。

OF的判定标准是:两个同号数相加,结果符号与操作数符号相反,则OF=1;两个异号数相加,不可能产生有符号溢出。减法判断类似:正数减负数得到负数,或负数减正数得到正数,都视为溢出。

用生活化方式理解:把有符号数想象成温度计,温度范围是-128到127。如果当前是100度,再加50度,读数会跳到“负温度”,这显然是读数被“拧断”了。OF=1就是这种“读数拧断”的警告。机械计数器、球赛记分牌出现负分,也可以类比这种感觉。

具体到机器行为,OF的硬件实现是“最高位进位”和“次高位进位”的异或。这两个进位如果相同,说明没有符号位错乱;如果不同,说明发生了溢出。这个判定细节,学数字电路的时候会更直观,汇编层面只需要记住最终结论即可。

一个经典例子:

mov al, 7Fh add al, 01h

7Fh(127)+ 1 = 128,超出了8位有符号数的上限。AL结果变成了80h(-128),从有符号数角度,结果符号错误,OF=1。同时,因为128需要用9个bit表示,但AL只有8个bit,最高位的进位丢了,CF=1。一条指令,两个溢出标志同时置位,恰好说明两个不同视角都出了问题。

一个让初学者很困惑的点:大多数“正常人”写程序时,其实不会同时在乎CF和OF。判断无符号数大小时看CF,判断有符号数大小时看OF,两者各管一摊,极少交叉。但CPU不管这些,它总是“同时”更新所有标志位,这恰恰是它的优势——你根据需要取用。

3.6 标志位更新方式的朴素总结

把上面几个标志位合并起来看,ADD和SUB执行完之后,标志寄存器的变化规律可以整理成一张速查表。这张表值得贴在调试器旁边。

标志位加法时的置位条件减法时的置位条件主要服务的视角
ZF结果为0结果为0通用
SF结果最高位为1结果最高位为1有符号/通用
CF最高位产生进位最高位产生借位无符号
OF同号相加符号异常异号相减符号异常有符号
AF低4位向高4位进位低4位向高4位借位BCD

这里面最核心的记忆口诀就一句话:“CF看高位、AF看低4位、OF看符号错乱、ZF看全零、SF看符号”。

4. ADC与SBB:把进位/借位传到高位的关键

单字节加减法只管8位数据,但实际项目里经常要处理16位、32位的数据。虽然寄存器本身可以是16位,但一旦数据宽度超过寄存器上限,就必须把“进位”逐级传递下去。ADC(Add with Carry)和SBB(Subtract with Borrow)就是干这个的。

4.1 ADC指令的行为

ADC的完整语义是:目标操作数 = 目标操作数 + 源操作数 + CF。当前CF的值也会被加进去。

一个典型的多字节加法流程:先把低16位加起来,然后用ADC加高16位。这样如果低16位产生了进位,ADC就会把它计入高位。

; 计算 DX:AX = DX:AX + BX:CX add ax, cx adc dx, bx

这段代码执行完,低16位已经存入AX,高16位存入DX。如果低16位加法有进位,ADC指令会自动把它带入高16位的结果中,整个过程不需要额外的条件判断。这正是CF标志位最有价值的使用场景。

很多读者第一次看到ADC会觉得“多此一举”,因为直接用ADD把高位相加,进位不就丢了吗?确实丢了。ADD不读CF,无法把上一段运算的进位纳入当前结果。遇到超出16位宽度的加法,只靠ADD会产生错误结果。ADC的出现,就是为了保证多精度算术的正确性。

4.2 SBB指令的行为

SBB的语义是:目标操作数 = 目标操作数 - 源操作数 - CF。这里的CF被看作“上一步减法产生的借位”。

; 计算 DX:AX = DX:AX - BX:CX sub ax, cx sbb dx, bx

和ADC的套路完全对称。低16位相减时,如果发生借位,CF=1;接下来直接用SBB做高位减法,把CF减去,就自动完成了借位传递。

这里要特别强调:SBB里的CF在减法操作中代表“借位”,但在硬件实现上,它依然是同一个标志位。SBB执行时,CPU会同时考虑这个标志的位置,逻辑上等价于“目标 = 目标 - 源 - 借位”。

写多字节减法时有个高频错误:低位的减法和高位的减法用了两条SUB,而不是“SUB+SBB”。这样做的结果就是低位借位被直接丢掉,高位结果没有减去借位,整个减法就算错了。

4.3 实战场景:两个32位无符号数相加

假设有这样一个数据布局,第一个数存放在BX:AX(高16位在BX,低16位在AX),第二个数存放在DX:CX,目标是计算它们的和并保存回BX:AX:

add ax, cx adc bx, dx

执行完成后,BX:AX内就是完整结果。这句代码用到的知识已经覆盖了ADD、ADC和CF的使用。如果把这里换成64位数据,无非是再多一组高位寄存器,关键仍然是把CF一级一级往下传。

5. 实操过程:用调试器见证标志位的每一步变化

这一节直接用调试器走完一段代码,把每一步的寄存器变化和对标志位的影响陈列出来。建议读者动手指跟一遍,尤其是第一次接触标志位的新手。

以下示例使用一个典型调试器(比如常见汇编教学环境里内置的调试功能)来单步执行。环境的具体名称不重要,关键是每个人都能在自己的工具链里做同样的验证。

5.1 示例代码准备

代码片段如下:

mov ax, 1234h mov bx, 0EDCCh add ax, bx sub ax, 2000h add ax, ax sub ax, ax

这段代码的逻辑很直白,但它涵盖了ADD、SUB、ADD自身相加、SUB自身清零四种典型情况。逐条执行并观察“执行前的寄存器、执行后的寄存器、相关标志位”三列信息,就能把前面讲到的理论全部变成亲眼所见的事实。

5.2 逐步执行与标志位观察记录

第一步,mov ax, 1234h。这一步只是加载数据,按x86的标准行为,MOV指令不会更新标志位。调试器显示AX=1234h,标志位继上一条指令之后保持不变。

第二步,mov bx, 0EDCCh,同样不影响标志位。

第三步,add ax, bx。这条指令是本轮观察的核心。1234h + 0EDCCh = 10000h,但这个结果超出了一般16位寄存器的容纳范围(如果以16位寄存器AX为目标,这里实际应关注AX的变化)。单步执行之后,AX的值取决于具体调试环境的寄存器宽度设定。在本例的16位视角下,AX的结果应为0000h,CF=1,ZF=1,SF=0,OF=0。进位被CF捕获,结果为零则ZF置1。

第四步,sub ax, 2000h。上一步AX=0000h,减去2000h,结果会得到一个负数(有符号视角)。AX=0E000h,CF=1(因为无符号视角发生了借位),SF=1(结果最高位为1),ZF=0,OF=0。这一条指令能同时看到CF、SF、ZF三个标志的联动变化。

第五步,add ax, ax。此时AX=0E000h,自加相当于乘以2。结果AX=1C000h,在16位寄存器里体现为0C000h,CF=1(进位被丢出),SF=1(最高位是1),OF=1或0取决于符号位变化情况。按16位内部行为推导,这里的OF具体值在不同宽度设定下有差异,读者可以在调试器里亲自确认。

第六步,sub ax, ax。这条指令执行后AX=0,CF=0,ZF=1,SF=0,OF=0。这也是上一节提到的“自己减自己”清空法的底层原理。

5.3 亲手验证标志位的最佳习惯

上面标记“取决于环境”的地方,不是在打太极,而是想说清楚:不同汇编器和调试器的默认数据宽度可能不同,I/O窗口显示的寄存器位数也可能不同,这会导致“同一条指令的数值结果”看起来不一样。遇到这种情况,不要慌,回到数据宽度上看:

  • 如果采用16位寄存器视角,把结果看成16位,CF记录进位。
  • 如果采用32位寄存器视角,结果自然不同,CF是否置位也取决于最高位在哪。

这是整个实操环节最大的坎。强烈建议在调试器里做一个固定动作:每次单步执行完,先看CF和ZF,再看SF和OF。这四个标志位一起看,能迅速判断出这条加法/减法指令是不是按预期工作的。

6. 小练习项目:标志位诊断器

光看指令执行还不够,真正把知识内化的方法是做一个完整的小项目。这里给出一套适合初学者的练习方案,目标是写一段程序,对一批无符号数和有符号数分别做加法、减法运算,并在每次运算后把标志位状态“转换”成可读的提示信息,也就是一个标志位诊断器。这一步会用到后面章节才正式讲解的跳转指令,但作为练习可以先动手过一遍思路,遇到不会的指令再翻资料。

6.1 目标与模块拆分

模块任务内容涉及的知识点
数据准备定义若干8位/16位数据,包括正数、负数、零、边界值DB/DW伪指令、数值补码表示
加法模块对无符号数做加法并提取CF,对有符号数做加法并提取OFADD、JNC/JC/JNO/JO等跳转思路
减法模块类似加法模块,但关注借位与有符号溢出SUB、SBB的配合、标志位读取
结果输出用字符等可视方式打印“CF=1”“OF=1”等信息简单中断调用或输出函数

6.2 数据设计思路

建议准备这样的测试向量:

  • 无符号边界:0FFFFh + 1,检验CF;
  • 有符号边界:7FFFh + 1,检验OF;
  • 减法借位:0 - 1,检验CF;
  • 减法溢出:7FFFh - 0FFFFh,检验OF;
  • 零结果:1234h - 1234h,检验ZF。

这组数据覆盖了标志位的大部分关键行为,而且每种情形都有明确的预期结果。把这些数据逐条执行并核对,基本就能确认对ADD/SUB的理解没有偏差。

6.3 移植到更高位宽

如果用的是32位汇编或64位汇编,只需要把寄存器从AX换成EAX或RAX,数据定义从DW换成DD或DQ,其余逻辑一致。这种做法能让这套练习的知识在今后工作中无缝迁移,不会只停留在16位古董模式下。

7. 常见问题与排查技巧实录

这一节记录实际学习中最多被问到的问题,以及对应的排查思路。这些问题都来自真实使用场景,有些是逻辑错误,有些是认知误区。

7.1 明明结果是“对的”,为什么CF或OF不对

这种情况最常出现在“结果宽度”理解不到位的时候。举例:

mov al, 0FFh add al, 01h

从8位寄存器AL的视角看,结果是0,CF=1。但如果有人把AL理解成“总该是256吧”,就会觉得CF应该为0,因为“256没有溢出”。这种纠结的根源是:AL只有8位,根本存不下256。真正能存256的寄存器是AX,但上面这段代码操作的是AL,不是AX。寄存器宽度决定能表达的范围,范围决定进位行为是否发生。

排查办法:算之前先想清楚目标寄存器是多少位的,把结果按那个宽度截断,再看最高位有没有进位/借位。不要按数学直觉判断进位。

7.2 OF和CF到底怎么选

这也是高发困惑。一个常见场景:比较两个数谁大,到底看CF还是OF?

一句话概括:如果两个数被当成无符号数,看CF;如果被当成有符号数,看OF。CMP指令之后,无符号比较用JA/JB一类跳转(内部看CF/ZF),有符号比较用JG/JL一类跳转(内部看OF/SF)。编译器在生成代码时,会依据数据类型帮你决定该看哪个标志。手写汇编时,这个决定必须由你自己做。

7.3 多字节加法,高位的进位为什么老丢

典型错误代码:

add ax, cx add bx, dx ; 错误!忽略了低16位的进位

正确写法应该是:

add ax, cx adc bx, dx

如果现在还是觉得“ADD也能用”,只能说明CF的传递价值还没真正吃透。多字节算术不是数值拼图,进位必须一站一站传下去。SBB同理。

7.4 减法结果正确,但CF方向总搞反

很多人会问:既然SUB在硬件上是“加补码”,那“借位”和“进位”到底怎么对应?

这件事情的直观解释是:0 - 1 = -1,无符号视角不够减,要向高位借位,所以CF=1。很多芯片文档里明确写的是“SUB的结果在无符号解释下若为负,则CF=1”。用“借位”这个词来描述减法里的进位标志,虽然历史包袱很重,但也算约定俗成。记住结论就好:无符号减法发生借位时,CF=1。

7.5 调试时看到的标志位和手册对不上

先检查调试器是否开启了“标志位显示高级模式”,有的调试器默认隐藏AF,或者把OF显示在奇怪的位置。还有一个常见原因:刚执行完一条MOV指令,所有标志位维持原样,有些人看到标志位没变就以为自己操作错了。MOV不变标志位是特性,不是缺陷。判断一条指令是否影响标志位,可以查指令集手册里的“Flags Affected”栏,这栏才是权威。

8. 避坑清单:长期实战换来的提示

以下内容是长时间折腾汇编之后总结出的经验,供参考:

  • 不要让ADD/SUB指令的两个操作数同时指向内存,先加载到寄存器再运算。指令集不支持这种写法,硬写只会让汇编器报错。
  • 千万不要用ADD指令来做“带进位的加法”,除非你确实不想管低位进位。ADD和ADC的分工是不同的,用错场合就会得到错误结果。
  • 使用立即数时注意宽度匹配。add al, 100h这类写法,汇编器会在编译阶段就报警或截断,完全不进入运行阶段。
  • 判断有符号数溢出,一定要看OF,不要看SF。SF只是符号位,不能单独代表“结果错乱”。
  • 写多字节加减法时,先写低位和后写高位,顺序不能乱。低位指令影响CF,高位指令消费CF,顺序一旦颠倒,数据全错。
  • 手册中的“无符号”和“有符号”不是CPU层面的概念,而是程序层面的解释方式。CPU只按位操作和置标志,怎么解释是程序员的事。

9. 常见问题速查表

问题原因解决方案
加法结果无符号溢出但OF为0误用了有符号视角的判断判断无符号溢出时,看CF,不要看OF
减法结果正确,CF却为1无符号借位的正常体现理解SUB在硬件上等价于加补码
ADD执行后标志位完全没变前面刚执行的是MOV/LEA等不影响标志位的指令核对指令手册的Flags Affected栏
多字节加法少了一部分没用ADC,进位丢失低位ADD,高位ADC
多字节减法结果错误没用SBB,借位丢失低位SUB,高位SBB
负数相加结果看起来“错”没有检查OF看OF确认是否发生了有符号溢出
调试器显示的结果与手算不一致寄存器宽度不同或立即数被截断统一按目标寄存器的宽度来分析

10. 小结之后再往前走一步:这部分内容还能怎么用

累述一遍“ADD和SUB很重要”没有意义,倒不如说说这篇文章覆盖的知识点接下来能引向哪里。后续章节会进入AND、OR、XOR、NOT、TEST等逻辑运算指令,那些指令同样基于Flags机制工作。XOR清空寄存器的惯用法、TEST判断位状态,都和这一节里讲的标志位思路完全一致。学会了ADD/SUB与Flags的关系,逻辑运算指令基本是同一套方法论换皮。

再往后,乘除法指令对溢出行为的处理思路也与本文内容呼应。MUL和IMUL分别针对无符号与有符号数设置溢出标志,如果CF/OF理解不到位,乘除法调试会比加减法痛苦得多。

如果打算做一个小型项目来巩固本文内容,推荐实现一个“多精度十进制加法器”。这个项目会把ADD、ADC、AF标志、DAA调整指令全部串起来,整体难度不大,但足够训练“标志位驱动逻辑”的思维方式。

我个人在实际操作中的体会是:学汇编最忌讳的事就是“只看书不跟踪寄存器”。哪怕只是短短几行的ADD和SUB,只要在调试器里单步走一遍,标志位的直观印象就能建立起来;而一旦建立起这种印象,后面学什么指令都快。各位读者不妨从今天这份代码开始,把每条指令执行前后的寄存器值和四个核心标志位抄在一张草稿纸上,不再凭“感觉”写条件判断,一切以调试器的实际状态为准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询