简介:这份资源是面向微机原理与8086汇编语言学习者的字符匹配实验完整报告,针对“在任意长度字符串中搜索子串、且禁止使用串操作指令”这一经典上机题,提供从算法设计到调试验证的全过程记录。压缩包共1个docx文件,约839KB,内容涵盖题目要求、设计框图、主要循环体算法、带注释代码、EMU86与DOS双环境运行结果及实验心得。读者可重点参考其匹配成功与失败两类测试用例的对比分析,理解如何用变址寄存器与循环计数器替代串操作指令,掌握found、pos单元的置位逻辑,以及失败计数映射指针重新匹配的跳转条件设计。已有1709人学习下载,适合正在完成汇编课程实验、需要对照调试思路与报告写法的学生参考。
1. 从一道“禁用串操作”的匹配题说起
8086 汇编课上最容易被低估的一道题,是“在任意长度字符串里找子串”。题目本身不复杂,但加了一条限制——禁止使用串操作指令——难度立刻上了一个台阶。这意味着MOVSB、CMPSB、SCASB这些平时用来偷懒的指令全部不能用,只能老老实实用MOV、CMP、INC、LOOP这些基本指令,靠指针和计数器把匹配逻辑一步步搭出来。这份微机字符匹配实验完整报告,解决的正是这个问题:给定以00h结尾的主串str(长度小于200h)和子串substr(长度小于80),在不使用串操作指令的前提下完成搜索匹配,找到则把found置为ffh并把匹配位置写入pos,找不到则found置00h。它适合正在做微机原理课程设计、汇编语言上机实验,或者需要交一份完整实验报告的学生,也适合想重新捡起 8086 底层编程的从业者拿来做参考实现。
2. 需求拆解与算法设计:指针、计数器与三个出口
2.1 题目约束到底限制了什么
先把题目里的几个硬约束翻译成对程序结构的具体要求。主串str以00h结束、长度小于200h,说明不能用固定长度循环,必须以“遇到00h就停”作为主串的终止条件;子串substr以00h结束、长度小于80,同理。found和pos是两个结果单元,前者是字节标志,后者存放匹配位置,位置的计算基准是“以str首地址为 0 参考点”,也就是偏移量而不是绝对地址。
“禁止使用串操作指令”这条约束最容易被忽视。很多同学第一反应是用CMPSB配合REPE前缀,三行写完,但题目明确禁止。禁用之后,字符比较只能走MOV AL, [SI]/CMP AL, [DI]这条路,指针推进只能靠INC SI/INC DI,循环控制只能靠LOOP或手动的DEC CX+JNZ。这不是故意为难人,而是逼你把“指针 + 计数器”这套最底层的寻址模型吃透——后面学中断、学 DMA、看驱动代码,靠的都是这套思维。
还有一点题目没明说但必须处理:匹配失败后的回溯。假设主串是*bb*bbu*bbup*,子串是bbupt,当比较到bbup的最后一个p和子串的t不相等时,不能简单地把主串指针往后挪一位重新开始,因为bbup里可能还藏着新的匹配起点。正确的做法是记录本轮匹配的起始位置,失败后从“起始位置 + 1”重新开始。这个回溯逻辑是整个算法最容易翻车的地方,后面避坑章节会专门展开。
2.2 主循环体的三段式结构
把匹配过程拆开看,核心循环体只有三种状态:字符相等且子串未结束、字符相等且子串已结束、字符不相等。这三种状态对应三个出口,用一张状态转移表就能说清楚:
| 当前状态 | 判断条件 | 动作 | 下一状态 |
|---|---|---|---|
| 字符比较 | [SI] == [DI]且[DI] != 00h | SI++、DI++,继续比较 | 字符比较 |
| 匹配成功 | [SI] == [DI]且[DI] == 00h | found = ffh,pos = 起始偏移 | 结束 |
| 匹配失败 | [SI] != [DI]且[SI] == 00h | found = 00h | 结束 |
| 失配回溯 | [SI] != [DI]且[SI] != 00h | 起始偏移 +1,重置DI,重新比较 | 字符比较 |
这张表就是设计框图的文字版。实际写代码时,SI指向主串当前字符,DI指向子串当前字符,另外需要一个寄存器(比如BX)保存本轮匹配的起始偏移,一个寄存器(比如CX)做循环计数或失败计数。题目正文里提到的“失败计数值加一并映射到指针”,本质就是BX自增后重新计算SI = str首地址 + BX,只不过用偏移量表达更直观。
2.3 为什么不用 KMP,而用朴素回溯
有同学会问:既然学了 KMP,为什么不用?原因有三。第一,KMP 的next数组需要预处理子串,而子串以00h结尾、长度动态,预处理本身就要额外一轮扫描,在 8086 这种寄存器紧张的环境里,多一个数组就多一份内存管理负担。第二,KMP 的优势在长文本、重复前缀多的场景,而本题主串长度上限200h(512 字节)、子串上限80(128 字节),最坏情况下的朴素匹配也就几万次比较,8086 跑起来完全在可接受范围。第三,也是最重要的一点:这是一道教学实验题,考察的是指针操作和循环控制的基本功,朴素回溯的每一步都看得见、调得动,用 KMP 反而把核心逻辑藏进了next数组里,调试时成了黑匣子。
我一般会跟做这个实验的人说:先把朴素回溯写对、调通,再考虑优化。朴素版本能跑通,说明你对SI、DI、BX、CX四个寄存器的配合已经清楚了;这个基础打牢,后面学任何字符串算法都是水到渠成。
3. 完整实现:寄存器分配与逐段代码
3.1 数据段与寄存器约定
先把数据段定下来。str和substr用DB定义,末尾手动加00h;found和pos用DB和DW预留。寄存器分配遵循一个原则:谁的生命周期长,谁占固定寄存器。SI全程指向主串当前比较位置,DI全程指向子串当前比较位置,BX保存本轮匹配的起始偏移(生命周期贯穿整个外层循环),CX做内层循环的临时计数。AX用来做字符搬运和结果写入。
DATA SEGMENT STR DB '*bb*bbu*bbup*bbbupt*bbupt', 00H ; 主串,以00H结尾 SUBSTR DB 'bbupt', 00H ; 子串,以00H结尾 FOUND DB 00H ; 匹配标志:FFH成功,00H失败 POS DW 0000H ; 匹配位置(相对STR首地址的偏移) DATA ENDS这里有个细节:STR的末尾我特意放了*bbupt,是为了让匹配位置落在非零偏移上,调试时能一眼看出pos算得对不对。如果匹配位置是 0,pos初始值也是 0,成功和失败看起来一样,容易误判。
3.2 主匹配循环的汇编实现
下面是核心匹配段的完整代码。整体结构是“外层循环遍历主串起点,内层循环逐字符比较”,外层用BX做起始偏移,内层用SI、DI做双指针推进。
CODE SEGMENT ASSUME CS:CODE, DS:DATA START: MOV AX, DATA MOV DS, AX LEA SI, STR ; SI 指向主串首地址 MOV BX, 0 ; BX = 本轮匹配的起始偏移,从0开始 OUTER_LOOP: MOV AL, [SI] ; 取主串当前字符 CMP AL, 00H ; 主串是否已到结尾 JE NOT_FOUND ; 到结尾仍未匹配,跳失败出口 ; 内层比较:从 BX 偏移处开始,逐字符比对 MOV DI, OFFSET SUBSTR ; DI 重置到子串首地址 MOV CX, SI ; 暂存当前 SI,用于失败后回溯 PUSH SI ; 保存本轮起始 SI INNER_LOOP: MOV AL, [SI] ; 主串字符 MOV DL, [DI] ; 子串字符 CMP DL, 00H ; 子串是否已结束 JE MATCH_FOUND ; 子串结束且前面都相等,匹配成功 CMP AL, DL ; 比较两字符 JNE MISMATCH ; 不相等,跳失配处理 INC SI ; 相等,双指针后移 INC DI JMP INNER_LOOP MISMATCH: POP SI ; 恢复本轮起始 SI INC SI ; 起始位置后移一位,准备下一轮 INC BX ; 起始偏移加一 JMP OUTER_LOOP MATCH_FOUND: POP SI ; 平衡栈 MOV FOUND, 0FFH ; 置成功标志 MOV POS, BX ; 记录匹配偏移 JMP EXIT NOT_FOUND: MOV FOUND, 00H ; 置失败标志 MOV POS, 0000H ; 位置清零,避免脏数据 EXIT: MOV AH, 4CH INT 21H CODE ENDS END START逐段说明几个关键点。OUTER_LOOP开头先检查主串当前字符是否为00H,这是主串终止条件,放在外层而不是内层,是为了保证“主串扫完就停”,不会越界。PUSH SI和POP SI这一对是回溯的核心:进入内层前把本轮起始地址压栈,失配时弹出来再INC,就实现了“从下一个位置重新开始”。BX和SI是同步的,BX记录偏移量用于写POS,SI记录实际地址用于寻址,两者不能混用。
MATCH_FOUND里先POP SI再写结果,是为了保持栈平衡。虽然程序马上要退出,栈不平衡也不影响运行,但养成习惯很重要——后面做中断实验、做子程序调用,栈不平衡就是定时炸弹。
3.3 两个测试用例的预期结果
题目正文给了两组测试数据,我把预期结果整理成表,方便调试时对照:
| 用例 | 主串 | 子串 | 预期 found | 预期 pos |
|---|---|---|---|---|
| 匹配成功 | *bb*bbu*bbup*bbbupt*bbupt | bbupt | FFH | 18(十进制) |
| 匹配失败 | *bb*bbu*bbup* | bbupt | 00H | 0000H |
成功用例里,bbupt第一次完整出现是在*bb*bbu*bbup*bbbupt这一段之后,数一下偏移:*bb*bbu*bbup*bbbu共 17 个字符,所以bbupt从偏移 17 开始?这里要小心,偏移从 0 算起,*是 0,第一个b是 1,依次数下去,bbupt的起始b落在偏移 18。调试时如果pos显示 17,说明偏移基准算错了一位,这是最常见的 off-by-one 错误。
失败用例里,主串只到*bb*bbu*bbup*,子串需要bbupt,最后一个t对不上,程序应该扫完主串后走NOT_FOUND出口。这个用例专门用来验证“主串先结束”的分支,很多实现只测成功用例,失败分支根本没走到,交上去被导师一问就露馅。
4. 调试与验证:EMU8086 与 DOS 双环境对照
4.1 在 EMU8086 里单步看寄存器
EMU8086 的价值在于单步执行时能实时看到SI、DI、BX、CX和标志位的值。调试成功用例时,我一般会在INNER_LOOP的CMP AL, DL处下断点,每停一次就核对三件事:SI指向的字符是不是主串里预期的那个、DI指向的字符是不是子串里预期的那个、BX是不是本轮起始偏移。三者对得上,说明指针逻辑没问题;对不上,问题一定出在INC或POP的位置。
调试失败用例时,重点看OUTER_LOOP开头的CMP AL, 00H。当SI走到主串末尾的00H时,应该跳NOT_FOUND,如果没跳,说明SI越界了或者00H判断写错了位置。还有一种情况:程序在INNER_LOOP里死循环,SI和DI一直加但永远不相等——这通常是子串的00H终止符漏写了,DI一路读到数据段外面去了。
4.2 DOS 实机运行与结果比对
EMU8086 跑通不代表 DOS 实机没问题,两者在段寄存器初始化和内存布局上可能有差异。DOS 环境下用DEBUG加载.EXE,先用U命令反汇编确认代码段没错位,再用G命令直接跑到EXIT,然后D DS:0000查看数据段。FOUND和POS的值应该和 EMU8086 里看到的一致。
如果两边结果不一致,优先查两处:一是ASSUME伪指令和实际的DS赋值是否匹配,MOV AX, DATA/MOV DS, AX这两行漏了任何一行,数据段寻址就会跑到别的地方;二是POS的定义是DW还是DB,题目要求存匹配位置,偏移最大200H,用DW更稳妥,如果误写成DB,高位会被截断,偏移超过 255 时结果就错了。
提示:DOS 下用
DEBUG时,G命令后面跟的地址是CS:IP形式的断点,直接G不带参数会一直跑到程序结束或死循环,建议先U找到EXIT标号对应的偏移,再G 偏移精确停住。
4.3 边界用例补测清单
除了题目给的两组用例,交报告前我建议再补测四组,覆盖边界情况:
| 补测用例 | 目的 | 预期 |
|---|---|---|
主串为空(仅00H) | 验证主串立即结束分支 | found=00H |
子串为空(仅00H) | 验证空子串匹配语义 | 按题目约定处理,通常视为匹配在偏移 0 |
| 子串比主串长 | 验证不会越界读 | found=00H |
| 匹配位置在偏移 0 | 验证pos初值与成功值区分 | found=FFH,pos=0000H |
第三组尤其重要。如果实现里没有在INNER_LOOP每次比较前检查主串是否已到00H,子串比主串长时SI会越过主串末尾继续读,读到的是数据段里SUBSTR的内容,可能碰巧匹配上,产生假阳性。这种 bug 在测试用例里看不出来,一到真实数据就翻车。
5. 避坑与排查:五个最容易翻车的点
5.1 失配回溯时忘记恢复起始指针
现象:主串里明明有完整子串,程序却报found=00H。原因:失配后直接INC SI继续比较,但此时SI已经在内层循环里被推进了好几位,INC只是从“失配位置的下一位”开始,而不是从“本轮起始位置的下一位”开始,跳过了可能的匹配起点。解决:进入内层前PUSH SI保存起始地址,失配时POP SI再INC,确保每轮都从正确起点重新开始。这个坑我在带实验时见过太多次,本质是把“当前比较位置”和“本轮起始位置”混成了一个寄存器。
5.2 子串终止符判断顺序写反
现象:子串完全匹配时程序不报成功,反而继续往后比较,最后越界或死循环。原因:INNER_LOOP里先CMP AL, DL再判断DL是否为00H,当子串走到末尾时,DL=00H,AL是主串对应字符(非零),CMP结果不等,直接跳MISMATCH,永远到不了MATCH_FOUND。解决:把CMP DL, 00H/JE MATCH_FOUND放在字符比较之前,先确认子串是否已结束,再比字符。顺序一换,逻辑就顺了。
5.3 主串结束判断放在内层导致漏判
现象:主串已经扫到00H,程序还在内层循环里拿00H和子串字符比较,比较结果不等后走MISMATCH,SI继续后移,读到数据段外的随机值。原因:只在OUTER_LOOP开头判断主串结束,内层循环里没有二次检查。解决:内层每次取主串字符后,先判断是否为00H,是则直接跳NOT_FOUND。多一条CMP指令,换来的是不会越界。
5.4 POS 用 DB 定义导致偏移截断
现象:匹配位置超过 255 时,pos读出来是个小数。原因:POS DB 0只留了一个字节,而主串长度上限200H(512),偏移完全可能超过 255。解决:POS DW 0,写入时用MOV POS, BX(BX是 16 位),读取时按字读取。题目说“长度小于 200H”,这个 200H 是十六进制,等于十进制 512,不是 200,别被字面数字骗了。
5.5 栈不平衡导致返回地址错乱
现象:程序跑完INT 21H退出时行为异常,或者用RET返回时跳到错误地址。原因:PUSH SI之后在某条分支上直接JMP EXIT,没有对应的POP,栈顶残留了数据,INT 21H或RET取返回地址时取到了残留值。解决:所有从内层跳出的分支,在跳转前统一POP平衡栈;或者改用寄存器保存起始地址(比如用BP),彻底避开栈操作。教学实验里用BP更省心,但用PUSH/POP更能练栈的意识,看个人取舍。
6. 验收与答辩:怎么证明这份报告站得住
验收这份实验,核心不是“程序能跑”,而是“能证明程序在所有约定输入下都正确”。我一般会按三步走。第一步,对照题目逐条核验:str和substr是否都以00H结尾、长度是否在限制内、found和pos的写入是否符合约定、有没有出现被禁用的串操作指令(用U命令反汇编,搜MOVS、CMPS、SCAS、LODS、STOS这几个助记符,一个都不能有)。第二步,跑完题目给的两组用例加第 4 章补测的四组边界用例,每组记录found和pos的实际值,和预期值逐项比对,做成一张验收表附在报告里。第三步,随机生成一组主串和子串(比如主串里故意放多个近似前缀bbup、bbupx),手工算出预期匹配位置,再跑程序对照,验证回溯逻辑在“部分匹配后失配”的场景下没有漏掉正确起点。
答辩时导师最爱问的三个问题,提前准备好答案:为什么不用串操作指令、失配后指针怎么回溯、pos的偏移基准是什么。第一个问题答“题目约束,且能考察指针基本功”;第二个问题画一下PUSH SI/POP SI/INC SI的时序;第三个问题强调“以str首地址为 0,不是绝对地址,也不是 1 起始”。三个问题答顺了,这份实验基本就稳了。
有个血泪教训:我第一次交这个实验时,只测了成功用例,失败用例的pos忘了清零,残留了上一轮的值,导师当场用DEBUG一查就发现了。从那以后我每次写这类带结果单元的程序,都强制在失败出口把pos显式清零,不管题目要不要求——脏数据比没数据更害人。希望这份拆解能帮你少走一遍我踩过的弯路。
本文还有配套的精品资源,点击获取