8086汇编循环程序设计:从LOOP指令到嵌套循环的避坑指南

发布时间:2026/10/17 15:05:35

8086汇编循环程序设计:从LOOP指令到嵌套循环的避坑指南 第一次用8086汇编写循环大概率不是为了实现多复杂的功能多半就是做一个延时程序——我当时也是这样。写之前觉得无非是重复执行某段代码真正写完一跑才发现一个晚上全耗在它为什么不停下来上。回头看问题全都出在我对循环本质的理解上8086的指令集里根本没有循环这个概念只有跳转只有一个计数寄存器CX。所谓循环程序设计就是用有限的指令组合把一段代码重复执行多少次这件事精确控制住。这篇文章我会从最底层的JMP加条件跳转说起把LOOP指令的执行细节、条件循环、嵌套循环的计数器保护完整过一遍最后送上我实际调试循环代码时积累的排查方法以及几个让循环更稳的写代码习惯。内容不绕弯子代码可以直接抄原理部分也会把为什么讲清楚。无论你是在准备微机原理考试、做课程设计还是以后要接触嵌入式、驱动这类每天跟寄存器打交道的开发这些东西都用得上。1. 循环的本质8086没有循环指令只有跳转 计数1.1 从无条件跳转到条件跳转8086里和循环直接相关的指令其实就两大类一类是JMP这种无条件跳转一类是Jcc这种条件跳转。JMP不管三七二十一直接改IPJcc要看标志位——ZF、CF、SF、OF——满足条件才跳不满足就顺序往下执行。这就是循环的全部原材料。想让一段代码重复跑唯一办法是给它贴个标签在代码末尾写一条满足条件就跳回标签的跳转指令。重复几次靠一个计数器来管。这里有个值得停下来琢磨的点高级语言的for是先判断次数够不够够了再执行汇编里最常见的写法反而是先执行一遍再判断要不要再来一次。一个前置判断、一个后置判断在小工作量循环里体感差异不明显但一开始就把这个思维转过来后面读嵌套循环、分析边界条件会顺很多。1.2 DEC JNZ最原始的循环骨架先看一个不依赖任何循环专用指令的写法MOV CX, 8 ; 计数初值打算循环8次 NEXT: ; 这里是循环体随便放什么指令 DEC CX ; 每轮结束计数减1 JNZ NEXT ; CX不是0就跳回NEXT继续这个骨架干的事情非常直白CX从8开始每执行一轮循环体就减1一直减到0JNZ发现ZF为1不再跳转程序从NEXT后面继续走。整个过程正好执行8次循环体。别小看DEC JNZ这个组合。它其实是理解后面所有循环的原型LOOP指令无非就是把这俩动作打包了。你先看明白这个原始版本再去看LOOP就知道打包到底省了什么、保留了哪两个动作。不少教材把这层拆开讲一遍就过去了实际写代码时恰恰是这层拆解能帮你判断一个循环到底会跑几次——是N次、N-1次还是N1次很多bug就差在这一个数上。2. LOOP指令的完整执行细节它是DECJNZ但有三个关键区别2.1 LOOP的执行顺序和CX0的陷阱LOOP是8086里最典型的计数器循环指令。标准写法MOV CX, 100 LOOP_START: ; 循环体 LOOP LOOP_START它执行时做了两件事先把CX减1然后判断CX是不是0不为0就跳回目标地址。注意顺序是先减后判。这个先减后判带来一个非常反直觉的陷阱如果你把CX设成0想让循环执行0次结果会执行65536次。因为CX先减1变成FFFF非0于是跳转。8086的CX是16位寄存器从0回绕到FFFF是十六进制下正常的行为。我最初写延时程序时随手设了MOV CX, 0结果程序表现得像彻底卡死。实际不是卡死是它正在卖力地执行65536次循环。所以记住MOV CX, 0意味着满次数循环而不是零次循环。想循环N次就写MOV CX, N并且你要清楚N至少是1。这跟高级语言里i0; in的直觉完全不同初学阶段最容易栽在这里。2.2 LOOP与DECJNZ的三个区别既然LOOP是DECJNZ的封装那直接用它不就行了可以但你必须知道三个区别否则会在奇怪的地方翻车。对比点LOOPDEC JNZ指令条数1条2条是否影响标志位不影响任何标志位DEC会改ZF、SF、OF跳转范围短跳转约±128字节JNZ也是短跳转但可搭配JMP扩展CX为0时的行为CX回绕为FFFF执行65536次同样回绕为FFFFJNZ成立同样65536次第一个区别最容易被遗忘。DEC会修改标志位如果你在循环体里刚刚做了一次CMP指望循环体结束时ZF还保留着刚才的比较结果DEC这一下就把它冲掉了。LOOP不碰标志位在某些场景下反倒是个优点。第二个区别是跳转范围。LOOP和所有条件跳转一样跳转目标只能在当前IP前后约127字节内。循环体稍微写长一点——中间多展开几条复杂指令——就可能超出范围汇编阶段直接报错。这种时候最实用的处理方式是下面这个加了兜底的结构MOV CX, 100 LOOP_START: ; 这里放任意长度的循环体 ; 如果中途想退出可以用分支直接跳到LOOP_DONE DEC CX ; 把判断放到循环末尾 JZ LOOP_DONE ; CX变成0退出。DONE就在旁边短跳转够用 JMP LOOP_START ; 近跳转覆盖任意长度的循环体 LOOP_DONE: ; 循环结束后继续执行的代码DEC和JZ挨在一起两者之间的距离只有几个字节所以JZ的短跳转范围不会爆而回到循环头的JMP是无条件的近跳转理论上能覆盖整个64KB段。这样就把短跳转范围这个限制绕过去了。当然现实中最常见的做法还是把循环体里能提出去的代码提出去让循环体保持精简。这既是性能需要也是可维护性需要——循环体越长出错的概率越高。2.3 数组求和一个可以直接抄的完整例子把上面的知识合起来写一个完整的8086汇编循环程序求5个字节数据的和。DATA SEGMENT ARR DB 10, 20, 30, 40, 50 TOTAL DB 0 DATA ENDS CODE SEGMENT ASSUME CS:CODE, DS:DATA START: MOV AX, DATA MOV DS, AX XOR AX, AX ; 累加器清零别让AH里的残留值混进来 MOV CX, 5 XOR SI, SI ; SI作下标从0开始 SUM_LOOP: ADD AL, ARR[SI] ; 取ARR[SI]加到AL INC SI LOOP SUM_LOOP MOV TOTAL, AL ; 存结果 MOV AH, 4CH INT 21H CODE ENDS END START这里有几个细节值得说。第一累加用的是AL而不是AX所以求和前必须确认结果不超出8位范围否则要用ADD AX, ...并且循环前用XOR AX, AX把整个AX清零一劳永逸地排除高位残留。第二SI在整个循环里只被INC修改没有其他指令碰它这就是一个干净的指针寄存器用法——循环程序设计里给每个寄存器分配好单一职责能少掉大半的调试时间。第三循环体只有三条指令完全在LOOP短跳转范围内不用担心越界报错。写完这个例子你已经把计数器循环的三板斧都摸到了初始化计数器、写循环体、修改计数器或指针。接下来看条件循环那是循环程序的第二个世界。3. 条件循环不知道循环次数时靠标志位说话3.1 用CMPJcc搭出直到……为止的循环很多时候循环的次数事先根本不知道。比如扫描一个以0结尾的字符串你得判断当前字符是不是结束符是就停不是就继续。这时候计数器派不上用场需要的是每轮去检查一个条件。核心写法是这样的LEA SI, STRING ; SI指向字符串开头 SCAN: MOV AL, [SI] ; 取出当前字符 CMP AL, 0 ; 和结束符0比较 JZ DONE ; 相等就跳出去 ; —— 这里是每轮对字符的处理 —— INC SI JMP SCAN ; 处理完回到扫描点 DONE:这个模式可以理解成高级语言里的while循环先检查条件条件成立就在循环体内干活干完继续检查条件不成立就彻底退出。和LOOP的区别在于循环体里必须自己负责改变下一步条件——上面例子里就是INC SI让下一次检查的是下一个字符。如果忘了INC这个循环就永远检查同一个位置结果必然是死循环。这类死循环不像CX0那种还有迹可循它看起来完全是卡死但本质上只是少了关键的那一行。3.2 把WHILE和DO-WHILE翻译成汇编条件循环的检查位置放在哪是有讲究的。放在循环开头就是while (条件) {体}条件一开始不成立循环体一次都不执行。放在循环体末尾就是do {体} while (条件)至少执行一次。上面的字符串扫描是while结构。如果想把检查放到末尾会变成LEA SI, STRING SCAN: MOV AL, [SI] ; 对字符的处理 INC SI CMP AL, 0 JNZ SCAN ; 不是结束符就继续注意这个版本检查的是上一轮取到的字符循环体至少会执行一次——哪怕字符串本身就是空串第一个字符就是0你仍然会对它做一轮处理。要不要这样取决于你的语义需求。实际写代码时我建议明确告诉自己你要的是至少执行一次还是可能零次。教材例题里多的是do-while形态因为它天然适合先处理再判断的循环代码紧凑。但真实需求经常是while形态这种时候不要硬套do-while否则边界情况必错。判断的标准就一句话这个循环如果一开始就不满足条件应不应该做任何一次处理应该就用do-while形态不应该就用while形态。3.3 循环的可读性优先于一切技巧还有一个容易被忽略的细节在循环次数少、内部又有复杂分支时LOOP的先减后判反倒不如DECJNZ直观。因为DECJNZ让你能随时在循环体里提前跳出而且你能清楚看到计数变化的时刻。至于循环体可能执行0次的场景可以先判断再进入循环或者利用JCXZ这类专门检查CX是否为0的指令作为入口守卫。说到底循环程序设计的核心不是什么炫技指令而是把什么时候继续、什么时候停表达清楚。选择标准很简单明确知道要执行多少次或者次数可以预先算出来——用LOOP顺手。次数要等运行到某个条件才确定——用CMPJcc的条件循环。循环体可能要执行0次——用while形态必要时先判断再进入。计数和条件判断都具备的场景两种组合都行优先可读性。4. 嵌套循环里最容易翻车的计数器争夺战4.1 外层CX被内层LOOP冲掉一次典型翻车嵌套循环最常见的就是延时程序内层循环负责磨时间外层循环负责把整个延时拉长到指定长度。初学版本长这样MOV CX, 100 ; 外层计数 OUTER: MOV CX, 200 ; 内层计数这里就出问题了 INNER: LOOP INNER LOOP OUTER这段代码的外层循环永远只执行一次。为什么因为执行到MOV CX, 200的时候外层计数100已经被覆盖了。内层循环把CX从200减到0退出后LOOP OUTER发现CX是0直接退出。整个程序只跑了内层200次循环外层一次完整的轮次都没跑完。这个问题的根源是8086能用的寄存器本来就少而LOOP偏偏固定用CX。你在内层用CX就把外层的信息冲掉了。如果用的是高级语言编译器这种事编译器会帮你安排寄存器手写汇编就得自己想办法。4.2 标准解法PUSH CX / POP CX以及换寄存器方案教科书标准答案很朴素进入内层前把外层计数压栈内层结束后再弹回来。MOV CX, 100 OUTER: PUSH CX ; 保护外层计数 MOV CX, 200 ; 设置内层 INNER: NOP ; 循环体NOP只是占位 LOOP INNER POP CX ; 恢复外层计数 LOOP OUTER这样外层CX在进内层前被压到栈里内层随便怎么用CX都不影响外层内层结束后POP回来LOOP OUTER读到的是正确的外层计数。虽然压栈、弹栈多花几条指令的时间但对循环结构来说这点开销完全值得——正确性永远优先。这里我踩过一次很典型的坑内层循环里如果有条件分支提前跳出一定要记得在分支路径上也处理好POP否则栈里面的数据会错位程序后面要么弹错值要么RET时连返回地址都弹错。我的习惯是任何一段含PUSH/POP的循环代码在写之前先把所有退出路径上的栈平衡画一遍不要只顺着主路径想。不想用栈的话还有一个更简单的方案把内层计数器换成BX或DX。MOV CX, 100 ; 外层用CX OUTER: MOV BX, 200 ; 内层用BX做计数 INNER: DEC BX JNZ INNER ; BX没到0就继续 LOOP OUTER内层用DECJNZ而不是LOOP计数寄存器从CX换成了BX两边互不干扰也不用压栈。缺点自然也有DEC会影响标志位循环体里如果依赖标志位就得小心。两种方案各有取舍我自己一般优先用压栈方案因为它保持了计数器统一放CX的惯例可读性更好。4.3 延时程序嵌套循环的实战价值课堂之外嵌套循环做延时是最经典的用途。给一个能估算运行时间的两级延时MOV CX, 100 ; 外层重复100轮 DL_OUT: PUSH CX MOV CX, 0FFFFH ; 内层65535轮 DL_IN: LOOP DL_IN POP CX LOOP DL_OUT按标准8086的时序手册来算LOOP指令在跳转成立时大约17个时钟周期跳转不成立最后一次约5个周期。内层65535次LOOP跳转成立的有65534次合计约111万个周期外层重复100轮总计约1.11亿个周期。如果系统主频是5MHz每周期0.2微秒整个延时大约是22秒。这个数字不需要背但你要明白思路循环延时的时间等于总周期数×单周期时间。想算出预期时长就得对每条指令的周期数有大致概念。不过要提醒一句这种循环延时的时间完全依赖CPU主频而且会被中断严重影响。真实场景做准确延时一般用8253/8254这类可编程定时器或者在循环里读取系统时钟。循环延时只适合大概拖个几毫秒到几秒这种要求不高的场合——课程设计里做个闪烁灯、写个动态扫描数码管显示完全够用。5. 循环写错之后一份完整的排查链路5.1 给症状做个对照表循环写错症状其实就那么几类。我把自己这些年踩过、帮别人排过的问题归了个类症状最可能的原因检查方向程序卡死像死循环条件永远为真或MOV CX, 0造成65536次循环检查退出条件、检查CX初值循环体少执行一次计数器初值少1或while/do-while形态选错重数到底要执行几次结果值不对但循环次数看着对寄存器在循环体里被无意修改单步追踪SI、DI、AL等值数组越界、内存被写乱指针没递增或递减循环次数和数组长度不匹配看SI、DI和CX的对应关系栈错乱、RET乱跳循环体有PUSH没POP或分支路径栈不平衡检查每条退出路径的栈操作这张表不是让你背的而是排查时的入口。下面我顺着一次完整的翻车经历把排查过程完整走一遍。5.2 一次完整的排查排序循环为什么越界假设写了一段选择排序外层循环用CX管理排序轮数内层又用CX找最小值结果程序一运行内存就被写乱。我是这样排查的。第一步先不猜用调试器单步。进入循环前用R命令看CX、SI、DI的初值确认它们和数组长度对得上。这一步最容易被跳过但90%的问题在初值上就能暴露。比如数组长度是10结果CX初值是9那就直接定位到计数初值错误。第二步单步执行几轮内层循环每轮停下来看SI的变化。排序的内层循环里SI应该从i1一路走到数组末尾如果走过头了还没退出或者跳过了某个位置说明内层循环的退出条件写错了。这一步能快速验证指针更新和循环边界的匹配关系。第三步查内层循环用的计数器——果然内层又用了CX而且进入内层前没有保存外层CX。于是外层循环的轮数在第一次内层结束时就已经全乱排序实际执行的轮数不等于原定的轮数内存越界就这么发生了。排到这一步问题基本清楚了。修复方案就是前面讲的PUSH CX/POP CX或者干脆把内层计数器换成DX、BX。但我想强调的不是解法本身而是排查顺序先查初值再单步跟踪地址变化最后才怀疑指令用错。很多人一上来就反复读代码读三遍读不出问题不如老老实实单步走五遍。汇编代码的量级本来就不大单步反而比盯代码猜快得多。5.3 循环体里看不见的手子程序清空CX的教训还有一个坑我印象特别深。有次调一个循环结构看起来非常标准初始化、循环体、LOOP全都在可循环就是多了个莫名少了最后一个元素的毛病。后来发现问题不在循环本身而在循环体里调用了一个子程序那个子程序内部用了CX做自己的计数退出时也没有恢复。这就是手写汇编的经典陷阱循环体里的每次CALL都可能把CX、SI、DI这些寄存器全部改掉而你毫无察觉。高级语言有调用约定保护寄存器手写汇编可没有编译器帮你完成这些。所以我的习惯是所有内部子程序在开头就写清楚本子程序会修改哪些寄存器要么用PUSH/POP保护它要用的寄存器要么在调用方这边进子程序前保存、返回后恢复。如果你发现循环次数神秘地不对先别怀疑LOOP写错了回头看看循环体里有没有调用子程序、子程序有没有动CX。这个排查方向比盯着LOOP烧脑高效得多。6. 让循环更稳的三条实战习惯6.1 循环入口想清楚每个寄存器是干什么的手写汇编最大的敌人是状态混乱。写循环之前花一分钟用注释写清楚循环入口的寄存器约定收益非常大。比如; 循环功能把SRC的前COUNT个字节复制到DST ; 入口CX COUNTSI SRC地址DI DST地址 ; 出口AX 0DI DST COUNTCX 0 COPY_LOOP: MOV AL, [SI] MOV [DI], AL INC SI INC DI LOOP COPY_LOOP别嫌注释啰嗦。等程序出了问题你要能倒推此刻CX还剩多少、SI指向哪没有这行约定你只能靠猜。我后来读别人写的汇编第一件事也是看他有没有写这种约定——写了的程序多半能看懂没写的只能痛苦地人肉执行。6.2 循环内能算出结果的就不要反复算循环体里尽量避免重复计算那些与循环变量无关的值。比如数组首地址、某个固定偏移、一个常量掩码在循环前算好放进寄存器。这既是性能问题也是正确性问题——循环体越短LOOP的短跳转范围越不容易爆出错的概率也越低。举个小例子如果每次循环都要用到一个固定基地址与其在循环体里反复用LEA重算不如循环前算好放BX或DI里循环体里直接用[BXSI]这种基址加变址寻址。又省指令又少一个用错寄存器的机会。顺带一提循环结束后验证循环后的状态断言是个被低估的好习惯。写循环前先想好执行N次之后CX应该等于0SI应该指向最后一个元素之后的位置AX应该累加了N个数的和。然后拿实际跑的结果去验证这些断言。汇编程序的正确性本质上就是这些循环前后状态是否成立。想清楚这一点再多的坑也都能绕开。6.3 循环标签的命名是小细节但影响很大循环标签我习惯用表意功能名加唯一编号比如SUM_LOOP、SCAN_ROUND、DL_OUT、DL_IN而不是L1、L2一路堆到底。理由很实际嵌套循环里盯着L1/L2很容易搞混哪个是哪个。标签注释里顺手写上此标签是内层循环入口这类信息调试时一眼就能定位。最后再分享一个小习惯。写完一个循环我会在纸上先写下它的预期状态迁移从CXN、SI首地址开始每轮处理一个元素N轮之后CX0、SI越过最后一个元素。然后用调试器验证这个迁移是否真实发生。汇编不像高级语言有那么多编译器帮你兜底循环正确与否最后靠的就是你对寄存器状态是否了如指掌。把这个基本功练扎实循环程序设计这门课你就算真正过关了。
延伸阅读

