news 2026/11/3 8:19:29

16位实模式TSR卸载器源码拆解:DOSBox+MASM编译与中断钩子排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
16位实模式TSR卸载器源码拆解:DOSBox+MASM编译与中断钩子排查

简介:这份文档围绕小米创始人雷军早年的汇编作品整理而成,是一份代码赏析资料,面向对底层编程、汇编语言与计算机系统感兴趣的技术人员、学生及技术史爱好者。文档以1994年7月创建的RI.ASM(V2.12)为主线,展示这段常被提及收入教科书的代码如何借助热键卸载后台驻留的TSR程序,并列出可被移除的SPDOS、WPS、Game Busters等软件清单,同时呈现8259初始化、XMS/EMS内存申请、鼠标驱动重置等实现细节。压缩包内共有1个PDF文件,体积约116KB,是排版后的阅读版本,篇幅精炼,便于通读与摘录。代码中的V2.04至V2.12修订记录,还原了Windows DOS提示符下无法正常工作、热键设置报错、高位内存释放等问题的排查过程,读者可从中体会早期DOS环境下内存管理与兼容性调试的思路,对理解寄存器操作与中断调用的写法也有直接参考价值。目前已有2367人学习。

1. 一份 1994 年的 TSR 卸载器源码:为什么值得花时间读汇编代码

翻硬盘翻出一份 1994 年 7 月的汇编代码,文件名RI.ASM,头部标着Revision 2.12、RAMinit Release 2.0,作者署名只有几个字母。它做的事很朴素:常驻内存,按一个热键,把在它之后加载的 TSR 程序连同 PSP、MCB 链、中断向量、8259 掩码、显示模式、软驱参数全部还原,相当于给 640KB 时代的 DOS 环境准备了一次「后悔药」。抛开「这段代码的作者后来去做消费电子」这层流传背景,它本身就是一份少见的完整样本:INT 9 键盘钩子、INT 1C 定时钩子、INT 2F 自定义功能号、键盘状态字节、PSP 回溯、MCB 遍历,一个不落。想补 16 位实模式底子、想搞懂 TSR 怎么和中断向量表掰手腕的人,都值得把它跑一遍。

2. 环境搭起来:DOSBox 挂载与 MASM 6.11 完整编译链路

拿到一份.ASM文件之后第一件事是确认它面向什么运行环境。这份代码开头就是.model tiny、.code、org 100h、jmp Main,最后是int 27h——标准的 16 位实模式 COM 程序骨架,运行在 MS-DOS 上。你不可能拿 Visual Studio 或者 64 位 gcc 直接吃下它,因为里面到处是mov es, ax、out 21h, al、int 2fh这类直接操作段寄存器和 I/O 端口的指令,现代工具链在保护模式下的段检查会把这些全部拦下来。

2.1 为什么绕不开 MASM + DOSBox 这条路

可选方案其实有三个,我按实际踩过的顺序排一下:

第一种是用 NASM 写BITS 16+org 0x100+-f bin直接产出 COM。这条路干净,但 MASM 的locals @@局部标签语法、db LeftCtrl or RightCtrl这种带操作符的数据定义、RET与iret混用的地方都要逐处改写,改错一个标签名就是「链接通过、运行乱跳」。第二种是开一台 FreeDOS 虚拟机,用真实硬件环境跑,最贴近原始行为,但 8259 中断控制器初始化那段在虚拟机里可能被 hypervisor 吞掉一部分效果,调起来反而更绕。第三种就是我最后选的:DOSBox 挂载宿主机目录 + MASM 6.11 在虚拟 C 盘里编译。理由很实在——DOSBox 对 16 位实模式的模拟足够老实,int 27h、键盘缓冲区0040:001A、BIOS 数据区都按真机布局映射,而目录可以挂到宿主机上,源码用任意编辑器改完保存,切回 DOSBox 重新编译就行,不用来回拷文件。

提示:MASM 6.11 和 5.0 都能吃这份代码,6.11 的/AT选项能一步生成 COM,省掉exe2bin那一环。如果你手上只有 5.0,走masm→link /TINY→exe2bin的三步也行,只是中间产物要多管一次。

2.2 目录布局与 DOSBox 挂载

先按下面这个结构把文件放好。DOSBox 的老版本对长文件名支持一般,路径里别带中文、空格和超过 8 个字符的主文件名,否则masm那一步会报cannot open file,而这种报错十有八九不是语法问题,是路径问题。

