简介:本资源是面向高校《微机原理与接口技术》课程学生的经典实验教学材料,聚焦循环程序设计核心能力训练,重点解析重复控制指令、串操作指令及重复前缀的语法、功能差异与适用场景。通过验证题(双数据区比对)与设计题(首个零值后正负数统计)两大典型任务,系统训练计数控制与条件控制循环结构的建模能力,并深入对比SI/DI寻址方式、段寄存器配置及自动增量机制等底层细节。资源为1个224KB的Word文档(.doc),完整包含实验报告模板、详细目的要求、两组验证程序源码(含注释)、设计题完整汇编代码、关键步骤分析及上机调试说明,结构清晰、即开即用。目前已有2188人学习下载,适合初学8086汇编语言的学生巩固循环逻辑、理解串操作本质,并为后续接口编程打下扎实基础。
1. 循环程序设计:不是写 for 就完事,而是让 CPU 真正“转起来”的硬核现场
你有没有试过在 DOSBox 里敲完一段汇编,t单步到第 17 次LOOPZ时,ZF=0突然被清掉,但FLAG还是没变——结果一查发现SI多加了 1,指针早越界到FLAG单元上把值给覆盖了?这不是玄学,这是《微机原理与接口技术》实验三的真实翻车现场。这个项目不是教你怎么用高级语言写循环,而是逼你亲手把CX当计数器、把ZF当开关、用REPZ CMPSB让 CPU 自动递增SI/DI——它解决的是内存块比对、零值定位、符号统计这三类嵌入式级底层任务,面向的是刚啃完 8086 寻址方式、正对着ASSUME DS:DSEG, ES:DSEG发懵的本科生。它不跑 Linux,不连网络,不调 API,只靠INT 21H退出;它不抽象,不封装,每个INC SI都对应一个时钟周期,每条JZ OK都是条件跳转的血泪经验。如果你正在调试一个“看起来逻辑对但结果总错”的循环程序,或者想搞懂为什么REPZ CMPSB比手写LODSB/SCASB少两行却更稳,那这份 2009 年淮海工学院的实验报告,就是你该打开的黑匣子。
2. 架构拆解:三个段、两套循环、四种指令组合的物理落地
这个项目表面是两道题(验证题 + 设计题),实则暗藏三层架构:数据段定义 → 控制流组织 → 指令级执行路径。它不用 Makefile,不生成 ELF,所有逻辑压在.COM可执行文件的 64KB 内存模型里。核心不是算法,而是如何让 8086 的寄存器、标志位、段地址协同工作。下面按真实执行顺序,一层层剥开。
2.1 数据段:静态内存布局决定一切
所有数据都在DSEG或data segment中硬编码,没有动态分配。关键不是“存什么”,而是“怎么对齐、怎么计算长度、怎么预留空间”:
DSEG SEGMENT BUF1 DB 21H,3aH,36 ; 显式字节序列,注意 36 是十进制 36,非 ASCII '6' BUF2 DB 31H,0c7H,36 ; 0c7H = -57(有符号补码),体现负数参与比较 COUNT EQU $-BUF2 ; $ 表示当前地址,$-BUF2 = BUF2 长度(3 字节) FLAG DB 0 ; 单字节存储单元,后续直接 MOV FLAG,-1 修改 DSEG ENDS提示:
COUNT EQU $-BUF2是经典技巧——$是汇编器当前汇编位置,$-BUF2即从BUF2起始到当前位置的字节数。这里BUF2后无其他数据,所以等于BUF2长度。若BUF2后还有BUF3,此写法会出错,必须显式写COUNT EQU 3。
再看设计题的数据定义,问题更尖锐:
data segment use16 buf db 23h,04h,11h,00h,0a3h,15h,6bh,0a7h,0c8h ; 9 字节,含一个 00h num equ $-buf+1 ; 注意:+1!原因见下文分析 posi db 0 nega db 0 data endsnum equ $-buf+1中的+1是血泪教训:$-buf计算的是buf到当前地址(即posi前)的长度,为 9;但num要作为循环计数器初值,需覆盖从buf起始到最后一个有效数据(0c8h)的全部位置。由于buf定义中00h是目标零值,后续0a3h...0c8h共 5 字节需统计,而num实际用于控制“找零”阶段的扫描次数。此处+1是为了凑够扫描全部 9 字节(含末尾0c8h)所需的CX初值,否则DEC CL后可能提前为 0 导致漏判。这不是笔误,是早期汇编中常见的边界补偿手法。
2.2 堆栈段:80H DUP(0) 不是摆设,是防溢出的缓冲垫
SSEG SEGMENT STACK DB 80H DUP(0) ; 128 字节堆栈空间 SSEG ENDS80H(十六进制)= 128(十进制)。为什么是 128?因为CALL指令压入IP(2 字节),PUSH压入 2 字节寄存器,整个程序无深层递归,128 字节足够容纳CS:IP返回地址 + 若干临时寄存器。若此处写10H DUP(0)(16 字节),CALL compute时堆栈就会溢出,RET后跳转到随机地址——这种错误不会报错,只会静默崩溃。我一般会留 256 字节(100H DUP(0)),尤其当后续要加调试输出时。
2.3 代码段:两条循环路径,直击 8086 流水线本质
整个CSEG的灵魂在于START标签后的初始化和主循环。它不依赖 C 运行时,所有段寄存器必须手动加载:
START: MOV AX,DSEG ; 加载数据段地址到 AX MOV DS,AX ; DS ← AX,使 [SI] 指向 DSEG 段内数据 ; 若用 REPZ CMPSB,还需 MOV ES,AX(因 CMPSB 比较 DS:[SI] 与 ES:[DI])这里暴露一个关键事实:8086 的段机制强制你思考数据在哪一段。BUF1和BUF2在同一段(DSEG),所以例1用DS即可;但例2用REPZ CMPSB,该指令硬性规定源操作数为DS:[SI],目的操作数为ES:[DI],因此必须MOV ES,AX。漏写这一句,CMPSB会去ES段(通常是 0)读垃圾数据,比对必然失败——这是新手最常踩的坑,且极难定位。
2.4 循环结构分类:计数控制 vs 条件控制的物理差异
项目明确要求区分两种循环:
- 计数控制:
CX为计数器,LOOP/LOOPZ依赖CX自减为 0 或标志位。 - 条件控制:无固定次数,靠
CMP+Jxx跳转,如JZ next1。
验证题例1是典型计数控制:
MOV CX,COUNT ; CX = 3 NEXT: INC SI ; 手动递增指针 INC DI MOV AL,[SI] CMP AL,[DI] LOOPZ NEXT ; CX>0 且 ZF=1 时继续,否则退出 JZ OK ; LOOPZ 退出时若 ZF=1,说明全等而设计题的“找第一个零”是条件控制:
loop: dec cl ; CL 递减(CL 是 CX 低 8 位) cmp byte ptr [si],0 jz next1 ; 遇到 0 则跳转,不依赖 CX 归零 inc si jmp loop区别在哪?计数控制适合已知长度的遍历(如数组比对),条件控制适合未知位置的搜索(如找首个 0)。前者效率高(LOOPZ单指令完成计数+判断),后者灵活性强(可随时JMP跳出)。但注意:LOOPZ在 8086 上比DEC CX; JNZ慢 1 个时钟周期,现代 CPU 已废弃,但在教学中它强制你理解标志位与计数器的耦合关系。
3. 核心模块逐行解析:从 BUF1/BUF2 比对到正负数统计的指令级真相
现在进入最硬核部分:把源码当电路图读。我们不讲语法,只问“这条指令执行后,CPU 寄存器、内存、标志位发生了什么变化”。
3.1 验证题例1:手写循环的指针偏移陷阱
MOV SI,OFFSET BUF1-1 ; 关键!SI 指向 BUF1 前一个字节 MOV DI,OFFSET BUF2-1 ; DI 指向 BUF2 前一个字节 NEXT: INC SI ; 第一次 INC 后,SI 指向 BUF1[0] INC DI ; DI 指向 BUF2[0] MOV AL,[SI] CMP AL,[DI] ; 比较 BUF1[0] 与 BUF2[0] LOOPZ NEXT ; 若相等且 CX>0,继续为什么SI和DI要-1?因为LOOPZ在循环体开头就执行INC,若初始指向首地址,则第一次INC后指向第二个元素,漏掉首元素比对。-1是预补偿,确保INC后立即命中首元素。这是早期汇编的惯用手法,但极易出错——若BUF1长度为 0,OFFSET BUF1-1会指向非法地址。现代做法是MOV SI,OFFSET BUF1,循环体内用MOV AL,[SI]后INC SI,逻辑更清晰。
再看标志位:CMP AL,[DI]本质是AL-[DI],结果不保存,只设置ZF(相等为 1)、SF(符号位)、CF(借位)。LOOPZ的Z即检查ZF=1。若BUF1[0]=21H,BUF2[0]=31H,则21H-31H产生借位,ZF=0,LOOPZ直接退出,JZ OK不执行,FLAG保持 0?不!FLAG DB 0初始为 0,但程序要求“不相同则置 -1”,所以必须有MOV FLAG,-1。原代码中JZ OK后是MOV FLAG,-1,但OK:标签前无JMP,导致无论是否相等,MOV FLAG,-1都会执行!这是原报告的逻辑漏洞。正确写法应为:
JZ OK ; 若全等,跳过置 -1 MOV FLAG,-1 ; 不等才置 -1 OK: ; 统一出口3.2 验证题例2:REPZ CMPSB 的自动指针管理
MOV SI,OFFSET BUF1 ; SI 指向 BUF1[0] MOV DI,OFFSET BUF2 ; DI 指向 BUF2[0] MOV CX,COUNT ; CX = 3 CLD ; 清方向标志 DF=0,使 SI/DI 自动 +1 REPZ CMPSB ; 重复比较字节,ZF=1 且 CX≠0 时继续 JZ OK ; REPZ 结束后 ZF=1 表示全等 MOV FLAG,-1 OK:REPZ CMPSB是原子操作:每次执行CMPSB(比较DS:[SI]与ES:[DI]),然后自动INC SI; INC DI(因CLD),再DEC CX,最后检查ZF和CX。它省去了手写INC和LOOP,且CMPSB设置的标志位更精准(CMPSB专为比较设计,CMP是通用指令)。但代价是:必须提前设置ES,且DF方向标志必须匹配数据走向。若忘记CLD,STD会使SI/DI递减,指针乱飞。
3.3 设计题:零值定位与符号统计的混合循环
这部分最考验对“状态机”的理解。程序需完成两个阶段:
- 阶段一(找零):扫描
BUF,找到第一个00h,记录其后起始位置; - 阶段二(统计):从该位置开始,遍历剩余数据,按符号分类计数。
原代码用一个循环实现,但逻辑嵌套深:
mov si,offset buf ; SI 指向 buf 起始 mov cl,num ; CL = 10(因 num = $-buf+1 = 9+1) xor bl,bl ; BL = 0(正数计数器) xor al,al ; AL = 0(负数计数器) loop: dec cl ; CL 递减,控制扫描总次数 cmp byte ptr [si],0 ; 检查当前字节是否为 0 jz next1 ; 是 0,跳转到统计阶段 inc si ; 不是 0,SI 指向下一位 jmp loop next1: inc si ; SI 指向 0 后的第一个字节(关键!) dec cl ; CL 再减 1,因当前 SI 已越过 0 cmp cl,0 ; 检查是否还有剩余数据 jz exit ; 无剩余,退出 cmp byte ptr [si],0 ; 再次检查(防 0 在末尾) jl next2 ; 小于 0(符号位为 1)→ 负数 jz next1 ; 等于 0?理论上不会,因已过首个 0,但代码冗余处理 inc bl ; 正数,BL++ mov posi,bl ; 存结果 jmp next1 ; 继续 next2: inc al ; 负数,AL++ mov nega,al jmp next1 exit:这段代码有严重缺陷:next1中inc si后dec cl,但未检查cl是否为 0 就直接cmp byte ptr [si],0,若cl=0时si已越界,访问非法内存。且jz next1在next2分支后又跳回next1,形成无限循环风险。正确做法应在next1开头加cmp cl,0; jz exit。这是典型的“条件分支未覆盖所有出口”错误。
4. 避坑指南:在 DOSBox 里调试循环程序的 4 条血泪经验
在 DOSBox 下跑这些程序,不是gcc a.c && ./a.out那么简单。寄存器看不见、内存摸不着、错误不报行号——全是黑匣子。以下是我在某高校实验室带学生调试时,总结出的高频翻车点:
4.1 现象:程序运行后直接返回 DOS,FLAG值没变,也不报错
原因:INT 21H功能号4CH要求AH=4CH,但代码中MOV AH,4CH前若AH被污染(如MOV AL,[SI]只改AL,AH仍为上次值),则INT 21H执行的是其他功能(如AH=02H是字符输出),导致异常退出。
解决:在INT 21H前,强制清空AH:XOR AH,AH或MOV AH,0,再MOV AH,4CH。更稳妥写法:MOV AX,4C00H(一次性加载AH和AL)。
4.2 现象:REPZ CMPSB总是提前退出,CX还剩 2 就停了
原因:CLD指令缺失,DF=1(方向标志置位),导致SI/DI递减,指针快速滑出数据区,CMPSB读到00H或其他值触发ZF=0。
解决:在REPZ CMPSB前必加CLD;若需反向扫描,用STD,但本实验所有数据正向存储,必须CLD。
4.3 现象:BUF1和BUF2明明相同,FLAG却被置为 -1
原因:COUNT EQU $-BUF2计算错误。若BUF2后有注释或空行,$位置可能偏移,导致COUNT偏大,CX初值过大,LOOPZ扫描超出BUF2末尾,读到FLAG单元(值为 0)与BUF1对应位置(非 0)比较,ZF=0。
解决:用DOS DEBUG工具查看.LST列表文件,确认COUNT值是否等于实际字节数;或改用显式定义:BUF2 DB 31H,0C7H,36H,0(末尾补 0 占位),再COUNT EQU 3。
4.4 现象:设计题中posi和nega值远大于预期,甚至为负
原因:BL和AL是 8 位寄存器,最大计数 255。若正数超过 255,BL溢出归零,导致posi写入 0。原题数据仅 5 字节,不会溢出,但若扩展数据区,此隐患必现。
解决:改用 16 位寄存器BX/AX计数,并声明posi dw 0(dw定义字,2 字节),mov posi,bx。同时注意mov bx,0清零整个BX,而非xor bl,bl(只清BL)。
5. 二次开发实战:把单次循环升级为可复用的汇编函数库
现在你已吃透原始代码,下一步不是重写,而是把它变成能塞进任何项目的“轮子”。我以“内存块比较”为例,展示如何改造为符合工业习惯的汇编函数。
5.1 接口标准化:用寄存器传参,避免硬编码段地址
原始代码中BUF1、BUF2地址写死在代码里,无法复用。改为函数式:
; 函数名:MemCompare ; 功能:比较两个内存块 ; 输入:SI = 源地址(DS 段),DI = 目标地址(ES 段),CX = 长度 ; 输出:ZF = 1 表示相等,ZF = 0 表示不等 ; 破坏:SI, DI, CX, ZF, CF MemCompare PROC PUSH AX CLD REPZ CMPSB POP AX RET MemCompare ENDP调用时:
MOV AX, DSEG MOV DS, AX MOV ES, AX ; 确保 ES = DS MOV SI, OFFSET BUF1 MOV DI, OFFSET BUF2 MOV CX, 3 CALL MemCompare JZ blocks_equal ; ZF=1 则相等这样,同一段MemCompare代码可被不同数据区调用,无需修改。
5.2 增加健壮性:支持不同数据类型与长度
原始代码只支持字节比较。扩展为支持字(WORD)比较:
; 函数名:MemCompareWord ; 输入:SI = 源地址,DI = 目标地址,CX = 字个数 ; 内部:用 REPZ CMPSW(比较字) MemCompareWord PROC PUSH AX CLD REPZ CMPSW ; CMPSW 比较 16 位字 POP AX RET MemCompareWord ENDP调用时MOV CX, 2表示比较 2 个字(4 字节),比手动计算字节数更安全。
5.3 错误处理:返回详细状态码而非仅 ZF
ZF只能表示“等/不等”,无法告知“在哪不等”。增加返回不等位置:
; 修改 MemCompare:返回不等处的偏移(SI - 初始SI) MemCompare PROC PUSH AX PUSH SI ; 保存初始 SI CLD REPZ CMPSB JZ equal_end POP AX ; 弹出初始 SI 到 AX SUB SI, AX ; SI - 初始SI = 偏移量 MOV BX, SI ; BX 返回偏移 POP AX RET equal_end: POP AX XOR BX, BX ; BX=0 表示全等 POP AX RET MemCompare ENDP调用后检查BX:0表示全等,非0表示在第BX字节处不等。这对调试大型数据区至关重要。
5.4 集成测试:用宏自动生成测试用例
为避免每次改代码都手动改BUF1/BUF2,用宏定义测试集:
; 宏:DEFINE_TEST_CASE ; 参数:name, buf1_data, buf2_data, expected_flag DEFINE_TEST_CASE MACRO name, buf1_data, buf2_data, exp_flag name&BUF1 DB buf1_data name&BUF2 DB buf2_data name&COUNT EQU $-name&BUF2 name&FLAG DB exp_flag ENDM ; 使用 DEFINE_TEST_CASE test1, <21H,3AH,36H>, <21H,3AH,36H>, <0> DEFINE_TEST_CASE test2, <21H,3AH,36H>, <31H,0C7H,36H>, <-1>汇编器会自动生成test1BUF1、test1BUF2等标签,测试逻辑与主程序分离,改数据不碰逻辑。
从那以后我每次写底层汇编,都强制走一遍“寄存器快照”流程:在关键点(如LOOPZ前)用DEBUG的R命令记下SI、DI、CX、ZF值,再单步执行,对比变化。这比读文档管用十倍。希望帮到你。
本文还有配套的精品资源,点击获取