ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

循环程序设计实战:LOOPZ与REPZ CMPSB选型及调试避坑指南

循环程序设计实战:LOOPZ与REPZ CMPSB选型及调试避坑指南

简介:这是一份面向微机原理与接口技术课程学习者的循环程序设计实验资料,围绕重复控制指令、串操作指令及重复前缀的格式与功能展开,包含验证题与设计题两道程序编写与调试任务,适合正在练习汇编循环结构、需要实验参考的学生使用。资料包为1个doc文档,体积224KB,内容涵盖实验目的、源程序清单、实验分析,以及验证题中两种比较程序的SI、DI初值设置与优劣对比;设计题要求统计BUF字数据区第一个零数据后正负数个数,并分别采用重复控制指令和串操作指令实现,有助于读者理解计数控制与条件控制循环的适用场合。该资源目前已有2187人学习下载,对于需要完成类似实验报告或巩固汇编循环编程的读者具有直接参考价值。

1. 循环程序设计没你想的那么简单:一次跑一下午的翻车经历

循环程序设计,在微机原理课的实验里看着只有“验证 + 设计”两道题,真正上手调 16 位汇编的人都知道,这个循环一点都不“简单”。我当时拿着实验三的题目,代码抄下来跑一遍,结果屏幕上出来的数字跟手算预期差得离谱——不是统计个数不对,就是 FLAG 单元的值始终没变化。折腾一下午,最后发现是 REPZ CMPSB 的段寄存器没设置,数据比较根本没发生在同一个段里。那种感觉就像被一块黑匣子卡住,逻辑看起来全对,机器就是不给面子。

这篇想跟你聊透“循环程序设计”这个实验真正要掌握的东西:重复控制指令、串操作指令和重复前缀的选用逻辑,以及计数控制循环和条件控制循环各自该在什么场合出场。如果你正准备做这个实验,后面三段源程序和踩坑复盘可以直接照搬思路;如果你已经交过报告但对“为什么这里用 LOOPZ、那里用 REPZ”还是糊的,这里也给了判断依据。最后我还总结了一套从验证题到设计题的通用手法,练熟了能少走很多弯路。

2. 验证题两种写法:LOOPZ 手写循环与 REPZ CMPSB 串比较的选型对比

验证题的场景再简单不过:有两块等长的数据区 BUF1 和 BUF2,要求从第一个字节开始逐个比较,完全相同就给 FLAG 置 0,只要中间有一个字节不同就给 FLAG 置 -1。题目本身不难,难的是它给了两份思路完全不一样的源程序,让人第一次意识到“循环”不止一种写法。这一章我会按拿到实验题时最自然的思考顺序来讲:先读懂两份代码的执行细节,再从工程角度做对比,最后提炼出判断两块数据区是否相同的通用套路。

2.1 两份源程序在 SI/DI 初值上的区别:先加后比与先比后加

先看第一份源程序,它走的是“手动循环”路线:

DSEG SEGMENT BUF1 DB 21H,3aH,36 BUF2 DB 31H,0c7H,36 COUNT EQU $-BUF2 FLAG DB 0 DSEG ENDS SSEG SEGMENT STACK DB 80H DUP(0) SSEG ENDS CSEG SEGMENT ASSUME DS:DSEG,SS:SSEG,CS:CSEG START: MOV AX,DSEG MOV DS,AX MOV SI,OFFSET BUF1-1 MOV DI,OFFSET BUF2-1 MOV CX,COUNT NEXT: INC SI INC DI MOV AL,[SI] CMP AL,[DI] LOOPZ NEXT JZ OK MOV FLAG,-1 OK: MOV AH,4CH INT 21H CSEG ENDS END START

这里最值得研究的细节是 SI 和 DI 的初值都减了 1:SI 指向 BUF1 首地址前一个单元,DI 指向 BUF2 首地址前一个单元。原因很简单——循环体是“先 INC 再比较”,通过把指针初始化为首地址减一,第一次进入循环 INC 之后正好落到第一个字节上。这样做的好处是,每次比较完两个字节后,循环体末尾的 LOOPZ 决定是否继续时,指针已经指向下一次要比较的位置了,不需要额外跳回去“补一个 INC”。初值减一,本质上是和循环体结构做了配合。