# 宿主机目录(示意) # D:/asm-lab/ # ├─ src/ri.asm <- 原始源码 # ├─ bin/ <- 编译产物落这里 # └─ tools/masm/ <- MASM 6.11 的 ml.exe / link.exe

进 DOSBox 之后依次挂载。mount的第一个参数是 DOS 里看到的盘符,第二个参数是宿主机的绝对路径;-t cdrom之类的类型参数不要乱加,这里就是普通目录。

mount c "D:/asm-lab" c: cd tools\masm set path=%path%;c:\tools\masm cd ..\..\src dir

执行完dir应该能看到RI.ASM和它的字节数。如果dir里显示的是RI ASM这种被截断的名字,说明挂载时文件名被 8.3 规则处理过,编译时要用截断后的名字,别照抄源码里的长名。

2.3 编译、链接、产出 COM 并首次加载

MASM 6.11 的ml支持/AT(assemble tiny),配合/c只汇编不链接,再手工 link,能看到每一步的中间产物,出问题时好定位。如果你的源码里保留了原作者的org 100h和.model tiny,/AT会自动处理成 COM 的地址偏置。

# 1. 汇编:/AT 生成 tiny 模型,/Zi 带调试符号,/Fl 输出列表文件 ml /AT /c /Zi /Flri.lst ri.asm # 2. 链接:/TINY 强制 tiny 模式,输出直接是 .com link /TINY ri.obj,ri.com; # 3. 校验产物 dir ri.com

这里三个参数值得记一下:/AT告诉汇编器按.model tiny处理并且假设代码从100h开始;/TINY是链接器的开关,它会让链接器在输出时去掉 EXE 头,直接吐出二进制镜像;/Fl生成的.lst文件是把每条指令、每个偏移量、每个宏展开都列出来的宝贝,后面排查跳转错位全靠它。编译成功之后ri.com的大小一般在一千多字节量级,具体数字取决于你改了多少内容,跟原始版本对不上是正常的,不用慌。

加载前先做一次干净启动,DOSBox 里可以直接跑。执行ri之后如果程序正常常驻,屏幕上会打印一行提示(源码里用int 10h的0Eh功能做字符输出,配合clsStr那张字符串表),然后回到命令行提示符——这个「回到提示符」恰恰是常驻成功的标志,因为int 27h会把进程挂起但保留在内存里。

# 加载 RI 并观察是否常驻 ri # 如果提示符回来了,说明 TSR 已经挂在中断链上 # 此时再加载一个测试用的 TSR,然后按热键验证清理效果

首次运行时最好只验证「能不能跑起来并常驻」,不要急着按热键。热键逻辑里有一段会重写 8259 的掩码寄存器,如果在 DOSBox 里触发时机不对,可能出现键盘直接失去响应,这时候只能关掉 DOSBox 重来。养成习惯:第一次跑只测加载,第二次跑才测触发。

3. 拆解 RI.ASM 的三个核心结构:钩子、热键、链回溯

把代码读薄以后会发现,整个程序真正撑起功能的只有三块:故障点最少但最容易写错的中断挂钩、看起来简单实则坑最多的热键判定、以及决定成败的 TSR 链回溯。剩下的两千多行,绝大部分是「还原现场」的收尾工作——恢复显卡模式、恢复软驱参数、恢复打印机口、恢复喇叭状态,一件件都是当年真实踩出来的。

3.1 INT 9 / INT 1C / INT 2F 三条钩子的分工

程序开头保留了几个原始向量地址槽位:oldInt9_Addr、oldInt1C_addr、oldInt2F_addr。这三个中断各自负责一件事。

INT 9 是键盘硬件中断,每次按键产生一次扫描码时 CPU 都会跳到这里。原作者把 INT 9 换成了自己的newINT9,在里面先调一次原始 INT 9 让 BIOS 把扫描码塞进键盘缓冲区,然后再检查缓冲区里的内容是不是热键。这一步的顺序不能反,先让 BIOS 处理再判断,否则会把按键吞掉,用户会觉得「键盘失灵」。

