简介:这份《微机原理与接口技术》课程的“循环程序设计”实验报告,面向正在学习汇编语言重复控制与串操作指令的高校学生,也适合作为课程实验报告模板。报告围绕判断两个数据区是否相同、统计字数据区第一个零数据后正负数个数两个典型题目展开,完整给出实验目的、源程序及运行分析;验证题给出普通循环与串比较两种实现,设计题要求分别用重复控制指令和串操作指令搜索第一个零数据,并练习计数控制与条件控制循环结构的选择。压缩包内为1个doc文档,大小224KB,含实验封面、源程序清单及实验分析等完整内容,便于直接阅读或按模板修改。已有2187人学习下载,适合微机原理实验备查、课程报告参考及汇编循环结构复习使用。
1. 循环程序设计:真正要解决的是「重复、终止、看标志位」三件事
“循环程序设计”这门实验拿到手,先别急着写代码。它真正考的是三件事:重复由谁计数、循环由谁终止、终止那一刻的关键状态藏在哪个标志位。这套实验材料用了两招来对比:验证题让你比较 BUF1 和 BUF2 两个数据区,分别用 LOOPZ 重复控制指令和 REPZ CMPSB 串操作指令;设计题让你先找第一个零数据,再统计其后正数和负数的个数。适合正在做微机原理与接口技术实验、复习汇编循环结构,或者想抄一份能跑通的参考源代码的人。我建议先把手上的源程序跑一遍,再回头分析 SI、DI 初值和 CX 之间的关系,这一章的核心就吃透了。
2. 验证题:LOOPZ 和 REPZ CMPSB 两种比较循环怎么选
2.1 程序 1 的循环结构:先加后比,SI/DI 从 OFFSET-1 起步
验证题给的第一段程序,用的是最朴素的循环比较方式。先看完整代码:
data segment use16 buf1 db 21h, 3ah, 36 buf2 db 31h, 0c7h, 36 count equ $-buf2 flag db 0 data ends stack segment para stack 'stack' use16 db 80h dup(0) stack ends code segment use16 assume cs:code, ds:data, ss:stack start: mov ax, data 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 code ends end start第一件事要确认COUNT EQU $-BUF2。$表示当前汇编位置,因为 BUF2 定义在 BUF1 后面,所以这里算出来是 3,正好等于两个数据区的字节数。注意这三个数里有两个是裸写的3aH和36,在 MASM 里裸数字默认按十六进制处理,所以36就是36H,不是十进制 36。很多新手在这里会误解数据区的实际内容。
关键设计在MOV SI,OFFSET BUF1-1和MOV DI,OFFSET BUF2-1。循环体里是先INC SI、INC DI,再去访问[SI]和[DI],所以指针必须从数据区首地址的前一个单元开始。如果用OFFSET BUF1作为初值,第一次INC之后就指向了第二个元素,第一个字节根本没参与比较。
循环终止判定依赖LOOPZ的语义:先把 CX 减 1,再判断 ZF,只有ZF=1且CX≠0时才继续跳。如果两个数据区完全相等,最后一次比较让 CX 减到 0,此时 ZF 仍为 1,JZ OK成立,FLAG 保持 0;如果中间某个字节不等,CMP会让 ZF=0,LOOPZ立即退出,JZ不成立,跳到MOV FLAG,-1。我对这个结构的评价是:它把“循环条件”和“结果判断”拆成了两个阶段,思路清楚,缺点是每轮要手动 INC 两个寄存器,代码冗余度略高。
2.2 程序 2 的串操作版本:ES:DI 与方向标志
第二段程序直接换成串操作指令REPZ CMPSB,代码短了一大截:
data segment use16 buf1 db 21h, 3ah, 36 buf2 db 31h, 0c7h, 36 count equ $-buf2 flag db 0 data ends stack segment para stack 'stack' use16 db 80h dup(0) stack ends code segment use16 assume cs:code, ds:data, es:data, ss:stack start: mov ax, data 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 code ends end start这里最值得注意的一点是:REPZ CMPSB固定使用DS:SI和ES:DI两个段寄存器,不能只写ASSUME DS:DSEG, ES:DSEG就完事。ASSUME只是告诉汇编器段的默认关系,运行时要真正让 ES 指向数据段,必须执行MOV ES,AX。这个动作漏掉,程序要么编译报错,要么运行时访问到错误的段。
另外CLD不能省。方向标志 DF 决定 SI、DI 是自动增加还是自动减少,CLD把它清 0,保证REPZ CMPSB每比较一个字节后 SI、DI 都向后移动。如果不写,程序依赖上一条指令遗留的 DF 状态,非常容易翻车。两段程序从结果上讲完全等价,区别只在比较的载体:程序 1 用MOV AL,[SI]把字节搬到 AL,程序 2 由硬件直接完成;程序 1 需要手动 INC,程序 2 完全自动。
2.3 两种方案的语义对比与 FLAG 结果推导
把两份程序放在一起看,差异很明显:
| 对比项 | 程序 1(LOOPZ) | 程序 2(REPZ CMPSB) |
|---|---|---|
| SI 初值 | OFFSET BUF1-1 | OFFSET BUF1 |
| DI 初值 | OFFSET BUF2-1 | OFFSET BUF2 |
| 比较载体 | MOV AL,[SI]+CMP | 硬件自动比较字节 |
| 段来源 | DS:SI / DS:DI | DS:SI / ES:DI |
| 指针推进 | 手动INC SI/INC DI | CLD后自动加 1 |
| 退出条件 | CX=0 或 ZF=0 | CX=0 或 ZF=0 |
用给定的数据手工推一遍。BUF1 是21H,3AH,36H,BUF2 是31H,0C7H,36H,第一次比较21H和31H已经不相等,ZF=0,LOOPZ立即退出,CX 还剩 2。所以两种程序走完以后,FLAG 都应该被写成-1,也就是0FFH。如果题目要求“相同置 0,不同置 -1”,那这一个测试用例的结果就是“不同”。上机验证时只要在JZ OK前后分别查看 FLAG 的值,就能确认程序逻辑是否正确。
我的建议是:如果目的是学习循环机制,反复单步跟踪程序 1;如果目的是完成项目里的数据比较,直接使用程序 2。前者能让你看清 ZF、CX 每个时刻的变化,后者效率高且少写代码。
3. 设计题:先找零再分类,正负数统计的两种循环实现
3.1 题目要求与循环控制方式的选择
设计题的要求是:统计 BUF 数据区第一个零数据之后的各数据中,正数和负数各有多少个,分别存到 POSI 和 NEGA。题目特意加了一个限制:搜索第一个零时,分别用重复控制指令和串操作指令实现。这本质上要求写两个版本,而不是只交一个。
这里涉及两个循环控制思路。找零的阶段适合条件控制,因为零出现的位置不固定,循环次数事先不知道,只能在循环体内判断[SI]是否为 0;找到零后的统计阶段可以用计数控制,因为从零的下一个单元到缓冲区末尾,剩余字节数是可以算出来的。如果数据区里根本没有零,那整个统计循环就不该进入,POSI、NEGA 保持初值 0。这个边界问题,很多学生在实验报告里没写,但上机调试时一定会撞上。
3.2 用重复控制指令实现:原报告思路与可上机修正
实验报告里给出的版本,我先把原文思路整理出来,仅把标签名改得不与指令助记符冲突:
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 find_zero: dec cl cmp byte ptr [si], 0 jz count_start inc si jmp find_zero count_start: inc si dec cl cmp cl, 0 jz exit cmp byte ptr [si], 0 jl count_neg jz count_start inc bl mov posi, bl jmp count_start count_neg: inc al mov nega, al jmp count_start exit: mov ah, 4ch int 21h code ends end beg这段代码的计数器设置有一点需要警惕:NUM EQU $-BUF+1把个数多算了 1 个。数据区实际是 9 个字节,$-BUF已经是 9,+1之后变成 10。配合DEC CL再比较的结构,程序会多扫出缓冲区末尾的一个未定义字节,如果那个字节恰好不是 0,就会被错误地统计进正数或负数。
我上机时会改成下面这个版本,把边界收紧:
data segment buf db 23h,04h,11h,00h,0a3h,15h,6bh,0a7h,0c8h num equ $-buf 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 cx, num xor bl, bl xor bh, bh find_zero: cmp byte ptr [si], 0 jz count_start inc si loop find_zero jmp exit count_start: inc si loop count_loop jmp exit count_loop: cmp byte ptr [si], 0 jz skip_zero jl count_neg inc bl jmp next_item count_neg: inc bh jmp next_item skip_zero: ; 统计阶段再遇到 0,按题目约定直接跳过,不计数 jmp next_item next_item: inc si loop count_loop jmp exit exit: mov posi, bl mov nega, bh mov ah, 4ch int 21h code ends end beg这次我用CX做完整计数,NUM取真实字节数 9。找零循环里,每比较一个不等于 0 的字节,LOOP会先把 CX 减 1,再判断是否继续;找到 0 时跳出,此时 CX 正好等于从零之后到缓冲区末尾的剩余字节数。统计阶段每处理完一个元素,LOOP count_loop再减一次,最后一个元素处理完 CX 变成 0,直接退出,不会越界。这里还要注意,判断正负用的是JL,它是符号数条件跳转。A3H按有符号数理解是负的,JL会正确跳到count_neg;如果误用JB,A3H会被当成无符号数 163,当成正数统计,结果就完全错了。
3.3 用串操作指令实现:REPNE SCASB 定位零元素
题目要求搜索第一个零数据时使用串操作指令,常见做法是写REPNE SCASB。SCASB 是把 AL 中的值与ES:DI指向的字节比较,配合 REPNE 前缀后,它会一直扫描到第一个相等的字节才停下。实现如下:
data segment buf db 23h,04h,11h,00h,0a3h,15h,6bh,0a7h,0c8h num equ $-buf posi db 0 nega db 0 data ends code segment assume cs:code, ds:data, es:data beg: mov ax, data mov ds, ax mov es, ax mov di, offset buf mov al, 0 mov cx, num cld repne scasb ; 找到第一个 0,DI 指向其后一个字节 jcxz exit mov si, di xor bl, bl xor bh, bh count_loop: jcxz exit mov al, [si] cmp al, 0 jz skip_zero test al, 80h jz count_pos inc bh jmp next_item count_pos: inc bl jmp next_item skip_zero: jmp next_item next_item: inc si dec cx jmp count_loop exit: mov posi, bl mov nega, bh mov ah, 4ch int 21h code ends end beg这段代码里REPNE SCASB的终止语义要记清楚:它每比较一个字节,DI 就自动加 1,CX 自动减 1。找到 0 时,DI 已经指向零后面那个字节,CX 正好是剩余待统计的字节数。比如数据区里00H在第 4 个位置,那么扫描完成后 CX=5,DI 指向0A3H,后面只需要循环 5 次。TEST AL,80H是检查符号位的另一种方式,效果等价于JL,对于熟悉标志位的同学更好理解。用这种方式比手动循环找零更简洁,也更能回答题目里“串操作指令”的要求。
3.4 手工推演:POSI 和 NEGA 的期望值
给定数据23H,04H,11H,00H,0A3H,15H,6BH,0A7H,0C8H,第一个 0 之前的23H、04H、11H都不参与统计。零之后剩下 5 个数:
| 数据 | 有符号值 | 分类 |
|---|---|---|
| 0A3H | -93 | 负数 |
| 15H | 21 | 正数 |
| 6BH | 107 | 正数 |
| 0A7H | -89 | 负数 |
| 0C8H | -56 | 负数 |
所以正确结果是 POSI=2,NEGA=3。上机验证时,程序结束后直接查看数据段里POSI和NEGA两个字节,或者把它们用调试器打印出来,都对得上这个数。
4. 设计题延伸:有符号字数组平均值里的除法边界
4.1 原代码在除法和计数器上的两个硬伤
实验材料的最后还有一道平均值题,题目是统计 BUF 数据区中有符号字数据的平均值。原始代码片段里有这样一段:
beg: mov ax, data mov ds, ax mov si, offset buf call compute mov al, count div al mov bx, ax call disp这里有两个问题。第一,COUNT EQU $-BUF算出来是 8,因为BUF DW 2134H,1256H,3015H,'$'里'$'作为 DW 也占 2 个字节,实际参与平均值的数只有 3 个,却拿 8 当除数。第二,DIV AL是 8 位除法,被除数固定是 AX,而这段代码没有把累加结果放进 AX,直接把COUNT塞进 AL 就除,结果完全不可控。原始代码这个写法属于典型的“看起来差不多,实际没法复现”,如果照抄到 MASM 里大概率得到溢出中断或垃圾值。
4.2 改成可上机的版本:累加循环与 adc 进位
我一般会改成这样,用一个循环完成累加,再做 16 位除法:
data segment use16 buf dw 2134h,1256h,3015h,'$' n equ 3 data ends stack segment para stack 'stack' use16 db 100 dup(?) stack ends code segment use16 assume cs:code, ds:data, ss:stack start: mov ax, data mov ds, ax xor ax, ax xor dx, dx mov si, offset buf mov cx, n sum_loop: add ax, [si] ; 累加一个字 adc dx, 0 ; 把进位加到 DX add si, 2 loop sum_loop mov cx, n div cx ; DX:AX / CX,商在 AX,余数在 DX mov bx, ax ; 把商送给显示例程 call disp mov ah, 4ch int 21h disp proc mov cx, 16 bit_loop: mov dl, '0' rcl bx, 1 jnc put_char mov dl, '1' put_char: mov ah, 2 int 21h loop bit_loop ret disp endp code ends end startADD AX,[SI]完成一个字相加,ADC DX,0处理进位。三个字累加时,即使和超过FFFFH,进位也会被 DX 接住,最后DIV CX使用 32 位被除数DX:AX,商放在 AX,余数放在 DX,除数和被除数的位宽都对齐了。显示例程用RCL BX,1把 BX 的 16 位逐位移到 CF,再从 CF 转成字符'0'或'1'输出,所以最终呈现的是二进制形式。
4.3 手工验算:为什么平均值是 212AH
三个字数据2134H、1256H、3015H转成十进制分别是 8468、4694、12309,累加和是 25471,除以 3 得商 8490,余 1。8490 转十六进制就是212AH。按理说经过程序输出,屏幕上应该显示 16 位二进制0010000100101010。先心算出这个结果,再去对照运行输出,能很快确认累加和除法两条路都没写错。这道题真正的收获是:8086 的除法指令对操作数宽度极其敏感,8 位除法用 AX,16 位除法用 DX:AX,任何一位没对齐,结果都是黑匣子。
5. 避坑清单:循环程序设计的 5 个踩坑现场
循环程序设计这个实验的代码量不大,但能踩的坑一个不少。下面几条是我实际调试中见过最多的问题,全部按“现象、原因、解决”三部分列出。
5.1 把 LOOPZ 换成 LOOPNZ,相同和不同的判断整体反转
现象:程序结果和预期完全相反,两个数据区明明不同,FLAG 却置 0;或明明相同,FLAG 却置 -1。 原因:LOOPZ 是“相等时继续循环”,LOOPNZ 是“不相等时继续循环”。这两条指令监视的 ZF 条件正好相反。比较循环一旦用错,跳转方向就反了。 解决:记住前缀Z代表 ZF=1 时循环,NZ代表 ZF=0 时循环。判断“两个区是否相同”,要在中途不相等时停下来,所以用 LOOPZ;如果题目要求找“第一个不相同的元素”,才考虑 LOOPNZ。
5.2 指针初值差了一个单元:OFFSET 和 OFFSET-1 用错
现象:第一个字节没有被比较,或者最后一个字节被重复比较,结果时对时错。 原因:循环体里如果先INC SI再访问[SI],初值就必须是OFFSET-1;反过来,如果先访问再INC,初值就是OFFSET。两种写法混着用,指针位置就整体偏移。 解决:写循环之前先定结构。程序 1 用“先加后比”,初值写OFFSET BUF1-1;程序 2 用串操作指令,初值直接写OFFSET BUF1。这行初值看起来只差一个字节,实际决定循环边界是否正确。
5.3 负数字节用了 JB,把负数当成大正数
现象:数据区里出现0A3H、0C7H这类字节时,统计结果里负数个数少了,正数个数多了。 原因:JB是低于转移,属于无符号数比较。0A3H作为无符号数是 163,作为有符号数是 -93。用JB判断,负数字节全部落到“非负”分支。 解决:正负判断必须用符号数条件转移JL,或者用TEST AL,80H直接检查最高位。只要涉及“正数、负数”这类描述,一律按有符号数处理。
5.4 串操作缺少 CLD 和 ES,数据和方向全裸奔
现象:REPZ CMPSB或REPNE SCASB运行结果不稳定,换一台机器结果都可能不同。 原因:串操作指令默认使用DS:SI和ES:DI,而方向由 DF 决定。只写了ASSUME ES:DSEG但没有执行MOV ES,AX,ES 指向的是残留段;没有CLD,DF 可能是 1,SI、DI 自动递减,比较顺序就倒了。 解决:每次用串操作指令前,固定写三行:MOV ES,AX、CLD、MOV CX,COUNT。这三行缺一不可,顺序也不要乱。
5.5 计数器多算一个字节,缓冲区后面被当数据读
现象:统计正负数时结果偶尔多出 1,或者程序在最后一个字节处崩溃。 原因:NUM EQU $-BUF+1这种写法把计数器多加了 1,循环会越界读缓冲区后面的未知字节;未知字节的值不确定,可能是正数、负数,也可能正好是 0,导致结果随机。 解决:先确认缓冲区真实字节数。$是当前地址,$-BUF已经就是长度,不要额外+1。真正要调整的应该是循环体内的比较顺序,而不是计数器的值。边界处最可靠的做法是“先比较、后移动指针、再用 LOOP 递减”,这样最后一个元素处理完正好退出。
6. 上机验证技巧:退出前把 POSI 和 NEGA 显示出来
6.1 一个两字节十六进制输出例程
调试循环程序最头疼的是数据存在内存里,看不见摸不着。我习惯在程序退出前加一个十六进制显示例程,把 POSI、NEGA 直接打到屏幕上。小一点的例程就够用:
; 入口:AL = 待显示字节 print_hex proc push ax push bx push cx push dx mov cl, 4 mov bl, al shr al, cl ; 取高四位 add al, 30h cmp al, 39h jbe digit1 add al, 7 ; 'A'-'F' 的修正 digit1: mov dl, al mov ah, 2 int 21h mov al, bl and al, 0fh ; 取低四位 add al, 30h cmp al, 39h jbe digit2 add al, 7 digit2: mov dl, al mov ah, 2 int 21h pop dx pop cx pop bx pop ax ret print_hex endp调用前把数据搬进 AL 即可,比如MOV AL,POSI、CALL PRINT_HEX,再输出一个回车换行,继续显示 NEGA。这样跑完程序,屏幕上直接出现结果,不用进调试器翻内存。显示例程本身也是一个 4 次循环的典型结构,和实验主题互相呼应。
6.2 自测用例表与单步观察点
我给自己定了一个习惯:任何循环程序上机前,先准备几组已知结果的数据。
| 缓冲区内容 | 期望 POSI | 期望 NEGA | 说明 |
|---|---|---|---|
1,2,3,0,4,5 | 2 | 0 | 零后有 2 个正数 |
1,2,3,0,0fh,80h,7fh | 2 | 1 | 混合正负 |
1,2,3 | 0 | 0 | 没有零,统计不执行 |
0,1,2,3 | 3 | 0 | 零在第一位 |
每组数据跑完,对照屏幕输出和表格,能直接定位是找零阶段出错还是统计阶段出错。如果输出不符,就在CMP [SI],0和JL count_neg两处设断点单步,观察 SI 指向的元素、ZF 值和 CX 剩余次数。循环程序最容易玄学的地方就是边界,而边界问题几乎都能靠“先推演、后显示、再单步”这三步解决。从那以后,我每次写循环程序都会先把边界条件写成注释钉在代码头部,比如“找零前共几个字节、零后还剩几个、遇到第二个零是否忽略”,然后才动手写指令。希望帮到你。
本文还有配套的精品资源,点击获取