8086循环程序设计:LOOPZ与REPZ CMPSB的取舍与调试

发布时间:2026/10/24 13:37:23
8086循环程序设计:LOOPZ与REPZ CMPSB的取舍与调试

简介:这份微机原理与接口技术课程的循环程序设计实验报告资源,面向计算机专业学生及需要掌握汇编循环控制技术的初学者。资源以doc文档形式完整呈现实验目的、验证题与设计题源码、调试分析及实验报告内容,重点讲解重复控制指令、串操作指令和重复前缀的格式与功能,并通过判断BUF1与BUF2数据区是否相同、统计BUF数据区第一个零数据后正负数个数等实例,对比不同循环结构、指针初始化和串操作指令的适用场合。资源为单文件doc文档,压缩包大小224KB,报告内包含实验步骤、源程序、SI/DI指针设置分析等关键细节,便于读者对照学习与上机实操,也可作为课程实验报告模板。目前已有2187人学习使用,适合作为课程实验参考或汇编程序设计专题复习材料。

1. 循环程序设计实验:一次读懂两套循环方案的真实取舍

同样是判断两个等长数据区是否相同,我习惯性地写了十来行LOOPZ循环,逐字节比较,小心翼翼地处理 SI、DI 的初值;结果对照实验指导书看到参考答案,才发现一行REPZ CMPSB就结束了战斗,还把数据段、附加段、方向标志安排得明明白白。这份实验报告的核心素材,就是围绕这类问题展开的:验证题给出了两套对比方案,设计题则要求用循环结构统计数据区里第一个零之后的元素分布。它真正回答三个问题——循环程序设计里计数控制与条件控制怎么选,重复控制指令与串操作指令各自擅长什么,以及写完汇编程序后如何用调试器验证正确性。适合正在学微机原理、被 8086 汇编段定义绕晕的在校同学,也适合想快速看懂一段经典教学汇编代码的开发者。

2. 实验背景与指令选型:重复控制指令与串操作指令的适用边界

2.1 实验目的拆解:计数控制与条件控制两种循环结构

实验报告里反复强调一个概念:循环程序设计不只是“用循环把代码包起来”,还要在动手前想清楚循环的控制方式。8086 汇编里的循环大体分成两类——计数控制循环和条件控制循环。计数控制循环靠CX寄存器做倒计数,每执行一次循环CX减一,减到零就退出;条件控制循环则靠状态标志位决定是否继续,典型的就是判断ZF、SF、CF的值来跳转。

这两种结构不是互斥的,实际代码里往往嵌套使用。比如设计题里统计零数据后的正负数个数,外层找零的过程是条件控制——遇到0就跳出找零循环;内层对零后数据做分类的过程,又同时依赖ZF判断零、SF/JL判断负数,再靠CL的递减来保证扫描不会越过数据区边界。也就是说,计数控制解决“循环多少次”的问题,条件控制解决“循环什么时候停”的问题。

适用场合可以这么归纳:数据区长度已知、每一轮要做的事情完全一样,优先考虑LOOP/LOOPZ这类计数控制的写法;数据区长度未知、或者循环必须在满足某个条件时提前终止,就用条件转移指令配合标记位来控制。验证题里的两个数据区长度固定,所以理论上两种方式都能做;实验的目的就是让人对比之后发现,串操作指令在这种场景下代码量要少得多。

2.2 重复控制指令速览:LOOP 与 LOOPZ/LOOPNZ 的执行流程

8086 的重复控制指令其实是一组以CX作为内置计数器的短跳转指令。LOOP的执行流程是先把CX减一,再判断CX是否为零,不为零就跳转到目标标签,为零就顺序执行下一条。LOOPZ在LOOP的基础上多了一个条件,它要求CX不为零并且ZF等于一才继续循环;LOOPNZ则相反,要求CX不为零并且ZF等于零才继续。