更多相关文章

2026/10/17 15:05:35

Ranger Admin 2.0部署实战:从解压到权限策略生效

简介:面向使用 Apache Ranger 2.0 对 Hadoop 生态组件进行统一权限管理的运维或开发人员,这份 tar 包直接提供 Ranger Admin 控制台的编译后产物,省去源码编译可能花费的较长等待时间,解决网上文档多为安装说明而缺少现成可部署包…

2026/10/17 15:05:35

人工智能赋能护理风险预警:从EHR数据到临床决策

简介:这份演示文稿围绕人工智能在护理领域的应用进展展开,面向护理专业学生、临床护理人员及医疗信息化从业者,梳理人工智能在护士职业替代、患者护理与健康管理三大场景中的落地路径,并介绍决策树、支持向量机、神经网络等算法的…

2026/10/17 18:55:58

RHEL 7.6环境Oracle 19c+ASM+Data Guard完整部署实战

简介:面向RHEL 7.6环境下的数据库运维与系统架构人员,这份指南聚焦Oracle 19C数据库基于ASM存储配置DataGuard物理备库的完整部署流程,帮助读者从零搭建具备高可用、高性能与灾难恢复能力的企业级数据库环境。包体为单个PDF文档,大…

2026/10/17 18:55:58

Python校园志愿服务管理系统开题答辩:设计思路与高频问答复盘

