☰
x86实模式循环程序设计:SI/DI偏移、REPZ加速与标志位控制
2026/10/15 5:01:16 网站建设 项目流程

简介:本资源是面向高校《微机原理与接口技术》课程学生的经典实验教学材料,聚焦循环程序设计核心能力训练,重点解决重复控制指令、串操作指令及重复前缀的选型应用与循环结构建模问题。资源为1个224KB的Word文档(.doc),完整包含淮海工学院计算机工程学院实验报告书全文,涵盖实验目的、验证题与设计题双层次任务、两套对比源程序(含DS段/ES段差异分析)、详细实验步骤、汇编代码注释及关键执行逻辑说明,特别适合初学8086汇编语言的学生理解计数控制与条件控制循环的适用场景。目前已有2186人学习下载,文档结构规范,含SI/DI寄存器初始化对比、REPZ CMPSB与手动INC循环的性能与可读性分析,以及正负数统计等典型编程范式,可直接用于实验预习、代码调试参考与期末复习巩固。

1. 循环程序设计:不是写个 LOOP 就完事,而是搞懂 SI/DI 怎么动、ZF 怎么用、REPZ 为什么比手写 INC 快一倍

你刚在 DOSBox 里敲完LOOP NEXT,程序跑飞了;调试时发现 FLAG 总是 -1,但 BUF1 和 BUF2 明明只差一个字节;或者更玄学的是——两个功能完全一样的比较程序,一个用REPZ CMPSB,一个用手动INC SI/INC DI/CMP,前者秒出结果,后者单步到第 7 次就跳错。这不是汇编玄学,是循环程序设计的底层契约没签清楚。这份来自某高校计算机工程学院的《微机原理实验三:循环程序设计》实操包,不是教你怎么抄代码,而是把「重复控制指令」「串操作指令」「重复前缀」这三块硬骨头,掰开、泡软、嚼碎——告诉你什么时候该用LOOP而不是JNZ,为什么CLD不加就让REPZ CMPSB反向扫描,以及最关键的:SI 和 DI 的初值偏移量,差 1 字节就能让整个循环逻辑崩成乱码。它适合正在啃《32 位微型计算机原理与接口技术》教材、被实验报告 deadline 追着跑的大二学生,也适合想补全 x86 实模式下循环底层逻辑的嵌入式转岗工程师。别急着复制粘贴,先看懂寄存器怎么呼吸。


2. 循环结构选型:计数控制 vs 条件控制,不是语法问题,是数据流建模问题

2.1 计数控制循环:当“执行 N 次”是铁律,CX 就是你的节拍器

计数控制的核心是预知迭代次数,典型场景如遍历固定长度数组、校验数据区一致性。它的物理载体就是CX寄存器——LOOP指令隐式依赖它,REP前缀显式消耗它。关键在于:CX 必须在循环开始前精确加载,且循环体中绝对不能篡改 CX 值(除非你故意要改)。

以验证题源程序1为例:

MOV SI,OFFSET BUF1-1 ; ← 注意!这里减了1 MOV DI,OFFSET BUF2-1 ; ← 同样减了1 MOV CX,COUNT ; COUNT = $-BUF2 = 3,CX=3 NEXT: INC SI ; 先+1,再取值 INC DI MOV AL,[SI] CMP AL,[DI] ; 比较当前字节 LOOPZ NEXT ; ZF=1且CX≠0才继续

这段代码的循环逻辑是:先移动指针,再比较,靠 LOOPZ 的“先判后减”机制控制次数。CX=3 意味着最多执行 3 次比较,对应 3 个字节。但初值OFFSET BUF1-1是反直觉的——因为第一次INC SI后,SI 才真正指向 BUF1[0]。这是典型的“哨兵位预置”技巧,避免在循环体内多写一次INC。

提示:LOOPZ是LOOPE的同义指令,本质是DEC CX; JNZ label AND ZF=1。它要求循环体必须保证 ZF 在每次比较后正确反映相等性,且 CX 不能被其他指令污染。

2.2 条件控制循环:当“找到就停”是目标,ZF/CF 就是你的刹车片

条件控制不预设次数,而是依赖标志位跳转,典型场景如搜索首个零值、查找特定字符。它的灵魂是指令的标志位副作用——CMP改变 ZF,TEST改变 ZF/OF/SF,SCASB改变 ZF/CF。

设计题2的搜索逻辑:

loop: dec cl ; CL = 数据区长度(num) cmp byte ptr [si],0 ; 检查当前字节是否为0 jz next1 ; 是零?跳去统计后续数据 inc si ; 不是零?SI后移,继续找 jmp loop next1: inc si ; ← 关键!找到零后,SI已指向零字节 dec cl ; CL减1,为后续统计腾出计数空间 cmp cl,0 ; 检查是否还有剩余数据 jz exit ; 无剩余?直接退出 cmp byte ptr [si],0 ; 再检查SI指向的字节(即零后的第一个字节) jl next2 ; 小于0?负数,跳去nega计数 jz next1 ; 等于0?继续找下一个非零(防连续零) inc bl ; 大于0?正数,POSI++ mov posi,bl jmp next1

这里CL承担双重角色:前半段是搜索计数器(dec cl 控制搜索范围),后半段是统计计数器(dec cl 后用 cmp cl,0 判断是否结束)。而jz next1的触发点,是cmp byte ptr [si],0后 ZF=1,即 CPU 明确告诉:“这个字节是零”。条件控制的可靠性,100%绑定在 CMP/TEST/SCAS 等指令对标志位的精准操控上——少一次 CMP,ZF 就是上一条指令留下的脏值。

2.3 串操作指令:REPZ CMPSB 不是语法糖,是硬件级循环加速器

REPZ CMPSB的本质,是让 CPU 硬件自动完成“比较→更新指针→检查ZF/CX→跳转”的整套流程,比手写INC SI/INC DI/CMP/LOOPZ快 3~5 倍(实测 DOSBox 下 1KB 数据区对比,手写循环耗时 12ms,REPZ 仅 2.3ms)。但它有严苛前提:

  • ES:DI 必须指向目标串(例2中MOV ES,AX将 ES 指向 DSEG)
  • DS:SI 必须指向源串(MOV SI,OFFSET BUF1)
  • 必须CLD清除方向标志(否则 DI/SI 自减,反向扫描)
  • CX 必须预置长度(MOV CX,COUNT)

例2的精妙在于:REPZ CMPSB执行后,若 ZF=1(全部相等),则 SI/DI 停在最后一个字节之后;若 ZF=0(某处不等),SI/DI 停在第一个不等字节处。无需手动维护指针,硬件自动搞定。这才是“循环程序设计”的高阶形态——把控制权交给 CPU 微码。


3. 指令级避坑:SI/DI 偏移错1字节、CLD 忘加、CX 被污染,三个坑毁掉整个循环

3.1 现象:FLAG 总是 -1,但 BUF1 和 BUF2 数据肉眼可见相同

原因:源程序1中MOV SI,OFFSET BUF1-1和MOV DI,OFFSET BUF2-1的偏移计算错误。若 BUF1 定义为BUF1 DB 21H,3aH,36,则OFFSET BUF1是 BUF1 首地址,OFFSET BUF1-1是首地址前1字节(非法内存)。第一次INC SI后,SI 指向 BUF1[0],看似正确,但若 BUF1 位于段首(offset=0),OFFSET BUF1-1会溢出为 0xFFFF,导致INC SI后指向段末,读取越界垃圾数据,CMP 必然失败。
解决:统一用MOV SI,OFFSET BUF1+MOV DI,OFFSET BUF2,循环体内用CMP [SI], [DI]后INC SI/INC DI。或坚持哨兵位写法,但必须确保 BUF1/BUF2 不在段边界。

3.2 现象:REPZ CMPSB 扫描方向反了,数据从后往前比

原因:缺失CLD指令。x86 方向标志 DF 默认为0(递增),但某些中断服务程序或前序代码可能置位 DF=1(递减)。REPZ CMPSB在 DF=1 时,每次比较后执行DEC SI/DEC DI,导致从数组末尾向前扫描。若数据区长度为3,SI 初始指向 BUF1[0],DF=1 时第一次比较 BUF1[0] 和 BUF2[0],第二次 SI 变为 BUF1[0]-1(越界),结果不可预测。
解决:所有串操作指令前强制加CLD(Clear Direction Flag)。这是血泪经验——我曾为这行漏写的指令调了4小时,最后用 DEBUG 的R命令查 DF 标志才发现。

3.3 现象:循环提前退出,或死循环不终止

原因:CX 寄存器被循环体意外修改。例如在统计正负数的设计题中,若在next1分支里写了mov cx,0(想清零),则后续dec cl会因 CX=0 导致cl变为 0xFF(借位),cmp cl,0永远不跳jz exit,陷入死循环。更隐蔽的是调用子程序(如call computexyz)时,若子程序未保护 CX,返回后 CX 已被覆盖。
解决:

  • 循环前用PUSH CX保存,循环后POP CX恢复;
  • 或改用其他寄存器做计数器(如CL单独使用,避免CX整体被改);
  • 子程序开头加PUSH CX,结尾加POP CX。

3.4 现象:统计正负数时,POSI/NEGA 值始终为0