newINT9: pushf db 9ah ; call far ptr oldInt9_Addr 的手工编码 oldInt9_Addr dw 0, 0 cmp cs:NoFlag, 0 jz newINT9_proc mov cs:NoFlag, 0 iret newINT9_proc: cmp cs:Flag, '!' ; 忙标志,防止重入 jnz @@0 iret @@0: mov cs:Flag, '!' ; 置忙

这里有两个设计细节值得单独说。第一,db 9ah后面跟两个dw是在手工拼一条call far ptr指令的操作数,因为目标地址在运行时才会被填进那个dw槽位。用 MASM 的call dword ptr cs:oldInt9_Addr也能写,但当年很多汇编器对这条的编码不统一,手写字节最保险。第二,Flag那个忙标志是必须的——INT 9 处理过程中如果又来了一个按键中断,递归进入函数会把栈搅烂,典型症状是「按快一点就死机」。

INT 1C 是 BIOS 定时器 tick 钩子,每秒大约 18.2 次。这里挂它的用途不是计时,而是做「窗口 DOS」的检测:newINT1C里比对当前 INT 9 向量的段地址,如果发现 INT 9 已经被别的程序(比如某个国产排版系统)换掉了,就把自己的StopFlag置 1,暂时让位。这是一种很聪明的妥协——两个都想接管键盘的程序硬碰硬只会两败俱伤,让一步反而都能活。

INT 2F 是多路复用中断,这里用了C0D7h到C0DAh四个自定义功能号,分别对应「返回 RI 段地址」「卸载 RI 之后的所有 TSR」「连同 RI 一起卸载」「删除 RI 的所有副本」。选这个号段是因为当年C0xx这一片基本没人抢,属于事实上的自由领地。

newINT2F: cmp ax, 0c0d7h ; 返回 RI 段地址 jnz @@1 push cs pop ax iret @@1: cmp ax, 0c0d7h+1 ; 保留自身,卸载后加载的 jnz @@2 jmp KeepSelf

把这四条钩子串起来看,整个程序的协作模型就清楚了:INT 9 负责感知按键,INT 1C 负责感知环境变化(别的程序抢了键盘),INT 2F 负责对外暴露控制接口,Flag和StopFlag两个字节负责在它们之间做状态同步。这套写法在今天的驱动开发里换个名字还在用,无非是自旋锁和状态机。

3.2 热键判定:[40:17h]状态字节加扫描码双保险

热键逻辑是这个程序里最容易改坏的部分。原作者的做法是「状态位比对 + 扫描码计数」两层。

第一层看 BIOS 数据区的0040:0017h,这个字节按位记录了左右 Shift、左右 Ctrl、左右 Alt 的按下状态,还有 ScrollLock / NumLock / CapsLock 的锁定状态。源码里的宏定义很直白:

LeftAlt equ 00101000b RightAlt equ 10001000b LeftCtrl equ 00010100b RightCtrl equ 01000100b LeftShift equ 00000010b RightShift equ 00000001b HotKey db LeftCtrl or RightCtrl ; 默认热键 = 左右 Ctrl 同时按

但真正比较的时候并不是拿整个字节硬碰,而是先跟0F0Fh做与运算,把高四位的锁定状态位全部清掉:

mov ah, es:[17h] ; 当前按下状态 mov al, cs:HotKey ; 期望的按键组合 push ax and ax, 0f0fh ; 屏蔽 Caps/Num/Scroll 锁定位 cmp al, ah pop ax jnz @@10 ; 不匹配,直接退出

为什么要屏蔽?因为如果用户开着 CapsLock,[40:17h]的第 6 位是 1,硬比整个字节的话热键就永远触发不了。这是血泪经验型的细节,注释里没写理由,但少这一行就是「有时候能按出来、有时候按不出来」的玄学故障。

第二层是AuxHotKey这个辅助键。它不检查状态位,而是直接翻键盘缓冲区0040:001Ah(头指针)到0040:001Ch(尾指针)之间的扫描码,找到匹配的就连续两次inc bx把尾指针往后推,等于把这个键从缓冲区里「吃掉」,不让它传给前台程序。

mov bx, es:[1ah] cmp bx, es:[1ch] jz @@10 ; 缓冲区空,跳过 push bx mov bl, es:[bx+1] ; 取扫描码 cmp bl, cs:AuxHotKey pop bx jnz @@10 inc bx inc bx ; 头尾指针各推一格,吃掉该键 cmp bx, 3eh jb @@_2 mov bx, 1eh ; 环形缓冲区,越界回绕 @@_2: mov es:[1ah], bx