第二份源程序则是完全不同的思路,直接用串操作指令的重复前缀来驱动循环:

DSEG SEGMENT BUF1 DB 21H,3aH,36 BUF2 DB 31H,0c7H,36 COUNT EQU $-BUF2 FLAG DB 0 DSEG ENDS ;use16 SSEG SEGMENT STACK ;use16 DB 80H DUP(0) SSEG ENDS CSEG SEGMENT ASSUME DS:DSEG,ES:DSEG,SS:SSEG,CS:CSEG START: MOV AX,DSEG MOV DS,AX MOV ES,AX MOV SI,OFFSET BUF1 MOV DI,OFFSET BUF2 MOV CX,COUNT CLD REPZ CMPSB JZ OK MOV FLAG,-1 OK: MOV AH,4CH INT 21H CSEG ENDS END START

CMPSB 这条串指令本身就是“先比较再自动调整指针”,所以 SI 和 DI 直接指向数据区首部就行,不需要减一。而它自动调整的是 SI 和 DI 两个寄存器,方向由 DF 标志位决定,因此前面必须放一条 CLD 把 DF 清零,保证从低地址向高地址逐个比较。此外还有一条关键设置:CMPSB 的目标操作数默认在 ES 段里,所以代码里必须让 ES 指向数据段,这也是验证题两份程序在段寄存器使用上最大的差异。

提示:第一份程序没有必要同时用 SI 和 DI 两个指针指向 DS 段内的不同位置;第二份程序必须显式设置 ES,否则 REPZ CMPSB 会到随机段地址取数,比较结果完全不可信。

2.2 从三个维度对比两种实现:代码量、执行逻辑与效率

两种实现摆在一起时,我一般会从三个维度去比较,这也是做选型时最实用的角度:代码清晰度、真实执行路径、以及指令开销。

先看代码量。手写循环版本要自己维护指针、自己写比较指令、自己组合 LOOPZ 判定退出;串指令版本从 CLD 到 REPZ CMPSB 总共三条有效指令,数据区比较的逻辑完全交给硬件循环。这个差距在只有三个字节的数据区上不明显,如果数据长度是几百甚至上千字节,手写循环的代码和出错概率都会线性上升。

再看执行路径。手写版本的循环体里有一条 INC SI、一条 INC DI、一条 MOV AL,再加 CMP 和 LOOPZ,相当于每一轮比较都要执行五次指令操作;REPZ CMPSB 版本每一轮只执行一条指令,指针移动和比较都由处理器内部完成。8086 的 LOOPZ 虽然自带“CX 减一 + ZF 判跳”的组合能力,但它终究还要配合循环体里的多条指令一起工作,而串指令的重复前缀是在一条指令内部完成整个循环,这个效率差距是本质性的。

边界行为也需要对照。两个版本在“全部相等”和“中途不等”两种情况下的表现一致:LOOPZ 是“ZF=1 且 CX 不等于 0 才继续循环”,REPZ 前缀语义完全相同,所以两种写法在退出循环后都必须用一条 JZ 判断“刚才是因为 CX 到 0 退出,还是因为比较不等退出”。如果全部相等,退出时 ZF=1,说明不必修改 FLAG;一旦不等,ZF=0,就执行 MOV FLAG,-1。

对比维度手写循环版本REPZ CMPSB 版本
代码行数约 8 条指令约 3 条指令
每轮比较指令数5 条左右1 条
指针维护方式循环体内手动 INC硬件自动 SI/DI 增减
段寄存器要求只用 DSDS 与 ES 都必须设置
可读性适合教学观察简洁但需要熟悉串指令语义
边界判断LOOPZ 退出后靠 JZ 分情况REPZ 退出后同样靠 JZ 分情况

如果只是验证数据区是否相同,我会毫不犹豫选 REPZ CMPSB 版本,但前提是内存紧张或数据量大;教学工作里手写版本反而更合适,因为它的每一步都摊开在眼前,方便对着实验报告讲“循环三要素”。这就是三个维度对比得出的第一层结论:没有绝对好坏,只有场合不同。

2.3 判断两块数据区是否相同的通用套路:循环退出与 ZF 标志的配合

