☰
8086汇编实现字符串匹配:禁用串操作指令的双指针与失配回退
2026/10/27 5:50:04 网站建设 项目流程

简介:这份资源是面向微机原理与8086汇编语言学习者的字符匹配实验完整报告,针对“在任意长度字符串中搜索子串、且禁止使用串操作指令”这一经典上机题目,提供从算法设计到调试验证的全过程记录。压缩包共1个docx文件,约839KB,内容涵盖题目要求、设计框图、主要循环体算法、带注释代码、EMU86与DOS双环境运行结果及实验心得,重点讲解用变址寄存器与循环计数器完成逐字符比较、匹配成功置found为ffh并记录pos、失败置00h的实现思路。已有1709人学习下载,适合正在完成汇编课程实验、需要参考算法流程与排错方法的学生对照使用,也可帮助读者理解无串操作指令条件下的字符串匹配策略与调试要点。

1. 没有串操作指令,8086 怎么把子串从主串里“抠”出来

8086 汇编里做字符串匹配,很多人第一反应是rep movsb、repne scasb这类串操作指令,一条指令顶一个循环,写起来很省事。但这份微机字符匹配实验报告偏偏把这条路堵死了:题目明确禁止使用串操作指令,要求只用基本的算术、逻辑、跳转指令,在一个以00h结尾、长度小于200h的主串str里,找出同样以00h结尾、长度小于80的子串substr,找到就把found置ffh、把匹配位置写进pos,找不到就置00h。

这个限制不是刁难,而是把字符串匹配的底层机制逼到台面上:指针怎么走、失配后回退到哪、结束条件怎么判,全部得自己用寄存器和跳转指令搭出来。搞懂这套手工指针逻辑,才算真正理解“匹配”这件事在机器层面到底发生了什么。下面从算法骨架讲到源码级实现,再落到调试时那些血泪经验。

2. 匹配算法的骨架:双指针、失配回退与结束判定

2.1 两个指针各管一段,比较只发生在当前字符

整个算法的核心是两组指针:一组指向主串str的当前位置,一组指向子串substr的当前位置。常见的实现思路是用si跟踪主串、di跟踪子串,每次循环把[si]和[di]各取一个字节进寄存器(比如al和bl),然后cmp al, bl。相等就两边指针同时后移,继续比下一位;不相等就进入失配处理。

这里有个容易被忽略的细节:主串和子串都以00h结尾,意味着00h本身不是可比较的字符,而是“到此为止”的哨兵。所以比较逻辑里必须先判结束、再判相等,顺序反了就会把哨兵当成普通字符去匹配,结果自然翻车。报告里“相等处理先判断 substr 是否结束、不相等处理先判断 str 是否结束”的描述,正是这个顺序的体现。

2.2 失配回退:为什么位置要“加失败计数再映射”

朴素匹配最绕的地方是失配后主串指针该退到哪。假设主串是*bb*bbu*bbup*bbbupt*bbupt,子串是bbupt,从主串第 0 位开始比,前几位可能一路相等,到某个字符突然不等。这时候不能简单把主串指针加一了事,因为已经比过的那一段里可能藏着新的匹配起点。

报告给出的做法是:失配时先判断主串是否结束,没结束就把“失败计数值加一”,再“映射到指针”,让下一轮从主串的第cx位重新开始。翻译成可操作的逻辑就是——记录本轮匹配尝试已经推进了多少个字符,失配后把主串起点回退到“本轮起点 + 1”,子串指针归零,重新开一轮。这个“失败计数”本质上就是本轮已经匹配的长度,用它来定位回退位置,避免了一个独立变量去记起点。

用伪代码表达这层骨架会更清楚:

si = str 首地址 di = substr 首地址 本轮起点 = si 循环: al = [si], bl = [di] 若 al == 00h: 主串结束 -> found=00h, 退出 若 bl == 00h: 子串结束 -> found=ffh, pos=本轮起点-str首地址, 退出 若 al == bl: si++, di++, 继续 否则: 本轮起点++, si=本轮起点, di=substr首地址, 继续

这段伪代码把“相等推进、失配回退、哨兵判停”三件事串成一条线,后面所有汇编实现都是它的展开。

2.3 结束判定的三种出口,少一个都会死循环

匹配过程只有三种合法出口:主串先到00h(失败)、子串先到00h(成功)、两者同时到00h(成功,且子串恰好是主串尾部)。很多初学者只写了前两种,漏掉“同时结束”的情况,或者在失配分支里忘了判主串结束,导致指针越过00h继续往后读,读到内存里的随机字节,程序要么死循环要么给出玄学结果。

