简介:这份资源是面向微机原理与8086汇编语言学习者的字符匹配实验完整报告,针对“在任意长度字符串中搜索子串、且禁止使用串操作指令”这一经典上机题,提供从算法设计到调试验证的全过程记录。压缩包共1个docx文件,约839KB,内容涵盖题目要求、设计框图、主要循环体算法、带注释代码、EMU86与DOS双环境运行结果及实验心得,完整呈现了字符比较、相等与不相等分支处理、found与pos单元置位等关键逻辑。已有1709人学习下载,适合正在做汇编实验、需要参考无串操作指令实现思路与排错方法的学生对照使用,也可帮助理解指针更新、失败计数与跳转条件的组织方式。
1. 从一次“指针回退”翻车说起:8086 汇编字符串匹配到底难在哪
第一次在 8086 上做字符匹配实验,我信心满满地写完循环,结果 EMU8086 里跑出来的pos永远比正确答案大 1,换一组输入又变成乱值。盯着屏幕看了半小时才反应过来:问题不在比较指令,而在“失配之后指针该退到哪”。这个实验表面上是写一个字符串搜索程序,实际上考的是你对指针回退逻辑、循环边界、结束符判断这三件事的掌控力。题目要求很明确:str以00h结束、长度小于200h,substr以00h结束、长度小于80,找到就把found置ffh并把匹配位置(以str首地址为 0 参考点)写进pos,找不到就置00h,而且禁止使用串操作指令。这意味着你不能靠rep movsb、cmpsb这类指令偷懒,必须老老实实用si、di、cx和条件跳转把逻辑搭出来。这篇内容面向三类人:正在做微机原理上机实验、需要一份能跑通且讲得清的报告的同学;想搞明白“无串操作指令下字符串匹配怎么设计”的开发者;以及调试时总在指针回退上翻车、想找一套可复现排查思路的人。我会把算法思路、完整代码结构、两组测试用例的验证过程,以及我踩过的坑一条条拆开讲,新手能照着改,熟手能看到判断依据。
2. 算法设计:不用串操作指令,匹配逻辑怎么搭
2.1 两个指针加一个失败计数:核心循环体的拆解
这个实验的算法骨架其实不复杂,但每一步的边界条件都得想清楚。我一般会这么比:把整个匹配过程看成“主串指针si定锚点、子串指针di逐位比对、失配后锚点右移一位重来”。具体来说,用si指向str当前匹配起点,di指向substr当前比较位置,cx在失配时承担“失败计数”的角色,用来把si映射回下一个起点。
循环体分三条路走:
第一,字符比较。把[si]和[di]分别取到寄存器(比如al和bl),用cmp比较。这一步没什么玄学,但要注意取的是字节还是字——题目里字符串以00h结束,按字节处理最自然,所以用mov al, [si]和mov bl, [di]。
第二,相等处理。字符相等时,先看substr是不是已经走到结束符。判断方法是看[di]是否为00h:如果是,说明整个子串匹配完了,此时把found置ffh,并把当前锚点位置写进pos。这里有个容易翻车的点:pos存的是“以str首地址为 0 参考点”的偏移,所以要么在程序开头把str首地址存到一个基准寄存器里,用si减基准得到偏移;要么直接用si相对段首的偏移,前提是str就放在段首。我建议显式保存基准,别赌数据段布局。
第三,不相等处理。字符不等时,先判断str是否结束:如果[si]是00h,说明主串都扫完了还没匹配上,直接found = 00h收工。如果没结束,就把失败计数加一,然后让si回退到“上一个起点 + 1”的位置,di归零重新指向子串开头,进入下一轮。这里的“失败计数”本质上是记录当前锚点已经试到第几个位置,回退时用si - cx + 1或者类似方式算出新起点。很多同学写错就错在这一步:要么忘了di归零,要么si回退多减了一位。
提示:
substr为空串(第一个字节就是00h)时,按逻辑应该立即匹配成功、pos = 0。虽然实验用例一般不会给空串,但写判断时顺手处理掉,调试时少一个黑匣子。
2.2 结束符判断与边界:为什么 00h 比长度寄存器更可靠
题目给的是“以00h结束”,而不是给长度。这意味着你不能靠cx预存长度来控制循环,必须每轮都检查当前字节是不是00h。这个设计的好处是字符串长度可以动态变化,坏处是结束符判断的位置极其关键:判断早了会漏掉最后一个字符,判断晚了会越界读到脏数据。
我的做法是把结束符判断嵌在比较之后、跳转之前。相等分支里先判substr结束,不等分支里先判str结束。顺序不能反:如果先判str结束再判字符相等,那么当str最后一个字符恰好和substr当前字符相等、但substr还没结束时,你会误判为失败。反过来,如果相等分支里先改指针再判结束,又会把结束符本身当成有效字符比进去。
另一个边界是长度上限:str小于200h、substr小于80。这两个数字在实验里更多是约束数据段布局用的,程序逻辑上不需要硬编码,但调试时可以用它们来检查si、di有没有跑飞。我习惯在 EMU8086 里单步时盯着si的值,一旦超过200h还没结束,基本就是结束符判断写漏了。
2.3 寄存器分工表:谁存指针、谁存计数、谁存结果
把寄存器分工定清楚,后面写代码和调试都会顺很多。下面这张表是我实际写的时候用的分配方案,你可以按自己习惯调整,但建议保持“一个寄存器只干一件事”:
| 寄存器 | 用途 | 说明 |
|---|---|---|
si | 主串当前锚点指针 | 指向str中本轮匹配的起始位置 |
di | 子串比较指针 | 每轮从substr首地址开始,逐位后移 |
cx | 失败计数 / 偏移辅助 | 失配时累加,用于计算下一个锚点 |
al | 主串当前字符 | 从[si]取出 |
bl | 子串当前字符 | 从[di]取出 |
dx | 主串首地址基准 | 用于计算pos的相对偏移 |
found和pos放在数据段里,用mov found, 0ffh这类指令直接写。注意found是字节还是字要和数据段定义一致,实验里一般定义成db,写ffh和00h都按字节来。
3. 完整代码与逐段注释:从数据段到匹配收尾
3.1 数据段与结果单元定义
先把数据段搭好。str和substr都用db定义,末尾手动加00h。found和pos初始值给 0,方便调试时观察是否被正确改写。base用来存str首地址,算pos时用得上。
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 首地址为 0 base dw 0000h ; 保存 str 首地址,用于算偏移 DATA ENDS这里pos用dw是因为偏移可能超过 255,虽然实验用例里位置不大,但按字处理更稳妥。base在代码段开头用mov base, offset str赋值,后面算pos时用si - base得到相对偏移。
3.2 主循环代码与关键跳转说明
下面是核心匹配段。为了可读性,我把“相等处理”和“不等处理”分成两个标签,跳转关系一目了然。
CODE SEGMENT assume cs:CODE, ds:DATA START: mov ax, DATA mov ds, ax mov base, offset str ; 保存主串首地址 mov si, offset str ; si 指向主串当前锚点 mov cx, 0 ; cx 作为失败计数,初始为 0 MATCH_LOOP: mov di, offset substr ; 每轮子串从头开始 mov al, [si] ; 取主串当前字符 cmp al, 00h ; 主串是否已结束 je NOT_FOUND ; 结束则匹配失败 COMPARE: mov al, [si] ; 重新取主串字符(si 可能已后移) mov bl, [di] ; 取子串字符 cmp bl, 00h ; 子串是否已结束 je FOUND ; 子串结束,匹配成功 cmp al, 00h ; 主串是否已结束 je NOT_FOUND ; 主串结束,匹配失败 cmp al, bl ; 比较当前字符 jne MISMATCH ; 不等,转失配处理 inc si ; 相等,主串指针后移 inc di ; 子串指针后移 jmp COMPARE ; 继续比较下一位 MISMATCH: inc cx ; 失败计数加一 mov si, offset str ; 回到主串首地址 add si, cx ; 加上失败计数,得到下一个锚点 jmp MATCH_LOOP ; 从新锚点重新匹配 FOUND: mov found, 0ffh ; 置匹配成功标志 mov ax, si ; 当前 si 指向匹配结束后的位置 sub ax, offset substr ; 减去子串长度(用偏移差近似) sub ax, base ; 减去主串首地址,得到相对偏移 mov pos, ax ; 存入 pos jmp EXIT NOT_FOUND: mov found, 00h ; 置匹配失败标志 mov pos, 0000h ; 位置清零,避免脏值 EXIT: mov ah, 4ch int 21h CODE ENDS END START这段代码里有几个点值得展开说。第一,MATCH_LOOP开头先判一次主串结束,是为了处理“锚点已经移到结束符”的情况,避免进入COMPARE后拿00h去和子串首字符比。第二,COMPARE里先判子串结束再判主串结束,顺序和前面分析的一致,保证最后一个字符能被正确比较。第三,MISMATCH里用mov si, offset str加add si, cx来算新锚点,等价于“上一个起点右移一位”,因为cx记录的是已经失败过的起点数。第四,FOUND里算pos的方式是si - 子串长度 - base,但更稳妥的做法是在匹配开始时把锚点存到一个临时单元,匹配成功时直接取那个值。上面代码用偏移差近似,是因为si在匹配过程中已经后移了子串长度那么多,减回去就是起点。如果你觉得绕,可以在MATCH_LOOP开头加一句mov temp, si,成功时mov ax, temp再减base,逻辑更直白。
注意:
sub ax, offset substr这行依赖substr的长度恰好等于si后移的字节数。如果子串里有特殊字符或者长度变化,建议改用临时单元保存锚点,别用偏移差硬算。
3.3 匹配成功与失败两条路径的收尾差异
成功路径和失败路径的收尾不只是found值不同,pos的处理也有讲究。成功时pos必须写真实偏移,失败时我习惯把pos清零,这样调试时一眼能看出“没匹配上”而不是残留上一次的脏值。另外,成功路径里算完pos后直接跳EXIT,不要再掉进NOT_FOUND,否则found会被覆盖成00h。这个跳转关系在单步调试时特别容易看出来:如果found先变ffh又变00h,八成是成功分支没跳走。
4. 调试与验证:两组用例在 EMU8086 和 DOS 下的对照
4.1 匹配成功用例:str 含 bbupt 时的 pos 验证
第一组用例用题目给的:str = '*bb*bbu*bbup*bbbupt*bbupt',substr = 'bbupt'。手动数一下,str里第一个bbupt出现在末尾倒数第二个位置附近,具体偏移可以在 EMU8086 里单步看si的值。跑完之后检查found应该是ffh,pos应该指向第一个完整匹配的起始偏移。
调试时我一般分三步看:先在COMPARE标签处设断点,观察al和bl是否按预期逐位相等;再在FOUND处看si的值,确认它指向匹配结束后的位置;最后用si减去子串长度和base,核对pos是否等于手动数的偏移。EMU8086 的寄存器窗口能直接看si、di、cx,比 DOS 下盲跑友好得多。DOS 环境跑一遍主要是验证程序不依赖调试器的特殊行为,结果应该和 EMU8086 一致。
4.2 匹配失败用例:子串不存在时 found 与 pos 的落点
第二组用例:str = '*bb*bbu*bbup*',substr = 'bbupt'。主串里没有完整的bbupt,所以found应该是00h,pos按我的写法清零。这组用例的重点是看失配后的指针回退有没有把si推出界。单步时盯着cx递增、si从offset str + cx重新开始,直到si指向00h触发NOT_FOUND。如果程序卡死或者si跑到200h以外,基本就是MISMATCH里的回退公式写错了,或者COMPARE里漏了主串结束判断。
两组用例跑完,EMU8086 和 DOS 的结果一致,说明程序没有依赖调试器的隐含初始化。这里有个血泪经验:EMU8086 有时会对未初始化寄存器给 0,DOS 下可不一定,所以cx、si、di在进入主循环前一定要显式赋值,别指望“默认是 0”。
4.3 单步调试时该盯哪几个寄存器
单步调试不是从头到尾按 F7,而是有重点地看。我的习惯是:进入COMPARE后看al、bl是否等于当前期望字符;jne MISMATCH触发时看cx是否等于“当前锚点相对首地址的偏移”;FOUND触发时看si和base的差值是否等于子串长度加偏移。这三个检查点覆盖了绝大多数逻辑错误。如果found值对但pos不对,问题一定在FOUND段的偏移计算;如果found一直是00h但明明有匹配,问题多半在COMPARE的结束符判断顺序。
5. 避坑与排查:五条真实踩坑记录
5.1 现象:pos 总比正确答案大 1
原因:FOUND段算偏移时,si已经指向匹配结束后的下一个位置,减子串长度时多减或少减了一位,或者base没减。
解决:在MATCH_LOOP开头用临时单元保存锚点,成功时直接取临时单元减base,不要用si反推。改完之后pos一次就对。
5.2 现象:失配后程序死循环,si 不前进
原因:MISMATCH里cx加了但si没重新赋值,或者si回退公式写成了si - cx导致原地打转。
解决:确保MISMATCH里先inc cx,再用mov si, offset str加add si, cx重算锚点。单步时看si每轮是否严格递增,不增就是公式错了。
5.3 现象:最后一个字符匹配不上,found 误判为 00h
原因:COMPARE里先判主串结束再判字符相等,当主串最后一个字符和子串当前字符相等、但子串还没结束时,主串的00h结束符被提前读到,走了NOT_FOUND。
解决:调整判断顺序,先判子串是否结束(cmp bl, 00h),再判主串是否结束,最后才比较字符。顺序对了,最后一个字符就能正常参与比较。
5.4 现象:EMU8086 能跑,DOS 下结果不对
原因:某些寄存器在进入程序时没有显式初始化,EMU8086 默认给 0,DOS 下是随机值,导致cx或si初值不可控。
解决:START段里把si、di、cx全部显式赋值,found和pos也在数据段给初值。别依赖任何“默认值”。
5.5 现象:子串为空时程序跑飞
原因:COMPARE第一轮读到substr的00h,如果判断顺序不对,可能先拿主串字符和00h比,误判失配后cx递增、si越界。
解决:在MATCH_LOOP进入COMPARE前加一句子串首字符判空,为空直接FOUND且pos = 0。实验用例虽不考,但加上之后调试更安心。
6. 把匹配逻辑收在一个临时单元上:一个让调试少走弯路的习惯
回过头看,这个实验最值得带走的不是某一行代码,而是“锚点用临时单元保存、别用指针反推”这个习惯。我后来再做类似的子串搜索、模式匹配,甚至非汇编环境下的手写匹配,都会先把当前尝试的起点存到一个明确的位置,成功时直接读它,失败时基于它加一。这样pos的计算永远只有一次减法,不会因为匹配过程中指针移动了多少而提心吊胆。下面这个小改动就是把FOUND段从“反推”改成“直读”的关键:
; 在 MATCH_LOOP 开头保存锚点 MATCH_LOOP: mov temp, si ; temp 是数据段里新增的 dw 单元 mov di, offset substr ... ; FOUND 段直接读 temp FOUND: mov found, 0ffh mov ax, temp ; 直接取锚点 sub ax, base ; 减主串首地址 mov pos, ax jmp EXITtemp在数据段里加一句temp dw 0000h就行。这个改动看起来只省了几行,但它把“匹配位置”和“匹配过程中的指针移动”解耦了:不管COMPARE里si后移了多少次、MISMATCH里怎么回退,temp始终是这一轮的起点,pos的语义清清楚楚。调试时也简单,断点打在FOUND,看temp和base的差就是答案,不用再心算子串长度。
另一个习惯是两组用例必须都跑,而且失败用例往往比成功用例更能暴露问题。成功用例只能证明“能找到”,失败用例才检验“找不到时指针有没有跑飞、found有没有被脏值污染”。我那次pos大 1 的翻车,就是在失败用例里先发现pos残留了上一次的值,顺藤摸瓜才定位到成功分支的偏移计算。从那以后我每次写完匹配逻辑,都强制先跑一遍“子串不存在”的用例,看found和pos是不是干净地落到00h和0,再跑成功用例核对偏移。这份报告里的代码结构、寄存器分工和五条排查记录,就是按这个顺序整理出来的,你拿去改改数据段就能直接上机验证。希望帮到你。
本文还有配套的精品资源,点击获取