简介:本资源是面向高校计算机类专业学生的《微机原理与接口技术》课程实验配套材料,聚焦循环程序设计核心技能训练,重点解决重复控制指令、串操作指令及重复前缀的选用与对比问题。资源以完整实验报告形式呈现,涵盖验证题(BUF1/BUF2数据区比对)与设计题(首个零值后正负数统计),并附有两套汇编源程序(含SI/DI寻址差异分析)、详细实验步骤、指令执行逻辑说明及结构化实验分析,助学习者深入理解计数控制与条件控制循环的适用场景。压缩包为1个DOC文档(224KB),内容包含淮海工学院标准实验报告模板、代码段注释、寄存器状态说明及上机调试要点,结构清晰、即开即用。目前已有2189人学习下载,适合微机原理课程实践环节巩固、期末复习及汇编语言循环结构专项突破。
1. 循环程序设计到底是什么:就像工厂流水线上的质检员,一遍遍重复做同一件事
你有没有见过老式工厂里那种传送带?零件一个个滚过来,质检员站在那儿,不说话、不休息,只干一件事:拿起零件,看一眼,放回去,再拿下一个……直到整条线跑完。他没换动作,也没改流程,只是把“看一眼”这个动作,机械地、稳定地、一遍遍执行下去——这就是最朴素的「循环」。
在微机原理里,“循环程序设计”说的正是这件事:让 CPU 像那个质检员一样,自动、可控、不厌其烦地重复执行一段指令序列。它不是靠人手敲一百遍MOV AX, 1,而是写一次逻辑,告诉机器:“从这里开始,做够 N 次,或者做到满足某个条件为止。”
为什么值得搞懂?因为现实中 80% 的底层数据处理任务——比如比对两段内存、统计一串数字里有几个正数、逐字扫描字符串找第一个空格——本质上都是“重复检查+判断+计数”的组合。不会写循环,等于只会用计算器算单步,永远迈不进真正编程的门槛。而这份《微机原理实验三:循环程序设计》不是讲概念,它是一份带着真实汇编代码、调试痕迹和血泪经验的“流水线操作手册”:里面两个验证题,一个用手工递增指针(SI/DI),一个用 REPZ 自动推进;两个设计题,一个手动跳转实现条件搜索,一个用串指令加速匹配——它们不是教科书里的理想模型,而是当年某高校实验室里,学生调了整整一下午才跑通的真实快照。你看懂了它们,就等于亲手拧紧了自己脑子里那台 CPU 的循环齿轮。
2. 为什么非得用循环?没有它,CPU 就是“单次射击的弓箭手”
2.1 循环的本质:用三行指令,替代一百次复制粘贴
想象你要把内存地址BUF1开始的 100 个字节,原样拷贝到BUF2。最笨的办法是什么?写 100 行:
MOV AL, [BUF1] ; 第1个字节 MOV [BUF2], AL MOV AL, [BUF1+1] ; 第2个字节 MOV [BUF2+1], AL ; ……(中间96行) MOV AL, [BUF1+99] ; 第100个字节 MOV [BUF2+99], AL这不仅累,更致命的是:一旦数据长度变成 101,你就得重写全部;长度变成 50,你得删掉一半——程序完全失去弹性。而循环把它压缩成三步:
- 设起点:
MOV SI, OFFSET BUF1 - 设次数:
MOV CX, 100 - 写核心动作+自动推进:
LOOP_START: MOV AL, [SI] MOV [DI], AL INC SI INC DI DEC CX JNZ LOOP_START ; 如果 CX≠0,跳回去重做
提示:
JNZ(Jump if Not Zero)是循环的“心跳开关”。它不关心你做了什么,只盯着CX寄存器:只要里面还有数,就立刻跳回开头。CPU 就是靠这个“看数跳转”的简单机制,实现了无限复用。
2.2 计数控制 vs 条件控制:两种“停下来的理由”,决定循环的生死边界
所有循环都必须回答一个问题:“我什么时候停下来?”答案只有两类,选错就死循环或漏处理。
计数控制(For-style):像倒计时炸弹,明确知道要炸几次。
典型场景:处理固定长度数组、执行固定次数运算。
实现方式:用CX寄存器装总数,每次循环DEC CX后用JZ/JNZ判断是否归零。
优势:绝对可控,绝不会多跑一步。
风险:如果实际数据少于预设数(比如COUNT EQU $-BUF2算错了),会越界读取垃圾内存。条件控制(While-style):像守门员,只认一个信号——“看到0就停”。
典型场景:搜索第一个特定值、读取未知长度字符串、等待外设就绪。
实现方式:用CMP+JZ/JNZ/JL/JG等条件跳转,在循环体内实时检测状态。
优势:适应性强,数据多长它跟多长。
风险:如果条件永远不满足(比如内存里根本没0),CPU 就卡死在原地,风扇狂转。
在实验的设计题中,搜索“第一个零数据”必须用条件控制——因为你根本不知道零藏在第几个位置;而比对BUF1和BUF2是否完全相同,则天然适合计数控制(已知长度COUNT),但实验偏偏又要求你用REPZ CMPSB这种带条件终止的串指令来对比,这就引出了下一个关键问题:为什么同一个任务,要用两种完全不同的循环写法?
2.3 重复控制指令 vs 串操作指令:CPU 内置的“快捷键”和“全自动流水线”
这是本实验最烧脑也最实用的核心对比。它不是“哪个更好”,而是“在什么物理约束下,哪个更省力”。
| 对比维度 | 重复控制指令(如LOOP) | 串操作指令(如REPZ CMPSB) |
|---|---|---|
| 本质 | 程序员写的“循环壳子”:自己管理计数器、指针、跳转逻辑 | CPU 硬件级“专用加速器”:一条指令触发一连串原子操作 |
| 寄存器依赖 | 任意寄存器(CX计数,SI/DI指针可自定义) | 强制绑定:CX必须为计数器,SI必须指向源,DI必须指向目标,DS/ES段寄存器必须正确设置 |
| 推进方式 | 手动写INC SI,INC DI(易错!) | 自动推进:CMPSB执行后,SI和DI自动 ±1(由CLD/STD控制方向) |
| 终止条件 | 完全由程序员控制(JZ/JNZ/JCXZ等) | 由前缀决定:REPZ= “重复直到ZF=0或CX=0”,REPNZ= “重复直到ZF=1或CX=0” |
| 适用场景 | 逻辑复杂、需中途干预(如边比对边统计)、教学理解底层机制 | 标准化操作:内存块拷贝(MOVS)、比较(CMPS)、填充(STOS)、扫描(SCAS) |
看实验中的两个验证题程序:
- 例1(手工循环):
MOV SI, OFFSET BUF1-1—— 故意把指针设在首地址前一位,这样INC SI后正好落到第一个字节。它用LOOPZ NEXT(LOOP+ZF=1条件),每次比较后手动INC指针。优点是逻辑透明,缺点是多写4行指令,且SI/DI初值容易算错(报告里专门分析了这个坑)。 - 例2(串指令):
CLD清方向标志 →REPZ CMPSB一条指令搞定全部。CPU 自动:取[SI]和[DI]比较 → 若相等且CX≠0,则SI++,DI++,CX--,再跳回比较;若不等或CX=0,立即退出。它把“比较+推进+计数+条件判断”四件事压进一个硬件周期,效率碾压手工循环。
注意:串指令不是万能的。它要求数据必须严格按字节/字/双字对齐,且
ES段寄存器必须指向目标段(例2中MOV ES, AX就是为此)。如果目标内存不在ES段,或者你要在比较时顺便把相等的字节改成0xFF,串指令就无能为力了——这时候,手工循环的灵活性就是救命稻草。
3. 循环程序设计的常见误区:别把“能跑通”当成“写对了”
3.1 误区一:认为LOOP指令和JMP+DEC CX+JNZ完全等价
为什么混:教材常简化说“LOOP就是DEC CX; JNZ target的缩写”,初学者就真当它们是同一回事。
怎么区分:
LOOP是单条指令,执行时:先CX = CX - 1,再判断CX == 0,若为0则不跳转,否则跳转。它不改变任何标志位(ZF/SF/CF等)。- 而
DEC CX; JNZ target是两条指令:DEC CX会修改ZF(当CX减到0时ZF=1),JNZ正是靠这个ZF判断跳转。
翻车现场:
MOV CX, 1 MOV AX, 0FFFFH DEC CX ; CX=0, ZF=1 JNZ SKIP ; 因为 ZF=1,所以不跳,执行下一行 → OK SKIP: MOV BX, 1但如果换成LOOP:
MOV CX, 1 MOV AX, 0FFFFH LOOP SKIP ; CX先减为0,因CX==0,不跳 → 也OK SKIP: MOV BX, 1看起来一样?错!当你在循环体里用了CMP或SUB,它们会改ZF,而LOOP完全无视ZF,只认CX。此时若误用JNZ替代LOOP,逻辑就崩了。血泪经验:LOOP只用于纯计数循环;涉及条件判断的循环,务必用DEC CX; JNZ/JZ显式控制。
3.2 误区二:REPZ CMPSB比对失败,第一反应是“数据写错了”
为什么混:看到FLAG没置0,直觉是BUF1和BUF2里有不同字节。
怎么区分:REPZ CMPSB的终止条件是ZF=0(不等)或CX=0(比完了)。如果CX设小了,比如COUNT EQU $-BUF2算成了2,但实际数据有3字节,那么第三字节根本没比,ZF还是上次的残留值(可能是1),REPZ就以为“全等”,直接退出——结果FLAG错误地保持为0。
排查口诀:
- 先查
CX初值是否等于数据长度(用DEBUG看CX寄存器); - 再查
SI/DI是否真指向数据首地址(DEBUG中D DS:SI和D ES:DI对比); - 最后确认
CLD是否执行(方向错会导致SI/DI乱减,越界读取)。
3.3 误区三:搜索“第一个零”时,用LOOP+CMP就够了,何必分两种写法?
为什么混:觉得“反正都是循环,能出结果就行”。
怎么区分:实验设计题明确要求“分别采用重复控制指令和串操作指令”,这不是为了炫技,而是暴露底层差异:
- 重复控制版(手工):
dec cl; cmp byte ptr [si],0; jz next1; inc si; jmp loop
它在CMP后立刻用JZ跳转,SI指针只在找到零之后才INC,所以next1标签处的[si]指向的是零的下一个字节——这正是题目要求的“第一个零数据后的各数据”。 - 串操作版(若强行用):
REPNE SCASB(扫描直到找到AL中的值)
但SCASB扫描时,DI(不是SI!)会自动推进,且扫描结束时DI指向匹配值的下一个位置。而题目给的数据区在DS段,SCASB默认用ES段——你得先把数据MOV ES, AX,再MOV DI, OFFSET BUF,再MOV AL, 0,最后REPNE SCASB。多出3条指令,还容易段寄存器配错。
结论:对于“搜索后处理”的复合逻辑,手工循环的指针控制更精准;串指令只适合“纯扫描”或“纯比较”这种单一目标。
3.4 误区四:use16伪指令可有可无,现代电脑都64位了
为什么混:看到use16就跳过,以为只是历史遗留。
怎么区分:use16是告诉汇编器:“这段代码运行在16位实模式下,用16位寄存器(AX/BX),地址计算用segment:offset”。实验所有代码(MOV SI, OFFSET BUF1、CLD、REPZ)都是16位实模式专属语法。如果你在64位 Windows 下用ml64(64位汇编器)直接编译,会报一堆error A2006: undefined symbol——因为OFFSET在64位下语义不同,REPZ需要REPE前缀,DS/ES段寄存器在64位模式下被废弃。
现实踩坑:某同学用 VS Code 装了nasm插件,写完就点运行,结果提示invalid instruction。折腾半天才发现,nasm默认生成64位代码,而实验要求必须用nasm -f obj -l listing.lst file.asm生成16位.obj,再用link16链接。use16就是这整个工具链的“安全阀”,缺它,代码从第一步就废。
4. 循环结构怎么选?一张表看清“该用谁”和“千万别用谁”
4.1 任务场景决策树:从需求反推循环类型
我们把实验中的四个任务拆解,对照底层约束,给出选择依据:
| 任务描述 | 推荐循环类型 | 关键原因 | 实验中对应代码 |
|---|---|---|---|
| 比对两个等长内存块是否完全相同 | 串操作指令(REPZ CMPSB) | 数据规整、纯比较、长度已知 → 硬件加速收益最大,代码最简 | 验证题例2 |
比对两个等长内存块,但需分析SI/DI初值影响 | 重复控制指令(LOOP+ 手动指针) | 需要精确控制指针起始位置(如-1偏移)、理解每步推进细节 → 教学目的优先 | 验证题例1 |
| 统计“第一个零之后”的正负数个数 | 条件控制循环(CMP+JZ+ 手动INC SI) | “第一个零”是动态位置,无法预知次数;且找到后需立即切换到后续数据处理 → 手工指针最灵活 | 设计题2(主逻辑) |
| 计算固定长度字数组的平均值(已知3个字) | 计数控制循环(LOOP或DEC CX+JNZ) | 长度确定(count equ $-buf)、操作统一(累加)→ 计数循环最稳,避免条件判断开销 | 设计题1(compute过程) |
提示:没有“绝对最优”,只有“当前约束下最稳妥”。比如设计题2若强行用
REPNE SCASB找零,虽可行,但需额外处理DI到SI的地址转换(SCASB用DI,而后续统计用SI),徒增复杂度——这就是典型的“为用而用,画蛇添足”。
4.2 参数陷阱自查表:5个必检项,避开90%的循环崩溃
循环写完跑不通?别急着重写,先对着这张表快速过一遍:
| 检查项 | 正确做法 | 常见错误示例 | DEBUG 验证方法 |
|---|---|---|---|
| 1. 计数器初值 | CX必须等于循环总次数(字节数/字数),用EQU $-label计算时,确保label定义在数据开头且无填充 | BUF DB 1,2,3,0后面多写了个DB 0,$-BUF算成5,但有效数据只有4 | R命令看CX值,D DS:0看数据实际长度 |
| 2. 指针初值 | SI/DI必须指向第一个要处理的数据单元,注意REPZ CMPSB要求SI指源首,DI指目标首 | MOV SI, OFFSET BUF1-1(例1)是对的,但若忘了-1,第一次INC SI就跳过首字节 | D DS:SI和D ES:DI直接查看内存内容 |
| 3. 方向标志 | CLD(清零)→SI/DI自动++;STD(置1)→SI/DI自动--;串指令前必须显式设置 | REPZ CMPSB前漏CLD,导致SI/DI乱减,越界读取FFFFH地址的垃圾数据 | R命令看DF标志位(0=CLD, 1=STD) |
| 4. 段寄存器 | REPZ CMPSB用DS:SI和ES:DI;MOVS同理;必须MOV AX, DSEG; MOV DS, AX; MOV ES, AX | 只设了DS,忘了ES,REPZ CMPSB会从ES=0段读取目标数据,得到全0结果 | R命令看DS和ES值是否相等 |
| 5. 终止条件逻辑 | JZ/JNZ必须紧跟在影响ZF的指令(CMP/SUB/TEST)之后;LOOP只认CX,与ZF无关 | CMP AL, 0后插了MOV BL, 1,再JZ next→MOV改ZF,JZ判的是MOV的结果,不是CMP的! | 单步执行(T命令),每步后R看ZF和CX变化 |
4.3 一份真实的调试日志:如何从FLAG=-1追到SI多加了1
这是实验报告里没写的“幕后故事”。某同学的验证题例1始终FLAG=-1(判定不等),但DEBUG查BUF1和BUF2明明一样。他这样做:
G =100运行到JZ OK行(地址100H),暂停;R查ZF=0(不等),说明CMP AL,[DI]刚执行完,结果不等;D DS:SI显示21 3A 36(BUF1),D ES:DI显示31 C7 36(BUF2)——首字节21Hvs31H,确实不等;- 但
BUF1和BUF2定义是DB 21H,3aH,36和DB 31H,0c7H,36,36是十六进制36H,不是36十进制!36十进制 =24H,而36H= 54 十进制。 - 他重新
D DS:0,发现BUF1后面跟着36H(即54),BUF2后面是36(即36十进制 =24H),二者不同! - 根源:汇编中
DB 36默认是十进制36,要写DB 36H才是十六进制36。他抄题时漏了H,导致数据本身就不等。
这个案例说明:循环逻辑可以完美,但输入数据错了,结果必然错。调试循环,永远要先确认“数据本身是否符合预期”,再查“循环是否按预期执行”。
5. 什么时候用循环程序设计?一份来自真实实验的避坑指南
5.1 该用循环的三个铁律场景
铁律一:处理连续内存块,且操作可抽象为“对每个元素做同一件事”
典型如:
- 数组求和、求平均(设计题1)
- 字符串转大写(
CMP AL,'a'; JNB skip; SUB AL,20H) - 内存清零(
MOV AL,0; REP STOSB)
为什么必须用:手工写MOV [SI],0; INC SI; MOV [SI],0; ...不仅冗长,且长度一变全报废。循环用CX控制次数,SI自动推进,一行REP STOSB顶一百行。
铁律二:需要根据运行时状态动态决定执行次数
典型如:
- 读取用户输入直到回车(
CMP AL,0DH; JNZ loop) - 搜索数组中第一个负数(设计题2的“找零”逻辑)
- 等待硬件就绪(
IN AL,20H; TEST AL,80H; JZ wait)
为什么必须用:FOR循环无法预知“第几次遇到回车”,只能用WHILE式条件循环,靠CMP+JZ实时响应。
铁律三:性能敏感,且操作高度标准化
典型如:
- 大块内存拷贝(
REP MOVSB) - 两段内存比较(
REPZ CMPSB) - 屏幕填充(
REP STOSW)
为什么必须用:REP前缀触发 CPU 内部优化流水线,单周期完成“取指-取数-比较-推进-判终”五步,比手工循环快3倍以上。实验例2比例1快,不是玄学,是硬件实锤。
5.2 什么时候坚决别用循环?四个高危雷区
雷区一:循环体内部有不可预测的跳转或长延时
比如在循环里调用INT 21H读键盘(AH=01H),每次调用耗时几十毫秒。若循环1000次,程序卡死10秒。
正确做法:把INT 21H移到循环外,或改用非阻塞查询(AH=06H)。
雷区二:循环变量(CX/SI/DI)在循环体中被意外修改
常见于调用子程序时,子程序忘了保护寄存器。例如compute过程里ADD BX,[SI+0],若SI被子程序改了,下次SI+2就错位。
正确做法:子程序开头PUSH SI,结尾POP SI;或用PUSHF/POPF保存标志位。
雷区三:用LOOP处理超过65535次的循环CX是16位寄存器,最大值65535。若要循环100000次,LOOP会从0xFFFF直接变0x0000(溢出),导致只执行1次。
正确做法:用32位寄存器(ECX)+JECXZ(32位模式),或嵌套循环(外层CX=100,内层BX=1000)。
雷区四:在中断服务程序(ISR)中写复杂循环
中断要求“快进快出”,一个REPZ CMPSB可能执行上千周期,导致其他中断丢失。
正确做法:ISR里只做标记(MOV FLAG,1),主程序轮询FLAG后再用循环处理。
5.3 一个完整避坑案例:从“程序崩溃”到“稳定运行”的七步实操
这是某同学调试设计题2的真实路径,我把他的笔记整理成可复现步骤:
问题现象:程序运行后直接黑屏,POSI和NEGA都是0,DEBUG单步到jmp loop时CL已经是0,但SI指向0C8H(最后一个字节),明显没找到零。
Step 1:确认数据定义
buf db 23h,04h,11h,00h,0a3h,15h,6bh,0a7h,0c8h num equ $-buf+1 ; 错!$-buf=9,+1=10,但实际只有9字节→修正:num equ $-buf(去掉+1)
Step 2:检查指针初值mov si,offset buf正确,SI指向23h。
Step 3:跟踪CL变化
单步执行:
dec cl→CL=9(初始10-1)cmp byte ptr [si],0→[SI]=23h≠0,ZF=0jz next1→ 不跳,执行inc si→SI指向04hjmp loop→ 回到开头
循环6次后,SI指向00h,CL=3,cmp后ZF=1,jz next1成功跳转。
Step 4:检查next1后逻辑next1: inc si→SI从00h指向0A3h(第一个零后的数据)dec cl→CL=2cmp cl,0→CL=2≠0,继续cmp byte ptr [si],0→[SI]=0A3h,最高位=1 → 负数 →jl next2成立next2: inc al→AL=1(负数计数)
但AL是8位,最多计255,而nega db 0是字节单元,没问题。
Step 5:发现致命错误next2: inc al后是mov nega,al,但AL是累加器,nega是内存单元,MOV语法正确。
再看next1分支:inc bl; mov posi,bl——BL也是8位,但posi db 0同样是字节,没问题。
Step 6:终极排查——DEBUG看内存D DS:0显示:23 04 11 00 A3 15 6B A7 C8(共9字节)R查SI:在next1后SI=0004H(偏移4),[SI]=A3H,正确。
但nega和posi值仍是0?
→发现:mov nega,al执行后,DEBUGD DS:offset nega看到01,但程序退出前没显示。原来int 21h退出时,AL值被覆盖,nega内存虽已改,但没输出。
验证:在exit:前加MOV DL,nega; MOV AH,2; INT 21H,果然输出1。
Step 7:最终稳定版关键修复
num equ $-buf(去+1)next1后加mov posi,0/mov nega,0初始化(原代码依赖xor bl,bl,但BL可能被污染)exit:前加输出语句验证结果
从那以后我每次写循环,都强制走一遍这七步:查数据长度 → 查指针初值 → 单步跟计数器 → 检查跳转条件 → 验证内存写入 → 用DEBUG直接看内存 → 加输出验证。哪怕多花10分钟,也比对着黑屏抓狂两小时强。希望帮到你。
本文还有配套的精品资源,点击获取