报告里“不相等处理先判断 str 是否结束”就是在堵这个口子:失配时如果主串已经到00h,说明后面没有字符可匹配了,直接判失败,不再回退。这个判断放在失配分支而不是循环开头,是为了让“相等推进到主串末尾”和“失配撞到主串末尾”走不同的出口,逻辑上更省一次比较。

3. 源码级实现:用 si/di/cx 手工搭出匹配循环

3.1 寄存器分工与数据段布局

在 8086 实模式下,字符串放在数据段,si、di默认以ds为段基址做变址寻址,所以主串和子串可以放在同一个数据段里,用si指主串、di指子串。cx在这里不承担loop指令的计数职责(因为回退逻辑需要手动控制),而是作为“本轮已匹配长度”的临时计数器,失配时用它算出回退偏移。

数据段大致长这样:

DATA SEGMENT STR DB '*bb*bbu*bbup*bbbupt*bbupt', 00H SUBSTR DB 'bbupt', 00H FOUND DB 00H POS DW 0 DATA ENDS

FOUND初值给00h,失败时不用再写;POS用字类型,因为主串长度上限200h超过一个字节的表示范围,位置必须用 16 位存。这个细节报告没展开,但长度上限摆在那,pos用DB会在主串较长时截断,属于典型的踩坑点。

3.2 主循环的汇编骨架

下面这段是匹配循环的核心骨架,去掉了报告里可能存在的宏和伪指令,保留可验证的指令序列:

MOV AX, DATA MOV DS, AX LEA SI, STR ; si 指向主串当前比较位 LEA DI, SUBSTR ; di 指向子串当前比较位 MOV BX, SI ; bx 保存本轮匹配起点 MOV CX, 0 ; cx 记录本轮已匹配长度 MATCH_LOOP: MOV AL, [SI] MOV DL, [DI] CMP AL, 00H ; 主串先判结束 JE NOT_FOUND CMP DL, 00H ; 子串结束即成功 JE FOUND_OK CMP AL, DL JNE MISMATCH INC SI ; 相等:双指针后移 INC DI INC CX ; 已匹配长度加一 JMP MATCH_LOOP MISMATCH: INC BX ; 本轮起点后移一位 MOV SI, BX ; 主串指针回到新起点 LEA DI, SUBSTR ; 子串指针归零 MOV CX, 0 ; 已匹配长度清零 JMP MATCH_LOOP FOUND_OK: MOV FOUND, 0FFH MOV AX, BX LEA DX, STR SUB AX, DX ; 匹配位置 = 起点 - 主串首地址 MOV POS, AX JMP EXIT NOT_FOUND: MOV FOUND, 00H EXIT: ...

这段代码里几个关键点值得逐条拆开看。CMP AL, 00H放在最前面,保证主串一到哨兵就判失败,不会拿00h去和子串字符比;CMP DL, 00H紧随其后,子串先到哨兵说明整段匹配完成,此时BX还停在本轮起点,正好用来算pos。失配分支里INC BX后把SI重置到BX、DI重置到子串首地址、CX清零,三件事缺一不可——少重置DI会让子串指针带着上次的偏移继续比,少清CX会让失败计数越滚越大。

pos的计算用BX - STR首地址,而不是直接存SI,因为题目要求“以字符串 str 首地址为 0 参考点”。如果主串不在数据段偏移 0 处,直接存SI会带上段内偏移,结果对不上。这个减法看起来简单,但调试时如果pos总是差一个固定值,八成就是忘了减首地址。

3.3 为什么不用loop指令而用手动jmp

loop指令隐含用cx做递减计数,且只在cx不为零时跳转。这套匹配逻辑里cx被征用为“已匹配长度”,而且循环次数不是预先确定的——主串长度未知,回退次数也未知,用loop反而要把cx拆成两个用途,容易互相踩。手动jmp配合显式比较,虽然多写几行,但每个跳转条件都摆在明面上,调试时看标志位和指针值就能定位问题,这也是报告强调“确定循环体与跳转条件”的原因。

4. 调试避坑:那些让匹配结果变成玄学的细节

4.1 现象:匹配成功但pos总是差 1

原因通常是BX的更新时机错了。如果在相等分支里就INC BX,BX会跟着SI一起后移,等到成功时BX已经指向匹配段的下一位,算出来的pos自然大 1。正确做法是BX只在失配时更新,相等期间保持不动,让它始终锚定本轮起点。

解决方式是把INC BX严格放在MISMATCH标签下,成功出口直接用BX减首地址,不再做任何修正。

