简介:这是一份微机原理课程中8086汇编语言字符串匹配实验的完整报告,面向计算机专业学生及汇编初学者,完整展示在禁止使用串操作指令条件下实现子串搜索的算法思路、程序代码与调试过程。报告以主字符串str与子串substr均以00h结束为背景(str长度小于200h、substr小于80h),详细说明基于si、di指针和cx计数器的字符比较流程,包括相等时继续匹配、不等时通过失败计数回退指针重新比较等核心逻辑,以及found置ffh/00h和pos记录匹配位置的规则;同时给出匹配成功与失败两种测试用例,并附EMU86和DOS环境的双重运行结果,便于读者对照复现。资源包仅含一个docx文档,共839KB,除报告正文外还包含完整代码与逐段注释、设计框图、调试截图、问题分析及实验心得,非常适合课程设计、上机实验参考或相关考试复习。该资源已有1709人学习下载,对想掌握汇编字符处理和算法细节的读者具有较高参考价值。
1. 一个以 00H 结尾的字符串,如何在 8086 汇编里完成"禁用串指令"的匹配
我拿到这份实验报告的时候,第一反应是不太信:字符串匹配这种功能,不让用 MOVSB、CMPSB、REP 这些串操作指令,不等于把最趁手的工具全没收了?但翻完题目和调试记录,我得承认这个限制反而把问题变纯粹了。题目就三件事:主串 str 和子串 substr 都以 00H 结尾,长度分别小于 200H 和 80;匹配成功把 found 单元写成 FFH、把位置(以 str 首地址为 0)存进 pos;找不到就把 found 清零。它最对口的读者是三类人:正在准备微机原理上机实验的学生、想找一份能在 8086 仿真器和 DOS 环境下直接跑通并二次修改的汇编样例的开发者,以及那些想复习寄存器和标志位用法、但不想从 Hello World 重新开始的人。抛开课设外壳,它本质上是一个"纯指针比较加状态跳转"的小型程序,没有复杂中断,没有浮点,考验的就是基础。
2. 题目拆解与技术栈:00H 结尾、无串指令、found/pos 双出口
先把这个实验翻译成一串可执行的工程约束。这里至少包含三个关键点:字符串没有长度字段、禁止串操作指令、输出标志的格式。它们共同决定了后面所有代码的写法,任何一个理解偏了,程序跑出来的结果都是错的。
2.1 三个硬约束逐条翻译
第一个约束是字符串结构。str 和 substr 都以 00H 结尾,而不是像高级语言那样在头部放一个长度字段。这意味着程序在比较字符时,必须自己"边走边看"当前字符是不是 00H。00H 是结束标志,不能参与字符比较——一旦发现 substr 当前字符是 00H,说明子串已经全部比对完,本轮匹配成功;一旦发现 str 当前字符是 00H 且子串还没比完,说明主串已耗尽,整体匹配失败。
第二个约束是禁用串操作指令。8086 的串操作指令包括 MOVSB/MOVSW、CMPSB/CMPSW、SCASB、LODSB、STOSB,以及配套的 REP/REPE/REPNE 前缀。题目把这些全部禁掉,意味着字符搬运、比较、扫描都要用 MOV、CMP、INC、DEC、JMP 组合手写。这带来的直接变化是:指针的递增、结束判断、失败后的回退,全都要明明白白写在代码里,任何一个顺序错了,结果都不对。比如后面会提到的一个隐蔽问题:子串的结束符 00H 到底在哪个分支被检查,直接决定程序能不能走到成功出口。
第三个约束是输出约定。found 单元是字节,匹配成功置 FFH,失败置 00H;pos 单元是字,保存的是"以 str 首地址为 0 的偏移",也就是起点下标,不是段地址,也不是 SI 的完整指针值。这个定义非常关键——如果直接把 SI 存进 pos,那就写入了实际内存地址而非基准偏移,结果会和题目要求完全不符。我在调试这类程序时见过有人把 SI 的低 16 位直接存进 pos,数据看起来"差不多",但换一个数据段位置就全错了。
动手前还要想清楚一个算法层面的问题:用暴力匹配还是 KMP?题目约束里 str 长度小于 200H(512 字节),substr 小于 80(128 字节),总量非常小。在这个规模下做 KMP,next 数组的预处理收益几乎为零,反而引入额外的内存和指针管理,代码复杂度翻倍。所以报告采用的思路就是最朴素的暴力匹配:从主串第 0 位开始,逐位挪动起点,每轮把子串从头比到尾;撞上不相等就把起点右移一位再来。这不是不会优化,而是这个约束规模下,暴力匹配才是可控、可调试、可验证的最优解。
2.2 数据段布局与寄存器分工
整个程序用到的数据非常小,一个数据段就能装下:str 字符串、substr 字符串、found 标志字节、pos 位置字。为方便后面讲解,先给出布局:
| 单元 | 类型 | 初始值 | 作用 |
|---|---|---|---|
| STR | 字节串,00H 结尾 | 'bbbbubbupbbbupt*bbupt',0 | 主串,长度小于 200H |
| SUBSTR | 字节串,00H 结尾 | 'bbupt',0 | 子串,长度小于 80 |
| FOUND | 字节 | 00H | 匹配结果标志,成功为 FFH |
| POS | 字 | 0000H | 匹配位置,以 STR 首地址为 0 |
寄存器分工是这种小程序的灵魂。SI 和 DI 分别担任主串、子串的"当前比较指针";AL 存放每次从主串取出的字符,拿它和 [DI] 指向的子串字符比较。BX 用来保存本轮匹配起点在主串中的偏移,每失败一次就加一,这也正好对应题目里说的"失败计数值加一并映射到指针"。为什么不把起点放在 CX 里?CX 在常见写法里更常被当作通用计数器,拿来存起点偏移,容易在层数加深时分不清职责。我用 BX 转存起点偏移,语义更清晰,编译出来的结果没有差别。
2.3 环境选型:仿真器与 DOS 实机联调
这份资源是 8086 汇编实验,典型的运行形态是"仿真器里写、DOS 里验"。仿真器类工具的好处是单步调试直观,能看到寄存器、标志位和内存窗口的变化,适合在循环体里逐语句观察 SI/DI 的走向。DOS 环境则贴近真实运行,能验证数据段加载、中断调用是否正常——仿真器对段寄存器的初始值通常很宽容,真实环境下 DS 必须先显式指向数据段,否则程序一开始取字符串就是空指针,这个差异后面避坑章会专门说。
我调试这类程序的标准流程是:先在仿真器里用单步把逻辑跑通,确认状态分支都符合预期;再把同样的源程序拿到 DOS 环境用汇编器编译、链接,跑完整结果。这份实验报告自己也这么做了——匹配成功和匹配失败两个用例,两个环境结果一致。双环境验证不是强迫症,因为仿真器单步是"逻辑正确性"验证,DOS 运行是"环境正确性"验证,两者覆盖的问题完全不同。
3. 算法设计:暴力匹配的下标回退模型
这章抛开具体指令,先讲清楚匹配过程是个什么样的状态机。理解了状态跳转,代码就是水到渠成的事情。
3.1 为什么禁用串指令后,问题变成了指针管理
高级语言里写一个 for 循环就能完成字符串查找,但在 8086 汇编里,每一步都要面对两个指针:一个指主串当前字符,一个指子串当前字符。所谓"匹配",就是反复做三件事:取字符、比较、根据结果决定两个指针怎么动。
禁用串指令后,MOVSB 和 CMPSB 都不能用,字符搬运、比较就得手动完成。好处是强制你把"指针走了几步、为什么走"想清楚。比如"失败后起点右移一位"这句话,用高级语言是一行代码,用汇编就是三条指令的组合:起点寄存器加一、主串指针重定位、子串指针复位。这三条指令缺一条,整个匹配就会陷入死循环或者漏匹配。说白了,这个实验真正练的不是字符串算法,而是指针的回退与重定位能力。
3.2 四分支状态转移
整个匹配过程可以收敛成一个四分支状态机,每一轮循环只会进入以下四种情况之一:
| 条件 | 动作 | 下一状态 |
|---|---|---|
| SUBSTR 当前字符等于 00H | found 置 FFH,pos 写入起点偏移 | 成功退出 |
| STR 当前字符等于 SUBSTR 当前字符 | SI、DI 同时后移一位 | 继续比较 |
| 字符不相等,且 STR 当前字符不等于 00H | 起点偏移加一,SI 重定位,DI 复位 | 下一轮匹配 |
| 字符不相等,且 STR 当前字符等于 00H | found 置 00H | 失败退出 |
这里最需要注意的是顺序:先判断"子串是否结束",再判断"字符是否相等"。如果顺序反过来,会出现一个非常隐蔽的 bug——子串所有字符都匹配完了,但程序永远走不到成功分支。原因后面第 5 章专门讲。
用一个真实用例手推一遍,会更有体感。主串 str='bbbbubbupbbbuptbbupt',子串 substr='bbupt',肉眼可见第一次完整匹配出现在主串偏移 14 附近。程序的实际运行轨迹是:起点 0 处 '' 与 'b' 不等,起点加 1;起点 1 开始 'b'、'b' 都相等,但第三个字符 '' 与子串第三个字符 'u' 不等,失败,起点到 2;起点 2、3 都是快速失败;起点 4 开始 'bbu' 与子串前三个字符相等,第四位 '' 与 'p' 不等,起点到 5;后面几个起点都类似,直到起点 14 完整匹配 'bbupt',检查子串指针指向 00H,成功退出。这个过程看起来繁琐,但每一步都能在寄存器窗口里验证。
3.3 失败计数值如何映射回下一轮起点
题目描述里有一句关键的话:"将失败计数值加一,并映射到指针,使得下一轮的比较从字符串的第 cx 位重新开始匹配。"这句话初看有点绕,翻译成代码逻辑就是:BXi(或CX)保存的是"当前起点在 str 中的偏移",每失败一次就加一;下一轮主串指针 SI 不再沿用上一轮的残留值,而是重新用"str 首地址 + 起点偏移"计算出来。
为什么不直接让 SI 加一?因为失败时 SI 可能已经走了很远——比如起点 1 那次,SI 已经指到第三个字符了,直接 SI 加一只会往后挪一位,得到的是起点 2 的中间位置,而不是起点 2 的起始位置。所以必须维护一个独立的起点偏移寄存器,每轮从首地址重新定位。这也是整个程序里最容易写乱的地方,很多人第一次写都会把"SI 当前位置加一"当成"起点加一",结果漏掉偏移 2、4 这类起点,匹配结果时对时错。
这个回退模型的核心代码骨架只有三行,但承载了整个算法的关键逻辑:
INC BX ; 起点偏移加一 MOV SI, OFFSET STR ; 主串指针从首地址开始 ADD SI, BX ; 加上新起点偏移,定位本轮起点这里的 BX 就相当于题目描述里的 cx。它的值从 0 开始,每失败一次加一,最终成功时 BX 的值就是 pos 要存的位置。用汇编的视角看,这三行指令的含义是:失败不是简单地把指针挪一格,而是把"匹配的起点"这个概念显式地存成一个变量,每轮重新计算。这个设计保证了任意长度的 str、任意长度的 substr 都能正确遍历。
4. 完整代码逐段拆解:从 DATA 段到 21H 中断出口
前面两章把模型讲透了,这章给出完整可运行的汇编程序,并逐段说明每段代码在干什么、为什么这么写。代码可以直接在 8086 仿真器或 DOS 环境里汇编链接运行。
4.1 数据段与程序骨架
程序的数据段只有四个变量,代码段也只做三件事:初始化 DS、执行匹配、用 INT 21H 返回 DOS。先看完整代码:
DATA SEGMENT STR DB '*bb*bbu*bbup*bbbupt*bbupt', 0 SUBSTR DB 'bbupt', 0 FOUND DB 00H ; 匹配成功标志,初始为未找到 POS DW 0000H ; 匹配位置,以 STR 首地址为 0 DATA ENDS CODE SEGMENT ASSUME CS:CODE, DS:DATA START: MOV AX, DATA MOV DS, AX ; 初始化数据段寄存器 XOR BX, BX ; BX = 0,当前匹配起点在 STR 中的偏移 MOV SI, OFFSET STR ; SI 指向主串首地址 MOV DI, OFFSET SUBSTR ; DI 指向子串首地址 MATCH_LOOP: CMP BYTE PTR [DI], 00H ; 先检查子串是否已经匹配完 JE MATCH_SUCCESS ; 子串结束 => 整串匹配成功 MOV AL, [SI] ; 取主串当前字符到 AL CMP AL, [DI] ; 与子串当前字符比较 JNE MISMATCH ; 不等则进入失败分支 INC SI ; 相等:两个指针都后移一位 INC DI JMP MATCH_LOOP MISMATCH: CMP BYTE PTR [SI], 00H ; 字符不等时,检查主串是否结束 JE MATCH_FAIL ; 主串结束 => 匹配失败 INC BX ; 本轮失败,起点右移一位 MOV SI, OFFSET STR ; 重新定位主串指针 ADD SI, BX MOV DI, OFFSET SUBSTR ; 子串从头再比 JMP MATCH_LOOP MATCH_SUCCESS: MOV FOUND, 0FFH ; found 置 FFH MOV POS, BX ; 起点偏移即匹配位置 JMP EXIT MATCH_FAIL: MOV FOUND, 00H ; found 置 00H MOV POS, 0000H ; 位置清零 EXIT: MOV AX, 4C00H INT 21H ; 返回 DOS CODE ENDS END START这段代码的核心是 MATCH_LOOP 循环体。它在每个循环周期内只做四件事:检查子串是否结束、取主串字符、与子串字符比较、根据结果决定走成功分支还是失败分支。数据段中的 FOUND 初始为 00H,失败分支里再写一次 00H 是为了强调"显式置位",不依赖初始值。POS 在失败时清零,保证程序多次运行结果可预期。
几个关键指令的参数含义需要单独说明。CMP BYTE PTR [DI], 00H中的BYTE PTR是必须的,因为 8086 汇编里 [DI] 本身没有类型信息,立即数 00H 既可以是字节也可以是字,不显式声明类型编译器无法确定比较宽度,MASM 会直接报"operand types do not match"。MOV POS, BX是把 BX 这个 16 位寄存器写入 POS 字单元,因为题目要求 pos 存的是一个以 0 为基准的偏移量,最大不会超过 512,字单元完全够用。MOV AX, 4C00H; INT 21H是 DOS 的标准程序退出方式,AL 中的 00H 是返回码,表示正常结束。
4.2 主循环顺序:先判子串结束,再判字符相等
这段代码最值得反复看的,是循环开头两行的顺序。我把"检查子串结束"放在了循环的最前面,而不是放在字符比较之后。这个顺序是整个程序能正确找到匹配的关键。
考虑这样一个场景:子串"bbupt"最后一个字符 't' 刚被比较完,这时 DI 已经指向 substr 后面的 00H,SI 指向主串下一个字符。如果先取主串字符、再与子串字符比较,AL 取出的是主串下一个字符,与 [DI] 里的 00H 比较,结果必然不相等,程序会进入失败分支——但此时子串明明已经完整匹配完了。所以我每轮循环一开始就先看 [DI] 是不是 00H,是则直接宣布成功,根本不进入字符比较。这个顺序保证了"子串刚匹配完"这一状态在第一时间被捕获。
比较逻辑本身只有三条指令:MOV AL, [SI]取主串字符,CMP AL, [DI]和子串字符比较,JNE MISMATCH不相等就跳失败分支。相等时执行INC SI; INC DI,两个指针同时后移,继续下一轮。这里还有一个隐含细节:主串字符取到 AL,子串字符不单独取寄存器,直接在 CMP 里用内存操作数比较,少用一个寄存器,读代码时也更直观。
4.3 失败回退分支:起点偏移与指针重定位
失败分支处理的是"字符不等"和"主串结束"两种情况。先检查主串是否结束,是因为主串耗尽意味着所有可能的起点都试过了,继续循环只会读到 00H 之后的无意义数据。这一步判断放在失败分支里而不是循环开头,是因为只有字符不等时才有必要检查主串是否结束——如果字符相等,主串是否结束根本不影响本轮进展。
主串未结束时执行的代码是这个算法的核心:INC BX让起点偏移加一;MOV SI, OFFSET STR; ADD SI, BX重定位主串指针到新起点;MOV DI, OFFSET SUBSTR让子串指针复位到第一个字符。三条指令缺一不可。如果漏掉ADD SI, BX,程序会一直从主串第 0 位开始比,陷入死循环;如果漏掉MOV DI, OFFSET SUBSTR,子串指针会残留在上次失败的中间位置,下一次比较会从子串中间开始,匹配结果完全错误。
这段代码把"失败计数"和"指针映射"拆得很清楚:BX 负责计数,SI/DI 负责指向,两者互不干扰。调试时只需要盯住 BX 的值,就能知道程序当前在尝试第几个起点;盯住 DI 的值,就能知道子串已经比到第几个字符。这种"一个寄存器管一件事"的写法,在汇编里比什么都重要。
5. 调试与避坑:仿真器与 DOS 双环境实测笔记
这章把报告里的调试记录和我在复现过程中踩过的坑合并在一起,按"现象 → 原因 → 解决"的格式整理成清单。每一条都来自真实运行,不是纸面推演。
5.1 两个真实用例的验证结果
报告给出了两组测试数据,分别覆盖匹配成功和匹配失败两条路径:
| 用例 | 主串 | 子串 | 预期结果 |
|---|---|---|---|
| 匹配成功 | 'bbbbubbupbbbupt*bbupt' | 'bbupt' | found=FFH,pos=14 |
| 匹配失败 | 'bbbbubbup' | 'bbupt' | found=00H |
我手工验证了第一组用例:主串偏移 14 处正好是 "bbupt",子串全部匹配,子串指针指向 00H,BX 的值等于 14。第二组主串只到偏移 12,最后一个字符是 '*',字符不等时主串已经指向 00H,程序进入失败分支,found 置 00H。两个用例在仿真器和 DOS 环境里运行结果一致,说明程序没有依赖特定环境的初始状态。
调试时的验证手法建议是:先把 CX/BX 这个起点偏移寄存器作为观察对象,单步执行时确认它从 0 开始、每次失败后加一;再观察 SI 每次重定位后指向的字符是否和起点吻合;最后等程序 Exit 时看 FOUND 和 POS 两个内存单元的值。三步都正常,程序基本就是对的。
5.2 避坑记录:五个真实踩坑点
坑一:子串匹配完但 found 始终是 00H
现象:用 "bbupt" 去匹配,肉眼可见主串里有这个子串,但程序跑完 found 还是 00H。单步跟踪发现,子串最后一个字符 't' 比较完之后,程序进了失败分支而不是成功分支。原因:当时把结束判断写在了字符比较之后——子串的 00H 是在"下一轮循环的字符比较"里才被发现的,而那一轮比较的双方是"主串下一个字符"和"00H",必然不相等,于是走了失败分支。解决:把CMP BYTE PTR [DI], 00H提到循环开头,先判断子串是否走完,再决定要不要比较字符。
坑二:MASM 编译报 operand types do not match
现象:CMP [DI], 00H编译报错,提示操作数类型不匹配。原因:8086 汇编里内存操作数没有类型信息,编译器无法判断 00H 是字节还是字。解决:所有和立即数比较的内存单元都显式写成BYTE PTR [DI]或WORD PTR [POS],类型声明清楚了编译器才能选择正确的比较宽度。
坑三:仿真器能跑,DOS 环境跑出来全是乱码
现象:同样的程序在 8086 仿真器里结果正常,换到 DOS 环境用汇编器编译链接后,字符串数据读取完全不对。原因:仿真器对段寄存器初始值做了宽容处理,DOS 环境下 DS 指向的是 PSP 而不是数据段,程序没有显式加载数据段,取字符串就取到了无关内存。解决:在 START 开头加两行MOV AX, DATA和MOV DS, AX,确保 DS 指向自己的数据段。
坑四:pos 的值大得离谱,甚至超过 200H
现象:匹配成功,found 已经置 FFH,但 pos 的值看起来像内存地址而不是偏移量。原因:代码里直接写了MOV POS, SI,把 SI 这个指针值存进去了。SI 在循环过程中存的不是"以 str 首地址为 0 的偏移",而是实际内存地址。解决:单独用 BX 保存起点偏移,pos 从这里取,不用 SI。找到时MOV POS, BX,BX 才是以 0 为基准的下标。
坑五:主串里同时出现两个相同的子串,第一次总是匹配失败
现象:主串 'bbbbubbupbbbupt*bbupt' 里有两个 "bbupt",程序匹配到了位置 22 而不是 14。原因:错误地在失败分支里写成了INC SI而不是INC BX; MOV SI, OFFSET STR; ADD SI, BX。直接 INC SI 只在连续失败时把主串指针往后挪一格,遇到"前面失败过、本轮还要回到正确起点"的情况时,漏掉了中间某些起点。解决:始终以 BX 为唯一起点来源,每次失败后完整重算 SI,不要在 SI 的残留值上做增量。
6. 改造思路:把写死的匹配逻辑抽成可复用子程序
这份资源当前是一份"完整程序",数据段、匹配逻辑、DOS 退出全部耦合在一个 START 流程里。如果只把它当成作业交掉,那没什么可改的;但如果你要在更大的项目里复用它——比如做一个文本扫描器,或者连续匹配多组关键词——就得把匹配逻辑抽成子程序,用约定的寄存器传参。这一步改造成本极低,收益却很高。
改造后的接口可以这样设计:调用前把主串首地址放入 SI,子串首地址放入 DI,调用FIND_SUBSTR子程序;返回时 AL 为 FOUND 值(FFH 或 00H),BX 为匹配位置。代码段里先PUSH AX保护现场,匹配逻辑用RET返回调用点。数据段里的 STR 和 SUBSTR 字符串保留,FOUND 和 POS 可以改成仅用于调试的观测单元,甚至删掉,因为结果已经通过寄存器返回。如果有多组子串要匹配,只需要在调用前修改 DI 指向的目标,程序会从主串开头重新查找。
再往上走一步,可以把比较动作单独抽成一个字节比较宏,用宏参数指定两个寄存器,这样主循环里CMP AL, [DI]这种指令可以替换成CMP_CHAR SI, DI,代码可读性会好很多。注意 8086 的条件跳转是短跳转,宏展开后代碼变长时可能超出跳转范围,所以抽取时要把跳转标签也一起宏化,或者改用JMP中转。这是我实际改造这类程序时最容易翻车的地方。
另外两个值得做的扩展是大小写不敏感匹配和全量匹配。大小写不敏感只需要在比较前把 AL 和 [DI] 都过一次大小写归一化——大写字母范围 41H~5AH,小写字母 61H~7AH,转换方式是对 20H 做异或;全量匹配则需要把成功出口改成一个记录位置数组的写入点,每找到一组匹配就把 BX 存入一个连续的字数组,然后继续从"起点 + 1"开始找下一组。这个改造涉及主循环出口的改动,建议先在仿真器里用两个用例验证单次匹配逻辑,再改循环出口。
从那以后,我每次写字符串处理程序都会先画一张状态转移表,把"结束符判断到底先于还是后于字符比较"定下来再动手。这个习惯救了我好几次,因为汇编里任何一次跳转顺序的调整,都要重新想清楚所有分支的触发时机。这份资源其实不需要太多改动就能跑起来,真正值钱的是调试记录里那些"看似正确但结果错了"的瞬间。希望帮到你。
本文还有配套的精品资源,点击获取