这里有个新手容易忽略的细节:LOOPZ的退出有两种可能,要么CX减到零顺其自然退出,要么ZF被清零提前退出。验证题的程序一就是用这个特性实现“逐字节比较,一旦发现不同就立刻停止”的语义。如果只用LOOP,比较完全部字节才会停下来,中间发现的不同就只能靠后续的JZ再判断,效率低一截,而且当数据区恰好全部相同时,两种写法结果一样,很容易让人误以为LOOP也能正确完成这个任务——这属于典型的“概率正确”。

指令循环条件(CX先减一)典型用途
LOOPCX ≠ 0确定次数的计数循环
LOOPZCX ≠ 0 且 ZF = 1比较过程中相同则继续
LOOPNZCX ≠ 0 且 ZF = 0比较过程中不同则继续

2.3 串操作指令与重复前缀:CLD、CMPSB、REPZ 的组合逻辑

串操作指令是 8086 里一类非常特殊的指令,它们操作的对象是内存里的连续数据,而不是寄存器里的单个值。CMPSB比较DS:SI与ES:DI各自指向的一个字节,比较结果反映在标志位上,然后自动修改 SI 和 DI,让它们指向下一个地址。自动修改的方向由方向标志DF决定,CLD把DF清零,SI/DI 递增;STD把DF置一,SI/DI 递减。

单条CMPSB只能比较一个字节,要比较整个数据区,就得配合重复前缀REPZ。REPZ CMPSB表示“重复比较,直到 CX 为零或者 ZF 为零”,注意这里的ZF条件来自每一条CMPSB的比较结果——两个字节相等时ZF=1,继续比较下一个;两个字节不等时ZF=0,终止重复。所以REPZ CMPSB天然就是“比较两个等长数据区是否相同”的专用指令,循环结束后再判断一次ZF就能区分是全部相同还是中途发现了差异。

串操作指令对内存段有强制约定:CMPSB的比较对象固定是DS:SI对ES:DI。这要求程序设计者在初始化时不仅要把DS指向数据段,还要把ES也指向数据段。实验报告里的验证题程序二因此多了一行MOV ES,AX,并在段属性里写了ASSUME DS:DSEG,ES:DSEG。如果漏掉这一步,DI指向的 ES 段会在默认数据段之外,比较结果完全不可控——这是这类实验最高频的翻车点之一。

3. 核心代码拆解:验证题两版程序的 SI/DI 与 LOOPZ/REPZ 差异

3.1 方案一:LOOPZ 逐个字节比较的完整注释版

验证题第一版程序的核心思路是手动管理指针,每轮循环先让 SI、DI 自增,再取字节比较,最后用LOOPZ决定是否继续。完整代码加注释后是这样:

; 方案一:LOOPZ 逐个字节比较 DSEG SEGMENT BUF1 DB 21H,3aH,36 ; 数据区1,共3个字节 BUF2 DB 31H,0c7H,36 ; 数据区2,共3个字节 COUNT EQU $-BUF2 ; 当前位置减去BUF2起始地址,得到字节数3 FLAG DB 0 ; 结果标记,0表示相同,-1表示不同 DSEG ENDS SSEG SEGMENT STACK DB 80H DUP(0) ; 定义128字节堆栈 SSEG ENDS CSEG SEGMENT ASSUME DS:DSEG, SS:SSEG, CS:CSEG START: MOV AX,DSEG MOV DS,AX ; 初始化数据段寄存器 MOV SI,OFFSET BUF1-1 ; SI 指向 BUF1 前一个单元 MOV DI,OFFSET BUF2-1 ; DI 指向 BUF2 前一个单元 MOV CX,COUNT ; 循环次数等于字节数3 NEXT: INC SI ; 先自增,SI 指向 BUF1 当前字节 INC DI ; 先自增,DI 指向 BUF2 当前字节 MOV AL,[SI] ; 取出 BUF1 的当前字节 CMP AL,[DI] ; 与 BUF2 当前字节比较,结果影响ZF LOOPZ NEXT ; CX减1后不为0且ZF=1,继续循环 JZ OK ; 退出循环后若ZF仍为1,说明CX耗尽且全军相同 MOV FLAG,-1 ; ZF=0,说明中途遇到不同字节,置-1 OK: MOV AH,4CH INT 21H ; 返回DOS CSEG ENDS END START

