在 8086/8088 系统上软件模拟 Intel 8087 浮点协处理器(FPU),远非简单地将浮点指令替换为宿主机的浮点运算那样直接。这项工作的核心难度在于:8087 并非一个独立运行的处理器,而是通过复杂的硬件接口与 CPU 紧密耦合。要在纯软件环境中重现这一行为,开发者必须精确理解和重建这套通信协议,否则模拟器将面临死锁、状态不一致或程序崩溃等问题。
硬件接口的本质:ESC 指令与总线监听
8087 协处理器采用一种独特的 “总线监听” 模式与主处理器交互。当 CPU 执行指令时,8087 通过监听地址总线来追踪 CPU 的预取队列(Prefetch Queue),利用 QS0 和 QS1 引脚获知 CPU 正在执行的指令流。当检测到 ESC 前缀码(8087 指令以 ESC 操作码开头,通常为 0xD8 至 0xDF)时,8087 立即介入执行对应的浮点微操作,而 CPU 则在大多数情况下先执行一条 WAIT(即 FWAIT)指令来等待协处理器完成。
这种设计的深层逻辑在于:8087 并非简单地从内存读取指令,而是实时观察 CPU 的执行状态。问题恰恰出在这里 —— 在没有物理 8087 芯片的模拟环境中,CPU 的 TEST 引脚永远无法收到 BUSY 信号的下降沿,导致 WAIT 指令陷入无限等待,系统彻底死锁。这正是早期 DOS 程序在未安装协处理器的机器上运行时常会冻结的根本原因。
双重同步机制:BUSY 信号与 RQ/GT 引脚
理解 8087 的同步机制是软件模拟成败的关键。该协处理器实际上维护着两套相互独立的同步通路:
第一套是程序员可见的 BUSY 信号机制,用于 “长时间” 同步。当 8087 执行耗时数百个周期的浮点运算时,CPU 可以并行处理其他任务。WAIT 指令正是通过轮询 TEST 引脚(连接至 8087 的 BUSY 输出)来实现等待。这种同步方式需要程序员显式编码 —— 例如在 FPU 指令存储结果后、CPU 读取该结果前插入 WAIT 指令,以确保数据一致性。
第二套是透明的 RQ/GT(Request/Grant)请求 - 授权机制,它对程序员不可见,却承担着关键的短期同步职能。当 8087 需要执行诸如 FNINIT 或 FNSTCW 这类无需等待的指令时,它通过 RQ/GT 引脚在总线级别 “锁定” CPU,防止 CPU 抢先执行后续指令从而产生竞争条件。具体而言,当 FNSTCW 向内存写入控制字时,8087 几乎立即获取总线控制权,阻止 CPU 执行 POP 指令(该指令需要从内存读取数据),从而确保写入操作在 CPU 读取之前完成。
FPU 检测的工程难题
软件模拟面临的另一个经典挑战是 FPU 检测协议。以 FNINIT 指令为例:若在无 8087 的系统上执行 WAIT 形式的 FINIT 指令,CPU 将因等待不存在的 BUSY 信号而永久挂起。因此,检测代码必须使用非等待形式的 FNINIT 和 FNSTCW 指令。这两种形式的区别至关重要:FNINIT 不会触发 WAIT 等待,可安全地在无协处理器环境中执行;而 FINIT 则会在执行前自动插入 WAIT,导致死锁。
有趣的是,Intel 官方文档对这套检测机制讳莫如深。在 8086/8088 的官方手册中,几乎找不到任何关于如何检测协处理器存在的说明 —— 这套协议似乎是在产品上市后,由社区和开发者逐步摸索出来的。80287 手册中才出现了明确的示例代码,明确警告 “切勿在无 8087 的 8086/8088 上执行 WAIT 指令”。
软件模拟的核心实现策略
基于上述硬件特性,软件模拟器在实现时必须考虑以下工程要点:
在指令拦截层面,模拟器需要在 CPU 模拟器中识别 ESC 指令序列,提取完整的操作码(包括可能存在的 MODR/M 字节),然后将控制权转交给模拟的 FPU 单元。这要求精确还原 8087 对指令流的解析逻辑。
在状态同步层面,当模拟器执行 FPU 指令时,必须模拟 RQ/GT 总线仲裁行为 —— 在特定指令完成后才能允许 CPU 继续执行后续指令,而非简单地将控制权立即交回。对于涉及内存读写的指令,这一点尤为关键。
在异常处理层面,模拟器需要完整实现 8087 的异常模型,包括无效操作、溢出、精度丢失等六种异常标志,并通过修改控制字(Control Word)来模拟异常屏蔽行为。
最后,在检测协议层面,模拟器应提供一种 “伪硬件” 模式,使得针对真实硬件编写的检测代码能够正确运行 —— 特别是让 FNSTCW 这类非等待指令能够通过总线仲裁机制安全地将结果写入指定内存位置。
资料来源:OS/2 Museum 关于 8087 硬件接口的深度分析文章。