第二份源程序里有一个技术细节值得单独讲:CMP 指令不允许两个操作数都是内存单元。手写版本里必须先写 MOV AL,[SI],再执行 CMP AL,[DI],就是因为单条指令里没法写“比较 [SI] 与 [DI]”,必须借寄存器中转。这个限制在 8086 汇编里是硬性的,很多新手第一次写循环比较时直接写 CMP [SI],[DI],编译就报错,然后卡在这一步不知道该把哪个值搬到寄存器。实际上只要你按“取一个到 AL,另一个作为比较目标”的模式来写,问题就自动消解。

通用套路可以归纳成三步。第一步,确定比较方向与指针初值,先加后比就把指针设为首地址减一,先比后加就直接指向首地址。第二步,用 LOOPZ 或者 REPZ 驱动循环,两者语义相同,退出后都要靠 JZ 区分退出原因。第三步,单独处理 FLAG 赋值,全部相等时 ZF=1,路径直接落到 OK;一旦中途比较不等,ZF=0,LOOPZ 会立刻中断循环,跳出去后 JZ 不成立,自然执行 FLAG 置 -1 的操作。

; 判断两个等长数据区是否相同的标准过程 ; 入口:SI = BUF1 偏移,DI = BUF2 偏移,CX = 字节数 ; 出口:FLAG = 0 表示相同,FLAG = -1 表示不同 TEST_CMP: CLD REPZ CMPSB JZ SAME MOV FLAG,-1 JMP DONE SAME: MOV FLAG,0 DONE: RET

这套三段式结构(初始化 → 重复比较 → 按 ZF 分支)就是在处理“多条件循环”时的规范答案。循环终止有两个条件,一个来自计数寄存器 CX,一个来自数据本身(两个字节是否相等),LOOPZ 把它们合并成一条指令的判断逻辑,而退出后必须用 ZF 再回答一次“到底哪个条件触发了退出”。这个“先合再分”的思路,无论你以后写 8086 汇编,还是转去写 C 语言里的 memcmp,底层都是同一个模型。

3. 设计题攻防:定位第一个零数据后统计正负数,两种搜索指令的写法差异

设计题比验证题上了一个台阶:先在 BUF 数据区里找到第一个 0 数据,再统计它后面那些数据中正数和负数各有多少,结果分别存入 POSI 和 NEGA。题目还专门要求搜索第一个零数据时要分别尝试重复控制指令和串操作指令,换句话说就是要阅读者掌握两套搜索工具。这章的攻防点在于:不只是写出能跑的循环,还要理解搜索循环和统计循环各自属于哪种控制方式。

3.1 原始程序的流程分析:先定位零,再分类统计的循环结构

原始源程序里给出的版本是纯手写循环,完整流程如下:

data segment buf db 23h,04h,11h,00h,0a3h,15h,6bh,0a7h,0c8h num equ $-buf+1 posi db 0 nega db 0 data ends code segment assume cs:code,ds:data beg: mov ax,data mov ds,ax mov si,offset buf mov cl,num xor bl,bl xor al,al loop: dec cl cmp byte ptr [si],0 jz next1 inc si jmp loop next1: inc si dec cl cmp cl,0 jz exit cmp byte ptr [si],0 jl next2 jz next1 inc bl mov posi,bl jmp next1 next2: inc al mov nega,al jmp next1 exit: mov ah,4ch int 21h code ends end beg

这个数据区是 23H、04H、11H、00H、0A3H、15H、6BH、0A7H、0C8H,第一个零在第四个位置。按题目要求,跳过这个零之后,从 0A3H 开始统计:0A3H 是负数,15H 是正数,6BH 是正数,0A7H 是负数,0C8H 是负数,所以正确结果应该是正数 2 个、负数 3 个。

程序的第一段搜索循环用的是“手动移动指针 + 比较后跳转”的模式:每轮 DEC CL 减少计数,用 CMP 判断当前字节是否为零,为零就跳出搜索循环进入统计阶段,不为零就 INC SI 继续。这里本质上是一个条件控制循环,因为它不是机械地循环固定次数,而是依赖数据内容来决定何时终止,尽管它同样用 CL 做计数,但计数只是防止越界的手段,真正中断循环的是“找到零”这一条件。

