YAOTU INSIGHTS

汇编循环程序设计:LOOPZ与串操作指令的边界控制与避坑指南

汇编循环程序设计:LOOPZ与串操作指令的边界控制与避坑指南
简介这份《微机原理与接口技术》课程的“循环程序设计”实验报告面向正在学习汇编语言重复控制与串操作指令的高校学生也适合作为课程实验报告模板。报告围绕判断两个数据区是否相同、统计字数据区第一个零数据后正负数个数两个典型题目展开完整给出实验目的、源程序及运行分析验证题给出普通循环与串比较两种实现设计题要求分别用重复控制指令和串操作指令搜索第一个零数据并练习计数控制与条件控制循环结构的选择。压缩包内为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只有ZF1且CX≠0时才继续跳。如果两个数据区完全相等最后一次比较让 CX 减到 0此时 ZF 仍为 1JZ OK成立FLAG 保持 0如果中间某个字节不等CMP会让 ZF0LOOPZ立即退出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 结果推导把两份程序放在一起看差异很明显对比项程序 1LOOPZ程序 2REPZ CMPSBSI 初值OFFSET BUF1-1OFFSET BUF1DI 初值OFFSET BUF2-1OFFSET BUF2比较载体MOV AL,[SI]CMP硬件自动比较字节段来源DS:SI / DS:DIDS:SI / ES:DI指针推进手动INC SI/INC DICLD后自动加 1退出条件CX0 或 ZF0CX0 或 ZF0用给定的数据手工推一遍。BUF1 是21H,3AH,36HBUF2 是31H,0C7H,36H第一次比较21H和31H已经不相等ZF0LOOPZ立即退出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 $-buf1 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 $-BUF1把个数多算了 1 个。数据区实际是 9 个字节$-BUF已经是 91之后变成 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如果误用JBA3H会被当成无符号数 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 ; 找到第一个 0DI 指向其后一个字节 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 就自动加 1CX 自动减 1。找到 0 时DI 已经指向零后面那个字节CX 正好是剩余待统计的字节数。比如数据区里00H在第 4 个位置那么扫描完成后 CX5DI 指向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负数15H21正数6BH107正数0A7H-89负数0C8H-56负数所以正确结果是 POSI2NEGA3。上机验证时程序结束后直接查看数据段里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 位除法用 AX16 位除法用 DX:AX任何一位没对齐结果都是黑匣子。5. 避坑清单循环程序设计的 5 个踩坑现场循环程序设计这个实验的代码量不大但能踩的坑一个不少。下面几条是我实际调试中见过最多的问题全部按“现象、原因、解决”三部分列出。5.1 把 LOOPZ 换成 LOOPNZ相同和不同的判断整体反转现象程序结果和预期完全相反两个数据区明明不同FLAG 却置 0或明明相同FLAG 却置 -1。 原因LOOPZ 是“相等时继续循环”LOOPNZ 是“不相等时继续循环”。这两条指令监视的 ZF 条件正好相反。比较循环一旦用错跳转方向就反了。 解决记住前缀Z代表 ZF1 时循环NZ代表 ZF0 时循环。判断“两个区是否相同”要在中途不相等时停下来所以用 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,AXES 指向的是残留段没有CLDDF 可能是 1SI、DI 自动递减比较顺序就倒了。 解决每次用串操作指令前固定写三行MOV ES,AX、CLD、MOV CX,COUNT。这三行缺一不可顺序也不要乱。5.5 计数器多算一个字节缓冲区后面被当数据读现象统计正负数时结果偶尔多出 1或者程序在最后一个字节处崩溃。 原因NUM EQU $-BUF1这种写法把计数器多加了 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,520零后有 2 个正数1,2,3,0,0fh,80h,7fh21混合正负1,2,300没有零统计不执行0,1,2,330零在第一位每组数据跑完对照屏幕输出和表格能直接定位是找零阶段出错还是统计阶段出错。如果输出不符就在CMP [SI],0和JL count_neg两处设断点单步观察 SI 指向的元素、ZF 值和 CX 剩余次数。循环程序最容易玄学的地方就是边界而边界问题几乎都能靠“先推演、后显示、再单步”这三步解决。从那以后我每次写循环程序都会先把边界条件写成注释钉在代码头部比如“找零前共几个字节、零后还剩几个、遇到第二个零是否忽略”然后才动手写指令。希望帮到你。本文还有配套的精品资源点击获取