news 2026/10/22 1:08:23

8086汇编字符串匹配:禁用串操作指令后的寄存器回溯与pos推导

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
8086汇编字符串匹配:禁用串操作指令后的寄存器回溯与pos推导

简介:面向学习汇编语言的读者,这份实验报告围绕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 循环体的判定顺序:子串结束优先于主串结束

这份报告里的循环体不是简单的"相等就继续,不等就回溯",而是要处理四个判定层次。我把它们按执行顺序写出来:

  1. 检查DI指向的字符是否为00h,是则子串有效部分已经全部匹配,跳转成功;
  2. 检查SI指向的字符是否为00h,是则主串先耗尽,跳转失败;
  3. 比较AL与BL,相等则SI和DI同时递增,继续下一轮;
  4. 不相等则进入失配回溯。

注意顺序不能调换。子串结束判断必须放在主串结束判断之前,原因在于:当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 main

found写入的必须是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首地址为0CX值直接等于匹配起点偏移
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比较过程结果
00str[0..1]="ab"匹配,str[2]="a"与"c"不等失配,CX=1
11str[1]="b"与"a"不等失配,CX=2
22str[2..3]="ab"匹配,str[4]="a"与"c"不等失配,CX=3
33str[3]="b"与"a"不等失配,CX=4
44str[4..6]="abc"全部匹配success,pos=CX=4

在str='abababc'、substr='abc'这个例子里,最终pos等于4,与肉眼观察完全一致。这个纸面推演法最大的价值是:在写汇编代码之前,先用一张表把每个分支后CX、SI、DI的期望值写清楚,写完代码后按表对拍,能发现一半以上的逻辑坑。我当时照着这份报告复现,用这种方式提前揪出了子串结束判断的顺序问题,省下了好几个小时的断点调试时间。

如果程序里确实有别的操作会破坏CX,还有个兜底办法:在success分支前单独用一个寄存器暂存CX,或者定义一个字变量在失配回溯时同步更新,但这就失去了报告里"一个寄存器管两件事"的精简设计。从那以后我每次做这类汇编实验,都强制自己先画一遍寄存器状态跟踪表再动笔写代码,这个习惯帮我避开过不少莫名其妙的死循环和越界。希望这份拆解对你有用。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 1:08:17

基于STM32与单总线传感器的HVAC分布式温度采集系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/22 1:06:20

基于STM32F746ZG与PJ85718DM的本地远程温度监测系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/22 1:06:20

PJ85718DM+PIC24HJ256GP610工业温度监测系统设计与抗干扰实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/22 1:06:12

PJ85718DM与PIC32MX695F512L温控组合的工业级设计实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/22 1:05:58

C语言内存无类型本质:原理、陷阱与零成本泛型实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/22 1:05:55

基于PIC18F87J50与PJ85718DM的本地远程温度监测系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华