统计阶段的处理也值得注意:找到零之后先 INC SI 跳到零后面的第一个数据,然后进入一个逐字节判断的循环。先判断剩余次数是否归零,再判断当前字节正负。负数用 JL,正数用递增 BL 的方式,遇到下一个零则跳过继续统计。这个循环结构把“计数控制”和“条件控制”混在了一起,但它每一轮都必须检查 CX 和符号两个条件,属于典型的多条件循环。

3.2 用 REPNZ SCASB 重写零定位:串扫描指令的正确打开方式

题目要求搜索第一个零数据时采用串操作指令,常规解法是把“搜索零”这个阶段换成 REPNZ SCASB。SCASB 是串扫描指令,它把 AL 的值和目标串逐个比较,每比较一次自动调整 DI,加上 REPNZ 前缀后,就会重复扫描直到相等或 CX 归零。因为我们要找的是零值,所以先把 AL 清零,再让 DI 指向 BUF 首地址,设置长度后一句扫描指令就能替代上面那一整段手写搜索循环。

; 用 REPNZ SCASB 定位第一个零字节 ; 入口:DS=ES=数据段,DI = BUF 偏移,CX = 数据区字节数 mov al,0 mov di,offset buf mov cx,num cld repnz scasb ; 退出后 DI 已经指向零后面的第一个字节 mov si,di

这段代码的核心收益在于省掉了一段十行的搜索循环,而且 DI 的自动递增逻辑比手写循环更可靠。注意 REPNZ 的语义是“不等则继续”,也就是说每次比较后只要 ZF=0 就重复执行,直到相等才停下,这和“找第一个相等元素”的需求完全吻合。退出后 DI 已经越过零,指向它的下一个单元,正好拿来做统计循环的起点,不需要额外的 INC。

那段的设置是关键。SCASB 的目标地址默认在 ES:[DI],这意味着使用这条指令前必须保证 ES 和数据段指向同一个段,除非你刻意要跨段扫描。很多人在这个实验里翻车,不是因为逻辑写错,而是忘记初始化 ES,导致 REPNZ SCASB 在错误的段里搜遍整个内存也找不到那个零,循环次数耗尽后进入统计阶段,所有结果自然全错。这个翻车点我在后面排查章节会再展开一次,但它值得在这里提前预警。

注意:REPNZ SCASB 的退出有两种情况,一种是真的找到了零,ZF=1;另一种是 CX 耗尽但没找到,ZF=0。设计题的源数据里零是存在的,但如果你换测试数据时把零删了,程序会在统计阶段读到数据区后面的随机字节,这是这类串扫描代码最隐蔽的边界问题。

3.3 计数控制与条件控制的适用场合:什么时候该用哪种循环

两段程序跑下来,最核心的认知应该是:循环程序不只是 LOOP 一条指令覆盖所有场景,而是按“能否预先确定循环次数”划分成两大类。计数控制的典型代表是验证题里的长度为 COUNT 的循环,循环次数在初始化时就是确定的,循环体按固定次数重复,终止条件只有 CX 归零一个。条件控制则完全不同,循环次数在写代码时未知,什么时候停靠数据内容说了算,设计题里“找第一个零”就是最典型的场景。

这两种控制方式的判断路径我一般这样走。先问第一个问题:进循环之前我是否知道最多重复多少次?如果知道,并且循环体里没有需要中途退出的判断,就放心用 LOOP 指令。如果循环体里存在“某个条件成立就立刻停止”的需求,就要用 LOOPZ 或 LOOPNZ,把“计数归零”和“条件成立”两个终止信号合流。如果一开始就不知道要循环多少次,完全依赖扫描结果来终止,那就应该走条件控制路线,用 CMP + Jcc 组合,或者直接用 REP 前缀配合 SCAS、CMPS 这类串指令。

控制方式终止条件典型指令适用场景
计数控制仅 CX 归零LOOP数据区长度已知,必须从头跑到尾
条件控制数据内容满足特定条件CMP + JZ / JL搜索特定值,或按符号/大小分支
计数与条件混合CX 归零与数据条件同时存在LOOPZ / LOOPNZ顺序比较两个数据区直到不等或跑完
硬件重复CX 归零与数据条件由处理器判断REPZ CMPSB、REPNZ SCASB块比较、块扫描、块传送

