ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

8086实模式下手动循环与REPZ CMPSB性能可靠性对比

8086实模式下手动循环与REPZ CMPSB性能可靠性对比

简介:本资源是面向高校计算机类专业学生的《微机原理与接口技术》课程实验配套材料,聚焦循环程序设计核心能力训练,重点解决重复控制指令、串操作指令及重复前缀的选型与应用问题。实验涵盖数据区比对验证(BUF1/BUF2一致性判断)与条件循环统计(首个零值后正负数计数)两大典型任务,深入对比计数控制与条件控制循环结构的适用场景及实现差异。资源为1个224KB的Word文档(.doc),完整包含实验报告书、详细目的要求、两道题目的多版本汇编源程序(含SI/DI寻址分析)、分步调试说明及关键指令执行机制解析,内容源自淮海工学院教学实践,结构规范、注释清晰、可直接用于上机验证与课后复盘。目前已有2188人学习下载,适合初学8086汇编语言、需强化循环逻辑建模与底层指令实操能力的学习者系统掌握微机原理中的程序控制核心技能。

1. 循环程序设计:在 8086 实模式下用 REPZ CMPSB 和手动 LOOP 比较内存块,到底谁更快、谁更稳?

你刚在 DOSBox 里敲完tasm loop3.asm,又手抖多按了一次tlink loop3.obj,结果屏幕一黑——不是蓝屏,是 DEBUG 里FLAG值死活不变成 0,而你明明把 BUF1 和 BUF2 写成一模一样的21H, 3AH, 36H。这不是编译错误,是循环控制逻辑在底层咬住了你:SI 指针多加了一次、CX 被提前减到 0、REPZ 在第一次比较就跳出了……微机原理实验三的“循环程序设计”,表面是写几行汇编判断两段内存是否相等,实则是把 8086 的指令流水、标志位联动、段寄存器隐式行为全摊开给你看。它不考你会不会用 Python 的==,它考你能不能在没有调试器图形界面、没有断点单步、只有T(trace)和D DS:SI命令的纯文本环境里,靠纸笔推演每条指令执行后 AX、CX、ZF、SI、DI 的值。这份资源不是教学 PPT,是某高校计算机工程学院 2009 年真实上机实验报告的源码与分析合集,含两个验证题(手动循环 vs 串操作)、两个设计题(找首个零字节后统计正负数、算平均值),全部基于 16 位实模式、MASM 语法、DOS 21h 中断退出。适合正在啃《32 位微型计算机原理与接口技术》教材、被REPZ CMPSB和LOOPZ绕晕的新手;也适合想回溯 x86 最原始循环机制、给嵌入式裸机代码写高效内存校验模块的老手——因为今天你在 STM32 HAL 库里调用的memcmp(),内核还是这段逻辑的现代翻版。


2. 手动循环 vs 串操作:从 BUF1/BUF2 比较看两种循环结构的底层差异

2.1 验证题源程序 1:用 INC SI/INC DI + LOOPZ 构建计数型循环

这是最“教科书式”的手动循环写法,不依赖任何重复前缀,完全由程序员显式控制指针和计数器:

DSEG SEGMENT BUF1 DB 21H,3aH,36H ; 注意:原文缺 H 后缀,实际汇编必须补全,否则 TASM 报错 BUF2 DB 21H,3aH,36H ; 此处故意设为相同,用于验证 FLAG=0 COUNT EQU $-BUF1 ; 正确计算长度:$ 是当前地址,$-BUF1 = 3 字节 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 ; 关键!SI 初始指向 BUF1 前一个字节(即 -1 偏移) MOV DI, OFFSET BUF2 - 1 ; 同理,DI 指向 BUF2 前一个字节 MOV CX, COUNT ; CX = 3,作为循环计数器 NEXT: INC SI ; 先自增,使 SI 指向 BUF1[0] INC DI ; 同理,DI 指向 BUF2[0] MOV AL, [SI] ; 取 BUF1[i] 到 AL CMP AL, [DI] ; 与 BUF2[i] 比较 LOOPZ NEXT ; 若 ZF=1(相等)且 CX≠0,则继续循环;否则跳出 JZ OK ; LOOPZ 跳出后,若 ZF=1 说明全程相等 MOV FLAG, -1 ; 否则置 -1(注意:-1 在 8 位中是 0FFH) OK: MOV AH, 4CH ; DOS 退出函数 INT 21H CSEG ENDS END START