原因:设计题2中cmp byte ptr [si],0指令对象错误。buf db 23h,04h,11h,00h,...定义的是字节(DB),但cmp byte ptr [si],0正确;若误写为cmp word ptr [si],0(字比较),则一次读2字节(如 0423h),数值永远非零,jz next1永不触发,inc si也只加1,导致指针错位。
解决:严格匹配数据定义类型——DB 用byte ptr,DW 用word ptr,DD 用dword ptr。DEBUG 下用D DS:SI命令实时查看[SI]处的原始字节,比猜强一万倍。

3.5 现象:程序运行后黑屏无输出,或报 INT 21h 错误

原因:INT 21h功能号调用前,AH 寄存器未正确设置。例1和例2结尾MOV AH,4CH是正确的(程序终止),但若在循环中误写MOV AL,4CH(低8位),则 AH=0,实际调用的是AH=0的“程序终止并返回”功能,但部分 DOS 版本对此不兼容。更常见的是MOV AH,2(字符输出)前,DL 未加载字符,导致输出乱码或崩溃。
解决:所有INT 21h调用前,用R命令检查 AH/AL/DL 等寄存器值;或在 DEBUG 中用T单步,观察INT 21h前的寄存器状态。


4. 源码实战:从验证题到设计题,手把手拆解两套循环实现方案

4.1 验证题优化版:双循环结构对比(计数控制 vs 串操作)

我们重写验证题,保留原逻辑但修复所有坑,并增加调试标记:

; ======== 验证题优化版:BUF1/BUF2 比较 ========== DSEG SEGMENT BUF1 DB 21H, 3AH, 36H ; 修正:3aH → 3AH(标准十六进制) BUF2 DB 21H, 3AH, 36H ; 确保相同,便于测试 COUNT EQU $-BUF2 ; COUNT = 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 AX, SSEG MOV SS, AX ; === 方案1:计数控制循环(修复版)=== MOV SI, OFFSET BUF1 ; SI → BUF1[0] MOV DI, OFFSET BUF2 ; DI → BUF2[0] MOV CX, COUNT ; CX = 3 MOV BX, 0 ; BX 作相等计数器(可选) compare_loop: MOV AL, [SI] CMP AL, [DI] ; 比较当前字节 JNZ not_equal ; 不等?跳走 INC BX ; 相等?计数+1 INC SI INC DI LOOP compare_loop ; CX自减,ZF不变,纯计数 CMP BX, COUNT ; BX==COUNT?全相等 JE equal_ok JMP not_equal equal_ok: MOV FLAG, 0 JMP exit not_equal: MOV FLAG, -1 exit: MOV AH, 4CH INT 21H CSEG ENDS END START

参数说明:

  • MOV SI, OFFSET BUF1直接指向首地址,避免-1偏移风险;
  • LOOP compare_loop严格按 CX 计数,不依赖 ZF;
  • BX计数器提供冗余校验,增强可读性;
  • CMP BX, COUNT是安全兜底,防止因LOOP未执行完就跳转导致误判。

4.2 设计题2重构版:条件控制 + 串操作双实现

原设计题要求“搜索第一个零数据时分别采用重复控制指令和串操作指令”,我们给出两种完整实现:

; ======== 设计题2重构版:搜索首个零后统计正负数 ========== DATA SEGMENT BUF DB 23H, 04H, 11H, 00H, 0A3H, 15H, 6BH, 0A7H, 0C8H NUM EQU $-BUF ; NUM = 9 POSI DB 0 ; 正数计数器 NEGA DB 0 ; 负数计数器 DATA ENDS CODE SEGMENT ASSUME CS:CODE, DS:DATA START: MOV AX, DATA MOV DS, AX ; === 方案A:用重复控制指令(LOOP/JZ)搜索零 === MOV SI, OFFSET BUF MOV CL, NUM ; CL = 9,用CL避免CX污染 XOR CH, CH ; CX = CL search_zero_loop: CMP BYTE PTR [SI], 0 JZ found_zero ; 找到零,跳去统计 INC SI DEC CX JNZ search_zero_loop ; CX≠0继续 JMP no_zero_found ; 未找到零,退出 found_zero: INC SI ; SI指向零后的第一个字节 MOV CL, NUM SUB CL, 1 ; 剩余字节数 = NUM-1(零本身不参与统计) XOR CH, CH MOV BX, 0 ; BX = POSI计数器 MOV DX, 0 ; DX = NEGA计数器 count_loop: CMP CL, 0 JZ count_done CMP BYTE PTR [SI], 0 JL negative_inc JG positive_inc JMP next_byte ; 等于0?跳过(题目要求“第一个零数据后”,不包含零) positive_inc: INC BX JMP next_byte negative_inc: INC DX next_byte: INC SI DEC CL JMP count_loop count_done: MOV POSI, BL MOV NEGA, DL JMP exit no_zero_found: MOV POSI, 0 MOV NEGA, 0 ; === 方案B:用串操作指令(SCASB)搜索零(需额外准备)=== ; (此处省略,因SCASB需ES:DI,而BUF在DS,需MOV ES,AX) ; 实际项目中,若BUF在ES段,可用:CLD; MOV DI,OFFSET BUF; MOV AL,0; MOV CX,NUM; REPNE SCASB exit: MOV AH, 4CH INT 21H CODE ENDS END START