设计题的高质量做法是把两段循环分开设计:前一段“找零”用条件控制或串扫描指令,后一段“统计正负”用条件控制逐步遍历。如果硬要把整个统计过程压缩进一条 LOOP 循环里,反而会因为跳转分支过多变得难读、难调。第一段循环决定“从哪里开始”,第二段循环决定“每个数据怎么归类”,结构清晰了,调试的时候才能一眼看出问题出在搜索阶段还是统计阶段。

4. 循环实验常见问题排查:五个容易翻车的细节与修复

循环程序设计实验的每一个坑,几乎都出在“指针、计数、标志位”这三样东西的状态没对齐上。这一章按我当时调试时实际踩到的问题排序,列出五个高发故障,每一条都按“现象 → 原因 → 解决”来写。你要是跑数据结果不对,直接按这个清单逐一排查,多数情况下能在十分钟内定位。

4.1 COUNT 越界与 EQU 位置:数据区大小算错导致循环次数错乱

现象:验证题程序运行后 FLAG 值不对,打印出来是乱七八糟的数字,有时甚至是负数。把数据区字节数逐个手数,明明只有 3 个字节,程序却像比较了 7、8 次一样。 原因:COUNT EQU $-BUF2 是通过当前位置减去 BUF2 起始地址来算字节数的,它必须出现在 BUF2 定义之后。如果把这条 EQU 写到了数据区定义之前,$ 取到的地址还是段的开头,减出来的结果完全不是数据区长度;更隐蔽的写法错误是把它放在 BUF2 之后但漏掉了其他数据项,导致 COUNT 统计进去的字节数与真正的数据区不一致,循环就会多比或者少比。 解决:写完数据区后立即定义 COUNT,并且用表格或手数核验一次它与 BUF2 数据的元素个数相等。如果中途修改过数据区内容,记得重新计算 COUNT,这种“改数据忘了改长度”的问题最容易在实验当场出现。

4.2 指针初值习惯性写错:第一个元素被跳过或重复比较

现象:源程序 1 跑出来的结果和源程序 2 不一样,比如 BUF1 的第一个字节 21H 和 BUF2 的第一个字节 31H 明明不等,FLAG 却显示数据区相同。 原因:手写循环版本要求“先 INC 再比较”,如果按常规思维把 SI 直接设为 OFFSET BUF1,进入循环后第一次 INC 会越过 BUF1 的首字节,从第二个字节才开始比较,整个数据区相当于在“错位”状态下对比。反过来,如果拿到串指令版本的代码却把指针初值减一,CMPSB 的自动递增会把第一个元素重复比较一次。 解决:写循环之前先标好指针动作的方向。先 INC 再比较就把初值设为首地址减一;先比较再 INC 就把初值直接设为首地址。遇到串指令时记住 CMPSB 内部自动做“比较 → 修改指针”,它的初值永远是数据区首地址,不需要减一。

4.3 条件转移写错导致死循环:先判断后跳与先处理后跳

现象:设计题的程序运行起来没有输出,或者调试器里看到程序卡在某一行反复执行,SI 地址一直停留在同一个位置。 原因:搜索循环里如果先 INC SI 再比较,那么当数据区里没有零时,指针会一直往前走直到越界,而越界后读回来的数据可能永不为零,循环就停不下来。另一种常见写法错误是把 JZ 写成了 JNZ,把“为零跳转”写成“非零跳转”,直接导致搜索循环翻转了逻辑,结果要么立刻误判,要么在相同条件下反复执行同一个分支。 解决:在定位循环里,先判断当前字节是不是零,再把“不是零才移动指针”的 INC 放在跳转目标里,保证搜索逻辑是“非零则移动,为零则停下”。用 JMP 回跳时,一定要检查循环结构是否存在从循环体中部跳回的路径,这种半路跳回最容易造成指针不更新、条件不变,从而死循环。调试时用单步执行盯住 SI 的值变化,就能快速确认指针到底有没有往前走。

4.4 寄存器语义不清:AL 当计数器用导致统计结果凭空翻倍

