2026/10/14 14:22:01

雷军1994年汇编代码深度拆解:TSR原理与DOS内存管理

雷军1994年汇编代码深度拆解:TSR原理与DOS内存管理 简介雷军于1994年编写的RI.ASM汇编程序源码完整收录在一份PDF文档中。这段名为“RAMinit Release 2.0”的代码曾因其精巧的TSR内存管理设计被纳入教科书适合程序员、技术爱好者及汇编语言学习者研读。文档内含程序完整注释、版本迭代记录V2.04至V2.12及对鼠标驱动、Windows DOS提示符兼容性等问题的修复思路可直观感受早期系统级编程的细节处理。资源为单个PDF文件大小约116KB轻量易读目前已获2352人学习收藏。通过这份文档读者既能理解汇编语言与硬件交互的基础原理也能借鉴雷军对缓冲区、EMS/XMS内存释放及热键检测等技术的实战手法是一份兼具历史价值与学习意义的底层编程资料。1. 为什么雷军的这段 1994 年汇编代码至今还是值得拆的系统程序样本雷军这个名字在多数人印象里已经和手机、生态链绑在一起但他 1994 年 7 月写下的 RI.ASM 这段汇编代码反而是技术圈流传多年的硬核样本。文件头标注 Revision 2.12版权注释从 1989 年延续到 1994 年代码量不大功能却相当“狠”按下设定好的热键它能把内存里一串常驻程序TSR按顺序卸掉顺带恢复中断向量、内存控制块、8259 中断控制器状态把 DOS 环境还原到接近加载前的样子。今天打开这段代码不是在考古怀旧而是能看到三件事x86 实模式编程的真实写法、1994 年 PC 软件生态的内存瓶颈以及一个系统工程师对底层资源的控制欲。适合想搞懂 TSR 原理、研究内存布局或者单纯想看看二十多年前“高手写汇编”是什么风格的人。2. TSR 卸载工具为什么难写RI.ASM 的原理与代码骨架2.1 1994 年 TSR 生态内存占用与卸载难题TSR 全称 Terminate and Stay Resident程序结束时不释放内存而是驻留在 DOS 内存里靠中断挂钩在后台响应事件。1994 年前后输入法、打印队列、磁盘缓存、鼠标驱动、游戏克星这类工具几乎全是 TSR 形态。好处是随手可用坏处是每装一个就吃掉一段 640KB 常规内存而且大多数 TSR 只管驻留不管卸载装多了只能重启。RI.ASM 解决的就是这个痛点。它自己也是一个 TSR三十多年前的代码里反而体现了当时系统级软件的通病要想卸载别人你得把别人动过的东西全部还原。中断向量表要恢复、内存控制块链要理顺、设备端口要复位、BIOS 数据区BDA被改过的字节要写回去、甚至软盘驱动器参数和打印机状态都要管。别以为恢复几个中断向量就够了TSR 之间互相挂钩是常态谁先装谁后装、谁替换了谁的向量、链上有没有断点全是细节。看 RI.ASM 文件头的版本日志能明显感到这个工具是被真实环境逼出来的版本修订内容V2.04用鼠标驱动软件重置方式初始化鼠标1993/2/17V2.05修复 RI 在 Windows DOS 提示符下无法工作的问题1993/3/9V2.06修复 XMS 无法分配 1K 内存时 RI 挂死、RI 重复释放 EMS 内存的问题V2.07修复热键设置错误V2.08处理键盘缓冲区问题V2.10支持释放 EMM386/QEMM386/S-ICE/386MAX 的高端内存块增加 RI 多副本标志V2.12修复 Init8259 过程中的严重错误同时保存 40:F0-FF 用户数据区每一条更新都对应一次真实的踩坑。文件头还列出了它支持的卸载对象SPDOS v6.0F、WPS v3.0F、Game Busters III/IV、NETX、PC-CACHE、Norton Cache、Microsoft SmartDrv、SideKick 1.56A、MOUSE Driver、RAMBIOS v2.0、386MAX 6.01 等。这些名字今天看着陌生在当时都是装机必备。2.2 COM 文件布局org 100h、数据区与jmp Main先看 RI.ASM 的入口结构.dosseg .model tiny .code .locals org 100h Start: jmp Main org 103h ... db 0dh db Revision db ??date db 26 org 114h tsrLength dw 0这不是随手写的布局。.model tiny配合org 100h决定了编译产物是一个 DOS COM 文件不超过 64KB所有段寄存器指向同一个段。COM 文件加载时DOS 会在段内偏移 100h 处放置程序前面的 100h 字节是 PSP 程序段前缀不是代码区。因此第一条真实指令从 100h 开始jmp Main的唯一任务就是跳过中间一大块数据区落到后面的 Main 逻辑。这段代码里最关键的是org 103h。注意它不是从 102h 连续排列而是故意在 103h 重新定义位置。结合文件头结构来看100h 处的jmp指令占 2 字节EB xx所以真实数据从 103h 开始。用org 103h后文件头里的版本字符串、日期占位符、tsrLength、MachineID、AuxHotKey这些字段全部有了固定偏移。为什么不像高级语言那样直接定义结构体因为 COM 文件没有额外的文件头所有字段都裸露在加载后的内存里后续代码需要按绝对偏移访问它们。RI 在驻留后还要继续使用这些字段偏移错一位整个热键和卸载逻辑全乱。这种“代码和数据挤在一个段里、靠 org 手动布局”的做法在今天的汇编教学里已经很少见但阅读 TSR 源码时几乎必遇。拿到任何一个古董 COM 文件第一步就是看 100h 附近的跳转和 org 偏移搞清楚数据区边界否则后面全是地雷。2.3 热键检测与键盘 BIOS 数据区RI 的热键触发逻辑在newINT9_proc里它挂接的是 INT 9 键盘硬件中断。来看核心判断mov ax, 40h mov es, ax cmp cs:AuxHotKey, 0 jz _1 mov bx, es:[1Ah] cmp bx, es:[1Ch] jz 10 push bx mov bl, es:[bx1] cmp bl, cs:AuxHotKey pop bx jnz 10 _1: mov ah, es:[17h] mov al, cs:HotKey push ax and ax, 0f0fh cmp al, ah pop ax jnz 10它读取的是 BIOS 数据区BDA中偏移 40h:17h 的键盘状态字节。这个字节每一位对应一个修饰键左 Alt、右 Alt、左 Ctrl、右 Ctrl、左 Shift、右 Shift。代码里用LeftAlt equ 00101000b、RightCtrl equ 01000100b这种方式预定义了组合然后and ax, 0f0fh把高低半字节分别过滤出来做比较判断当前按下的修饰键组合是否匹配。更巧妙的是AuxHotKey。如果AuxHotKey非零RI 会先检查键盘缓冲区40:1Ah和40:1Ch。这两个指针维护的环形队列里存放着最近敲击产生的扫描码es:[bx1]取出的是队列中某个键的扫描码。也就是说除了修饰键组合RI 还允许再指定一个普通按键作为辅助热键必须在修饰键按下且辅助键扫描码命中的双重条件下才触发卸载。默认代码里AuxHotKey db 0意味着只要修饰键组合命中就算。这段逻辑放到今天依然有价值它展示了在只有一个 8 位状态字节的情况下如何靠位掩码做按键组合匹配而不是像现代内核那样维护一个完整的键状态表。2.4 中断链设计INT 2F API、INT 9 热键、INT 1C 兜底RI 不只是热键卸载工具它还给外部程序留了一个查询和控制接口走的是 DOS 的 INT 2F 多路复用中断newINT2F: cmp ax, 0c0d7h ; LEI Hanzi GB Code jnz 1 push cs pop ax iret 1: cmp ax, 0c0d7h1 jnz 2 jmp KeepSelfINT 2F 的惯例是不同程序占用不同的 AX 功能号相互之间共用这一个中断入口通过 AX 值区分调用者。RI 选了0C0D7h这个数值注释里写得很直白LEI Hanzi GB Code。0xC0D7 正是 GB2312 编码里“雷”字的区位码转内码后的结果。用汉字内码做自定义中断功能号是当年程序员的一个“私货”习惯反汇编时看到 C0D7h 这种不规整的数值很容易猜到作者身份。RI 的 API 约定如下AX 传入值功能0C0D7h返回 RI 所在的段地址0C0D8h卸载 RI 之后加载的所有 TSR 程序0C0D9h卸载所有 TSR 程序包含 RI 自身0C0DAh卸载所有 RI 副本除了 INT 2FRI 还挂了 INT 1C。INT 1C 是 BIOS 的定时器中断大约每秒触发 18.2 次很多 TSR 拿它做后台轮询。RI 在这里做了一件很细的事检查当前 INT 9 中断向量是否还是自己的newINT9。原因写在注释里WPS 退出再进入后RI 可能已经失去对键盘中断的控制StopFlag 会被置起来避免重复挂钩。newINT1C每轮检查一次发现 INT 9 向量已经被别的程序接管就停止干预发现恢复成自己的就继续工作。这种基于共享中断向量的“自我取证”今天在做驱动或外挂钩子时依然常用。3. 编译与调试把 RI.ASM 变成可以下断点跟踪的 COM 文件3.1 工具链怎么选TASM / MASM 编译参数拿到 RI.ASM 源码后第一步自然是编译。这段代码用的语法是 Borland 风格.model tiny和.dosseg这种伪指令在 MASM 6.11 和 TASM 里都能认。我一般用 TASM 4.0/5.0兼容性最稳tasm /zi ri.asm tlink /t /zi ri.obj/zi生成调试信息tlink /t表示输出 COM 文件而非 EXE。如果手头只有 MASM 6.11也能编译ml.exe /c /Zi ri.asm link16.exe /TINY ri.obj, ri.com, ri.map, /CO两条命令产出的都是 ri.com。注意不要用高版本 MASM 的默认模式去编它可能把org 100h解释成偏移 0生成出来的 EXE 加载后与源码预期完全不一致。编译通过不代表能直接跑。RI 是一个 TSR加载后它会立刻驻留内存命令行上不会有任何回显。要想真正理解它在干什么必须配合调试器单步跟踪。建议在编译时额外生成一个列表文件方便对照源码看生成后的机器码偏移tasm /la ri.asm/la会输出 ri.lst里面每一行左边是生成的字节右边是源码。RI.ASM 里大量使用org调整位置没有列表文件你几乎无法在调试器里把内存地址对应回源码行号。3.2 用 TSR 编程常识判断 RI 的加载与驻留RI.ASM 的驻留方式非常典型入口处的jmp Main跳到主逻辑Main 里完成环境检查和初始化后调用int 27h驻留退出。int 27h只能驻留 64KB 以内的 COM 程序驻留长度靠 DX 寄存器指定。RI 的做法是先把tsrLength装载进 DX然后用int 27h结束进程但保留内存块。这个设计里有个容易被忽略的细节RI 卸载其他 TSR 时是通过NextDataSeg字段把自己管理的 TSR 串成链表。NextDataSeg dw 0ffffh作为链尾标志。每次加载新副本RI 会把当前副本的段地址写进前一个副本的NextDataSeg。这样从最早加载的副本出发就可以沿着链表找到所有后续副本。卸载时逐个把copies减一直到链条清空。这就是为什么 API 里会区分“卸载以后的所有 TSR”和“卸载所有 RI 副本”9: jmp dword ptr cs:oldInt2F_addr这个跳转把控制权交还给原始 INT 2F 处理器确保 RI 自己不处理的功能能继续沿链传递。TSR 编程里最忌讳的就是拦截了中断却不把不关心的请求传给下一层RI 在这里做得非常规范。3.3 在 DEBUG 里跟踪一次热键触发路径DOS 自带的 DEBUG 工具虽然简陋但跟踪 COM 文件足够用。先确认加载后 RI 的驻留段地址debug ri.com -u 100 105-u 100 105反汇编入口处的前几条指令。正常能看到jmp xxxx其中跳转目标就是 Main 的实际偏移。继续往下走-g 104把执行停在数据区起始位置附近然后直接查看数据段内容。RI 的版本字符串、日期、热键字段都在这里用-d cs:110 l10能看到原始字节跟源码注释里写的字段一一对上。真正想跟踪热键触发需要你在另一个 DOS 窗口中先加载一个普通 TSR后面第四章会给一个最小测试台再加载 RI然后按热键。由于 DEBUG 会占用实模式环境更稳的方案是直接在源码里临时把Main的循环体改成断点指令比如int 3重新编译后在 DEBUG 下用-g跑到断点。对于实模式汇编调试Turbo Debugger 的-wd窗口模式体验更好TASM 打了/zi之后可以直接用 td 加载 ri.com能看到符号名、数据段、中断向量表快照比 DEBUG 直观得多。4. 在 DOSBox 复现 RI.ASM 的完整卸载流程最小可运行测试台4.1 模拟环境选择与配置真机跑 DOS 已经不现实DOSBox 0.74 是目前兼容性最好的模拟器。RI.ASM 里大量使用jcxz $2做 I/O 延时这类技巧依赖 CPU 时序DOSBox 的动态核心能较好地模拟 AT 总线的行为。不建议直接用 Windows 自带的 NTVDM16 位子系统跑NTVDM 对 I/O 端口的拦截比较激进RI 在Init8259阶段写入 20h、21h、A0h、A1h 端口的动作很可能被静默吞掉导致卸载流程走不完。DOSBox 配置里注意两个点一是machinesvga_s3或machinepcjr会影响 BIOS 数据区的初始状态RI 依赖40h:10h等经典字段建议用默认的svga_s3二是emstrue要打开RI 的 V2.06 更新里专门处理了 EMS 内存块重复释放的问题不开启 EMS 的话相关代码路径压根不会执行。加载 RI 之前先跑一遍mem命令确认常规内存、EMS、XMS 三种内存的状态后面测试卸载效果才有对比基准。4.2 一个只显示滴答点的小 TSR 测试台用来验证 RI 卸载能力的宿主不能随便选最好自己写一个 20 行的微型 TSR。下面这个程序挂接 INT 1C 定时器中断每次中断到 16 次时在屏幕上打一个点证明它还在后台运行.model tiny .code org 100h start: jmp init old1C dd 0 count dw 0 new1C: push ax push cs pop ds inc cs:count mov ax, cs:count and ax, 000Fh jnz skip push ax push bx mov ah, 0Eh mov al, . int 10h pop bx pop ax skip: pop ax jmp dword ptr cs:old1C init: mov ax, 351Ch int 21h mov word ptr old1C, bx mov word ptr old1C2, es mov dx, offset new1C mov ax, 251Ch int 21h mov dx, offset init int 27h end start这个测试台只替换了 INT 1C 一个中断向量驻留体积极小。int 21h的 35h/25h 号功能分别用于获取和设置中断向量这是 DOS 官方推荐的 TSR 挂接方式。计时器的点是低频观测手段每秒最多出现一次不至于把屏幕刷花。4.3 加载顺序与观察点测试分成三步。第一步在纯 DOS 环境加载微型 TSR屏幕上开始出现点。第二步加载 RI.ASM此时点仍然在打因为 RI 默认只观察不干预。第三步按热键如果 RI 工作正常点会立刻停止说明微型 TSR 的 INT 1C 挂钩被摘除定时器中断恢复为原始 BIOS 处理。这里要注意加载顺序。RI 管理的 TSR 链保留在自己数据段里它只能处理“加载顺序在它之后”的程序。因此正确的顺序是先加载微型 TSR再加载 RI否则 RI 看不到已经驻留的微型 TSR热键只能把自己卸掉。如果验证失败优先查这个顺序问题而不是怀疑代码逻辑。用 DOSBox 的好处是可以开-debug命令行参数出现异常时能直接看到 CPU 是否进入保护模式、端口 I/O 是否被异常拦截。RI 的IsWinDOS函数专门检测 Windows 3.x 增强模式DOSBox 下这个调用走 1600h 分支模拟结果与实际 DOS 一致不必担心被误判成 Windows 环境。5. 避坑指南RI.ASM 里最容易让人翻车的五个细节5.1 注意把数据区当代码反汇编你会得到一堆“假指令”现象在 DEBUG 里从 100h 一路-u发现反汇编结果里有大量乱码指令比如adc al, [bxsi]、sbb ax, 1234h看起来完全不像正常的汇编逻辑。 原因100h 处的jmp Main跳过了数据区但 DEBUG 不会自动跳过数据字节被强行翻译成指令。RI.ASM 在 103h 之后放置的是版本字符串、日期、热键标志、TSR 长度等字段其中很多字节数值并不对应合法操作码。 解决不要从 100h 一直往下反汇编找出jmp的目标偏移从那里开始-u。想验证数据区字段用-d 103 114按字节查看对照源码里的 equ 定义逐个核对。5.2 注意jcxz $2不是死代码是 I/O 时序补偿现象阅读 Init8259 过程时看到每一条out指令后面跟着jcxz $2再跟一条jcxz $2。有些人会误以为这是无害跳转直接删掉删掉后程序在某些模拟器上完全正常但换到真机就随机花屏或死机。 原因这是经典的 ISA 总线 I/O 时序补偿。AT 时代 CPU 对 8259 这类慢速外设的写入需要几个时钟周期的等待jcxz $2本身不跳转或跳到下一条指令但每条指令都会消耗 CPU 周期等效于插入一条小型延时。RI.ASM 里两条连续jcxz $2提供了约 4 个时钟周期的等待窗口确保外设可靠锁存数据。 解决不要为了“美观”删掉这些指令。DOSBox 可以正常执行但真机移植或嵌入式环境复制这段代码时保留结构才能保证时序正确。5.3 注意修改热键要同时改 HotKey 与 AuxHotKey现象只改了HotKey db LeftCtrl or RightCtrl为LeftAlt or RightAlt重新编译后按 Alt 组合没有反应按 Ctrl 组合反而还能触发。 原因热键判断有两条路径。HotKey控制的是 40:17h 键盘状态字节的修饰键位而AuxHotKey控制的是 40:1Ah 键盘缓冲区中的扫描码。如果AuxHotKey保持默认 0RI 只做修饰键匹配一旦你设置过非零的辅助键RI 会要求修饰键和扫描码同时命中。很多人在改热键时只盯着一处 equ 定义忽略了另一个分支的存在。 解决统一确认三条路径——HotKey db的位掩码、AuxHotKey db的扫描码以及文件尾部的AuxHotKeyName db X$提示字符串。三者指向同一个键才能避免精神分裂式的触发行为。5.4 注意Windows DOS 框下别指望 RI 能正常工作现象在 Windows 3.1 或 Windows 9x 的 DOS 窗口里加载 RI按热键只是“滴”一声程序纹丝不动。 原因RI 的版本日志里已经写明V2.05 的修复就是“RI cannot work in Windows DOS prompt”。Windows 增强模式下保护模式的内存管理器拦截了大量实模式中断和端口访问RI 直接操作物理 8259 和实模式中断向量的动作会被系统吞掉或拒绝。IsWinDOS检测到 DOS 扩展环境后会让卸载流程提前退出并只发出提示音。 解决不要和 Windows DOS 窗口较劲。要么切换到纯 DOS 启动盘要么用 DOSBox 的纯虚模式要么准备一台无操作系统的旧机器。RI 的目标场景是实模式下的完整硬件控制权任何保护模式中介都会破坏这个前提。5.5 注意0C0D7h 这个 API 号与“雷”的关系现象看 INT 2F 处理逻辑时误以为 0C0D7h 只是一个随手挑的未使用中断号想改成自己顺眼的 0C0D8h 之类。 原因0xC0D7 是 GB2312 编码中“雷”字的机内码。RI 特意用它作为 API 标识是为了在调试器中一眼辨认出这个驻留程序的身份。直接改数值会破坏与作者原意的对应也会让你在分析旧环境时丢失这个文化线索。 解决代码注释里写着“LEI Hanzi GB Code”这就是一个带作者信息的签名。想理解为什么用这个值把 GB2312 编码表翻出来查一下就能验证。如果不是做兼容性替换保留原值是最好的选择。6. 把 RI.ASM 改造成自己的 TSR 调试器热键与私有 API 扩容RI.ASM 的价值不在“能跑”而在你可以拿它当模板做一套自己的 TSR 管理方案。我最常用的改造场景是把热键从默认的左 Ctrl/右 Ctrl 改成不容易冲突的组合同时把私有 API 的功能号替换成自己的标识。热键改动比较简单直接改数据区定义HotKey db LeftCtrl or RightAlt ; 自定义组合 AuxHotKey db 2Dh ; 对应键盘扫描码 X扫描码 2Dh 对应字母 X。配合LeftCtrl or RightAlt可以避开普通打字时的误触。改完后要同步修改AuxHotKeyName db X$ 这个字符串否则热键按下时屏幕提示仍然是旧的。API 号的替换有一个更通用的方法用 Python 计算你自己姓氏或项目名的 GB2312 内码def gb_code(ch): raw ch.encode(gb2312) return 0x%02X%02X % (raw[0], raw[1]) print(gb_code(某)) # 换成你自己的字或词把输出的十六进制数填到newINT2F的cmp ax, 0xxxxh处同时修改后面0xxxxh1、2、3三个连续功能号。注意功能号之间的间隔必须是 1因为代码里用的是jnz 2这类连续比较跳过一个数字就会漏收调用。RI.ASM 的 TSR 卸载清单是硬编码在数据段里的文件头里的“.Removed Softwares by RI:”段落就是一份完整的支持列表。想加入你自己的 TSR先观察源码里RestoreMCB、RestoreVecList、CloseFiles这三个子程序的操作顺序它依次恢复内存控制块、中断向量和文件句柄。你的 TSR 只要没有额外占用 XMS/EMS 高端内存这三个子程序基本够用如果它改了 40:00 段里的 BIOS 数据还要补一段RestoreBiosData风格的保存和回写逻辑。从那以后我每次拆 90 年代中期的 DOS 工具源码都强制自己先走一遍“入口跳转、org 数据区、中断挂钩清单”这条路径再动手改代码RI.ASM 教会我的这套习惯后来在分析不少古董级系统软件时都救了场。希望这篇拆解帮到你也建议你下载资源里的 RI.ASM 原件对照本文逐行读一遍体感会比看截图和注释来得更直接。本文还有配套的精品资源点击获取