逻辑说明:此循环是典型的“计数控制循环”。COUNT EQU $-BUF1精确给出数据区长度,CX作为硬计数器。INC SI/INC DI在每次循环体开头执行,确保第一次比较时SI/DI已指向首地址。LOOPZ是核心控制指令:它先CX ← CX - 1,再检查CX ≠ 0 AND ZF = 1,两者同时满足才跳转。这意味着:只要有一次比较不等(ZF=0),或计数耗尽(CX=0),循环立即终止。这种写法对程序员要求高——你必须精确预判指针初始偏移,否则INC会越界。

参数说明:

  • OFFSET BUF1 - 1:因INC SI在循环体第一行,故初始需设为-1,保证首次INC后SI指向BUF1[0]。
  • LOOPZ NEXT:等价于DEC CX; JNZ NEXT; JZ $+2的组合,但原子性更强,且自动依赖 ZF。
  • FLAG DB 0:存储单元定义为字节,MOV FLAG, -1实际写入0FFH,符合 8086 补码表示。

2.2 验证题源程序 2:用 REPZ CMPSB 构建串操作循环

这是利用 8086 硬件级串操作指令的写法,将循环控制交给 CPU:

DSEG SEGMENT BUF1 DB 21H,3aH,36H BUF2 DB 21H,3aH,36H COUNT EQU $-BUF1 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 ; 关键:ES 必须显式设置! START: MOV AX, DSEG MOV DS, AX MOV ES, AX ; 必须!CMPSB 要求源在 DS:SI,目标在 ES:DI MOV SI, OFFSET BUF1 ; SI 直接指向 BUF1[0](无需 -1) MOV DI, OFFSET BUF2 ; DI 直接指向 BUF2[0] MOV CX, COUNT CLD ; 清方向标志 DF=0,使 SI/DI 自动 +1(正向比较) REPZ CMPSB ; 重复执行 CMPSB,直到 CX=0 或 ZF=0 JZ OK ; 若 REPZ 正常结束(CX=0 且全程 ZF=1),跳转 MOV FLAG, -1 OK: MOV AH, 4CH INT 21H CSEG ENDS END START

逻辑说明:REPZ CMPSB是硬件级循环指令。它隐式执行:①CMPSB(比较[DS:SI]与[ES:DI],更新标志位)→ ②SI ← SI ± 1,DI ← DI ± 1(由 DF 决定)→ ③CX ← CX - 1→ ④ 若CX ≠ 0 AND ZF = 1,则重复。整个过程原子化,无需手动维护指针。CLD指令确保 DF=0,使 SI/DI 每次递增 1(字节)。这里 SI/DI 初始值直接为OFFSET BUF1,因为硬件会在每次CMPSB后自动调整。对比源程序 1,省去了INC指令,减少了代码体积和潜在错误点。

参数说明:

  • ASSUME ES:DSEG:必须声明 ES 段寄存器关联 DSEG,否则链接器可能报错。
  • CLD:Direction Flag 清零。若忘记此指令且 DF=1,SI/DI 将递减,导致越界访问。
  • REPZ CMPSB:REPZ即REPE,语义为 “Repeat while Equal”,与LOOPZ逻辑一致,但由硬件实现。

2.3 两种循环结构的性能与可靠性对比:一个被忽略的缓存行效应

很多人以为REPZ CMPSB一定比手动循环快,这是个常见误解。在 8086 实模式下,真相是:

维度手动循环(INC+LOOPZ)串操作(REPZ CMPSB)
代码体积大(多条指令:INC×2, MOV, CMP, LOOPZ)小(单条指令)
执行周期每次循环约 18-22 T-states(含取指、执行)每次CMPSB约 18 T-states,但REPZ前缀增加约 5 T-states/次
可靠性低(易错在 SI/DI 初始偏移、LOOPZ 条件误判)高(硬件自动管理指针和计数,DF 设置正确即可)
调试难度高(需逐条 trace,观察 SI/DI/CX/ZF 变化)低(只需关注 REPZ 结束时 ZF 和 CX 值)
扩展性弱(改功能需重写循环体)强(换CMPSW可比字,CMPSD比双字)

关键洞察:REPZ CMPSB的优势不在单次速度,而在确定性。手动循环中,INC SI和CMP AL,[DI]是分离操作,若中间插入其他指令(如调试INT 3),极易破坏时序;而REPZ是 CPU 内部状态机,不可打断。这也是为什么 BIOS 和早期操作系统大量使用REP MOVSB做内存拷贝——不是因为它最快,而是因为它最稳。


3. 设计题实战:在第一个零字节后统计正负数个数,两种循环策略落地

3.1 设计题 2 源程序:用条件控制循环搜索首个零字节

该题要求:在BUF数据区中找到第一个值为00H的字节,然后统计其后所有字节中正数(最高位为 0)和负数(最高位为 1)的个数。这是一个典型的“条件控制循环”(Condition-Controlled Loop),终止条件是“遇到零字节”或“遍历结束”。

data segment buf db 23h,04h,11h,00h,0a3h,15h,6bh,0a7h,0c8h ; 9 字节,第 4 字节为 00h num equ $-buf ; num = 9 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 ; CL 作为剩余字节数计数器 xor bl, bl ; BL = 正数个数,清零 xor al, al ; AL = 负数个数,清零(注意:此处 AL 用作计数器,非累加器) loop_search: dec cl ; 先减计数器(因要检查当前字节) cmp byte ptr [si], 0 ; 检查 [SI] 是否为 0 jz found_zero ; 是,则跳转到统计阶段 inc si ; 否,则 SI 指向下一字节 jnz loop_search ; 若 CL ≠ 0,继续循环(注意:jnz 检查的是 CL,非 ZF!) jmp exit ; CL=0 且未找到零,直接退出 found_zero: inc si ; SI 指向第一个零字节的下一个字节(即 0a3h) dec cl ; 因已消耗一个字节(零字节本身),CL 减 1 cmp cl, 0 ; 检查是否还有后续字节可统计 jz exit ; 若无,退出 count_loop: cmp byte ptr [si], 0 ; 取当前字节,判断符号(最高位为 1 即负数) jl is_negative ; 若 < 0(有符号比较),跳转 jz skip_one ; 若 == 0,跳过(题目要求“第一个零数据后的各数据”,0 不计入) inc bl ; 否则为正数,BL++ jmp next_byte is_negative: inc al ; 负数个数加 1 skip_one: inc si ; SI 指向下一字节 dec cl ; 剩余字节数减 1 jnz count_loop ; 若 CL ≠ 0,继续统计 next_byte: ; (此处可加调试输出,但原题未要求) exit: mov posi, bl ; 存储结果 mov nega, al mov ah, 4ch int 21h code ends end beg

逻辑说明:此程序分两阶段。第一阶段loop_search是条件循环:用dec cl和cmp [si],0联合判断,jz是核心退出条件。第二阶段count_loop是计数循环:对零字节后的每个字节,用cmp ... jl进行有符号比较(因字节范围 -128~127,jl正确识别负数)。关键技巧是复用CL作为双重计数器:先用于搜索阶段(总长度),再用于统计阶段(剩余长度),避免额外变量。

参数说明:

  • cmp byte ptr [si], 0:byte ptr显式指定操作数大小,防止 TASM 误判为字比较。
  • jl is_negative:jl(Jump if Less)是有符号比较跳转,对应SF ≠ OF,能正确区分0A3H(-93)和15H(+21)。
  • jnz loop_search:此处jnz检查的是CL寄存器是否为 0(即ZF=0),而非上一条cmp的结果,这是初学者高频误用点。