现象:设计题的统计结果里,正数和负数的个数总比手算结果大一倍,比如数据区明明只有 2 个正数,POSI 里却存了 4,而且数值看起来很规整地翻倍。 原因:原始源程序里用 BL 统计正数、用 AL 统计负数,它们都是累加器式的“计数变量”。如果在循环体里不小心写错顺序,把 INC BL 放在了循环跳转之后而不是在正数分支内部,BL 每次经过扫描点时都会被额外加一,或者把 AL 和内部中断的某些返回值混用,计数就会被污染。更隐蔽的是,16 位汇编中 AL 是算术逻辑运算的默认寄存器,如果在统计循环里又想用 AL 做数据比较,原来的负数计数会被比较结果覆盖。 解决:统计功能尽量用 BL/BH 这类专用寄存器,避免用 AL,因为 AL 在整个循环过程中太容易被其他操作占用。如果要沿用 AL,每次累加前先确认在这之前没有执行过 MOV AL,[SI] 这类把统计数据冲掉的操作。另一个稳妥做法是把统计值直接累加到内存变量,每次用 INC POSI 或者 INC NEGA,虽然指令多了一点,但能避免寄存器互相踩踏。

4.5 题目说字数据区代码用 DB:审题与技术选型要对得上

现象:设计题要求统计“字数据区”,但源程序里定义用的是 DB,数据区只有 9 个字节;用字定义去重新写数据区,结果整个程序的逻辑都不对,统计个数远多于预期。 原因:这类实验题目里“字数据区”和“字节数据区”经常被混用,实际源程序按字节定义也能跑出正确结果,但如果你严格按“字”的概念去理解,把每个数据都当成 16 位来扫描,第一个零就永远找不到,因为高字节和低字节的零出现位置可能错开。审题不清和代码实现不一致,是这类报告最容易被扣分的地方。 解决:先确认数据区里存的是字节还是字,再看 JZ/JL 判断的长度单位。按题目要求重写时,要么把所有 CMP 都改成 CMP WORD PTR [SI],0 并相应调整步长,要么就明确输出结论为“按字节数据处理满足实验目标”,避免代码与题意脱节。这个习惯也能延伸到其他汇编实验里:数据宽度定了,比较、步长、计数全部跟着定,才不会出现“定义一套、使用另一套”的混乱。

5. 从验证题走到设计题的思维升级:三种循环结构的选型方法论

做完两个实验题,收获不只是会写几段汇编,而是建立了一套循环程序的分析框架。这套框架可以概括成“三要素”加“三问”,再辅以走查表来验证逻辑。到了这一步,你才算真正把循环程序设计从“抄代码”变成了“设计代码”。这一章把方法论讲透,以后再遇到任何循环场景,都能直接套用。

5.1 循环三要素:初始化、终止条件、指针流动

任何循环程序,无论指令是 LOOP 还是 REPZ,都逃不开三个构成要素。第一个是初始化,包括循环计数器的初值、指针的初值、方向标志位的设置。验证题两份程序在 SI/DI 初值上的一点点差别,就是初始化阶段的设计差异,它决定了整个循环体的写法。第二个是终止条件,计数控制循环的终止条件是 CX 归零,条件控制循环的终止条件是数据比较结果,混合循环则要同时考虑两类条件,LOOPZ 这类指令就是为混合条件设计的。第三个是指针流动,手写循环靠 INC 显式移动,串指令靠 CLD/STD 控制方向的隐式移动,流动方向一旦和预期不一致,整个程序的错位就开始了。

把这三个要素想清楚,写代码时就有了骨架。我习惯在写任何一段循环前,先用注释把三要素列出来,然后再写指令。比如验证题第二份程序的注释应该是“初始化:SI/BUF1,DI/BUF2,CX=3,方向清除;终止条件:CX=0 或比较不等;指针流动:CMPSB 自动 +1”。这样的注释放在实验报告里,老师一眼就能看出你是真懂还是抄代码。

只要有循环三要素做支撑,“为什么 LOOPZ 退出后还要 JZ 再判断一次”这个问题就不需要背答案。LOOPZ 把“CX 不为零”和“ZF 为 1”合在一起作为继续循环的条件,退出时有两种可能,必须再读一次 ZF 区分。如果漏掉 JZ,程序就把“全部相等”和“中途不等恰好 ZF=1 后归零”两种情况混为一谈,结果自然错乱。

5.2 选型判断路径:从循环次数到终止条件到指令选择

拿到一个循环需求,我一般会在心里快速问三个问题,然后决定用哪种结构。