这段代码最值得琢磨的是 SI、DI 的初值。OFFSET BUF1-1看起来奇怪,但配合循环体开头的INC SI就顺理成章:第一次进入循环时 SI 自增回OFFSET BUF1,恰好指向第一个字节。这种“先自增再使用”的写法,本质是避免在循环体末尾再做一次指针修正,代价是初值必须向前偏移一,两处逻辑要配套修改,少一个-1效果完全不一样——这个坑在后面专门讲。

3.2 方案二:REPZ CMPSB 的完整注释版

第二版程序的思路是彻底甩开手动指针管理,用串操作指令让 CPU 自己完成“取字节、比较、指针自增、计数递减”这一整套动作。完整代码加注释后是这样:

; 方案二:REPZ CMPSB 串比较 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, ES:DSEG, SS:SSEG, CS:CSEG START: MOV AX,DSEG MOV DS,AX MOV ES,AX ; ES也必须指向数据段,CMPSB固定使用ES:DI MOV SI,OFFSET BUF1 ; SI直接指向BUF1首部,不需要-1 MOV DI,OFFSET BUF2 ; DI直接指向BUF2首部 MOV CX,COUNT ; 重复次数=3 CLD ; 方向标志清零,确保SI/DI递增 REPZ CMPSB ; 重复比较字节,ZF=1且CX≠0时继续 JZ OK ; 结束后ZF=1说明全部相同 MOV FLAG,-1 ; 否则置-1 OK: MOV AH,4CH INT 21H CSEG ENDS END START

REPZ CMPSB这一条指令在内部展开后,相当于执行了三次“比较、修改指针、CX 减一、判断 ZF”的序列。重点在最后的JZ OK:如果三个字节全部相同,最后一次CMPSB执行后ZF保持为 1,随后CX减到零,重复结束,ZF=1说明没有遇到不同字节;反之,只要某个字节不等,那次CMPSB会把ZF清零并立即终止重复,JZ OK不会跳转,FLAG 被置为 -1。这里CMPSB和LOOPZ的判断逻辑完全等价,但寄存器开销少了很多。

3.3 两方案的核心差异:初值设置与指针自增的责任归属

实验报告里专门要求“对 SI、DI 初值设置上的区别作出分析”,这个问题的答案就藏在两个程序的指针管理方式上。

方案一里,SI、DI 的自增责任在程序代码本身。INC SI和INC DI是显式指令,初值因此必须放在BUF1-1、BUF2-1的位置,让第一次自增恰好落到数据区首字节。它的本质是“偏移量预减一”,和 C 语言里while(++p)的语义一模一样。方案二里,指针自增的责任被CMPSB指令内部接管,初值只需要直接指向首字节,CLD负责决定自增方向。如果把方案二的 SI 也改成OFFSET BUF1-1,第一个被比较的字节就会变成BUF1[0]对BUF2[-1],属于访问到数据区外。

从程序质量看,方案二的代码行数更少,寄存器的使用也更干净——SI、DI 不需要为了配合循环体而修改初值,语义直白。方案一的优势在于循环体内可以插入额外逻辑,比如比较的同时统计某种特征,灵活性更高。这个取舍在真实开发里也常见:能用专用指令完成的重复操作,优先用专用指令;需要在重复过程中做复杂判断的,再退回手动循环。

3.4 执行流程对比:两条指令路径的微观视角

