简介:这份资源是面向微机原理与8086汇编语言学习者的字符匹配实验完整报告,针对“在任意长度字符串中搜索子串、且禁止使用串操作指令”这一经典上机题,提供从算法设计到调试验证的全过程记录。压缩包共1个docx文件,约839KB,内容涵盖题目要求、设计框图、主要循环体算法、带注释代码、EMU86与DOS双环境运行结果对比,以及实验心得。读者可从中获取字符比较与指针更新、found与pos单元置位、失败计数映射等关键实现思路,并对照匹配成功与失败两组测试用例理解跳转条件与循环控制,适合需要完成同类实验或复习汇编语法与程序设计技巧的学生参考。目前已有1709人学习下载。
1. 从一道“禁用串操作”的匹配题说起
8086 汇编课上,字符匹配这道上机题几乎是绕不开的坎:给定一个以 00h 结尾、长度小于 200h 的主串 str,再给一个以 00h 结尾、长度小于 80 的子串 substr,要求在不使用串操作指令的前提下,找出 substr 在 str 中首次出现的位置,找到就把 found 置 ffh、把位置写进 pos,找不到就把 found 置 00h。很多人第一反应是repne scasb一把梭,题目一句“禁止使用串操作指令”直接把这条路堵死,只能老老实实用指针加循环体手搓。这份微机字符匹配实验完整报告,解决的正是“从算法框图到 EMU8086 与 DOS 双环境跑通”的完整交付问题,适合正在做微机原理课设、汇编上机实验,或者需要交一份能复现、能答辩的实验报告的人直接参考。
2. 需求拆解与算法设计:把匹配过程拆成三个判断分支
2.1 题目约束背后的真实难点
这道题看着简单,真正动手才会发现约束条件一个比一个刁。str 长度小于 200h,意味着位置索引最大 0x1FF,用 16 位寄存器存 pos 完全够用,但循环计数器不能想当然地拿整个串长去跑;substr 长度小于 80,说明内层匹配最多推进 0x7F 次,嵌套循环的边界要分开算。00h 作为结束标志,意味着不能用长度寄存器直接控制循环,得靠“读一个字节、判一次零”来驱动,这本身就是一种典型的汇编思维训练。
最容易被忽略的是“禁止使用串操作指令”这条。movsb、cmpsb、scasb这些指令本质上是把“取字节、比较、指针自增、计数器自减”打包成一条微码,禁掉它们等于要求你把这层封装手动展开。血泪经验是:很多人代码写出来能跑,但一被问“你这算不算变相用了串操作”,就答不上来。合格的写法是全程只用mov、cmp、inc、dec、jmp、条件跳转这几类基本指令,指针推进靠显式的inc si/inc di,这样答辩时任何一行都能讲清楚。
还有一点,题目要求“匹配位置以 str 首地址为 0 参考点”,也就是 pos 存的是偏移量而不是绝对地址。这个细节在调试时特别容易翻车——如果你直接把 si 的当前值存进 pos,而 si 初始化时指向的是 str 的标号地址,那 pos 里就是绝对地址,跟题目要的偏移量差了一个基址。通常的做法是单独维护一个偏移计数器,或者用si - offset str算出来再存。
2.2 主循环体的三分支结构
算法思路在报告里写得很清楚,核心是一个“字符比较 → 相等处理 → 不相等处理”的三分支循环体。把它翻译成可执行的逻辑,大概是这么个骨架:
; 寄存器约定: ; SI -> str 当前比较位置 ; DI -> substr 当前比较位置 ; BX -> 本轮匹配在 str 中的起始偏移(即候选 pos) ; CX -> 失败回退计数 / 内层已匹配长度 ; AL -> str 当前字符,DL -> substr 当前字符 MATCH_LOOP: mov al, [si] ; 取主串当前字符 cmp al, 0 ; 主串是否已到结尾 je NOT_FOUND ; 到结尾仍未匹配,失败 mov dl, [di] ; 取子串当前字符 cmp al, dl jne MISMATCH ; 字符不等,走回退分支 ; ---- 相等分支 ---- inc si inc di cmp byte ptr [di], 0 ; 子串是否已结束 je FOUND ; 子串走完,匹配成功 jmp MATCH_LOOP MISMATCH: ; ---- 不等分支:回退到候选起点 +1 重新开始 ---- inc bx ; 候选起点后移一位 mov si, bx ; SI 回到新候选起点 lea di, substr ; DI 重置到子串首 jmp MATCH_LOOP FOUND: mov found, 0FFh mov pos, bx ; BX 即匹配起始偏移 jmp DONE NOT_FOUND: mov found, 00h DONE: ; 程序结束这段骨架里,BX 承担了“候选起点”的角色,是整个算法的锚点。相等时 SI、DI 同步前进,一旦失配,SI 不是简单加一,而是回到 BX+1 重新对齐——这正是报告里“将失败计数值映射到指针”那句话的落地方式。用 BX 显式记录候选起点,比用 CX 做失败计数再反算指针要直观得多,调试时在 EMU8086 里盯着 BX 和 SI 两个寄存器就能判断回退逻辑对不对。
参数上要注意:cmp byte ptr [di], 0里的byte ptr不能省,否则汇编器可能按字比较,把子串结尾后面的字节也读进来,导致“子串明明结束了却判不出来”的玄学 bug。另外lea di, substr在 EMU8086 里对数据段标号是有效的,但如果你的数据段和代码段分开定义,要确认assume ds:data已经写对,不然 DI 会指到错误段。
2.3 边界条件与失败回退的正确性
回退逻辑是这道题最容易写错的地方。举个具体例子:str =*bb*bbu*bbup*bbbupt*bbupt,substr =bbupt。当 SI 走到第一个bbu时,前三个字符b、b、u都匹配上了,第四个字符 str 是*、substr 是p,失配。这时候如果只是inc si从失配位置继续,就会漏掉“从第二个 b 开始可能匹配”的情况。正确做法是回到候选起点 BX 的下一位,也就是从第一个b后面的那个b重新开始比。
这种“部分匹配后回退”的模式,本质上是朴素字符串匹配算法(暴力匹配)的手写版。它的时间复杂度是 O(n×m),n 是主串长度、m 是子串长度,在这道题的规模下(n < 512,m < 128)完全够用,不需要上 KMP。但要注意一个坑:如果 substr 是空串(首字节就是 00h),按上面的逻辑会在第一次比较时cmp byte ptr [di], 0直接命中 FOUND,pos 存的是 0。这在语义上算“空串在任何位置都匹配”,但题目没明确要求处理这种情况,通常的做法是在程序入口先判一次 substr 首字节,为空则直接置 found=ffh、pos=0,避免后续循环出现未定义行为。
3. 完整程序实现:数据段、代码段与双环境验证
3.1 数据段与变量布局
数据段的布局直接决定了调试时能不能一眼看懂内存。我一般会把 str、substr、found、pos 四个变量按顺序放,并在 str 和 substr 后面显式补 00h,这样即使手写测试数据时忘了结尾符,也能靠显式补零兜底:
DATA SEGMENT str db '*bb*bbu*bbup*bbbupt*bbupt', 00h substr db 'bbupt', 00h found db 00h pos dw 0000h DATA ENDS CODE SEGMENT assume cs:CODE, ds:DATA START: mov ax, DATA mov ds, ax ; 初始化数据段 lea si, str ; SI 指向主串首 lea di, substr ; DI 指向子串首 xor bx, bx ; BX = 候选起点偏移,从 0 开始 ; ... 主循环体(见 2.2 节骨架) CODE ENDS END START这里pos用dw定义而不是db,因为 str 长度上限 200h,偏移量可能超过 255,用字节存会截断。found 用db就够,它只有 00h 和 ffh 两个值。数据段里 str 的测试串故意设计成包含多个“近似匹配”片段(bbu、bbup、bbupt层层递进),就是为了在调试时能观察到回退逻辑是否真的从正确位置重启,而不是碰巧蒙对。
3.2 主循环的完整落地与寄存器分工
把 2.2 的骨架补全成可汇编的完整循环,关键是把“相等分支里子串结束判断”和“不等分支里主串结束判断”这两个出口分清楚。完整版本如下:
MATCH_LOOP: mov al, [si] cmp al, 0 je NOT_FOUND ; 出口1:主串耗尽 mov dl, [di] cmp al, dl jne MISMATCH inc si ; 字符相等,双指针前进 inc di cmp byte ptr [di], 0 je FOUND ; 出口2:子串耗尽,匹配成功 jmp MATCH_LOOP MISMATCH: inc bx ; 候选起点右移一位 mov si, bx ; SI 回到新候选起点 lea di, substr ; DI 重置 cmp byte ptr [si], 0 je NOT_FOUND ; 出口3:回退后主串已到尾 jmp MATCH_LOOP FOUND: mov found, 0FFh mov pos, bx jmp DONE NOT_FOUND: mov found, 00h mov pos, 0FFFFh ; 约定:未找到时 pos 置 FFFFh,便于区分 DONE: mov ah, 4Ch int 21h寄存器分工是这套实现的核心:SI 是“当前比较位置”,DI 是“子串内位置”,BX 是“本轮候选起点”,AL/DL 只做临时字符暂存。CX 在这版里没有用上,因为回退靠 BX 直接定位,不需要失败计数反算——这是和报告里“失败计数值加一并映射到指针”略有不同的实现选择,效果等价但更不容易算错。如果你坚持按报告的 CX 方案写,那 CX 要每轮失配后inc cx,再用si = 候选起点 + cx算新位置,多一步加法,调试时多一个寄存器要盯。
pos在未找到时置 FFFFh 是我个人的习惯,题目只要求 found=00h,没规定 pos 的值,但留一个哨兵值能让测试用例断言更干净——答辩时被问“没找到时 pos 是什么”,答“约定为 FFFFh,避免残留旧值误导”比答“没管它”要稳。
3.3 EMU8086 与 DOS 双环境的一致性验证
报告里做了两组测试:成功用例 str=*bb*bbu*bbup*bbbupt*bbupt、substr=bbupt,预期 pos 指向最后一个bbupt的起始偏移;失败用例 str=*bb*bbu*bbup*、substr=bbupt,预期 found=00h。这两组用例的设计意图很明确:成功用例里前面埋了bbu、bbup这些“半截匹配”陷阱,专门考验回退;失败用例则是主串在候选起点耗尽前就结束了,考验出口 1 和出口 3 的衔接。
在 EMU8086 里验证时,单步执行到 FOUND 标签,看 BX 的值是不是等于手工数出来的偏移。以成功用例为例,*bb*bbu*bbup*bbbupt*bbupt里最后一个bbupt前面有 19 个字符(含前面的*和bbu片段),所以 pos 应该是 0x13。如果 BX 停在 0x13,说明回退逻辑正确;如果停在更小的值,多半是失配后 SI 没回到 BX+1 而是直接 inc 了。DOS 环境下用debug加载 exe,g命令跑到结束,d查看 found 和 pos 所在内存,结果应与 EMU8086 一致。两个环境结果对不上,最常见的原因是数据段初始化漏了mov ds, ax,导致 DOS 下读到的是 PSP 段的数据,这种 bug 在 EMU8086 的默认配置里有时被掩盖。
提示:EMU8086 的“Emulate”和“Compile and Run”行为不完全一样,前者是软件模拟、后者可能走内置的 DOS 兼容层。验证时以 Emulate 单步结果为准,再用真实 DOS(或 DOSBox)跑一遍 exe 做交叉确认,两边一致才算过。
4. 调试避坑:四个最容易翻车的点
4.1 现象:匹配成功但 pos 总是差一个固定值
原因几乎都是 pos 存了绝对地址而不是偏移。SI 初始化时lea si, str拿到的是 str 在数据段内的偏移,如果直接把 SI 存进 pos,得到的是“str 基址 + 匹配位置”,比题目要的偏移多了 str 的起始偏移。解决方法是全程用 BX 记录相对偏移(从 0 开始inc bx),pos 只存 BX,永远不存 SI。这个坑的隐蔽性在于:如果 str 恰好定义在数据段偏移 0 处,两种写法结果一样,换个数据段布局就露馅。
4.2 现象:某些用例死循环,EMU8086 跑飞
原因通常是失配分支里 SI 没有真正前进。比如写成mov si, bx但忘了inc bx,SI 回到原候选起点,下一轮又同样失配,无限循环。或者lea di, substr写成了mov di, substr,后者取的是 substr 地址处的字内容而不是地址本身,DI 指向垃圾数据,比较结果随机。解决方法是失配分支严格按“先 inc bx、再 mov si,bx、再 lea di,substr”的顺序写,并在循环入口加一个“SI 是否超过 str 末尾”的兜底判断,防止越界读。
4.3 现象:子串结尾判断失效,把后面的字节也匹配进去
原因是cmp byte ptr [di], 0漏了byte ptr,汇编器按字比较,读的是 DI 处两个字节,低字节是 00h 但高字节非零时比较结果不为零,子串结束判断永远不成立。另一种情况是数据段里 substr 后面紧跟着 found 变量,如果 substr 忘了补 00h,程序会把 found 的初值 00h 当成子串结尾,碰巧能跑但语义完全错。解决方法:substr 定义时显式补, 00h,比较时强制byte ptr,并在数据段里把 substr 和 found 之间留一两个填充字节,避免“忘补零也能跑”的假象。
4.4 现象:EMU8086 通过、DOS 下 found 值不对
原因是段寄存器初始化不完整。EMU8086 的默认模板有时会自动帮你设好 DS,但裸写 exe 在 DOS 下跑,mov ax, DATA/mov ds, ax少一步,DS 就还是 PSP 段,所有变量读写都偏了。表现是 found 读出来是随机值,或者 pos 指向莫名其妙的位置。解决方法:程序入口固定写三行段初始化,assume伪指令写全,编译后用debug的r命令确认 DS 确实等于数据段段址,再往下跑。
5. 验收与答辩:怎么证明这套匹配真的对
验收这道题,光说“跑通了”不够,得拿得出可复现的证据链。我一般按三步走:先手工推演预期值,再单步验证中间状态,最后用边界用例压一遍。
第一步,对每组测试数据手工算出预期 pos。成功用例*bb*bbu*bbup*bbbupt*bbupt配bbupt,从前往后数,最后一个bbupt的起始偏移是 0x13,那验收断言就是 found=FFh、pos=0x13。这一步必须在写代码前做完,否则跑出什么结果都觉得“大概对”。
第二步,在 EMU8086 里单步到 FOUND 标签,检查 BX、SI、DI 三个寄存器的值是否符合推演。重点看失配那几轮 BX 有没有按预期递增、SI 有没有回到 BX 指向的位置。这一步能抓出 90% 的回退逻辑错误。
第三步,补边界用例。下面这张表是我答辩时常用的验收清单:
| 用例 | str | substr | 预期 found | 预期 pos | 考察点 |
|---|---|---|---|---|---|
| 成功-多陷阱 | *bb*bbu*bbup*bbbupt*bbupt | bbupt | FFh | 0x13 | 回退正确性 |
| 失败-主串耗尽 | *bb*bbu*bbup* | bbupt | 00h | FFFFh | 出口 1/3 |
| 首字符即匹配 | abc | a | FFh | 0x00 | 偏移 0 边界 |
| 子串等于主串 | abc | abc | FFh | 0x00 | 等长匹配 |
| 子串长于主串 | ab | abc | 00h | FFFFh | 越界保护 |
答辩时被问得最多的两个问题:一是“你为什么不用 KMP”,答“题目规模 n<512、m<128,暴力匹配最坏 6 万多次比较,8086 主频下毫秒级完成,KMP 的 next 数组构建反而增加代码复杂度和调试成本”;二是“禁串操作你怎么保证没变相用”,答“全程只有 mov/cmp/inc/dec/jmp 和条件跳转,指针推进逐条可见,没有任何 rep 前缀或串指令助记符”。把这两问答利索,基本就稳了。
从那以后我每次交汇编实验,都强制自己先手推预期值、再单步对寄存器、最后补边界表,三步缺一不交。这套习惯帮我躲过了好几次“碰巧跑通”的假象。希望帮到你。
本文还有配套的精品资源,点击获取