3eh是缓冲区尾端的边界值,1eh是起始偏移,超出就回绕——键盘缓冲区是个标准的环形队列,回绕逻辑写错会导致键盘彻底不响应,这是第二个容易翻车的点。

3.3 RemoveTSR:PSP 回溯与 MCB 链倒着拆

真正决定这个程序价值的,是RemoveTSR那段。它的思路是:从当前 PSP 出发,沿着 MCB(Memory Control Block)链往前找,每找到一个属于 TSR 的内存块就还原它改过的东西,然后释放。

MCB 的结构在 DOS 里是固定的:块首字节'M'表示后面还有块、'Z'表示这是最后一块;偏移 1 处的字是拥有者 PSP 段地址;偏移 3 处的字是以 16 字节为单位的块大小。核心循环大致是这样的:

@@_0: mov ax, ds:NextDataSeg ; 上一个 TSR 的 PSP 段 cmp ax, -1 jz @@_1 ; 链到头了 mov cs:PrevDataSeg, ds mov ds, ax jmp @@_0 ; 继续往前滚

每进入一个 TSR 的 PSP,就依次调用一串还原函数,顺序很有讲究:

调用顺序函数还原内容
1RestoreEnvStr环境块字符串
2RestoreMCB内存控制块链的拥有者字段
3CloseFiles该进程打开的文件句柄(从 5 号开始扫 15 个)
4RestorePort0040:0000起的 8 个端口字
5RestoreLEDs键盘 LED 状态与修饰键位
6RestoreVecList中断向量表中的标记项
7RestoreFloppyParam软驱参数表[0000:0525h]
8RestoreCVTchainCVT 链
9RestoreMemoryManagerXMS / EMS 句柄
10RestoreBiosData[40:10h]、[40:A8h]、[40:F0h]三块 BIOS 数据
11Enable82598259 中断控制器的掩码寄存器
12RestoreClockSpeedCPU 时钟速度
13ResetDisk磁盘系统复位

这个顺序不能随便调。比如RestoreMCB必须在CloseFiles之前,因为文件句柄表是挂在 PSP 上的,如果先把 MCB 的拥有者改回去了,再去找 PSP 就可能拿到一块已经被标记为自由的内存。RestoreBiosData放在靠后也是同理——它要拷[40:F0h]那个用户数据区,而这块区域在前面几步里可能还被临时占用着。

还有一段容易看漏的:进入RemoveTSR时第一件事是重建栈。

RemoveTSR: pop ax cli ; 关中断,改栈期间不能被打断 mov sp, cs mov ss, sp mov sp, 100h ; 用自己段内的 100h 当新栈顶 sti push ax

为什么要重建栈?因为要卸载的 TSR 可能有自己的栈段,你现在正踩在别人的栈上,一旦把对方的内存块释放掉,返回地址就没了。换成自己的段内栈之后再动别人的内存,才安全。cli/sti夹住这一段也是必须的,改ss:sp的瞬间如果来了中断,CPU 会往一个半新半旧的栈上压现场,直接 triple fault。

4. 参数与配置:可改变量清单和改完必做的回归

代码读完就该动手改了。这份源码的好处是把几乎所有可调项都提在文件头部,用db/dw定义成一串具名变量,改起来不用满文件找魔数。但改完之后有几处联动必须一起动,漏一个就是「编译通过、运行没反应」。

4.1 可改参数一览表

先把这个头部区域里真正影响行为的变量挑出来。表格里「联动项」那一列是我实际改的时候踩出来的,原注释里没有。

变量名默认值作用改动时的联动项
HotKeyLeftCtrl or RightCtrl主热键的修饰键组合改完要同步改Kbd102分支里的位移逻辑
AuxHotKey0(关闭)附加热键的扫描码非 0 时必须同时设AuxHotKeyName
AuxHotKeyName'X$ '附加热键的显示字符长度要跟ColorPrintStr的格式匹配
Copies'1'允许同时驻留的副本数改成'1'以上要重算tsrLength
PowerTrue是否执行 8259 重初始化在虚拟机上建议先设False观察
Flag'!'忙标志初值不要改,改了重入保护就失效
StopFlag1是否暂停接管键盘首次调试设 1,确认钩子挂上再放开
Kbd1020是否按 101/102 键盘布局处理 Ctrl笔记本外接键盘场景需要设 1