对比维度方案一 LOOPZ方案二 REPZ CMPSB
指针初值OFFSET BUF1-1 / BUF2-1OFFSET BUF1 / BUF2
指针自增方式循环体内 INC SI / INC DICMPSB 自动完成
循环终止条件CX=0 或 ZF=0CX=0 或 ZF=0
数据段要求只用 DS必须同时设置 DS 和 ES
代码行数约 6 行核心循环1 条指令
灵活性可在循环内加判断逻辑难以插入额外操作

方案一执行 3 次循环,每次经历“INC、INC、MOV、CMP、LOOPZ”五条指令,一共 15 条指令;方案二只有一条REPZ CMPSB,虽然内部等效于 3 条CMPSB,但指令预取和译码开销更低,在 8086 的流水线限制下差距被放大,这正是实验要求对比优劣的实际意义。对初学者来说,能同时写出这两种方案,说明已经理解了指针、计数器、条件标志三者如何协同工作。

4. 设计题实战:统计零数据后正负数个数的完整环路

4.1 题目分析与循环结构设计

设计题的任务分两段:先在BUF字数据区中找到第一个零数据,再统计它后面各个数据中正数和负数的个数。测试数据是 9 个字节,内容为23h,04h,11h,00h,0a3h,15h,6bh,0a7h,0c8h,第一个零出现在第 4 个位置,零之后有 5 个字节,其中0a3h、0a7h、0c8h最高位为 1 是负数,15h、6bh是正数,预期结果是 POSI=2、NEGA=3。

按照 8086 的规则,判断正负最直接的方式是用JL指令。JL在SF与OF异或结果为 1 时跳转,对 8 位有符号数来说,最高位是符号位,负数比较时小于零成立,所以“当前字节小于 0”就等价于它是负数。不过实验报告代码里还有一个细节:字节用CMP byte ptr [si],0比较,这不仅判断了负数和正数,还把零单独区分出来,保证统计范围里不存在零干扰。

4.2 完整注释代码与寄存器职责

原实验报告里的设计题代码逻辑上能跑通,加注释后逐段功能如下:

data segment buf db 23h,04h,11h,00h,0a3h,15h,6bh,0a7h,0c8h num equ $-buf+1 ; 数据区字节数+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 ; SI 作为扫描指针,先指向 buf 首部 mov cl,num ; CL 作为总扫描次数,初始为 10 xor bl,bl ; BL 清零,用它累加正数个数 xor al,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

这段程序里寄存器分工很明确:SI是唯一的数据指针,CL是双重使用的计数器——找零阶段控制总扫描次数,统计阶段控制剩余元素次数;BL和AL分别是正负计数器。DEC CL出现在NEXT1入口处不是笔误,它保证了每次读取一个数据就消耗一次计数,最后CL归零时退出。

4.3 关键词跳转分析:从找零到分类计数的完整回路

整个程序的循环结构是两层嵌套:外层是loop标签到jmp loop的找零循环,内层是next1到jmp next1的统计循环。找零循环用jz next1作为出口条件,命中零数据立刻跳走;统计循环用jl next2处理负数、jz next1跳过零、inc bl处理正数,三个分支共用next1作为回路入口。

以测试数据手推一遍:初始 CL=10,找零阶段把23h、04h、11h、00h四个字节依次扫过,前三个非零使 SI 从 0 推进到 3,第四个字节命零进入next1,此时 CL 被消耗掉 4 次,剩 6。进入next1后INC SI让 SI 指向0a3h,DEC CL后剩 5,正好对应零后 5 个数据。后续每读一个数据就DEC CL一次,读满 5 个之后 CL 归零,程序从exit退出。实际运行结果 POSI=2、NEGA=3,与预期完全一致。

这个例子展示了条件控制循环的一个优点:循环次数不必预先精确到字节数,用一个上限值加条件跳转,就能在动态数据中灵活定位。代价是必须保证CL的递减次数与数据读取次数同步,否则要么漏统计最后一个元素,要么越界读到posi、nega的值。

4.4 附带的平均值程序:COUNT 与 DIV 的常见误用

