ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

8086汇编循环程序设计:数据区比较与重复控制指令详解

8086汇编循环程序设计:数据区比较与重复控制指令详解 简介面向微机原理与接口技术课程的《实验三 循环程序设计》实验报告文档聚焦循环结构、重复控制指令、串操作指令及重复前缀的选用适合正在学习汇编语言与微机接口实验的学生使用。文档从实验目的、验证题到设计题逐步展开验证题比较两个等长数据区是否相同并给出分别采用普通循环与 REPZ CMPSB 指令的两种源程序设计题要求统计 BUF 字数据区第一个零数据后的正负数个数附有完整可调试代码。对例1、例2 的 SI、DI 初值设置差异和程序优劣也作了对比分析可帮助读者理解计数控制与条件控制循环的适用场景。资源仅含 1 个 doc 文件压缩包大小约 224KB内容结构紧凑适合实验预习、课后复习或参考撰写实验报告。该资源已有 2187 人学习浏览数据较完整对完成同类微机原理实验有直接参考价值。1. 循环程序设计实验从一个等长数据区比较的小需求说起不少同学第一次接触汇编语言里的循环习惯性地按 C 语言的for、while思路去套结果在LOOPZ、REPZ CMPSB、串操作前缀这几个指令面前直接翻车。我当年带这门课的实验时A同学拿到的第一个任务很简单判断两个等长的数据区是否完全相同相同就把FLAG置 0不同就置 -1。题目看着人畜无害可真要在 8086 汇编里做出来却要同时弄清楚重复控制指令和串操作指令两套体系。这个实验名为“循环程序设计”真正要训练的并不是“怎样写循环”而是在一个数据区比较、一个零值搜索这类具体任务里搞清楚计数控制与条件控制各自的使用场景并且能在LOOPZ与串操作指令之间做出靠谱的选型。2. 总体设计重复控制指令与串操作指令的选型博弈2.1 两类循环结构计数控制与条件控制的适用场景汇编层面的循环没有for语法糖本质上只有两件事一是改计数器二是条件跳转。计数控制意味着循环次数在进入循环前就是确定的典型写法是MOV CX, COUNT之后配合LOOP系列指令条件控制则是循环何时结束取决于运行时的某个条件比如遇到 0 数据或比较结果出现差异典型写法是CMP加JZ、JL这类条件跳转。在这个实验里这两个场景都出现了。验证题是比较等长数据区数据区长度在编译期就能用COUNT EQU $-BUF2算出来天然适合计数控制设计题是统计第一个 0 数据之后的正负数个数0 出现在哪个位置事先不知道所以搜索 0 的阶段必须用条件控制找到 0 之后再进入第二个循环统计正负。一个程序里同时出现两种循环结构这正是题目设计得比较聪明的地方。设计题还要求分别用重复控制指令和串操作指令各写一版搜索 0 的代码。这里有个容易误解的点题目说的是“搜索第一个零数据时分别采用重复控制指令和串操作指令”实际上是指在两个不同的程序版本里分别使用而不是在同一个循环里既用LOOP又用REPZ。我见过有同学硬把两套指令塞进同一个循环结果寄存器冲突、标志位互相干扰调试到崩溃。2.2 重复控制指令与串操作指令同一个任务的两种底层实现重复控制指令包括LOOP、LOOPZ/LOOPE、LOOPNZ/LOOPNE它们的共同点是隐式使用CX作为计数器每次执行时先把CX减 1再判断条件和CX是否为 0。LOOPZ的继续条件是ZF1且CX≠0也就是说“上一次比较结果相同并且还没比完”才继续循环。注意这里隐含了一个顺序CX是先减后判断所以CX的初值必须等于实际循环次数否则会多比一次或少比一次。串操作指令则是另一套体系CMPSB比较DS:SI与ES:DI指向的两个字节比较后自动修改SI、DI方向由DF标志决定。加上REPZ前缀后硬件会自动重复比较直到CX0或ZF0。这个“硬件循环”和软件循环的区别被我当时的导师一句话点透了软件循环每次迭代都要取指、译码、执行至少四条指令而REPZ CMPSB在一个指令的内部完成全部重复动作效率差距不止一个量级。2.3 段寄存器与方向标志串操作指令的隐式依赖用REPZ CMPSB之前必须把源指针放进SI、目标指针放进DI同时把DS和ES都设置好。CMPSB的语义固定是DS:SI与ES:DI比较所以即使两个数据区都在同一个段里也必须把ES赋成和DS一样的值。很多同学漏了这一步结果程序一跑就访问到随机内存地址。方向标志DF决定了串操作后SI、DI是自增还是自减。CLD清DF后指针向高地址方向移动适合从头到尾的比较STD置位后指针递减适合从尾部往前处理。我建议凡是进入串操作之前都显式写一条CLD别指望上电时DF一定是 0。这个习惯在那次实验中救了我一回。3. 验证题实现两种数据区比较方案的源码剖析3.1 方案 ASI、DI 以 -1 起步的 LOOPZ 版本DSEG SEGMENT BUF1 DB 21H, 3AH, 36 ; 第一个数据区3 个字节 BUF2 DB 31H, 0C7H, 36 ; 第二个数据区3 个字节 COUNT EQU $-BUF2 ; 用当前地址减 BUF2 首地址得到长度 FLAG DB 0 DSEG ENDS SSEG SEGMENT STACK DB 80H DUP(0) SSEG ENDS CSEG SEGMENT ASSUME DS:DSEG, SS:SSEG, CS:CSEG START: MOV AX, DSEG MOV DS, AX MOV SI, OFFSET BUF1 - 1 ; 关键点首地址减 1 MOV DI, OFFSET BUF2 - 1 MOV CX, COUNT NEXT: INC SI ; 先自增再取值 INC DI MOV AL, [SI] ; 取 BUF1 的当前字节 CMP AL, [DI] ; 与 BUF2 的当前字节比较 LOOPZ NEXT ; ZF1 且 CX≠0 则继续循环 JZ OK ; 若退出时 ZF1说明全部相同 MOV FLAG, -1 ; 否则标志置 -1 OK: MOV AH, 4CH INT 21H CSEG ENDS END START这个版本的逻辑核心在NEXT标签处的四条指令。SI和DI故意以OFFSET BUF1 - 1起步因为循环体开头必须先INC再访问这样一来第一次进入循环时正好指向数据区首元素。LOOPZ NEXT每执行一次就把CX减 1只要字节相等ZF1且没比完就继续。如果中间某次比较不等ZF变 0LOOPZ的条件不成立循环退出接着用JZ OK判断——注意这里JZ判断的仍然是上一次比较产生的ZF所以退出路径必须是“不等退出”才能走到MOV FLAG, -1。这段代码还体现了一个细节CMP AL, [DI]是比较 AL 寄存器的值和内存值每次循环都重新从DS:SI取字节存入AL覆盖上次的值。有人会问为什么不用CMP [SI], [DI]直接比较两个内存操作数——8086 指令系统里不允许两个操作数同时来自内存必须经过寄存器中转。这个限制是理解汇编寻址方式的一道坎。3.2 方案 BCLD 与 REPZ CMPSB 配合的串操作版本DSEG SEGMENT BUF1 DB 21H, 3AH, 36 BUF2 DB 31H, 0C7H, 36 COUNT EQU $-BUF2 FLAG DB 0 DSEG ENDS SSEG SEGMENT STACK DB 80H DUP(0) SSEG ENDS CSEG SEGMENT ASSUME DS:DSEG, ES:DSEG, SS:SSEG, CS:CSEG START: MOV AX, DSEG MOV DS, AX MOV ES, AX ; 串操作要求 ES:DI必须设置 ES MOV SI, OFFSET BUF1 MOV DI, OFFSET BUF2 MOV CX, COUNT CLD ; 清方向标志SI/DI 向高地址递增 REPZ CMPSB ; 硬件级重复比较 JZ OK ; 比较结束后 ZF1 则完全相同 MOV FLAG, -1 OK: MOV AH, 4CH INT 21H CSEG ENDS END START方案 B 的代码量比方案 A 少了三分之一性能也有明显优势。REPZ CMPSB是单条指令CPU内部完成“比较→修改 SI/DI→CX 减 1→判断继续”整个循环不需要显式跳转也不存在取指、译码的开销。比较完成后若CX因减到 0 而退出说明所有字节都相同此时ZF1若因字节不同而退出ZF0。所以循环结束后直接用JZ判断即可。写这个版本时最容易忘的是ES的设置。CMPSB的两个操作数一个是DS:SI一个是ES:DI即使BUF1和BUF2都在DSEG里也必须把ES指向同一段。我当时第一次跑这个程序时忘了写MOV ES, AX结果DI指向的段是随机的程序反复比较错误地址上的数据FLAG的值怎么都不对。3.3 两种方案的初值差异、比较语义与效率对比两个方案最直观的区别是SI、DI的初值方案 A 从首地址减 1 开始循环体里先自增方案 B 直接指向首地址由REPZ前缀在每条比较指令执行后自动调整指针。方案 A 的写法本质上是“先自增后使用”方案 B 是“先使用后自增”这个差异正好对应了两种循环控制方式对指针初值的不同要求。效率对比方面我大概估算过方案 A 每次循环至少执行INC SI、INC DI、MOV AL, [SI]、CMP AL, [DI]四条指令再加上LOOPZ本身一共五条方案 B 整个循环只有一条REPZ CMPSB虽然内部也要做同样的比较和指针调整但省掉了跳转和大多数取指开销。在CX3的规模下两者差别看不出来但如果数据区扩充到几千字节方案 B 的耗时大概只有方案 A 的三分之一。从“优劣比较”的角度看更值得说的不是性能而是语义清晰度。方案 B 一句话就说清了“重复比较直到不等或结束”方案 A 则需要读者逐条指令推演指针变化和标志位状态。实验报告里如果只贴方案 A 而不交代“为什么不用串操作指令”很容易被打回重写。4. 设计题实现先找零再数正负一个典型的多状态循环4.1 数据区布局与两阶段算法设计设计题的数据区定义如下BUF DB 23H, 04H, 11H, 00H, 0A3H, 15H, 6BH, 0A7H, 0C8H这是一个 9 字节的有符号数据区。要求是找到第一个 0然后统计 0 后面各数据中正数和负数的个数分别存入POSI和NEGA。注意是“第一个零数据”也就是说如果后面还有 0这个 0 不参与正负统计但需要跳过不能把它当成正数或负数。算法拆成两个阶段第一阶段从头扫描逐字节判断是否为 0找到后进入第二阶段第二阶段从 0 的下一个字节开始逐个判断正负遇到下一个 0 则跳过直到数据区末尾。这个“两阶段 状态切换”的思路比试图在一个循环里同时完成搜索和统计要清晰得多。初学者最容易犯的错是在一个循环里既找 0 又数正负结果把前面的正数也统计进去。4.2 完整源码实现与关键指令逐段说明DATA SEGMENT BUF DB 23H, 04H, 11H, 00H, 0A3H, 15H, 6BH, 0A7H, 0C8H NUM EQU $-BUF 1 ; 字节数 1配合循环开头的 DEC CL 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 ; BL 累计正数个数 XOR AL, AL ; AL 累计负数个数 LOOP1: DEC CL ; 计数器先减 1 CMP BYTE PTR [SI], 0 ; 判断当前字节是否为 0 JZ NEXT1 ; 找到第一个 0跳转 INC SI ; 没找到指针后移 JMP LOOP1 NEXT1: INC SI ; 跳过 0 本身 DEC CL ; 剩余未遍历的字节数 CMP CL, 0 JZ EXIT ; 数据区遍历完退出 CMP BYTE PTR [SI], 0 ; 统计阶段检查当前字节 JL NEXT2 ; 有符号小于 0记为负数 JZ NEXT1 ; 又遇到 0跳过 INC BL ; 正数计数 1 MOV POSI, BL JMP NEXT1 NEXT2: INC AL ; 负数计数 1 MOV NEGA, AL JMP NEXT1 EXIT: MOV AH, 4CH INT 21H CODE ENDS END BEG这段程序有几个设计点值得展开。首先是NUM EQU $-BUF 1这里的$是位置计数器$-BUF得到数据区长度 9再加 1 变成 10。为什么要加 1因为LOOP1开头就执行DEC CL所以CL初值必须比实际字节数多 1才能在遍历 9 个字节后恰好减到 0。这是“先减后用”的典型处理方式和方案 A 中SI初值为OFFSET BUF1 - 1是同一个思路——循环体里第一步动了什么初值就要往反向补偿。其次是正负判断CMP BYTE PTR [SI], 0配合JL NEXT2判断的是有符号数小于 0所以0A3H、0A7H、0C8H这些高位为 1 的字节会被正确识别为负数15H、6BH这类高位为 0 的字节落到INC BL分支。JZ NEXT1排在JL NEXT2之后处理的是统计阶段又遇到 0 的情况直接跳过不计数。最后是计数器的回写方式。这里用了INC BL; MOV POSI, BL的写法每统计一个正数就把新值写回内存变量。其实 8086 支持INC BYTE PTR POSI直接对内存操作数加 1效果一样且更简洁。实验报告里保留这种“寄存器中转”的写法也能说得通——它对初学者更直观每一步都能在调试器里看到BL的变化。4.3 三种循环控制方法在本题中的实际应用题目要求在搜索第一个 0 时分别采用重复控制指令和串操作指令上面这段源码用的是条件控制跳转CMPJZ。我可以补一版用REPNE SCASB搜索 0 的写法核心代码是这样MOV DI, OFFSET BUF MOV CX, NUM XOR AL, AL ; 要搜索的值0 CLD REPNE SCASB ; 不等则继续扫描找到 0 停止SCASB把AL的值与ES:DI指向的字节比较REPNE前缀让硬件在“不相等”时继续重复找到相等的字节就停下来。这一条指令替代了上面LOOP1里CMP、JZ、INC SI、JMP四条指令组成的软件循环。之后DI恰好停在 0 的后一个位置上正好可以直接开始统计正负数。这里有个容易搞错的地方REPNE SCASB结束后DI指向的是 0 数据的下一个字节还是 0 数据本身答案是下一个字节因为每次比较后DI会自动调整。这和REPZ CMPSB结束后SI/DI指向不匹配位置的下一个字节是同一个道理。理解了这一点统计阶段就可以直接从[DI]开始不需要额外的INC。5. 避坑指南这个实验最容易翻车的五个地方5.1 现象COUNT 的值总是比实际数据区多一个字节原因很容易踩COUNT EQU $-BUF2如果写在FLAG DB 0之后$的当前值就已经包含了FLAG的地址长度就多算了一个字节。实验报告里两个验证程序都把COUNT定义在FLAG之前这不是巧合而是正确写法。解决方法是把COUNT EQU紧跟在BUF2定义之后或者在FLAG之后再算长度但减去偏移量。这个坑在代码里藏得很深因为编译不报错只是运行时CX多 1最后一次比较会拿FLAG单元的内容和BUF1越界后的数据比结果完全随机。类似的还有设计题里的NUM EQU $-BUF 1这里的1不是随便加的是为了抵消循环开头DEC CL。如果你改成了先CMP再DEC的写法1就必须删掉。计数器的初值和循环体的第一条指令必须配套这是写汇编循环的血泪经验。5.2 现象REPZ CMPSB 比较方向反了数据区尾部开始倒着比现象是程序运行结果看起来“有些时候对有些时候错”而且和数据的对称性有关。原因基本可以锁定为没写CLD。上电时DF标志不保证为 0如果之前某个程序执行过STDSI、DI就会递减。解决方法是进入串操作之前必须显式写CLD。我在方案 B 里加的那条CLD不是可选项是必选项。调试时如果发现比较结果不对第一件事就是单步看SI、DI的变化方向而不是怀疑比较逻辑。5.3 现象ES 段未初始化串操作访问到随机内存地址CMPSB和SCASB都强制要求目标指针使用ES段。代码里只写了MOV DS, AX忘了MOV ES, AX编译能过运行时ES里存的是残留值DI指向完全随机的地址程序表现是“玄学”——每次运行结果可能都不一样。解决方法是把ASSUME里同时声明ES:DSEG并在MOV DS, AX后面补一行MOV ES, AX。反正BUF1、BUF2在同一段这种设置不会带来副作用。在调试器里看ES寄存器的值是否等于DSEG基址一眼就能确认。5.4 现象0 数据被统计进了正数或负数统计阶段的CMP BYTE PTR [SI], 0之后如果直接跟JL和JGE之类的分支0 会因为“不小于 0 且不大于等于”的身份掉进某个分支被计数。题目明确要求统计零数据之后的数据0 本身不算。解决方法是在正负判断之前先处理 0代码里的JZ NEXT1就是干这个的。写这段逻辑时我的经验是先把分支图画出来遇到 0 → 跳过负数 → 负数计数正数 → 正数计数。三个分支缺一不可。偷懒只写正负两个分支遇到 0 就会翻车。5.5 现象程序陷入死循环SI 越过数据区边界搜索 0 的LOOP1循环里如果没在循环开头写DEC CL或者计数器初值算错SI会一路往后扫越过BUF的末尾继续访问后面未知的内存。运气好扫到一个 0 退出来运气不好直接死循环。解决方法是给循环设定一个明确的边界条件要么用CX计数要么在数据区末尾放一个哨兵值。用CX的方案就是上面源码里的写法每次DEC CL后判断是否为 0自然退出。我自己调试时习惯在LOOP1入口处下一个断点盯着SI和CL两个寄存器看它们是否按预期递减和递增。6. 验收与答辩像工程师一样验证实验结论6.1 三步验收法单步跟踪、断点检查、边界用例实验做完不等于程序写对了还得有一套能说服导师的验收流程。我一般分三步走第一步单步跟踪在调试器里逐条执行确认SI、DI的初值和每次循环后的变化方向符合预期第二步断点检查在LOOPZ NEXT和REPZ CMPSB之后各设一个断点观察ZF、CX的值验证循环退出时机第三步跑边界用例把BUF2改成和BUF1完全相同验证FLAG0把设计题的 0 挪到第一个位置或最后一个位置验证搜索和统计逻辑在极端数据下仍然正确。拿设计题来说默认数据区23H, 04H, 11H, 00H, 0A3H, 15H, 6BH, 0A7H, 0C8H中0 出现在第 4 个字节后面 5 个字节里正数是15H、6BH两个负数是0A3H、0A7H、0C8H三个。运行后检查POSI2、NEGA3只是第一层验证把 0 移到开头POSI和NEGA应为 0把 0 移到末尾则 0 之前的数据全部不参与统计POSI4、NEGA4还是多少要按实际数据算一遍。这些边界用例跑到位才对得起“上机调试程序检查程序正确性”这一条实验要求。6.2 答辩怎么讲把“SI 为什么是 -1”讲成设计亮点答辩时导师最爱问的问题之一就是这个方案 A 里SI和DI为什么要从OFFSET BUF1 - 1而不是OFFSET BUF1起步这个问题回答得好整个循环设计的分数就到手了。我建议这样回答因为循环体NEXT的第一条指令是INC SI为了让第一次进入循环时SI恰好指向BUF1的首字节初值必须比首地址少 1。这个“先自增后用”的模式在汇编里很常见比如设计题LOOP1开头也是先DEC CL再比较所以CL初值取NUM而不是实际长度。本质上是在用初值抵消循环体内部第一条指令带来的偏移。顺着这个思路还可以对比方案 B 的SI初值为什么是OFFSET BUF1因为REPZ CMPSB是“先比较后自增”所以初值直接指向首地址即可。两种方案初值差异的背后是软件循环和硬件循环对指针更新时机的不同约定。把这层逻辑讲清楚导师会认为你不只是抄了代码而是真的理解了循环结构的机理。从那以后我每次写完汇编循环都强制自己走一遍“初值补偿”的检查先看循环体第一条指令对计数器或指针做了什么操作再反推初值应该怎么设。这个习惯帮我避免了好几次在实验报告里翻车的尴尬。希望这篇方案对你做这份实验或写课设有实际帮助。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表