Copies这个变量值得多说一句。它控制的是「同时能驻留几份 RI」,源码里通过 INT 2F 的C0DAh功能号把所有副本一起删掉。默认'1'表示只允许一份。如果你把它改成'2'以上,KeepSelf里那段用tsrLength计算常驻长度的逻辑就要重新核对,因为每多一份就多占一段内存,而tsrLength是在链接时由$符号算出来的固定值,不跟着变。改错了的症状很隐蔽:程序能加载、能触发,但卸载之后内存并没有真正释放,下次加载就报内存不足。

4.2 把热键改成左右 Alt 的完整流程

拿一个具体改动走一遍全流程,比看表格直观。目标是把默认的「左右 Ctrl」改成「左右 Alt」。

第一步,改头部定义。LeftAlt是00101000b,RightAlt是10001000b,或起来是10101000b,也就是0A8h:

; 改前 HotKey db LeftCtrl or RightCtrl ; 改后 HotKey db LeftAlt or RightAlt

第二步,检查Kbd102分支。源码里有一段对 101/102 键盘的特殊处理,会把HotKey右移四位再跟[40:18h]的303h掩码比较:

cmp cs:Kbd102, True jnz @@1 shr al, 1 shr al, 1 shr al, 1 shr al, 1 push ax mov ah, es:[18h] and ax, 303h cmp al, ah pop ax

这段是给右 Ctrl / 右 Alt 在 102 键布局下位置不同的机器做补偿的。改成 Alt 之后,如果你在笔记本上跑,Kbd102建议保持0,先在标准布局下验证;确认能触发再考虑打开。

第三步,重新编译并验证。验证不要一上来就按热键,先看两件事:编译产物大小有没有突变(突变说明某个宏展开多了一层)、加载后提示符是否正常返回。

# 重新编译 ml /AT /c /Zi /Flri.lst ri.asm link /TINY ri.obj,ri.com; # 加载,先不按热键 ri # 观察提示符是否返回,再按左右 Alt 测试

如果按下去没反应,先去翻ri.lst,搜HotKey找到它被引用的那几处,看and ax, 0f0fh之后al的值是不是你预期的0A8h & 0F0Fh = 0A08h。手算一遍比在 DOSBox 里反复重启快得多。

4.3 改完必做的三项回归

参数改完不是「能触发」就完事了。有三项回归我每次都会跑一遍,因为这三处坏了在正常操作下都不容易发现。

第一项,重入测试。按住热键不放,或者快速连按十几次。Flag那套忙标志如果被绕过,表现是程序卡死或者屏幕花掉。这一项在慢速机器上不明显,DOSBox 默认的 CPU 周期数下反而容易复现。

第二项,多副本测试。先加载一份ri,再加载一个无关的测试 TSR,然后触发热键,确认只有测试 TSR 被清掉、ri自己还在。接着执行ri的第二次加载,看它是否正确提示「已存在」并退出。这一步验证的是NextDataSeg那条链有没有接对。

第三项,退出 DOSBox 前的现场检查。触发一次热键之后直接执行mem命令看内存占用,再执行一次dir看磁盘访问是否正常。这两条命令分别压到了 MCB 链和磁盘复位两段逻辑,能跑通说明还原顺序没乱。

5. 避坑与排查:五个真实翻车现场

汇编这东西的调试体验跟高级语言完全不是一个量级。高级语言报个栈溢出至少给你一行行号,汇编翻车往往是「屏幕黑了」「键盘没了」「重启了」,连个错误码都没有。下面五条是我在这份代码上实际撞过的,按「现象 → 原因 → 解决」记下来。

5.1 编译与链接阶段的两个坑

坑一:link报fixup overflow,但代码里明明没有跨段引用。

现象是汇编通过、链接直接失败,错误信息指向某条mov或jmp。原因是.model tiny下所有代码和数据都挤在同一个 64KB 段里,一旦你加了几张字符串表或者多定义一个几百字节的缓冲区,段内偏移就可能超过0FFFFh,链接器算不出 16 位的偏移就报溢出。解决办法不是去调链接参数,而是老老实实看ri.lst里的段内偏移分布,把不必要的数据表挪走或者压缩。我当时的做法是把那张clsStr提示字符串表砍掉了三分之一,问题立刻消失。