这段经历还得从答辩教室的空调声说起。说实话,开题答辩之前,我一直以为它是个“走过场”——反正选题都定了、框架也搭出来了,老师象征性问两句也就结束了。真正站在讲台上讲完那十分钟,面对评委老师接二连三的追问时,…

2026/10/17 18:55:58

SQL Server 2012老机器安装图解教程:从下载到配置完整指南

简介:面向 SQL Server 2012 初学者的安装图解教程,围绕 Windows 7 SP1 32 位环境下的完整部署流程展开,重点讲解安装前软硬件参数核对、三个安装包的下载与解压、SQL Server 安装中心各模块用途、全新独立安装向导、实例配置、身份验证模式与…

2026/10/17 18:55:58

Python类型提示Type Hints详解:从基础语法到静态类型检查实践

Python 类型提示(Type Hints)详解说实话,我最早看到 Python 类型提示的时候,内心是抵触的。Python 本来就是动态语言,写起来自由,随手一个变量想塞什么塞什么,加类型注解总觉得像是自己给自己套…

2026/10/17 18:55:58

K8S核心数据底座:从零部署单节点etcd全流程解析

学K8S和真正动手搭K8S之间,隔着一个经常被忽略又极其关键的组件——etcd。我见过不少朋友聊Pod、Deployment头头是道,但问一句“K8S把集群状态到底存在哪”,答案就开始含糊了。实际上K8S控制面的所有状态,节点的调度结果、Pod的实…

2026/10/17 18:50:58

无线通信基础知识:从发射机、超外差接收机到PLL频率合成器

简介:面向通信工程、电子信息及相关专业初学者,这份无线通信基础知识 PPT 适合课堂辅助教学或入门自学。内容系统梳理无线通信的固定与移动两大形式,以及按工作频段、通信方式、调制方式和信息类型划分的多维分类体系;在此基础上&…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/17 14:20:58

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