实验报告里还附了一段求BUF字数据区平均值的代码,其中count equ $-buf得到的是数据区的字节总数,而DIV AL要求 AL 是除数。这段代码把字节总数当除数用,算出来的平均值显然不对——3 个字的平均值正确写法是MOV AL,3或先算出元素个数再除以元素个数。这种误用在汇编初学者里相当普遍,根源是混淆了“字节长度”和“元素个数”两个概念。

不过这段代码里有一个亮点值得单独拿出来讲:disp子程序用RCL BX,1逐位把 16 位二进制数送到 CF,再根据 CF 输出字符'0'或'1',实现二进制显示。这个技巧的核心是RCL的循环移位特性——将 BX 的最高位移入 CF,CF 原来的值移入 BX 最低位,配合LOOP指令循环 16 次,就能从最高位到最低位逐位输出。以后做任何需要按位输出的调试代码,都可以直接复用这个模板。

5. 汇编实验踩坑指南:五个常见运行异常与排查方法

5.1 现象:程序一运行就死循环,DOS 窗口卡住不退出

原因:设计题里如果BUF数据区中不存在零,找零循环里的jmp loop会让 SI 不断后移,越过数据区边界后继续读取posi、nega甚至栈区的内容,直到踩到不可预知的内存区域。

解决:给找零循环加一层计数保护,在执行inc si之前先检查CL是否为 0。我一般会这样改:loop: dec cl; jz exit; cmp byte ptr [si],0; jz next1; inc si; jmp loop,这样即使数据区里没有零,扫描到数据区末尾也会正常退出,不会把程序跑飞。

5.2 现象:验证题方案一去掉 OFFSET BUF1-1 的 -1 之后,大多数数据能判对,边界数据判错

原因:SI 初始为OFFSET BUF1时,循环体里第一步INC SI会让 SI 指向BUF1[1],第一个字节BUF1[0]根本没参与比较。如果BUF1[0]和BUF2[0]恰好相等,结果仍然正确;只有第一个字节不同时才暴露问题,属于修改代码后碰巧没触发错误。

解决:保持初值为OFFSET BUF1-1和OFFSET BUF2-1,并且两条INC必须都保留,任何一个去掉都会导致错位。更稳妥的做法是改成“先比较后自增”的循环结构,初值直接用OFFSET BUF1,比较完再INC SI,两种写法只能选一,混用必然出 bug。

5.3 现象:验证题方案二结果随机,有时相同误判为不同,有时不同误判为相同

原因:没写CLD。如果之前某处执行过STD,方向标志DF=1,REPZ CMPSB就会让 SI、DI 从数据区尾部向前递减比较。表现为两个数据区只要有一个对应位置相同而位置错开,就可能误判为相同,而且运行环境不同结果不稳定。

解决:在所有串操作指令(CMPSB、MOVSB、STOSB等)之前显式加一行CLD,确保方向标志被清零。这个习惯对后续做字符串拷贝、数据块搬移也通用。

5.4 现象:方案二初始化了 DS,却忘记把 ES 指向数据段,比较结果完全不符合预期

原因:CMPSB指令的硬件逻辑固定以DS:SI对ES:DI寻址。ASSUME DS:DSEG,ES:DSEG只是告诉编译器“我打算这样设置”,不会真的往段寄存器写值。如果只写了MOV DS,AX,ES 仍停留在加载程序时的初始值,DI指向的是一块未知内存,拿它和BUF1逐字节比较,结果自然离谱。

解决:初始化时期必须写上MOV ES,AX,并且它与MOV DS,AX的指令顺序无关,两行都在读取DSEG段地址之后执行即可。遇到串操作相关的实验,先检查这两个寄存器是否都指向了正确的段。

5.5 现象:设计题用num equ $-buf+1定义长度,程序偶尔对,但往数据区末尾追加数据后统计结果出错