坑二:生成的 COM 文件能加载但一执行就重启。

现象是敲ri回车,DOSBox 直接回到启动画面。这类故障九成出在org 100h那行。COM 程序被 DOS 加载时,代码段的起点固定在 PSP 之后,也就是段内偏移100h的位置,所有绝对地址都要以这个为基准。如果源码里org写的是0,或者被某些编辑器悄悄改成了别的值,jmp Main就会跳到错误位置,执行到一片随机数据上,重启是必然的。核对方法很土但有效:用十六进制编辑器打开ri.com,第一个字节应该是E9(jmp near)或者EB(jmp short),紧接着是跳转偏移。如果不是,先查org。

5.2 运行阶段的三次键盘事故

坑三:按了热键之后键盘失灵,只能强制关 DOSBox。

现象很吓人。原因是 8259 重初始化那段被触发了。Init8259里会根据MachineID判断是 AT 还是 PC/XT 机器,然后往20h/21h/A0h/A1h四个端口写 ICW1 到 ICW4 四个初始化字。这四个字写错任何一个,中断控制器的优先级或者屏蔽字就会错乱,键盘中断可能被永久屏蔽。解决办法是首次调试时把Power设成False,跳过整个 8259 流程,只验证其他还原逻辑;确认其他部分没问题了再把它打开,并且把old_8259里保存的原始掩码打印出来核对。源码里Enable8259最后那句out 021h, al/out 0a1h, ah就是把原始掩码写回去,这句如果漏了,症状同样是键盘失联。

坑四:Windows 下的 DOS 窗口里加载后热键完全无反应。

现象是在真实 DOS 下正常,进 Windows 的 DOS 窗口就不行。原因出在IsWinDOS那段检测。它用了两个 INT 2F 功能号:AX=1600h查是否运行在增强模式的 Windows 下,AX=4680h查版本号。检测到 Windows 就把StopFlag置 1,主动让位,因为 Windows 自己也接管了键盘。这个设计是对的,但如果你在虚拟机的「DOS 兼容模式」里跑,返回值可能落在00h到80h之间的灰色地带,导致判断走错分支。解决方式是临时把IsWinDOS的两个cmp都改成直接jmp @@9(返回 1),强制走「非 Windows」路径,先把功能跑通再回头细调判定。

坑五:卸载之后内存没有真正释放,第二次加载报内存不足。

现象是第一次加载、触发、卸载都正常,第二次加载时说内存不够。原因是RemoveTSR里RestoreMCB那段对 MCB 拥有者字段的处理不完整。MCB 链上每个块的第 8 个字节(偏移8)在某些 DOS 版本里还有额外含义,源码里有一句mov byte ptr es:[8], 0后面跟着日期注释,说明原作者也在这里打过补丁。如果你在 DOSBox 的某个特定版本上跑,这个字节的语义可能跟 1994 年不完全一致,导致块虽然被标记为自由,但链没接上。排查方法是卸载之后立刻执行mem /c,看那一段内存的归属;如果是Free但总量对不上,就是链断了,得手动在RestoreMCB里加一句把上一块的'M'标志重新对齐。

6. 两个可以带走的硬功夫:现场保护和内存核算

前面几章讲的都是「怎么让它跑起来」,最后说两个真正能从这份代码里带走、换到别的项目上也管用的东西。

第一个是中断现场保护的完整套路。这份代码里几乎每个钩子函数都以pushf开头、以iret结尾,中间必然有一句call far ptr old_xxx。这个模式的本质是:你只是链上的一环,不是链的主人。凡是替换别人向量的地方,都要先把原向量存下来,在自己的处理逻辑前后原样转发一次,最后用iret让 CPU 自动弹出 IP、CS、FLAGS 三个字。少弹一个字就是栈不平衡,多弹一个字就是跳飞。我后来写任何中断或回调钩子,都先按这个模板起手:

; 通用的向量钩子骨架(16 位实模式) newHandler: pushf call far ptr oldHandler ; 先转发,让原处理者做它该做的事 push ax ; 保护自己要用的寄存器 push ds ; ---- 自己的逻辑 ---- pop ds pop ax iret ; 恢复 IP/CS/FLAGS 并返回

注意这里只保护了ax和ds,因为其他寄存器我没用。多保护寄存器不犯错,但会多吃栈空间;少保护一个就是随机崩溃。判断标准很简单:函数体里出现过哪个寄存器,就在开头 push 哪个,一个都别省。

