简介:面向学习汇编语言的读者,这份实验报告围绕8086环境下不使用串操作指令的字符串匹配问题展开,主串和子串分别以00h结尾,长度上限为200h和80h,需逐字符比较并设置found与pos标志。报告从题目要求出发,绘制算法设计框图,详细分析主要循环体:借助SI和DI两个变址寄存器定位字符,用CX记录失败次数来实现回溯,通过JE与JNE指令控制分支,最终给出完整可运行的汇编代码及逐段注释。调试部分覆盖匹配成功与失败两类案例,并展示EMU86和DOS环境的运行结果与截图,有助于验证程序正确性。压缩包中仅含1份docx文档,约839KB,文档按实验目的、算法思路、代码、调试、心得组织,结构清晰。目前已有1700余人学习该资源,其模块化思路和排错经验,对完成类似汇编课程设计或复习备考可直接借鉴。
1. 禁用了串操作指令的8086字符串匹配实验:寄存器、回退与pos的联动
第一次看到这个题目的约束时,我愣了一下:一个8086汇编语言字符串匹配实验,居然明令禁止使用串操作指令。movsb、cmpsb、scasb这些能一键比较和搬运字符串的指令全部下线,匹配逻辑只能用mov、cmp、jmp这类最原始的零件手工拼。这份实验报告要解决的,恰恰是这类作业里最让人头疼的几件事:循环体怎么组织、失配之后怎么让主串指针回到正确位置、found和pos两个内存单元怎么同步更新。读完代码你会发现,匹配本身不难,难在失配回溯的偏移换算——报告里直接用CX寄存器当作失败计数,成功时把CX写进pos,省掉了用SI减首地址的绕路操作。这份资料适合正在写汇编作业的在校生,也适合想在DOS环境下处理字符串但不想碰串指令的从业者。
2. 去掉串操作指令之后:双指针匹配判断与CX驱动的失配回溯
先说结论:禁用串操作指令后,字符串匹配的难度从"比较字符"转移到了"失配回溯"。字符比较只是一条CMP,而失配后主串从哪里续上、子串怎么重置、pos拿什么算,才是真正决定程序能不能跑通的部分。这份报告给出的方案是用SI和DI两个变址寄存器分别指住两条字符串,用CX记录已经放弃的字符数,每一轮匹配失败后把SI重定位到str首地址加CX的位置。下面拆开讲。
2.1 寄存器分工:为什么是SI、DI和CX
匹配过程需要同时记录三件事:主串当前扫到哪、子串当前比到哪、主串已经放弃了几位。常见做法是让SI只管str,DI只管substr,CX记录失配次数。AL和BL作为两个临时寄存器,分别存放两个字符串当前字符。选SI/DI而不是BX、BP,是因为变址寻址在mov al,[si]之后接一条inc si就能完成步进,两个指针对应两条字符串,互不干扰。CX天然适合做计数器,更关键的是,在成功分支里CX的值可以直接作为pos写入,后面第3章会详细说。
寄存器分工可以整理成下面这张表:
| 寄存器 | 职责 | 关键动作 |
|---|---|---|
| SI | 主串str当前指针 | mov al,[si]、inc si |
| DI | 子串substr当前指针 | mov bl,[di]、inc di |
| CX | 失配计数/匹配起点偏移 | inc cx、lea si,str、add si,cx |
| AL/BL | 两个字符串的当前字符暂存 | 比较前先mov加载 |
这里要特别提醒:不要让CX同时兼任循环长度计数和失配计数。8086的loop指令默认用CX做计数器,一旦在匹配循环里用了loop,CX被循环递减,失配偏移就全乱了。后面第5章会单独展开这个坑。
2.2 循环体的判定顺序:子串结束优先于主串结束
这份报告里的循环体不是简单的"相等就继续,不等就回溯",而是要处理四个判定层次。我把它们按执行顺序写出来:
- 检查DI指向的字符是否为00h,是则子串有效部分已经全部匹配,跳转成功;
- 检查SI指向的字符是否为00h,是则主串先耗尽,跳转失败;
- 比较AL与BL,相等则SI和DI同时递增,继续下一轮;
- 不相等则进入失配回溯。
注意顺序不能调换。子串结束判断必须放在主串结束判断之前,原因在于:当DI已经走到substr的00h时,说明"bbupt"这类有效字符全部命中,无论主串当前字符是什么,都必须判定成功。如果把主串结束判断放前面,当主串剩余字符恰好不是00h时会误判成继续匹配,而当子串的有效部分已经匹配完、主串还有后续字符时(比如str="ababc",substr="ab"),先比较字符再判断子串结束会漏掉成功条件,最后走到失配分支。这类边界问题在调试时特别容易翻车,判断顺序就是唯一的坑位。
2.3 失配回溯:从第CX位重新开始匹配
回溯逻辑是这份报告的核心思想。失配时不保存"上一轮起点"指针,而是让CX加一,再用lea si, str加上add si, cx两条指令把SI重新定位到str的第CX个字符。每失配一次,匹配窗口整体右移一位,整个回溯过程只有一个CX在维护状态,没有额外的内存变量。
用报告里的测试串举例:str为'*bb*bbu*bbup*bbbupt*bbupt',substr为'bbupt'。第一轮从str下标0的*开始,与子串首字符b不等,CX变为1;第二轮从下标1的b开始,能连续匹配两个b,但str下标3是*,与子串第三个字符u不等,CX继续加。这样一直推到完整bbupt出现的位置才进入success。每个片段都比上一段多匹配一个字符,专门考验"匹配到一半发现不对"的回退路径。
这种朴素回溯的时间复杂度是O(n×m),但题目给了边界:主串长度小于200h,子串长度小于80h,两边的量级都很小,8086完全跑得动,不需要KMP这类优化。回溯不会跳过正确起点,因为每次只把窗口右移一位。
3. 把状态机写进8086代码:三段关键程序与参数边界
状态机想清楚了,代码就是按图施工。下面给出一个能直接跑通的完整框架,分数据段、匹配循环、收尾三部分拆解。
3.1 数据段布局与初始化
数据段要定义主串、子串、found和pos四个对象,其中found是字节,pos是字。代码如下:
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 main: mov ax, data mov ds, ax lea si, str ; SI指向主串首字符 mov cx, 0000h ; CX作为失配计数,初始为0 start_round: lea di, substr ; 每轮开始DI重新指向子串首字符为什么字符串末尾写00h而不是只写0?因为后面比较结束符时统一用cmp al, 00h,保持立即数写法一致能减少看错的风险。found是db类型,因为它只存ffh或00h两个值;pos是dw类型,因为偏移量可能到1FFh,一个字节装不下。初始化时给CX赋0而不是用xor cx,cx,并不是说后者不行,而是强调CX承担的是"匹配起点偏移"语义,从一开始就要保持清晰。
3.2 核心匹配循环与四层判定
核心循环把2.2里的四个判定顺序落实成代码。注意子串结束判断要放在循环开头,而不是先比较字符再判断:
match_loop: mov bl, [di] ; 先取子串当前字符 cmp bl, 00h ; 子串是否已到结束符 je success ; 已到结束符则有效部分全部匹配 mov al, [si] ; 再取主串当前字符 cmp al, 00h ; 主串是否已到结束符 je fail ; 主串先耗尽则匹配失败 cmp al, bl jne mismatch ; 当前字符不等则进入回溯 inc si ; 相等则双指针同时推进 inc di jmp match_loop mismatch: inc cx ; 失配计数加1 lea si, str ; 主串回到首地址 add si, cx ; 再从第CX位开始新一轮 jmp start_round这段代码的逻辑核心是:DI指向substr的00h时直接判定成功,这是"子串有效部分全部匹配"的唯一条件;SI指向str的00h时判定失败,说明主串耗尽而子串还没匹配完。进入mismatch时,AL和BL必然不相等,且AL已经确认不是00h,所以回溯分支不需要再重复判断主串是否结束,直接inc cx后重定位即可。注意整个循环里没有movsb、cmpsb、rep前缀,完全符合题目禁用串操作指令的要求。
这里有一个参数要点:CX既是失配计数,又是下一轮匹配的起点偏移,成功时直接写pos。因为每失配一次,本轮起点就往后挪一位,CX从0开始数,正好等于"以str首地址为0参考点"的位置偏移。这个映射关系是整个程序最精巧的一处设计。
3.3 成功与失败的收尾处理
成功和失败两个分支的收尾很短,但有几个细节不能错:
success: mov found, 0ffh mov pos, cx ; 匹配起点偏移就是CX jmp done fail: mov found, 00h ; pos保留初始值,验收时只检查found done: mov ah, 4ch int 21h code ends end mainfound写入的必须是0ffh而不是01h,题目明确要求found单元置为ffh,验收时如果按字节读内存,0ffh和01h肉眼就能区分。pos用CX直接写入,不需要用SI减str首地址做差值,节省了一条减法还少一个临时寄存器。fail分支里pos可以不管,也可以按题目要求额外清零,取决于验收标准,通常在found为00h时pos的值无人关心。
3.4 边界参数与匹配行为
这份报告涉及的边界参数不多,但每个都直接决定程序行为:
| 参数 | 取值约束 | 影响 |
|---|---|---|
| str长度 | 小于200h | 偏移量在16位范围内,CX不会溢出 |
| substr长度 | 小于80h | 每轮比较次数有限,暴力匹配可行 |
| pos参考点 | str首地址为0 | CX值直接等于匹配起点偏移 |
| found取值 | 仅00h或ffh | 字节变量足够,写入必须精确 |
| 串操作指令 | 完全禁用 | 指针步进全部用inc/跳转完成 |
str长度上限200h意味着主串最长512字节,CX计数不会超过512,一个字变量放这个偏移绰绰有余。substr长度80h上限128字节,即使最坏情况每轮都比较128次,总比较次数也在6万次以内,8086在几毫秒内就能跑完。理解这些边界能帮你判断什么时候可以放心用朴素回溯、什么时候需要优化算法。
4. 用两组拼接用例验证回退逻辑:EMU8086与DOS下的双环境调试
代码写完只是开始,真正值钱的是调试路径。报告里选用的两个测试串不是随手敲的,它们把匹配的中间状态全部暴露出来,能跑通这两组用例,基本可以确认回溯逻辑没有跳帧或死循环。
4.1 测试串的构造思路
第一组匹配成功用例:str为'*bb*bbu*bbup*bbbupt*bbupt',substr为'bbupt'。这个主串从短到长依次出现bb、bbu、bbup,最后才出现完整bbupt,每段都比上一段多匹配一个字符。如果你的回溯逻辑有问题,比如失配后起点没有正确右移,程序会在这里原形毕露:要么死循环,要么pos指向第二次出现的位置,要么found一直为00h。第二组匹配失败用例:str为'*bb*bbu*bbup*',substr仍为'bbupt'。主串每段都卡在最后一个t之前,专门验证"主串耗尽但子串尚未匹配完"的分支。两组合在一起,把成功路径、失败路径、部分匹配后失配的路径全覆盖了。
4.2 EMU8086里的单步观察方法
在EMU8086里验证,我一般按三步走。第一步,把上面的数据段原样填进代码,编译通过后打开Registers窗口和Variables窗口。第二步,在match_loop的mov bl,[di]处设一个断点,或者直接用单步键逐条执行,重点是观察每轮进入循环时CX的值。第三步,在success分支断下来之后,看Variables窗口里的found和pos。
观察时要确认三个对应关系:进入失配分支时CX应当逐次加1;每次从start_round重新开始时SI应当等于str首地址加CX;进入success时found变成ffh,pos等于当前CX。如果在某轮发现CX加了但SI没有重定位,问题几乎一定出在lea si,str和add si,cx之间,比如忘了先把SI复位。EMU8086的Variables窗口对db和dw变量能直接显示十六进制值,pos不对就先查CX,不要急着改代码。
4.3 DOS环境下验证结果一致性
EMU8086跑完还要在DOS环境下再验一遍。流程是:用MASM编译链接生成exe,用debug加载后执行。-r命令看寄存器,-d命令看数据段内存,手工找到found和pos的地址。DOS加载可执行文件时会重定位段地址,data段在内存里的绝对位置和EMU8086模拟器不一样,但数据段内部布局不变,按偏移去找就行。
两个环境结果一致,说明代码没有依赖模拟器才成立的特例行为。EMU8086对某些不太严格的段覆盖写法很宽容,DOS下可能直接异常,比如漏写assume ds:data或者跨段访问。这些差异只能在DOS环境里暴露出来,所以报告里专门做了双环境对照,而不是只跑一个模拟器就交差。
5. 常见问题排查:字符串匹配实验里五个必踩的坑
这段是本实验里最容易让人原地崩溃的五类问题,每条按现象、原因、解决的顺序写,基本覆盖了交作业前会遇到的绝大多数翻车点。
5.1 编译期翻车:双内存操作数比较
现象:cmp [si], [di]这种写法一编译就报错,提示操作数组合无效。
原因:8086指令集不允许CMP的两个操作数都来自内存,其中一个必须在寄存器里。cmp [si],[di]看着像能比较,其实指令编码里根本没有这种形式。
解决:先mov al,[si]、mov bl,[di],再cmp al,bl。别图省事,这一步省不掉,寄存器中转是8086的硬性规定。
5.2 pos结果偏大或偏小一位
现象:程序能跑到success,found也是ffh,但pos总不对,比如预期7输出成了12。
原因:成功分支里直接拿SI当前值当pos。SI在success时已经指向主串中与子串结束符对应的位置,甚至已经越过匹配区间,比真正的起始位置多出了子串的长度。
解决:直接用mov pos, cx。CX是从0开始累计的失配次数,等于本轮匹配窗口的起始下标,按题目"以str首地址为0参考点"的定义,这个值就是pos。不要用SI去减str首地址,绕远路还容易算错。
5.3 子串有效部分匹配完但主串还有剩余时误判失配
现象:str='ababc',substr='ab',程序却报匹配失败,found一直没被写成ffh。
原因:把子串结束判断放在了字符比较之后。当DI指向substr的00h、SI指向主串的'a'时,AL是'a'、BL是00h,两者不等,程序走了失配分支,完全没意识到子串的有效部分已经全部匹配完了。
解决:按第3章的写法,在循环开头先判cmp bl,00h,相等直接success。子串结束与否的优先级高于当前字符比较,这个顺序是整段逻辑的命门。
5.4 CX被无意破坏
现象:程序有时候能跑通,有时候死循环,found一直是00h。
原因:CX既当失配计数器使用,又有人在循环里用了loop指令,或者调用了会改CX的子程序。8086没有自动保存机制,CX被loop递减后,失配计数就全乱了,SI重定位到错误位置,就会出现时好时坏的表现。
解决:匹配循环内不用loop,统一用jmp控制流程。如果非要在循环里调用子程序,进子程序前push cx,返回后pop cx恢复。这是调试里最耗时的隐性坑,排了半天才发现是寄存器被吃了。
5.5 先比较字符再判断子串结束导致边界漏判
现象:主串和子串同时到达00h时能成功,但子串比主串先匹配完时失败,两个用例结果互相矛盾。
原因:判断逻辑写成cmp al,bl; jne mismatch; cmp bl,00h; je success。这种顺序下,当BL=00h而AL不等于00h时,CMP先触发不相等跳转,成功分支根本执行不到。子串有效部分匹配完成后,主串剩余字符是什么都不该影响结果,但先比较字符就把这条路径封死了。
解决:统一用第3章的状态机顺序:先判子串结束,再判主串结束,最后才比较字符。判断顺序一旦定下来就不要混用,否则就会出现"换一组测试串结果就变"的玄学问题。
6. 用CX直接推导pos:失配回溯与偏移计算的联动技巧
这份报告里最值得偷师的技巧,是发现pos可以直接从CX里读出来,而不是等匹配成功后回头算SI的偏移。题目要求pos以str首地址为0参考点,也就是匹配窗口在主串里的起始下标。观察失配回溯过程:每失配一次,起点右移一位,CX加一,所以CX从头到尾记录的就是"当前窗口起点在str中的位置"。一旦进入success,把CX写进pos就是正确答案。这让整个程序省掉了一次减法运算,也少了一个临时寄存器。
我在复现时用了一个更短的字符串做纸面推演,把每个分支后的寄存器状态写成表,几秒钟就能验证思路:
| 轮次 | CX | 比较过程 | 结果 |
|---|---|---|---|
| 0 | 0 | str[0..1]="ab"匹配,str[2]="a"与"c"不等 | 失配,CX=1 |
| 1 | 1 | str[1]="b"与"a"不等 | 失配,CX=2 |
| 2 | 2 | str[2..3]="ab"匹配,str[4]="a"与"c"不等 | 失配,CX=3 |
| 3 | 3 | str[3]="b"与"a"不等 | 失配,CX=4 |
| 4 | 4 | str[4..6]="abc"全部匹配 | success,pos=CX=4 |
在str='abababc'、substr='abc'这个例子里,最终pos等于4,与肉眼观察完全一致。这个纸面推演法最大的价值是:在写汇编代码之前,先用一张表把每个分支后CX、SI、DI的期望值写清楚,写完代码后按表对拍,能发现一半以上的逻辑坑。我当时照着这份报告复现,用这种方式提前揪出了子串结束判断的顺序问题,省下了好几个小时的断点调试时间。
如果程序里确实有别的操作会破坏CX,还有个兜底办法:在success分支前单独用一个寄存器暂存CX,或者定义一个字变量在失配回溯时同步更新,但这就失去了报告里"一个寄存器管两件事"的精简设计。从那以后我每次做这类汇编实验,都强制自己先画一遍寄存器状态跟踪表再动笔写代码,这个习惯帮我避开过不少莫名其妙的死循环和越界。希望这份拆解对你有用。
本文还有配套的精品资源,点击获取