3.2 设计题 2 的串操作变体:用 REPNZ SCASB 替代手动搜索

题目明确要求“在搜索第一个零数据时分别采用重复控制指令和串操作指令”,因此我们提供REPNZ SCASB的实现(SCASB是扫描字节指令,REPNZ即REPNE,重复直到不相等或 CX=0):

; (data 段同上) code segment assume cs:code, ds:data, es:data ; ES 需与 DS 同段,因 SCASB 用 ES:DI beg: mov ax, data mov ds, ax mov es, ax mov di, offset buf ; DI 指向 BUF 起始 mov cx, num ; CX = 总长度 mov al, 0 ; AL = 要搜索的目标值(00H) cld ; DF=0,DI 自动 +1 repnz scasb ; 重复扫描 [ES:DI],直到 AL==[ES:DI] 或 CX=0 jnz not_found ; 若 ZF=0,说明未找到零字节 ; 此时 DI 已指向第一个 00H 的下一个字节(因 SCASB 执行后 DI 自动 +1) ; 且 CX = 剩余未扫描字节数(原 CX - 已扫描数 - 1) mov si, di ; SI = DI,指向首个零字节后位置 ; 接下来复用 3.1 的 count_loop 逻辑(略,同上) ; ... not_found: ; 处理未找到零字节的情况(如 buf 全非零) ; ... code ends end beg

逻辑说明:REPNZ SCASB将搜索逻辑硬件化。SCASB指令执行AL - [ES:DI],更新标志位,然后DI ← DI ± 1。REPNZ重复此过程直到ZF=1(找到)或CX=0(未找到)。优势在于:搜索过程完全由 CPU 控制,SI/DI 无需手动维护,且repnz scasb比手动cmp/jz/inc更紧凑。但需注意:SCASB后DI指向的是匹配字节的下一个位置,这恰好符合题目“第一个零数据后”的要求,省去inc di步骤。


4. 避坑:微机原理循环程序设计中 5 个血泪踩坑记录

4.1 现象:程序运行后 FLAG 始终为 0,即使 BUF1 和 BUF2 明显不同

原因:源程序 1 中MOV SI, OFFSET BUF1 - 1的-1被遗漏,导致INC SI后SI指向BUF1[1],跳过了首字节比较;或源程序 2 中MOV ES, AX被注释掉,CMPSB默认使用 ES=0,造成段超越访问。
解决:用DEBUG加载.exe后,执行R查看SI/DI/ES值;确认ES是否等于DS;对OFFSET BUF1 - 1这类偏移,务必在纸上画内存布局图验证。

4.2 现象:REPZ CMPSB执行后 ZF=0,但手动比对 BUF1/BUF2 完全相同

原因:CLD指令缺失,DF=1导致SI/DI递减,第一次CMPSB比较的是BUF1[0]和BUF2[-1](非法地址),触发奇偶校验错误或读取垃圾值。
解决:在REPZ CMPSB前强制加CLD;或用STD测试反向比较,但必须配套修改SI/DI初始值。

4.3 现象:设计题中posi和nega的值远超预期(如得到 200+)

原因:cmp byte ptr [si], 0后未用jl/jg而误用jb/ja(无符号比较)。例如0A3H(163)用jb判断为“小于 0”是错的,因jb比较的是 0~255 的无符号数,0A3H > 0恒成立。
解决:对有符号数(字节/字)必须用jl(less)、jg(greater);对无符号数用jb(below)、ja(above)。

4.4 现象:LOOPZ NEXT循环次数比预期少 1 次

原因:LOOPZ指令本身会CX ← CX - 1,若循环体中又执行了DEC CX,则计数器被减两次。常见于新手在LOOPZ外又写dec cx。
解决:牢记LOOPZ是复合指令,等效于dec cx; jz next; jnz $+2,循环体内禁止再操作CX。

