# 边界

> 从四个版本的源码，看 86-DOS 如何把 CP/M 留在身后

<!-- 构建自 _build/content_boundary.py，勿直接改本文件 -->

---

## 目次

0. [为什么要做一套 8086 操作系统](#ch0)
1. [把接口对应关系量化](#ch1)
2. [槽 12：从 VERSION 到 FLUSHKB](#ch2)
3. [日期：新增四个系统调用](#ch3)
4. [6/16 与 7/7：分发表未变](#ch4)
5. [1.00 的空壳从哪里来](#ch5)
6. [槽 28：从 WRTPROT 到 GETFATPTDL](#ch6)
7. [结论：兼容边界如何变化](#ch7)
- [术语](#terms) · [文献](#refs) · [复现包](#repro)

---


## 0　为什么要做一套 8086 操作系统

1980 年春天，Seattle Computer Products（SCP）有一块难以销售的 8086 板卡。

那是 SCP 的 S-100 总线 8086 CPU 板，1979 年 11 月开始出货，比 IBM Personal Computer 早了将近两年。S-100 是当时连接 CPU、内存和外设板卡的一种总线标准；板卡使用的是 Intel 8086。

问题在于，8 位机时代的主流操作系统 CP/M 还没有可用的 8086 版本。Digital Research 正在开发 CP/M-86，但 SCP 不能一直等待：没有操作系统，板卡就缺少可以运行的应用软件。

1980 年 4 月，Tim Paterson 开始为 SCP 编写替代方案。QDOS 0.11 在同年完成，之后改名为 86-DOS。

## 接口继承与代码复制要分开讨论

早期 DOS 与 CP/M 的关系长期存在争议。现有公开材料至少把两个问题分开了：Bob Zeidman 的 2016 年源码和二进制比对报告称没有发现 DOS 复制 CP/M 代码的证据，同时确认大量系统调用号一致（文献 [2]）；OS/2 Museum 则从 8086 分段架构、磁盘管理和工具命名等角度指出 86-DOS 与 CP/M 的内部实现差异很大（文献 [3][4]）。

这些材料支持把「接口继承」与「代码复制」分开讨论，但不能替代法律判断。本文只分析源码、二进制和当事人材料能够支持的工程结论。

为什么系统调用号仍然相同？Paterson 在 1994 年写给《Microprocessor Report》的信中给出了一种解释。以下引文是译文，英文原文存于 `_evidence/vol3/paterson1994.txt`：

> 为了让主要软件厂商把他们的产品从 8080/Z80 移植到 8086，我认定必须把这件事做得尽可能容易。
> 我已经写好了一个 Z80 到 8086 的源码翻译器。我的计划是：开发者把一个 8080 CP/M 程序送进
> 翻译器跑一遍，就是移植到 8086 所需要的全部工作。

这句话里的关键词是翻译器（TRANS）。Paterson 先写了把 Z80 汇编源码翻成 8086 汇编源码的
程序，那么要让翻译后的程序尽可能不引入系统相关的修改，它所依赖的操作系统接口就
必须与 CP/M 一致：

> 应用程序请求操作系统服务所用的接口，在套用翻译规则之后，必须与 CP/M 完全一致。（以上为当事人材料。）

本文可以从这段当事人材料作出一个工程上的推断：接口保持一致，TRANS 翻译出的程序才有机会少改动地运行。因此 86-DOS 保留了 CP/M 的功能号、入口地址（CALL 5）和部分 FCB 布局；Paterson 把这些称为「所有应用可见的元素」。

他同时明确说，文件存储机制与 CP/M 不同，并估计这部分约占 86-DOS 代码的 80%。这个比例是 1994 年的事后估计，不是本文重新测量的结果。本文后面的量化分析只讨论接口槽位，不把这个比例当作源码统计。

也要把反面一并摆上：保留功能号、入口地址和部分数据结构，只能说明迁移约束被认真对待，
推不出「所有 CP/M 程序都能无修改运行」，1994 年 Wharton 的反驳正是冲着这一点来的
（文献 [1]）。

## 兼容目标留下的旁证

如果接口兼容只是宣传口号，源码和工具中不一定会留下对应结构。现有材料恰好提供了几类旁证。

0.11 社区逆向重建版的源码里有一个叫 `RDCPM.A86` 的工具。单看文件名只能猜它是「读 CP/M」，
但它的头部把用途写得很清楚（这是重建版源码实证，不是原始 1980 年源码原件）：

```
FCB:	EQU	5CH
FCB2:	EQU	6CH
OPEN:	EQU	15
CLOSE:	EQU	16
MAKE:	EQU	22
DELETE:	EQU	19
SEQWRT:	EQU	21
SETDMA:	EQU	26
GETDRV:	EQU	25
PRINTBUF:EQU	9
SYSTEM:	EQU	5
```

`SYSTEM EQU 5` 是 CP/M 那个著名的 5 号调用入口；`FCB EQU 5CH` 与 `FCB2 EQU 6CH` 是 CP/M
约定的程序段前缀里两个默认文件控制块的位置；而 9（打印字符串）、15（开文件）、16（关文件）、19（删文件）、
21（顺序写）、22（建文件）、25（取当前盘）、26（设 DMA 地址）这一串功能号，与 CP/M BDOS
逐一对应。

这份重建版程序把 CP/M 的调用约定具体写进了工具源码。它不能单独证明原始 1980 年源码的全部细节，但与 Paterson 的 1994 年说明、CP/M 对齐的接口和 `TRANS` 放在一起，可以支持一个较窄的结论：软件和数据迁移是 86-DOS 早期设计的重要目标之一。

> 证据档位说明：0.11 是社区逆向重建版（法律灰色），本册仅把它用作起点旁证，
> 不作行号基准，也不把它当成原始 1980 年源码。上述 EQU 定义及其与 CP/M BDOS 的对应关系，是重建版源码实证；设计动机另由 Paterson 的当事人材料支持。

## 接口保留并不等于完整兼容

CP/M 的分发表里，有些槽位在 86-DOS 的世界里根本没有对应的东西。

CP/M 12 号功能是 `Return Version Number`，返回版本号。CP/M 有版本概念，86-DOS 在 1981 年
4 月定版时也照着放了一个 `VERSION`。

但它什么都不做。

这正是下一章要量化的现象：接口槽位保留下来，并不代表每个槽位都已经实现同样的功能。


## 1　把接口对应关系量化

「86-DOS 兼容 CP/M」仍然过于宽泛。接口兼容至少涉及功能号、参数约定、返回值、副作用、数据结构和错误语义六个层面；仅凭功能号相同，不能推出程序一定能运行。

本册只量化其中一个层面：四份源码的 `DISPATCH` 表里，某个槽位是否与 CP/M 2 的同编号功能保持相同语义。先把口径限定清楚，再使用这组数字。

## 度量对象：CP/M 对齐槽

本册使用一个限定指标：CP/M 对齐槽（CP/M-aligned dispatch slots）。判定条件只有两条：功能号相同，且服务语义相同。这里的 `semantic id` 是本文建立的规范化标签，不是任一源码中的原生字段；名字不同不影响判定，例如 CP/M 的 `Return Current Disk` 与 86-DOS 的 `CURDRV` 可以归为同一语义。

判定口径分五类：

| 类别 | 定义 |
|---|---|
| 对齐 | 语义 id 相同 |
| 征用 | 语义不同，但 CP/M 在该槽有定义 |
| 填空 | CP/M 2 在该槽未定义 |
| 超出 | 86-DOS 有、CP/M 表止于 40，属新增 |
| 同位换对象 | 槽位号不变，但服务对象发生变化 |

例如，CP/M 7 号功能是「读 I/O 字节」，86-DOS 的 7 号槽却是 `RAWINP`，即无回显的控制台输入（0.80 版加入）。虽然编号相同，服务语义不同，所以归入「征用」而不是「对齐」。

> **档位声明**：CP/M 侧的分发表来自 seasip.info 的 BDOS 功能汇总与 classiccmp 邮件列表留存的
> CP/M 2 BDOS 分发表源码片段，属**外部资料档（二手转录）**。86-DOS 侧的分发表是从四个版本的
> 源码里用脚本提取的，属**源码实证**。两侧档位不同，不能混为一谈。

## 四个版本，一条时间序列

可用的内核源码快照有四个（另加一个灰色的 0.11 作起点旁证）：

| 版本 | 文件 | 日期 |
|---|---|---|
| 86-DOS 1.00 | `86-DOS_1.00/86DOS.ASM` | 1981-04-28 |
| 快照 | `PC-DOS_1.00_dev/86DOS_1981-06-16.ASM` | 1981-06-16 |
| 快照 | `PC-DOS_1.00_dev/86DOS_1981-07-07.ASM` | 1981-07-07 |
| MS-DOS 1.25 | `MS-DOS_1.25/MSDOS.ASM` | 1982-04（版本发行月份；不是本表用来定位源码的文件日期） |

从每个文件里提取 `DISPATCH` 表后的 `DW` 标号序列，得到的功能槽数是：

```
1.00  42 槽
6/16  46 槽
7/07  46 槽
1.25  47 槽
```

四个槽数由 `_evidence/vol3/vol3_divergence.py` 从源码提取，脚本会重新输出同样的结果。它们构成了本册的时间序列：先看接口有多大，再看每个位置的语义是否发生变化。

## 边界在哪里动

把四个版本逐槽比对，观察到的变化如下；未列出的槽位在这四份 `DISPATCH` 表中保持同名：

| 区间 | 变化 |
|---|---|
| 1.00 → 6/16 | 槽 12 `VERSION` → `FLUSHKB`；新增槽 42–45 |
| 6/16 → 7/07 | **零改动** |
| 7/07 → 1.25 | 槽 25 `CURDRV`→`GETDRV`；槽 28 `WRTPROT`→`GETFATPTDL`；新增槽 46 `VERIFY` |

三个区间共出现 8 个发生变化或新增的槽位：`12`、`25`、`28`、`42`、`43`、`44`、`45`、`46`。其中 1.00→6/16 有 1 个改写和 4 个新增，7/07→1.25 有 2 个改写和 1 个新增。其余槽位在这四份分发表中保持同名；这只说明分发表指向未变，不代表内核内部其他代码没有变化。

源码还显示，同一张分发表由两条入口共享，但两条入口的上限不同。
`COMMAND:`（中断入口）用 `CMP AH,MAXCOM` 判范围；`ENTRY:`（CALL 5 入口，注释原句是
「This entry point doesn't get as many calls」）用 `CMP CL,MAXCALL` 判范围。两个常量在
1.00 与 6/16 里分别是 `MAXCOM 41 → 45`、`MAXCALL 36 → 36`。

CP/M 兼容入口的上限从 1.00 到 6/16 一步没动，仍是 36；上移的是中断入口。
把链的末端也摆上桌：1.25 里 `MAXCALL` 仍是 36、`MAXCOM` 46（MSDOS.ASM:63–64），
兼容通道的天花板从 1.00 到 1.25 没有动过，46 号 `VERIFY` 一样进不了 CALL 5 这扇老门。
新增的 42–45（日期时间）与 46（VERIFY）全部落在 36 之外，它们从一开始就不在 CP/M 兼容
通道的服务范围里。本文因此称它们为「分发表新增的扩展入口」，而不笼统称为「新增系统
调用」：存在于 DISPATCH 表，与所有入口都能调到，是两回事。
OS/2 Museum 对 CALL 5 分派器的描述与源码吻合：它调整栈帧、把 CL 移到 AH，再进入与中断入口共享的分发路径（文献 [4]）。

从槽位变化看，接口修改主要集中在 1.00 → 6/16 这一段。

这段时间与 IBM PC 适配工作的时间线重合：Paterson 在 1981 年 5 月加入微软；他在 1994 年的信中说，Bob O'Rear 先完成了让 86-DOS 在 IBM 机器上运行的主要工作（文献 [1]）。因此，6/16 和 7/07 快照可以放在这段适配背景下阅读，但槽位变化本身不能证明每一处改动都由 IBM 需求直接触发。

## 本册问题与既有研究的关系

现有公开材料主要讨论三类问题：DOS 是否复制了 CP/M 的代码、86-DOS 与 CP/M 的接口和移植细节，以及 Paterson 清单的发现与恢复。本册采用相同的源码和外部资料，但把问题限定为「四个源码时间点中的分发表语义如何变化」。

这不是对既有研究的替代，而是换一个可复核的切口：把「兼容」拆成槽位、功能号和语义对应关系。四个槽数为 42、46、46、47；其中 6/16 到 7/07 的分发表保持不变。


![图 1](../_assets/figs/fig02-slots.svg)


## 2　槽 12：从 VERSION 到 FLUSHKB

槽 12 是四份源码中最清楚的一处语义替换：1.00 指向 `VERSION`，6/16 改为 `FLUSHKB`。

## 1.00 的实现

在 86-DOS 1.00（1981-04-28）的第 348 行，源码是这样的：

```
VERSION:
GETIO:
SETIO:
WRTPROT:
GETRDONLY:
```

五个标号依次排列，中间没有指令，随后共享 `MOV AL,0` / `RET`。因此，1.00 的 12 号功能虽然沿用了 CP/M 的「返回版本号」编号，实际只返回 0，并没有提供可用的版本号。完整的空壳簇会在第 5 章统计。

## 6/16 快照中的新实现

从差异清单 File 1 的 5 月 5 日基线算起，42 天后的 6 月 16 日快照中，同一个槽位变成了这样。若从 1.00 定版日 4 月 28 日计算，则相差 49 天；本文沿用差异清单的 5/5 基线口径。

```
FLUSHKB:
	XOR	BX,BX
	MOV	ES,BX		;Select segment 0
	SEG	ES
	MOV	B,[41AH],1EH	;Reset KB queue head pointer
	SEG	ES
	MOV	B,[41CH],1EH	;Reset tail pointer
	MOV	AH,AL
```

6/16 文件中不再出现 `VERSION` 标号。1.00 的空壳簇共有 7 个标号，6/16 的槽 12 则指向 `FLUSHKB`；后者把键盘缓冲指针写入 IBM PC BIOS 数据区。

## 那两个地址是什么

`[41AH]` 和 `[41CH]` 写在段 0 里，即绝对地址 `0040:001A` 与 `0040:001C`。

这是 IBM PC BIOS 数据区的固定位置（外部资料 [9][11]，两个来源一致）：

| 地址 | 含义 |
|---|---|
| `0040:001A` | 键盘缓冲区头指针（2 字节） |
| `0040:001C` | 键盘缓冲区尾指针（2 字节） |
| `0040:001E` | 键盘缓冲区本体（32 字节，可存 16 个按键） |

两条指令各写一个字节 `1EH` 到 `0040:001A` 与 `0040:001C`，`B` 即 byte，不是字写入。
按 IBM PC BIOS 对这块缓冲区的约定，头尾指针相等就表示队列为空；若高位字节为 0，
写入低位 1EH 的效果等同于把指针指回缓冲区起点。但源码本身只写了低字节，本文确认
它的设计意图是重置键盘队列，不宣称它做了完整的 16 位指针写入。这是一次标准的「清空键盘缓冲」操作，与 BIOS 数据区的定义
完全吻合。

因此，这段实现直接写入 IBM PC BIOS 数据区中的键盘缓冲指针。它使用的是该机型的固定内存布局，没有经过可移植的设备接口。

按第 1 章的 CP/M 2 对齐口径，四个时间点的对齐槽数为 34 → 33 → 33 → 32，超出 CP/M 2 表的新槽为 1 → 5 → 5 → 6。这个变化是逐槽发生的，没有在某个版本一次性完成。

## 一个槽位的语义变化

把三份代码的对应关系列出来：

```
CP/M 12 号槽   =  Return Version Number（返回版本号）
                        ↓
86-DOS 1.00    =  VERSION，一个返回 0 的空壳
                        ↓
86-DOS 6/16    =  FLUSHKB，直写 IBM PC BIOS 数据区
```

接口位置没有变，变的是它服务的世界。

同一个 12 号槽，从前承担的是兼容 CP/M 所需的版本接口；后来承担的，是 IBM PC 的键盘服务。

6/16 的 `MOV AH,AL` 还表明，`FLUSHKB` 会把调用者传入的输入功能号保留下来，随后继续进入相关输入处理路径；源码列出的相关功能号包括 1、7、8 和 10。

1994 年的 Paterson—Wharton 争论提供了历史旁证。Wharton 引用《Undocumented DOS》指出，DOS 的 0Ch 功能并不返回系统版本，而用于键盘读取（文献 [1]）。四份源码把这一变化定位得更早：6/16 快照已经使用 `FLUSHKB`。至于这一替换会影响多少 CP/M 2.2 程序，Wharton 的估算属于外部论证，本文不把它当作源码实验结果。

## 为什么是 12 号，而不是新开一个槽

一个自然的问题是：既然同期已经在尾部新增 42–45 号槽，为什么不把 `FLUSHKB` 也放在尾部？

可以确认的是，12 号在 1.00 中只返回 0；在四个快照里，真正新增的日期时间功能被放在 42–46 号等尾部槽位，而 12 号和 28 号后来分别被重新赋予新语义。由此可以提出一种与证据相容的解释：已有空壳可以复用，新能力通常另开槽位。

但源码没有注释说明为什么选择 12 号，所以这只是样本归纳，不是作者明确写下的设计规则。

## 这一处变化说明了什么

12 号槽从一个只返回 0 的实现，变成了直接写 IBM PC BIOS 数据区的键盘处理。相邻的 11 号 `CONSTAT` 和 13 号 `DSKRESET` 在四份分发表中仍保持原有标号。

因此，四个快照呈现的不是一次整体切换，而是少数槽位逐步改变语义。


![图 2](../_assets/figs/fig01-slot12.svg)


![照片 C](../_assets/photos/ibm_pc_5150.jpg)


## 3　日期：新增四个系统调用

12 号槽展示了旧位置的语义替换；42–45 号槽展示了新功能如何直接获得新的编号。

## 四个新增槽位

6/16 的分发表尾部多出来四个名字：

```
GETDATE  42
SETDATE  43
GETTIME  44
SETTIME  45
```

在 1.00 里它们一个都不存在。1.00 只有 42 个槽，最后一个是 `MAKEFCB`。

## 1980 年偏置如何返回给程序

看 6/16 第 3373 行，`GETDATE` 的实现：

```
GETDATE: ;Function call 42
	PUSH	CS
	POP	DS
	CALL	READTIME	;Check for rollover to next day
	MOV	AX,[YEAR]
	MOV	BX,[DAY]
	LDS	SI,[SPSAVE]	;Get pointer to user registers
	MOV	[SI+DXSAVE],BX	;DH=month, DL=day
	ADD	AX,1980		;Put bias back
	MOV	[SI+CXSAVE],AX	;CX=year
	XOR	AL,AL
	RET
```

注意第 3381 行那句 `ADD AX,1980 ;Put bias back`。

年份在内部以相对于 1980 的偏移保存，返回给调用者时再把 1980 加回去。这个调用路径说明 6/16 如何把内部年份转换成应用程序看到的完整年份。

`ADD AX,1980` 不能证明 1980 纪元在这一行才诞生，因为 1.00 的目录项注释已经写着 `year-1980`。它能证明的是：已有的日期编码现在被纳入了系统调用的返回值。

## 与 IBM 时钟的关系

7 月 8 日打印的清单页（文件创建于 7 月 7 日）写着，日期/时间功能是「为 IBM CLOCK」加的。源码能直接证明的是：6/16 起，内核提供了读取和设置日期时间的调用路径。

初代 IBM PC 5150 主板没有电池供电的实时时钟，开机时 DOS 时间默认为 01JAN1980，需要人工设定；实时时钟成为 IBM PC 的内置配置要到后来的 PC/AT（文献 [12]）。但不能反过来据此断言 SCP 机器「没有时钟所以不需要日期功能」；源码只显示 1.00 已有人工输入日期和文件日期字段，却没有面向应用程序的日期时间调用。

1.00 已经有日期字段：目录项注释写明 bit 9–15 是 `year-1980`，启动时日期由用户输入，写文件时写入目录项（第 65、1902–1903 行）。它缺少的是面向应用程序的日期时间系统调用，以及自动读取时钟的路径；6/16 新增的正是这条调用路径。

两条批注需要与源码快照分开阅读：

- 6/16 打印的 1.00 基线清单页写着 `PC-DOS 1.0 NO DATE/TIME FUNCTIONS`；
- 7/8 打印、文件创建于 7/7 的快照页写着 `DATE/TIME FUNCTIONS ADDED FOR IBM CLOCK`。

第一条批注属于 1.00 基线清单，第二条批注属于 7/7 快照的打印页；不能把第一条直接当作 6/16 源码的注释。`86DOS.DIF` 和 6/16 源码共同显示，日期时间入口在 5/5 基线之后、6/16 快照中已经出现；7 月批注进一步说明这项工作被当事人联系到 IBM 时钟。批注的具体书写时间和书写者没有更多记录。

## 1.25：通过 BIOS 向量调用

MS-DOS 1.25 的源码把日期、时间和控制台缓冲相关入口定义成运行时填充的向量。相关定义位于 :187–190：

```
BIOSSETDATE     DB      3 DUP (?)       ;Set date
BIOSSETTIME     DB      3 DUP (?)       ;Set time
BIOSGETTIME     DB      3 DUP (?)       ;Get time and date
BIOSFLUSH       DB      3 DUP (?)       ;Clear console input buffer
```

调用点也可以直接核对：`READTIME` 调用 `BIOSGETTIME`（:3435、:3438），`SETDATE` 调用 `BIOSSETDATE`（:3571），`SETTIME` 调用 `BIOSSETTIME`（:3613），`FLUSHKB` 调用 `BIOSFLUSH`（:414）。

与 6/16 直接写固定地址的做法相比，1.25 把硬件相关入口集中到运行时填充的向量中。这个变化支持「硬件依赖被进一步隔离」的判断，但不等于所有移植工作都自动完成。

12 号槽的实现路径因此变成：

```
1.00  VERSION（空壳）
6/16  FLUSHKB（硬编码 IBM PC 地址）
1.25  FLUSHKB → CALL FAR PTR BIOSFLUSH（走 BIOS 向量）
```

从 1.00 到 6/16 再到 1.25，12 号槽经历了空壳、固定 IBM PC 地址和 BIOS 向量三种实现形态。


## 4　6/16 与 7/7：分发表未变

本章比较 6/16 和 7/7 两份内核快照的 `DISPATCH` 表。结果是 46 个槽位逐项一致；这里的「一致」只指分发表指向相同，不代表两个文件的全部源码逐行一致。

这一结果不能推出 7 月没有继续开发。同期 `CHKDSK` 快照从 560 行增至 611 行（6 月 15 日与 7 月 15 日，`splitlines` 口径），说明同一材料集中仍有工具代码在变化。现有材料能证明的是：内核分发表在这两个时间点之间保持不变，而 `CHKDSK` 继续变化；二者的因果关系不能仅由时间线推出。

7 月 8 日打印、文件创建于 7 月 7 日的清单页上，还有一条批注：`DATE/TIME FUNCTIONS ADDED FOR IBM CLOCK`。它支持把日期时间功能与 IBM 时钟联系起来，但不单独确定功能具体在哪一次构建中加入；这一点需要与 1.00 基线和 6/16 源码一起阅读。


## 5　1.00 的空壳从哪里来

1.00 的 12 号槽是一个返回 0 的空壳。它对应 CP/M 的「返回版本号」位置，说明接口编号被保留下来，但实现尚未提供实际版本信息。结合 Paterson 对接口迁移的说明，可以把它理解为兼容层中的占位；源码没有留下更具体的设计说明。

## 其他空壳

脚本扫描 1.00 源码中连续标号共同落入 `MOV AL,0` / `RET` 的位置，共得到 10 个标号。其中 5 个出现在分发表中：

| 标号 | 1.00 槽位 | 语义出处 |
|---|---|---|
| `VERSION` | 12 | 返回版本号 |
| `WRTPROT` | 28 | 写保护磁盘 |
| `GETRDONLY` | 29 | 取只读向量 |
| `SETATTRIB` | 30 | 设文件属性 |
| `USERCODE` | 32 | 用户码 |

另外 5 个标号没有被分发表引用。`GETIO` 和 `SETIO` 的名称来自 CP/M 的 7、8 号 I/O 字节功能，但 1.00 的 7、8 号槽已经指向 `RAWINP` 和 `IN`。因此，CP/M 相关名称仍留在源码中，调用表却不再提供对应入口；这是源码中可以直接看到的一处接口与内部标号不完全同步。

## 这些空壳与迁移工具

0.11 社区逆向重建版中的 `RDCPM.A86` 把 CP/M 调用约定写进了一份工具源码；它只能作为重建版旁证。再把它与 Paterson 对 `TRANS` 的说明放在一起，才能讨论早期的迁移目标。

在这些早期材料里，可以同时看到三样东西：

- 与 CP/M 对应的系统调用（`SYSTEM EQU 5`、功能号 9/15/16/19/21/22/25/26）
- 一个读取 CP/M 磁盘的工具（`RDCPM`，见社区逆向重建版）
- 一个把 Z80 源码翻译成 8086 源码的翻译器（`TRANS`）

再结合 Paterson 1994 年对 TRANS 设计目标的说明，这组材料支持一个谨慎结论：软件与数据的迁移兼容，是 86-DOS 早期设计的重要目标之一，而不是后来偶然得到的副作用。

## 已有实现后来被移除

空壳不一定从一开始就是空的。把 1.00 与 6/16 的两个函数并排看：

| 槽位 | 名字 | 1.00 | 6/16 |
|---|---|---|---|
| 24 | `INUSE` | 有实现（第 2525 行起：`MOV AX,CS`…） | 只剩 `MOV AL,0` |
| 31 | `GETDSKPT` | 有实现（第 2472 行起：取 `[CURDRVPT]`…） | 只剩 `MOV AL,0` |

在 1.00 → 6/16 之间，两个原本有实现的函数变成了空壳（6/16 第 336–339 行，`vol3_lines.py` 可复核）。槽位仍然保留，但处理例程只返回 0；这说明接口位置的生命周期和具体实现可以分开变化。

> 证据提醒：0.11 是社区逆向重建版（法律灰色），此处只作起点旁证，不作行号基准。

现有材料能说明的是：一部分接口位置先被保留，之后有的被填入新实现，有的继续保持空壳；具体动机仍需依靠当事人材料或更多版本。


## 6　槽 28：从 WRTPROT 到 GETFATPTDL

槽 12 的变化并非唯一实例。到 MS-DOS 1.25，28 号槽也从一个空壳变成了文件系统相关功能。

## 28 号槽

1.25 的分发表里，28 号槽指向 `GETFATPTDL`；1.00 和 6/16 的同一位置则是 `WRTPROT`。两份早期源码中，`WRTPROT` 都落在 `MOV AL,0` / `RET` 空壳簇（1.00 第 348–356 行、6/16 第 330–339 行，`vol3_lines.py` 可复核）。

它是一个真函数（1.25 第 2623–2631 行，逐字转录；2632 行起是装填返回值的尾部，从略）：

```
GETFATPTDL:     ;System call 28
        PUSH    CS
        POP     DS
        MOV     AL,DL
        CALL    GETTHISDRV
        JC      NOSUCHDRV
        CALL    FATREAD
        MOV     BX,[BP.FAT]
        MOV     AL,[BP.CLUSMSK]
```

这段代码从 DL 取得驱动器号，定位对应驱动器，读取 FAT，然后返回 FAT 指针和簇掩码。它服务的是 1.25 的文件系统内部结构，与早期 CP/M 语义 `Write Protect Disk` 不同。

## 其他空壳的归宿

| 空壳 | 1.00 槽位 | 1.25 的归宿 |
|---|---|---|
| `VERSION` | 12 | **被取用** → `FLUSHKB` |
| `WRTPROT` | 28 | **被取用** → `GETFATPTDL` |
| `GETRDONLY` | 29 | 存活，名字未变 |
| `SETATTRIB` | 30 | 存活，名字未变 |
| `USERCODE` | 32 | 存活，名字未变 |

5 个分发表空壳中，2 个在 1.25 被换成新语义，3 个仍保留原名。这个结果不支持「所有 CP/M 遗留一次性清除」的说法；更准确的描述是，槽位在不同版本中按需要被保留或重新使用。四份源码没有直接说明每一次复用的动机。

## 新能力使用尾部槽位

四份源码中，新增能力都出现在分发表尾部：

- 6/16 新增 42–45：`GETDATE` / `SETDATE` / `GETTIME` / `SETTIME`
- 1.25 再增 46：`VERIFY`

6/16 新增 42–45 号日期时间功能，1.25 新增 46 号 `VERIFY`；而被复用的 12、28 号槽，分别替换了早期空壳或未实现语义。就这四个时间点而言，可以归纳出一个局部模式：新增能力使用尾部槽位，已有空壳有时被重新利用。这个模式是样本归纳，不是普遍规则。

## 硬件访问的另一处变化

1.25 还有一处不属于槽位、但涉及硬件边界的改动：
`FLUSHKB` 从硬编码 `0040:001A` 改成了 `CALL FAR PTR BIOSFLUSH`。

Paterson 在 1994 年的信中引用 1980 年末的 86-DOS 手册，称设备 I/O 处理程序可以针对不同硬件配置独立调整；他还描述过 SCP 8086 系统与 IBM 8088 系统使用同一 DOS 二进制的情况（文献 [1]）。1.25 中 `BIOSFLUSH` 等向量和调用点，确实显示硬件相关入口被集中起来。至于不同发行版是否在所有情况下使用同一内核二进制，仍应以具体版本的 I/O 层和初始化表为准。


![图 3](../_assets/figs/fig03-stubs.svg)


## 7　结论：兼容边界如何变化

四份源码的分发表显示，86-DOS 并没有在某一天整体停止兼容 CP/M。变化发生在具体槽位上：
12 号从 `VERSION` 变为 `FLUSHKB`，28 号后来从 `WRTPROT` 变为 `GETFATPTDL`；
42–46 号则作为新增功能出现在表尾。6/16 和 7/7 的分发表保持一致，说明接口变化也会进入暂时稳定的阶段。

## 四条可复核的观察

1. 兼容目标与 TRANS 的源码翻译计划有关。Paterson 的当事人材料把功能号、入口地址和部分 FCB 布局列为应用可见元素。
2. 1.00 中有 5 个分发表空壳，6/16 和 1.25 先后复用了其中的 12、28 号槽；另有 3 个空壳保留到 1.25。
3. 真正新增的日期时间和校验功能使用了 42–46 号尾部槽位。这个「新功能开新槽、空壳可复用」只适用于本册的四个样本。
4. 硬件访问从 6/16 的固定 IBM PC 数据区写入，发展到 1.25 的 BIOS 向量调用；这是硬件相关边界的变化，不等于所有移植问题都已解决。

因此，本册的结论不是一个笼统的「兼容率」，而是一条时间序列：大部分旧槽位被保留，少数空壳被重新利用，新能力追加到表尾，硬件接口在后续版本中逐步集中。这个结论只针对四份源码和本册使用的 CP/M 2 槽位基线。

## 复核方法

本册的主要数字可以由以下脚本复核：

- `vol3_divergence.py`：四点分发表逐槽比对（42 → 46 → 46 → 47）
- `vol3_stubs.py`：1.00 空壳簇及其后续归宿
- `vol3_lines.py`：正文引用行号的原文提取与断言

这些脚本读取源码并输出统计，不读取文章中的结论。行号、槽数和空壳归宿都可以回到源文件重新检查。


![图 4](../_assets/figs/fig04-curve.svg)


---

## 术语

- **DISPATCH 表**：内核里的系统调用分发表。86-DOS 用一串 `DW 标号` 把功能号映射到处理入口，本册四个版本的全部数字都从这张表提取。
- **功能槽**：分发表里的一个位置。功能号就是这个位置的下标（0 起）。本册关心的是「某个位置上的语义有没有变」。
- **CP/M 对齐槽**：本册的限定指标：功能号相同且服务语义相同的槽。它只比较 CP/M 2 的分发表对应关系，不是完整的兼容率。
- **空壳 / stub**：占着分发表位置但只返回默认值的函数。1.00 里表现为若干标号连续排列、共同落入 `MOV AL,0` + `RET`；四个快照中有两个槽位后来换入了新语义。
- **CALL 5**：CP/M 的系统调用入口：把功能号放进寄存器后调用地址 5。86-DOS 沿用，后来又提供 INT 21h 作为等价入口。
- **FCB（文件控制块）**：CP/M 与早期 DOS 用来描述打开文件的数据结构。1.00 的 FCB 为主体 32 字节 + NR 1 + RR 3 = 36 字节（第一册口径，勿写成 32 字节）。
- **BIOS 数据区**：IBM PC 上位于 0040:0000 的一段固定用途内存。本册用到其中0040:001A（键盘缓冲头）与 0040:001C（键盘缓冲尾）。
- **BIOS 向量**：MS-DOS 1.25 内核里一组运行时填充的入口（如 `BIOSFLUSH`），使内核不再假定硬件地址。与 6/16 的硬编码写法形成对照。
- **TRANS**：Paterson 写的 Z80→8086 汇编源码翻译器。Paterson 的 1994 年说明把它作为保持 CP/M 应用接口的设计背景。
- **DOS 纪元**：日期在 86-DOS 内部以 1980 为基准偏移存储。1.00 的目录项注释已经使用 year-1980；6/16 的 `ADD AX,1980` 把同一偏置转换成系统调用返回的完整年份。
- **0.11 重建版**：社区从 SCP 出厂二进制逆向重建的 86-DOS 0.11 源码。法律灰色，本册仅作起点旁证，不作行号基准。
- **MAXCALL / MAXCOM**：两个入口各自的功能号上限：CALL 5（CP/M 兼容入口）用 MAXCALL，中断入口用 MAXCOM。1.00 与 6/16 中 MAXCALL 恒为 36；MAXCOM 由 41 升到 45。

---

## 文献

1. **Tim Paterson 致《Microprocessor Report》的信（1994-10-03）**  
   当事人材料，含 Paterson 信与 John Wharton 回函（同页刊出）。Paterson 说明兼容动机是 TRANS 翻译器与软件迁移，依据为 1976 年《CP/M Interface Guide》；Wharton 回函引 《Undocumented DOS》证实功能 0Ch 用于读键盘。原文 PDF 已存档：_evidence/vol3/paterson1994.pdf。  
   <https://www.ardent-tool.com/CPU/docs/MPR/19941003/0813vp.pdf>
2. **Bob Zeidman, Was MS-DOS code copied from CP/M?（2016）**  
   外部资料。源码级与二进制级取证：DOS 未复制 CP/M 代码，但大量系统调用号一致。  
   <https://www.zeidmanconsulting.com/DOS_comparisons/Was%20MS-DOS%20code%20copied%20from%20CPM.pdf>
3. **OS/2 Museum, 86-DOS Was an Original**  
   外部资料。从分段架构、磁盘管理、工具命名论证 86-DOS 内部与 CP/M 完全不同。  
   <https://www.os2museum.com/wp?p=1380>
4. **OS/2 Museum, Who needs the address wraparound, anyway?**  
   外部资料。深入 CALL 5、FCB、地址回绕、BIOS 假设等移植细节。  
   <https://www.os2museum.com/wp/who-needs-the-address-wraparound-anyway/>
5. **Microsoft Open Source Blog, Continuing the story of early DOS development（2026-04-28）**  
   外部资料。微软官方把这些打印清单描述为「printed commit history」。  
   <https://opensource.microsoft.com/blog/2026/04/28/continuing-the-story-of-early-dos-development/>
6. **DOS-History / Paterson-Listings**  
   源码实证（主基准）。含 86-DOS 1.00、PC-DOS 1.00 开发快照、打印件转录与可编译源码。  
   <https://github.com/DOS-History/Paterson-Listings>
7. **Microsoft / MS-DOS v1.25 源码**  
   源码实证。本册第四点时间序列的终点，MIT 许可。复现脚本实际读取 Paterson-Listings 转录件（3_source_code/MS-DOS_1.25/MSDOS.ASM），已验证与官方仓内容一致、行号相同。  
   <https://github.com/microsoft/MS-DOS/tree/main/v1.25>
8. **DOS-History / 86-DOS 0.11 重建版**  
   起点旁证。法律灰色，仅用于 `RDCPM.A86` 的调用约定取证。  
   <https://github.com/DOS-History/86-DOS-0.11>
9. **BIOS Data Area 布局（耶鲁 cs.yale.edu）**  
   外部资料。确认 0040:001A 为键盘缓冲头、0040:001C 为键盘缓冲尾。  
   <https://www.cs.yale.edu/flint/feng/cos/resources/BIOS/Resources/biosdata.htm>
10. **Seattle Computer Products 公司史（s100computers 镜像）**  
   外部资料。Paterson 1981 年 5 月加入微软、1982 年 4 月因 IBM 需求压力返回 SCP 的时间线。  
   <http://cpmarchives.classiccmp.org/cpm/mirrors/www.s100computers.com/Hardware%20Folder/Seattle%20Computer%20Products/History/History.htm>
11. **ROM-BIOS Variables（Tech Help!）**  
   外部资料。40:001A 为键盘缓冲头、40:001C 为键盘缓冲尾的第二来源。  
   <http://www.techhelpmanual.com/93-rom_bios_variables.html>
12. **IBM 5150/5160 的实时时钟问题（minuszerodegrees.net）**  
   外部资料。初代 IBM PC 主板不含电池供电 RTC；每次开机 DOS 时钟默认为 01JAN1980 00:00:00，需人工设定；RTC 成为内置配置要到 PC/AT。  
   <https://www.minuszerodegrees.net/5150_5160/rtc_type_examples/5150_5160_rtc_type_examples.htm>
13. **OS/2 Museum, PC-86-DOS**  
   外部资料。说明 SCP 86-DOS 的启动盘布局、BIOS 层与 DOS 核心的组合方式，以及不同版本 I/O/初始化表不兼容会导致内核挂起的边界。  
   <https://www.os2museum.com/wp/pc-86-dos/>

---

## 复现包（全部数字可复跑）

- `_evidence/vol3/vol3_divergence.py`  
  四点 DISPATCH 表逐槽比对（42→46→46→47）  
  `python _evidence/vol3/vol3_divergence.py`
- `_evidence/vol3/vol3_stubs.py`  
  1.00 空壳簇识别与归宿（5 分发空壳，2 取用 3 存活）  
  `python _evidence/vol3/vol3_stubs.py`
- `_evidence/vol3/vol3_lines.py`  
  正文所引行号的原文提取与断言（7 处行号证据 + VERSION 清零）  
  `python _evidence/vol3/vol3_lines.py`
- `_evidence/vol1/vol1_evolve.py`  
  分发表提取器（被 vol3 两个脚本复用）  
  `python _evidence/vol1/vol1_evolve.py`
- `_evidence/vol1/vol1_cpm.py`  
  CP/M 2 分发表基线（外部资料档，非源码实证）  
  `python _evidence/vol1/vol1_cpm.py`

---
