8086实模式下汇编循环程序设计与调试实战
简介:本资源是面向高校计算机类专业本科生的《微机原理与接口技术》课程实验配套材料,聚焦循环程序设计核心能力训练,重点解析重复控制指令、串操作指令及重复前缀的语法、功能差异与适用场景。资源以淮海工学院标准实验报告为载体,完整涵盖验证题(BUF1/BUF2数据区比对)与设计题(首个零值后正负数统计),并提供两套对比源程序(含SI/DI寻址差异分析)、详细实验步骤、汇编代码注释及结构化实验分析,助力学习者深入理解计数控制与条件控制循环结构的工程选型逻辑。压缩包为1个224KB的DOC文档,内容排版规范,含实验目的、题目要求、源码清单、调试要点与教师评阅栏,便于直接打印提交或对照学习。目前已有2188人下载学习,适合微机原理实验课前预习、课后复盘及汇编语言循环编程专项强化。
1. 循环程序设计:在 8086 实模式下用纯汇编写“数据区比对”和“零值后统计”的硬核落地实践
你刚打开 DOSBox,敲下tasm loop3.asm,屏幕一闪——报错Undefined symbol: COUNT。不是环境没配好,是COUNT EQU $-BUF2这行宏定义被你抄漏了段落,导致后续所有CX初始化都崩了。这正是《微机原理实验三:循环程序设计》最真实的开局:它不讲高级语言的抽象语法糖,只给你一个.asm文件、一段裸奔在 8086 实模式下的汇编代码、以及必须亲手填平的寄存器陷阱。这个项目本质是用最原始的指令级控制,完成两个典型循环场景的闭环实现:一是跨数据区逐字节比对(验证题),二是带状态跳转的条件扫描+计数(设计题)。它适合正在啃《32位微型计算机原理与接口技术》教材、手头只有 MASM/TASM + DOSBox 的本科生,也适合想回溯底层循环建模逻辑的嵌入式老手——因为所有“玄学”行为,比如REPZ CMPSB为什么能自动增址、LOOPZ为什么要在INC SI后才判断零标志,全藏在每条指令的执行时序里。这不是玩具 Demo,而是当年在真实 IBM PC/XT 上跑过的实操代码,连堆栈段都只预留 128 字节(80H DUP(0)),稍一越界就蓝屏重启。下面我们就从跑通第一行开始,一层层拆开这个“循环黑匣子”。
2. 环境搭建与源码结构解析:DOSBox + MASM/TASM 下的 16 位实模式工程组织
2.1 开发环境复现:为什么必须用 DOSBox 而不是 WSL?
现代 Windows 已彻底移除 16 位 DOS 支持,直接双击.exe会弹出“此应用无法在你的电脑上运行”。正确路径是:
- 安装 DOSBox 0.74(官方稳定版,避免新版兼容性问题);
- 在 DOSBox 中挂载本地目录:
mount c D:\microcomputer\lab3; - 切换到盘符:
c:; - 使用TASM 5.0或MASM 6.15(非最新版!因新版默认生成 32 位目标文件,而本实验要求
use16段属性)。
提示:TASM 编译命令为
tasm /zi loop3.asm(/zi生成调试信息),链接用tlink /v loop3.obj(/v启用符号表)。若用 MASM,则需ml /Zi /c loop3.asm+link loop3.obj;。关键点在于:所有段声明必须显式标注use16,否则汇编器可能按默认 32 位处理,导致MOV SI, OFFSET BUF1地址截断。
2.2 源码模块划分:数据段、堆栈段、代码段的物理边界
整个实验包含两类题目,对应两套独立的.asm文件结构。我们以验证题的“例1”为例,解剖其段布局:
DSEG SEGMENT BUF1 DB 21H,3aH,36 ; 数据区1:3字节,起始地址 = DSEG:0000H BUF2 DB 31H,0c7H,36 ; 数据区2:3字节,起始地址 = DSEG:0003H COUNT EQU $-BUF2 ; 宏定义:$ 表示当前地址,即 0006H - 0003H = 3 FLAG DB 0 ; 标志单元:单字节,地址 = DSEG:0006H DSEG ENDSSSEG SEGMENT STACK DB 80H DUP(0) ; 堆栈段:128字节(0x80),从高地址向下生长 SSEG ENDSCSEG SEGMENT ASSUME DS:DSEG,SS:SSEG,CS:CSEG START: MOV AX,DSEG ; 加载数据段基址到 DS MOV DS,AX MOV SI,OFFSET BUF1-1 ; 关键!SI 指向 BUF1 前一个字节(0000H-1=FFFFH) MOV DI,OFFSET BUF2-1 ; DI 同理指向 BUF2 前一个字节(0003H-1=0002H) MOV CX,COUNT ; CX = 3,作为循环计数器 NEXT: INC SI ; 先自增,使 SI 指向 BUF1[0] INC DI ; DI 指向 BUF2[0] MOV AL,[SI] ; 取 BUF1[0] = 21H CMP AL,[DI] ; 与 BUF2[0] = 31H 比较 → ZF=0 LOOPZ NEXT ; 因 ZF=0,不跳转,CX 减1后继续循环 JZ OK ; 若全部相等,LOOPZ 会自然退出且 ZF=1,跳转 OK MOV FLAG,-1 ; 否则置 FLAG = 0xFF(-1 的补码) OK: MOV AH,4CH INT 21H ; DOS 退出中断 CSEG ENDS END START逻辑说明:
LOOPZ是“当 CX≠0 且 ZF=1 时循环”,它先检查 ZF 再减 CX。因此INC SI/INC DI必须放在循环体开头,否则第一次比较时 SI/DI 仍指向非法地址。OFFSET BUF1-1的设计是为了让INC SI后精准落到第一个有效字节,这是 8086 编程中经典的“预偏移”技巧。
2.3 设计题的数据结构:buf dw ...与buf db ...的字节对齐差异
设计题第二小题使用字节型数据区:
data segment buf db 23h,04h,11h,00h,0a3h,15h,6bh,0a7h,0c8h ; 9字节 num equ $-buf+1 ; 注意:+1!实际为 10 posi db 0 nega db 0 data ends这里num equ $-buf+1是易错点:$-buf计算的是从buf到当前地址的字节数(即 9),但后续循环中dec cl会先减1再判断,所以+1是为了补偿初始多减一次。而验证题中COUNT EQU $-BUF2无+1,因其循环逻辑是INC后立即比较,无需补偿。
3. 核心指令对比分析:LOOPZvsREPZ CMPSB的执行路径与性能边界
3.1 验证题例1:手动递增+条件跳转的显式循环链路
例1的循环体完全由开发者控制:
NEXT: INC SI ; 步骤1:显式修改地址寄存器 INC DI ; 步骤2:同上 MOV AL,[SI] ; 步骤3:读取源操作数 CMP AL,[DI] ; 步骤4:比较(影响 ZF) LOOPZ NEXT ; 步骤5:检查 CX>0 && ZF==1,决定是否继续执行流程图(单次迭代):
INC SI→ SI = FFFFH → 0000H(溢出后归零,但此处不会溢出)INC DI→ DI = 0002H → 0003H[SI] = 21H,[DI] = 31HCMP设置 ZF=0LOOPZ:因 ZF=0,跳过循环,执行JZ OK→ 不跳转 →MOV FLAG,-1
参数说明:
LOOPZ的循环次数由CX初始值决定,每次迭代CX自动减1;ZF 由上一条比较指令设置,必须确保 CMP 是 LOOPZ 前的最后一条影响标志位的指令,否则 ZF 可能被中间指令(如 MOV)覆盖。
3.2 验证题例2:REPZ CMPSB的隐式循环与段寄存器约束
例2改用串操作指令:
MOV SI,OFFSET BUF1 ; SI 指向 BUF1[0](0000H) MOV DI,OFFSET BUF2 ; DI 指向 BUF2[0](0003H) MOV CX,COUNT ; CX = 3 CLD ; 清方向标志 DF=0,使 SI/DI 自动递增 REPZ CMPSB ; 重复比较字节,直到 CX=0 或 ZF=0 JZ OK ; 若 ZF=1(全部相等),跳转REPZ CMPSB的原子操作序列:
- 比较
[DS:SI]与[ES:DI](注意:源在 DS,目标在 ES) - 若相等(ZF=1)且 CX>0,则
SI++,DI++,CX--,重复 - 若不等(ZF=0)或 CX=0,则退出,ZF 保持最后一次比较结果
关键区别:例1中
BUF1和BUF2同在DS段,例2强制BUF2在ES段(MOV ES,AX),这是CMPSB的硬性要求。CLD指令不可省略,否则 DF=1 会导致SI/DI递减,数据区反向扫描。
3.3 性能与可维护性对比:何时该用手动循环,何时用 REP 前缀?
| 维度 | LOOPZ手动循环 | REPZ CMPSB串操作 |
|---|---|---|
| 代码体积 | 5 条指令(INC×2, MOV, CMP, LOOPZ) | 1 条指令(REPZ CMPSB) |
| 执行周期 | 每次迭代约 18T(INC 3T, MOV 2T, CMP 3T, LOOPZ 10T) | 每次比较约 15T(CMPSB 12T + REP 开销 3T) |
| 调试难度 | 寄存器状态清晰,可单步跟踪每步变化 | 黑盒执行,无法观察中间 SI/DI 值 |
| 适用场景 | 需要循环中插入额外逻辑(如计数、分支) | 纯数据搬运/比较,追求简洁性和速度 |
实践经验:在教学实验中,
REPZ CMPSB更易暴露段寄存器配置错误(如忘记MOV ES,AX),而LOOPZ更易出现地址偏移错误(如OFFSET BUF1-1少写-1)。两者没有绝对优劣,选型取决于循环体复杂度——本实验中例2的纯比较场景,REPZ是更优解。
4. 避坑指南:5 个真实踩过的雷区与血泪排查过程
4.1 现象:编译通过,但运行后FLAG始终为 0,无论数据是否相同
原因:LOOPZ NEXT后缺少JZ OK跳转,导致即使全部相等,程序也会顺序执行到MOV FLAG,-1。
解决:严格按例1源码,在LOOPZ后紧跟JZ OK,OK标签内仅置FLAG=0并退出。
4.2 现象:REPZ CMPSB执行后ZF=0,但BUF1和BUF2明明相同
原因:未执行MOV ES,AX,ES段寄存器仍为 DOS 初始化的随机值,[ES:DI]读取的是垃圾内存。
解决:在MOV DI,OFFSET BUF2后立即添加MOV ES,AX(AX已加载DSEG基址)。
4.3 现象:设计题中搜索第一个零值后,posi/nega计数值远超预期
原因:num equ $-buf+1的+1多加了一次,导致cl初始值过大,循环越界读取后续内存(如posi单元本身被当数据读)。
解决:删除+1,改为num equ $-buf;并在循环内增加边界检查:cmp cl,0后jz exit。
4.4 现象:DOSBox中运行时报Error 205: Division by zero
原因:设计题第一小题的平均值计算中,div al用AL作除数,但AL未初始化为数据区长度,而是残留上一指令的垃圾值。
解决:显式赋值mov al,count(count需在数据段正确定义),再cbw将AL符号扩展为AX,最后div bl(用BL存长度,避免AL被覆盖)。
4.5 现象:程序退出后 DOS 提示Bad command or file name
原因:INT 21H的功能号4CH要求AH=4CH,但前序代码可能修改了AH(如MOV AH,2用于字符输出后未重置)。
解决:在INT 21H前强制mov ah,4ch,绝不依赖寄存器残留值。
5. 设计题深度改造:从“找第一个零”到“找第 N 个零”的可扩展实现
5.1 原逻辑复盘:单零值定位的硬编码缺陷
原始设计题第二小题代码:
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 ; ← 问题在此:遇到零又跳回 next1,无限循环! inc bl mov posi,bl jmp next1这段代码存在致命逻辑漏洞:当next1后再次读到00h,jz next1会形成死循环。正确做法应是将“找零”和“统计”分离为两个独立阶段。
5.2 改造方案:双阶段循环 + 可配置零序号参数
我们引入新变量ZERO_INDEX(查找第几个零),并重构为:
; 阶段1:定位第 ZERO_INDEX 个零的位置 mov bx,0 ; bx = 当前找到的零个数 mov di,offset buf ; di 指向数据区首 mov cx,num ; cx = 数据总长度 find_zero: cmp byte ptr [di],0 jnz not_zero inc bx cmp bx,ZERO_INDEX je found_it ; 找到目标零,跳转 not_zero: inc di dec cx jnz find_zero jmp no_zero_found ; 未找到足够多零 found_it: inc di ; di 指向第一个零后的数据 mov si,di ; si 作为统计起点 mov cx,num sub cx,di ; cx = 剩余长度(从零后到末尾) xor bl,bl ; posi 计数器 xor al,al ; nega 计数器 count_loop: cmp cx,0 je done mov dl,[si] test dl,80h ; 检查符号位(负数最高位为1) jnz is_negative inc bl jmp next_count is_negative: inc al next_count: inc si dec cx jmp count_loop done: mov posi,bl mov nega,al关键改进点:
ZERO_INDEX定义为ZERO_INDEX EQU 1,修改此值即可切换查找目标;- 使用
test dl,80h替代cmp dl,0判断符号,避免jl对无符号数的误判;sub cx,di计算剩余长度时,di是地址偏移量,需确保num足够大,否则cx可能为负(此时应jmp done)。
5.3 验证方法:用 DOSBox 的 DEBUG 工具单步追踪寄存器
在 DOSBox 中运行debug loop3.exe,输入以下命令:
-r ; 查看寄存器初始值(重点关注 CS, DS, SI, DI, CX) -u 100 120 ; 反汇编代码段 100H~120H,确认指令地址 -t ; 单步执行,观察 SI/DI 如何随 `inc si` 变化 -d ds:0 l 10 ; 查看 DS 段 0H~10H 内存,验证 BUF1/BUF2 值 -q ; 退出 debug重点监控SI在find_zero循环中的变化:当BUF为23h,04h,11h,00h,...时,SI应在第四次inc si后等于0003H(即00h所在地址),此时bx=1满足ZERO_INDEX=1,跳转found_it。
6. 二次开发实战:为循环程序添加调试输出与自动化测试框架
6.1 添加 ASCII 码输出:把posi/nega值转为可见字符
原始代码用INT 21H功能02H输出单字符,但posi是二进制值(如03H),直接输出会显示乱码(ETX 控制符)。需转换为 ASCII:
; disp proc: 将 BL 中的 0-9 数字转为 ASCII 并输出 mov ah,2 add bl,'0' ; '0'=30H, 0→30H, 1→31H... mov dl,bl int 21h sub bl,'0' ; 恢复原值 ret disp endp调用方式:mov bl,posi→call disp。此函数仅支持 0-9,若计数超9,需扩展为十进制转换(用div 10取余)。
6.2 构建最小化测试套件:用批处理驱动多组输入
创建test.bat:
@echo off echo === Test Case 1: BUF1=BUF2 === echo 21 3A 36 > input1.hex echo 21 3A 36 > input2.hex tasm /zi compare1.asm tlink /v compare1.obj compare1.exe echo. echo === Test Case 2: BUF1!=BUF2 === echo 21 3A 36 > input1.hex echo 31 C7 36 > input2.hex tasm /zi compare1.asm tlink /v compare1.obj compare1.exe技巧:用
echo生成测试数据文件,再修改源码中BUF1/BUF2的DB行为INCBIN 'input1.hex'(需 TASM 支持),实现数据与代码分离。
6.3 从这次实验我学到的硬核习惯
从那以后我每次写LOOP类指令,都强制走一遍“三查”:
- 查 CX 初始化:是否用
EQU或MOV正确赋值?有无受前序代码污染? - 查标志位来源:
LOOPZ前的CMP是否紧邻?中间有无MOV等改标志指令? - 查地址有效性:
SI/DI增址后是否仍在数据区内?用DEBUG的-d命令截图验证内存布局。
这些动作看似繁琐,但在 8086 的寄存器世界里,少一次检查就可能多一小时调试。希望帮到你。
本文还有配套的精品资源,点击获取