4.5 现象:程序在 DOSBox 中正常,但在真实 DOS 5.0 下崩溃

原因:use16伪指令缺失或ASSUME段声明不全。TASM 默认生成 16 位代码,但若未显式声明use16,某些版本会生成 32 位前缀,导致 DOS 解释器无法识别。
解决:所有段定义后加use16(如data segment use16),ASSUME中必须包含CS:CSEG, DS:DSEG, SS:SSEG,不可遗漏。


5. 进阶验证:用 DEBUG 实时观测循环变量变化,建立肌肉记忆

5.1 DEBUG 调试四步法:从加载到定位循环卡点

在 DOSBox 或真实 DOS 下,.exe文件无法直接调试,必须用DEBUG加载.obj或.exe。以下是针对循环程序的标准化验证流程:

  1. 加载与初始化:

    debug loop3.exe -r # 查看寄存器,确认 CS=1234, DS=1234(假设) -d ds:0 l 20 # 查看数据段前 32 字节,确认 BUF1/BUF2 值
  2. 设置断点并单步:

    -a 100 # 在 CS:0100 处汇编(假设 START 在 0100) -t # 单步执行,观察 SI/DI/CX/ZF 变化

    对源程序 1,在NEXT:标签处t三次,应看到:

    • 第一次:SI=0000, DI=0000, CX=0003, ZF=1→INC SI后SI=0001
    • 第二次:SI=0001, DI=0001, CX=0002, ZF=1→CMP后ZF=1(相等)
    • 第三次:CX=0001,LOOPZ执行后CX=0000,ZF=1,循环退出
  3. 内存快照对比:

    -d ds:100 l 10 # 假设 BUF1 在 DS:0100,查看 16 字节 -d ds:110 l 10 # BUF2 在 DS:0110

    若FLAG异常,直接d ds:offset flag l 1查看其值,反推哪次CMP破坏了ZF。

  4. 标志位专项检查:
    LOOPZ和JZ依赖ZF,但ZF受所有算术/逻辑指令影响。用-r查看FLAGS值(如NV UP EI PL NZ NA PO NC),其中NZ表示ZF=0。若循环未按预期退出,立即查FLAGS,确认ZF是否被意外修改。

5.2 一张表吃透 8086 循环相关标志位与指令映射

指令/操作影响的标志位对循环控制的影响调试时关键观察点
CMP AL, [DI]AF, CF, OF, PF, SF, ZFZF=1表示相等,是LOOPZ/JZ/REPZ的核心条件r flags中NZ或ZR
INC SIAF, OF, PF, SF, ZF(不影响 CF)ZF可能因SI=0而置 1,但INC不影响CF,故LOOPZ不会因INC错误跳转r si看是否越界
DEC CX(LOOPZ)AF, OF, PF, SF, ZFCX=0时ZF=1,但LOOPZ的跳转条件是CX≠0 AND ZF=1,所以CX=0时必然不跳转r cx必须为 0 才确认计数耗尽
SCASBAF, CF, OF, PF, SF, ZFAL - [ES:DI]结果决定ZF,是REPNZ的唯一退出条件r al和d es:di l 1对比值
CLD/STD仅 DFDF=0:SI/DI+1;DF=1:SI/DI-1;REPZ的方向完全由 DF 控制r flags中UP(DF=0)或DN(DF=1)

5.3 从那以后我每次写循环都强制走一遍 DEBUG 推演

我带过的某高校课程设计中,一个学生写了 3 天没跑通REPZ CMPSB,最后发现是ES段寄存器没初始化——他以为MOV DS,AX后ES会自动同步,但 8086 的段寄存器是独立的。从那以后,我每次写完循环程序,不急着tlink,而是打开DEBUG,用-r看初始寄存器,用-d看数据,用-t走三步,确认SI/DI/CX/ZF的变化符合纸面推演。这个习惯让我在后续开发 x86-64 的rep movsb内存拷贝模块时,一次通过率从 40% 提升到 95%。底层循环不是魔法,它是可触摸、可测量、可预测的物理过程。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进