问题一:进循环前循环次数是否已知?如果已知且不需要中途退出,走计数控制,直接 LOOP。如果次数未知,靠数据内容决定是否继续,走条件控制。问题二:循环体里是否存在两个及以上的终止条件?如果既有计数终止又有数据条件终止,优先考虑 LOOPZ/LOOPNZ,或者用 REPZ/REPNZ 前缀配合串指令,让硬件帮你管理“双条件同时判断”的复杂性。问题三:循环里是否有指针需要同步移动?如果只是简单遍历并统计,手写循环够用;如果要大块地比较或拷贝数据区,用串指令的前缀版本,指针维护交给硬件,既省指令又省心。

把这三个问题组合起来,选型路径是非常清晰的:次数已知 + 单条件 → LOOP;次数未知 + 数据条件 → CMP + Jcc;次数已知 + 双条件 → LOOPZ 族;数据块操作 → 串指令 + REP 前缀。这套路径用于验证题,得到的是 REPZ CMPSB;用于设计题的找零阶段,得到的是 REPNZ SCASB。两条结论在实验要求里都踩中了“重复控制指令和串操作指令的选用”这个考察点。

5.3 用走查表验证循环程序:边界值与标志位的核对方法

循环程序写完,怎么确认逻辑正确?我的方法是不急着上机,先做一轮纸面走查。走查表的列包括:步数、SI 当前指向的数据、CL/BX/AL 的值、比较结果、分支去向。以设计题的统计阶段为例,数据从零后面的 0A3H 开始,走查表的前几行应该是这样的:

步数当前字节CL 剩余正数计数 BL负数计数 AL分支结果
10A3H600jl next2,负数 +1
215H501非负非零,正数 +1
36BH411非负非零,正数 +1
40A7H321jl next2,负数 +1
50C8H222jl next2,负数 +1
6边界判断123cl 归零,退出

走查表最大的价值在于强迫你逐条执行指令,而不是在脑子里“感觉对”。表格走到最后,正数 2、负数 3,和手算预期一致,代码才算过关。这个习惯我到后来写任何语言的循环,包括 C 语言和 Python 里的循环逻辑,都还在用,只不过把表格换成了打印日志。程序越难 debug,越要回归到这种最原始的“人肉执行”。

6. 把循环程序设计练成肌肉记忆:一张自查清单与一个坚持的习惯

实验做完了,如果只是把报告交上去,过两周再写循环程序还是会卡壳。真正让这个实验产生长期价值的,是把“三要素 + 三问 + 走查表”固化成一个可重复使用的自查流程。这一章给出我后来一直在用的一组检查项,以及几个能帮你把验证题、设计题吃得更透的延伸练习。

检查项具体操作常见误判
计数初值核对 CX 或 CL 与数据量一致把元素个数和字节数混在一起算
指针初值先加后比设为地址减一,先比后加设为首地址两种模式互相套用导致错位
段寄存器用串指令前确认 DS 与 ES 一致忘记 MOV ES,AX
方向标志CLD 确保低到高扫描上一次程序把 DF 置 1 没清
终止条件LOOPZ/REPZ 退出后用 ZF 分支漏掉退出后的条件转移
数据宽度DB 配字节指令,DW 配字指令题目说字操作代码仍用 DB 比较
边界数据验证数据首尾是否有零或相等值最后一个字节越界后仍执行统计

三个延伸练习值得在实验报告之外自己动手做一遍:第一,把验证题改成熟练的 CMPSW 字比较,两个数据区分别定义成 DW,体会数据宽度变化后 COUNT 计算和指针步长的联动调整;第二,把设计题改成“统计第一个零到第二个零之间的正负数”,难度只增加一点点,但你需要同时处理两个条件控制点,对循环结构的理解会更扎实;第三,把设计题的统计阶段改用 CX 递减实现计数控制,和原版本对比效率,能直观看到条件控制和计数控制在指令数量上的差别。

从实验到现在,我每次进入汇编循环程序的调试状态,第一个动作都固定不变:先在纸上标注这段循环的终止条件和指针流动方向,再动笔写代码或读别人的代码。这个习惯帮我挡掉了无数次指针错位和段寄存器遗漏,直接缩短了调试时间。循环程序设计实验不值得被当成一个“抄报告”的任务,你在这里建立的对指令语义的敏感度,会在后面接口实验、中断程序实验里反复派上用场,也算是对那段崩溃调试时光最好的回报。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进