原因:EQU是汇编期求值,$是当前位置。原代码里num定义在buf之后、posi之前,$-buf等于 9,再加 1 变成 10;可一旦在num和buf之间插入其他变量,这个长度就会跟着变。+1是为了抵消next1入口处的DEC CL,属于“用奇怪初值迁就循环结构”的写法。

解决:要么删掉next1入口的dec cl,把num改回$-buf;要么把num的定义挪到posi、nega之后,用offset posi - offset buf表达“数据区加结果区的全部长度”。我个人更推荐第二种,语义清晰,后续加变量不会破坏长度计算。

6. 二次开发与改造思路:把实验代码封装成可复用子程序

6.1 改造方向一:将验证题逻辑封装为通用比较子程序

实验代码直接写在START里,只能在数据固定时运行一次。把它改造成子程序,入口用寄存器传参,返回值用 AX 表达,就能在别的程序里反复调用:

; 比较两个等长数据区是否相同 ; 入口:DS:SI=数据区1首地址,ES:DI=数据区2首地址,CX=字节长度 ; 出口:AX=0 表示相同,AX=-1 表示不同 compare_len proc push cx push si push di cld repz cmpsb jz same mov ax,-1 jmp done same: xor ax,ax done: pop di pop si pop cx ret compare_len endp

调用前只需设置 DS、ES、SI、DI、CX,然后CALL compare_len,返回后优先检查 AX。前三行PUSH保护调用方现场是必须的,否则 CALL 返回后 CX、SI、DI 的值全被破坏。这里注意PUSH和POP的顺序必须相反,栈才能平衡,少写一个POP会让 RET 取到错误返回地址。

6.2 改造方向二:从字节比较扩展到字比较和任意数据区

把REPZ CMPSB改成REPZ CMPSW,子程序就变成比较两个等长字数据区是否相同,入口参数里的 CX 从字节数改为字数。如果比较对象是 16 位字数组,记得用COUNT EQU ($-BUF2)/2计算字数,不要直接拿字节数当 CX,否则会多比较一倍数据,越界访问到 FLAG 单元。

对于不等长的数据区比较,可以在子程序里加一个入口参数表示比较长度,然后分别传入两个数据的最大公共长度;也可以先用MOVSB把短数据拷到缓冲区,再走等长比较。实际项目里最常见的用法反而是将compare_len应用于结构体数组——把结构体当连续字节流比较,只要结构体内没有填充字节,比较结果就能直接反映两个结构体是否相等。

6.3 验证方法:用 DEBUG 单步跟踪定位循环问题

写完子程序后不要急着跑完整程序,先在关键位置设断点。以REPZ CMPSB为例,单步跟踪时依次检查三处状态:指令执行前,确认 SI、DI、CX、DF 的值符合预期——SI/DI 分别指向两个数据区首部,CX 等于长度,DF 为 0;执行完第一条CMPSB后,观察 SI/DI 是否各加一、CX 是否减一;全部执行完后,检查 ZF 的值,它决定了JZ是否跳转。

断点位置观察对象预期结果
REPZ CMPSB 前SI / DI / CX / DFSI=BUF1,DI=BUF2,CX=3,DF=0
CMPSB 进行中SI / DI / CX / ZF每执行一次 SI+1、DI+1、CX-1
REPZ CMPSB 后ZF全部相同为 1,中途不同为 0

用 DEBUG 的T命令逐条执行,看到 SI 和 DI 的步进规律,循环结构就基本不可能出错了。我早期调试汇编时总喜欢直接运行看结果,结果代码里藏着“初值差一”“ES 没设”这类玄学问题,浪费大量时间;从那以后我每次写循环代码都强制先走一遍寄存器变化表,把每一步 SI、DI、CX 的预期值写在纸上,跑完对一遍,再难缠的循环 bug 也能快速定位。这份实验报告里的代码虽然简单,但把它拆开、改写成子程序、画出寄存器轨迹之后,循环程序设计的底层逻辑才算真正变成自己的东西,希望帮到你。

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