资讯详情

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

📅 2026/10/24 13:36:35 | 华诺云谱 👁 阅读
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的比较结果——两个字节相等时ZF1继续比较下一个两个字节不等时ZF0终止重复。所以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且ZF1继续循环 JZ OK ; 退出循环后若ZF仍为1说明CX耗尽且全军相同 MOV FLAG,-1 ; ZF0说明中途遇到不同字节置-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 ; 重复比较字节ZF1且CX≠0时继续 JZ OK ; 结束后ZF1说明全部相同 MOV FLAG,-1 ; 否则置-1 OK: MOV AH,4CH INT 21H CSEG ENDS END STARTREPZ CMPSB这一条指令在内部展开后相当于执行了三次“比较、修改指针、CX 减一、判断 ZF”的序列。重点在最后的JZ OK如果三个字节全部相同最后一次CMPSB执行后ZF保持为 1随后CX减到零重复结束ZF1说明没有遇到不同字节反之只要某个字节不等那次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 自动完成循环终止条件CX0 或 ZF0CX0 或 ZF0数据段要求只用 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是正数预期结果是 POSI2、NEGA3。按照 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 $-buf1 ; 数据区字节数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作为回路入口。以测试数据手推一遍初始 CL10找零阶段把23h、04h、11h、00h四个字节依次扫过前三个非零使 SI 从 0 推进到 3第四个字节命零进入next1此时 CL 被消耗掉 4 次剩 6。进入next1后INC SI让 SI 指向0a3hDEC CL后剩 5正好对应零后 5 个数据。后续每读一个数据就DEC CL一次读满 5 个之后 CL 归零程序从exit退出。实际运行结果 POSI2、NEGA3与预期完全一致。这个例子展示了条件控制循环的一个优点循环次数不必预先精确到字节数用一个上限值加条件跳转就能在动态数据中灵活定位。代价是必须保证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 的最高位移入 CFCF 原来的值移入 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方向标志DF1REPZ CMPSB就会让 SI、DI 从数据区尾部向前递减比较。表现为两个数据区只要有一个对应位置相同而位置错开就可能误判为相同而且运行环境不同结果不稳定。解决在所有串操作指令CMPSB、MOVSB、STOSB等之前显式加一行CLD确保方向标志被清零。这个习惯对后续做字符串拷贝、数据块搬移也通用。5.4 现象方案二初始化了 DS却忘记把 ES 指向数据段比较结果完全不符合预期原因CMPSB指令的硬件逻辑固定以DS:SI对ES:DI寻址。ASSUME DS:DSEG,ES:DSEG只是告诉编译器“我打算这样设置”不会真的往段寄存器写值。如果只写了MOV DS,AXES 仍停留在加载程序时的初始值DI指向的是一块未知内存拿它和BUF1逐字节比较结果自然离谱。解决初始化时期必须写上MOV ES,AX并且它与MOV DS,AX的指令顺序无关两行都在读取DSEG段地址之后执行即可。遇到串操作相关的实验先检查这两个寄存器是否都指向了正确的段。5.5 现象设计题用num equ $-buf1定义长度程序偶尔对但往数据区末尾追加数据后统计结果出错原因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字节长度 ; 出口AX0 表示相同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 / DFSIBUF1DIBUF2CX3DF0CMPSB 进行中SI / DI / CX / ZF每执行一次 SI1、DI1、CX-1REPZ CMPSB 后ZF全部相同为 1中途不同为 0用 DEBUG 的T命令逐条执行看到 SI 和 DI 的步进规律循环结构就基本不可能出错了。我早期调试汇编时总喜欢直接运行看结果结果代码里藏着“初值差一”“ES 没设”这类玄学问题浪费大量时间从那以后我每次写循环代码都强制先走一遍寄存器变化表把每一步 SI、DI、CX 的预期值写在纸上跑完对一遍再难缠的循环 bug 也能快速定位。这份实验报告里的代码虽然简单但把它拆开、改写成子程序、画出寄存器轨迹之后循环程序设计的底层逻辑才算真正变成自己的东西希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