第二个是内存占用的核算方法。TSR 程序的内存账目必须自己算清楚,因为 DOS 不会帮你。这份代码用tsrLength这个符号来标记常驻边界,它由链接器在$处求出,指的是从段首到当前指令的字节数。但真正驻留的不只是代码,还有数据区和栈区:

占用块归属是否随代码增长
代码段 + 已初始化数据tsrLength之前是
未初始化数据(.data?)tsrLength之后是
运行时栈(100h起)段内固定位置否
PSP 本身DOS 分配否,固定 256 字节
环境块DOS 分配视环境变量数量

核算方式是在KeepSelf里int 27h之前,把tsrLength加上 PSP 的 256 字节、环境块长度、再留一点余量,写进dx寄存器。int 27h会保留CS:DX以内的内容并把其余部分还给 DOS。源码里mov dx, es:tsrLength后面跟着的那句mov al, 0h加rep stosb,是主动把超出常驻边界的内存清零,避免把上一份程序的残留数据带进新副本——这个动作在当年是防止「卸载后还能读到旧数据」的必需品。

说实话,第一次跑通这份代码的时候我按了热键,看着屏幕上那行红色提示弹出来、内存里的测试 TSR 被清掉,那种感觉跟现在跑通一个分布式服务完全不是一回事。后来我改过好几次HotKey,有一次忘了改Kbd102的联动,在笔记本上折腾了整整两个晚上才定位到。从那以后我每次动这类老代码的参数,都强制先跑一遍ri.lst的手工偏移核对,确认每个改动的宏都在列表文件里落了正确的字节,再进 DOSBox。这份 1994 年的源码,代码量不大,但该有的坑一个都不少,跑通一遍比读十篇中断向量表的教程都实在。希望帮到你。

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

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

RN6854M:4路AHD接入MIPI CSI2的配置与调试避坑

简介&#xff1a;面向从事车载、安防与嵌入式视觉硬件开发的工程师&#xff0c;这份资料是台湾立敏RICHNEX RN6854M 四通道高清解码器的官方设计手册&#xff0c;重点帮助工程师解决AHD模拟高清信号转MIPI CSI2输出时的选型、原理图设计与寄存器配置问题。压缩包内为1个PDF文件…

作者头像 李华
网站建设 2026/11/3 8:18:44

Xposed模块开发实战:从官方文档到Hook技术深度解析

1. 为什么我要啃官方文档做这份教程搞Android深度定制的人&#xff0c;迟早会撞上Xposed这堵墙。市面上讲Xposed的中文资料&#xff0c;要么是几年前的碎片帖子&#xff0c;要么是直接抄示例代码不讲原理&#xff0c;真正把官方文档那套东西讲透的少之又少。我自己第一次接触Xp…

作者头像 李华
网站建设 2026/11/3 8:18:14

生物运动控制算法原理与机器人应用实践

1. 生物运动控制算法的现实意义在机器人技术快速发展的今天&#xff0c;如何让机械系统像自然界生物一样灵活运动&#xff0c;一直是控制算法领域的核心挑战。我十年前第一次看到波士顿动力的机器狗视频时&#xff0c;就被这种仿生运动的流畅性震撼到了——这完全颠覆了传统工业…

作者头像 李华
网站建设 2026/11/3 8:17:47

AI数据全流程自动化平台:从标注到模型部署的闭环解决方案

1. 项目概述&#xff1a;AI数据全流程自动化平台的核心价值在计算机视觉和机器学习项目的实际落地过程中&#xff0c;数据标注往往成为制约项目进度的关键瓶颈。传统人工标注不仅耗时费力&#xff0c;标注质量还受限于标注人员的专业水平。我们团队开发的这套一体化平台&#x…

作者头像 李华
网站建设 2026/11/3 8:17:39

从Claude Code到Pi:AI Coding工具迁移与harness控制权之争

1. 从 Claude Code 到 Pi&#xff1a;一场关于 AI Coding 工具选择权的迁移最近半年&#xff0c;我身边不少做 AI Coding 的朋友都在悄悄换工具。不是从 Cursor 换到 Windsurf 那种小打小闹&#xff0c;而是从 Claude Code 直接切到 Pi。这个现象挺有意思——Claude Code 在代码…

作者头像 李华