4.2 现象:子串比主串长时程序跑飞

原因是没有在进入循环前比较两者长度,或者失配回退时BX越过了主串的00h。当子串长度大于主串剩余长度时,SI会先撞到00h,理论上CMP AL, 00H能兜住,但如果BX已经指到00h之后,MOV SI, BX会把指针带到哨兵之外,后续读到的是内存里的随机字节。

解决方式是在失配分支回退前,先确认新起点BX指向的字节不是00h;更稳妥的做法是在循环开头统一判[BX]是否为00h,一旦本轮起点都到哨兵了,直接判失败。

4.3 现象:EMU86 跑通、DOS 实机结果不一致

常见原因是数据段初始化不完整,或者POS用了DB导致高位截断。EMU86 的模拟内存初值往往比较“干净”,越界读写不一定立刻暴露;实机上内存里残留的字节会让越界指针读到脏数据,表现成随机匹配位置。

解决方式是所有变量显式初始化,POS用DW,并在调试时用调试器逐次查看SI、DI、BX、CX四个寄存器的值,确认每次失配后SI都等于BX、DI都回到子串首地址。

4.4 现象:主串尾部恰好匹配时判失败

原因是结束判定顺序写反了:先判主串00h再判子串00h,当两者同时到哨兵时,主串判断先命中,直接跳失败。正确顺序是先判子串是否结束(成功优先),再判主串是否结束(失败兜底),这样“同时结束”会走成功出口。

解决方式就是调整CMP DL, 00H和CMP AL, 00H的先后,让成功判定优先于失败判定。

4.5 现象:失配后陷入死循环,指针原地打转

原因是失配分支里漏了INC BX,或者MOV SI, BX写成了MOV SI, SI。前者让本轮起点永远不动,后者让主串指针停在失配位置,下一轮比较的还是同一对字符,永远不相等。

解决方式是在MISMATCH标签下逐条核对:INC BX必须有,MOV SI, BX必须把新起点写回SI,LEA DI, SUBSTR必须重置子串指针,MOV CX, 0必须清零计数。四条缺一不可,调试时可以在MISMATCH处下断点,看每次进入时BX是否严格递增。

5. 从朴素匹配到设计取舍:为什么这份实验值得反复看

朴素匹配的时间复杂度是 O(n×m),主串每个起点都要和子串逐位比一遍,最坏情况下回退次数接近主串长度乘以子串长度。8086 上跑这种双重循环,主串接近200h、子串接近80时,指令条数会到万级,实机上能明显感觉到延迟。但这份实验的价值不在于追求效率,而在于把“匹配”拆到寄存器级别之后,你会自然理解后来 KMP 那类算法到底在优化什么——它们优化的正是“失配后回退到哪”这一件事,用预处理出来的部分匹配表替代逐位回退,把回退的代价从 O(m) 摊到 O(1)。

对比一下两种回退策略会更直观:

策略失配后主串指针失配后子串指针最坏回退次数
朴素回退(本实验)本轮起点 + 1归零接近 n×m
部分匹配表回退不动或小幅后移跳到已匹配前缀长度接近 n+m

表格里“部分匹配表”那一行不是这份实验的内容,但把两行摆在一起,就能看出朴素回退的代价花在哪:每次失配都把已经比过的信息全部丢掉,从子串第一位重新来。实验里CX记录的“已匹配长度”其实已经是一份最原始的部分匹配信息,只是没用它来指导回退,而是清零重来。理解到这一层,再去看 KMP 的next数组,就不会觉得它是凭空冒出来的玄学。

另一个值得琢磨的取舍是“禁止串操作指令”这条约束本身。repne scasb这类指令在硬件层面就是为扫描设计的,用它写匹配会短很多,但也会把指针推进、结束判定这些细节藏进指令语义里,调试时只能看cx和标志位,黑匣子感很强。手工用si/di搭循环,代码长了,但每一步的指针变化都可见,出问题时能精确到某一条INC或某一次MOV。我一般会这么理解:这份实验训练的不是“怎么写短”,而是“怎么让每一步都可验证”。

从那以后我每次写指针遍历类的逻辑,都会先把“起点锚点、当前指针、已推进长度”三个量在纸上列清楚,再动手写第一条指令,避免回退时把锚点一起改掉。这份报告里的代码和调试记录正好是这套习惯的完整样本,照着把匹配成功、匹配失败、尾部匹配三种情况各跑一遍,比看十页理论都管用。希望帮到你。

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

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

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

立即咨询