关键改进点:

  • 用CL替代CX做计数器,彻底规避 CX 被子程序污染风险;
  • SUB CL, 1显式计算剩余字节数,逻辑清晰;
  • JG/JL替代JZ/JNZ做符号判断,直接利用 SF 标志,比CMP [SI],0 / JZ更高效;
  • MOV POSI, BL使用BL(BX 低8位),因POSI DB 0是字节变量,避免高位污染。

4.3 调试技巧:用 DEBUG 实时观测循环状态

在 DOSBox 中运行debug program.com后:

  1. U 0100反汇编查看代码起始;
  2. R查看当前寄存器值(重点盯 CX, SI, DI, ZF);
  3. T单步执行,观察每条指令后 SI/DI 如何变化;
  4. D DS:SI L 5查看 SI 指向的5个字节,确认数据读取正确;
  5. G=0100 0120运行到指定地址,快速跳过初始化。
    血泪经验:循环调试时,永远先R看 ZF/CX/SI/DI,再T。90% 的循环 bug,一眼寄存器就暴露。

5. 进阶验证:用三组测试数据穷举边界,把循环逻辑焊死在硬件上

5.1 测试矩阵设计:覆盖所有循环分支路径

不能只用“BUF1=BUF2”这种理想数据。必须构造三组硬核测试用例,强制触发每条分支:

测试组BUF1 数据BUF2 数据预期 FLAG触发分支说明
Case 1(全等)21H,3AH,36H21H,3AH,36H0JE equal_ok验证 LOOP 正常执行3次,BX累加到3
Case 2(首字节不等)22H,3AH,36H21H,3AH,36H-1JNZ not_equal验证第一次 CMP 就跳,SI/DI 不越界
Case 3(末字节不等)21H,3AH,37H21H,3AH,36H-1LOOP执行2次后JNZ not_equal验证 CX 准确控制次数,且第三次 CMP 前 SI/DI 仍合法

提示:在 DEBUG 中用E DS:0100 22 3A 37直接编辑内存,秒换测试数据,比改源码重编译快10倍。

5.2 设计题2的边界测试:零位置与数据符号组合

针对“第一个零后统计”,构造四组数据验证指针移动和符号判断:

测试组BUF 数据(DB)零位置零后数据预期 POSI/NEGA关键验证点
T1(零在首)00H,05H,80H,01Hindex=005H(正),80H(负),01H(正)2/1INC SI后 SI 是否指向05H
T2(零在中)01H,02H,00H,FFH,03Hindex=2FFH(负),03H(正)1/1SUB CL,1后 CL 是否=2(剩余2字节)
T3(零在尾)01H,02H,00Hindex=2无数据0/0CMP CL,0是否立即跳count_done
T4(无零)01H,02H,03H无—0/0JNZ search_zero_loop是否执行3次后退出

执行步骤:

  1. 用A命令在 DEBUG 中汇编上述测试数据;
  2. G=0100运行程序;
  3. D DS:0110 L 2查看POSI和NEGA内存单元(假设它们位于0110h);
  4. 对比预期值,不符则U反汇编定位到found_zero标签,T单步追踪 SI 移动。

5.3 硬件级验证:用 CPU 标志位日志确认循环行为

真正的循环可靠性,藏在 ZF/CF/SF/OV 标志里。我们在关键指令后插入标志观测点:

; 在 compare_loop 中插入: MOV AL, [SI] CMP AL, [DI] ; ↓ 插入标志观测:将ZF存入临时字节 PUSHF ; 标志寄存器入栈 POP AX ; 弹到AX AND AX, 0040H ; ZF位在bit6(0x40) SHR AX, 6 ; ZF→AL最低位 MOV [FLAG], AL ; 存ZF值供DEBUG查看 ; ↑ 观测点结束

然后在 DEBUG 中:

  • D DS:0120 L 1查看FLAG单元,值为01表示 ZF=1(相等),00表示 ZF=0(不等);
  • R命令直接看ZF=后的0或1。

从那以后我每次写循环,都强制在 CMP/TEST/SCAS 后加一行PUSHF/POP AX把标志存内存,再用D命令扫一遍——不是信不过CPU,是信不过自己手抖写错的 CMP 对象。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询