z386:基于原始微码构建的开源80386
摘要
本文详述了z386,一款基于原始Intel微码构建的开源FPGA 80386 CPU。它能引导DOS 6/7、运行保护模式程序,并玩经典游戏如Doom,既是一种教育性重构,也是一个可用的FPGA CPU。
暂无内容
查看缓存全文
缓存时间: 2026/05/23 15:32
# z386: 基于原始微码的开源80386实现
来源: https://nand2mario.github.io/posts/2026/z386/
这是[80386系列](https://nand2mario.github.io/tags/386/)的第五篇文章。FPGA CPU现在已发展到足以运行真实软件,本文将介绍其工作原理。[z386](https://github.com/nand2mario/z386)是一个基于Intel原始微码的386级CPU,与[z8086](https://nand2mario.github.io/posts/2025/z8086/)思路相同。
其核心并非RTL中逐条指令的仿真器,而是尽可能还原原始机器的硬件结构,使恢复的386控制ROM能够驱动它。如今的z386可以启动DOS 6和DOS 7,运行DOS/4GW和DOS/32A等保护模式程序,还能玩Doom和Cannon Fodder等游戏。以下是与ao486的粗略对比:
| 指标 | z386 | ao486 |
|------|------|-------|
| 代码行数(cloc) | 8K | 17.6K |
| ALUT | 18K | 21K |
| 寄存器 | 5K | 6.5K |
| BRAM | 116K | 131K |
| FPGA时钟 | 85MHz | 90MHz |
| 3DBench FPS | 34 | 43 |
| Doom(原始)FPS,最高画质 | 16.5 | 21.0 |
在当前版本中,z386的性能相当于快速(约70MHz)带缓存的386级机器,或低端486。其运行时钟远高于历史386 CPU,但CPI(每指令周期数)稍差。当前缓存为16 KB、4路组相连的统一L1,部分原因是为了保持高时钟频率。实际的高端386系统通常使用更大的外部缓存,典型大小为32 KB到128 KB。
Doom II 在 z386 上运行
之前的四篇文章已经涵盖了大部分386微架构考古工作:[乘除法数据通路](https://nand2mario.github.io/posts/2026/80386_multiplication_and_division/)、[桶形移位器](https://nand2mario.github.io/posts/2026/80386_barrel_shifter/)、[保护与分页](https://nand2mario.github.io/posts/2026/80386_protection/)以及[内存流水线](https://nand2mario.github.io/posts/2026/80386_memory_pipeline/)。z386力求同时成为教育重建和实用的FPGA CPU。它保留了众多386特色结构:32项分页TLB、类似原设计的桶形移位器、ROM/PLA译码方式、保护PLA模型,以及最重要的37位宽、2560项的微码ROM。同时,在合理之处采用FPGA友好的简化设计,例如使用DSP块实现乘法以及小型快速L1缓存。
在本文中,我将填补设计的其余部分:指令预取、译码、微码序列器、缓存设计、测试、z386与ao486的差异,以及调试过程中的一些经验教训。
## 从 z8086 到 z386
首先简单回顾一下背景。去年我编写了[z8086](https://github.com/nand2mario/z8086),一个基于原始微码的8086,其基础是[reenigne的反汇编工作](https://www.reenigne.org/blog/8086-microcode-disassembled/)。该项目表明,利用恢复的微码构建可工作的CPU是可能的。接近年底时,我得知80386微码已被提取,并且reenigne和其他几位贡献者(文末致谢)正在进行反汇编。他们慷慨地与我分享了工作成果,z386由此起步。
386与8086面临的问题截然不同。指令集更大,内部状态丰富得多,且机器必须执行保护、分页、特权检查和精确异常处理。更重要的是,80386的微操作更为密集且依赖上下文。如果说8086微码读起来像简单的C程序,那么386微码更像手工优化的汇编:短小、微妙,且充满了对隐藏硬件的假设。
这个谜题花费了大约四个月的晚上和周末。结果虽然还不是完美的386,但已经足以运行真实的保护模式DOS软件。
## z386 高层面视图
从高层次看,386围绕八个主要单元组织。z386紧密遵循这一划分,因此原始的Intel框图仍然是一张有用的地图。
80386框图显示了总线接口、预取、指令译码、控制、数据、保护测试、分段和分页单元
该图实际上与真实的386芯片显微图吻合得很好,尽管各单元的相对位置有所不同。
标注了主要功能单元的Intel 80386芯片显微图
以下是这些单元在z386中的作用:
1. **预取单元**:从内存填充一个16字节的代码队列。分支、异常、中断和段切换可以刷新并重启该队列。
2. **译码器**:消费指令字节,跟踪前缀,识别ModR/M和SIB形式,收集立即数和位移量,并将指令映射到微码入口点。
3. **微码序列器**:获取扩展的微码字,处理跳转、延迟槽、异常和运行下条指令的行为。
4. **ALU与移位器**:实现算术、逻辑、标志、位操作、移位、旋转、乘法和除法的支持。
5. **分段单元**:计算逻辑地址到线性地址的转换,应用段基址和段限长,并存储隐藏的描述符缓存状态。
6. **保护单元**:重建386保护PLA的行为,用于选择子和描述符验证。
7. **分页单元**:处理TLB查找、页遍历、Accessed/Dirty更新、页错误以及从线性地址到物理地址的转换。
8. **总线接口单元/缓存/内存通路**:将CPU内存操作连接到分页、缓存、SDRAM、ROM、I/O以及周围的PC系统。
这种组织方式与现代RISC风格CPU通常显示的整洁流水线截然不同。386更适合看作几个大型、部分独立的状态机,它们重叠运行。当执行单元忙碌时,预取可以继续运行;译码可以准备后续指令;地址转换可以在总线需要之前启动;保护测试可以在几个周期后重定向序列器。Intel的论文描述多达六条指令同时处于不同的处理阶段,但执行单元仍然每个周期处理一条微指令。与486及后续处理器不同——它们将设计重构为更细粒度的流水线,目标是每时钟一条指令——386即使对于简单的寄存器到寄存器指令,也至少需要两个微码周期。
之前的文章已经深入介绍了第4至第8单元。这里我们从前端开始:预取、译码和微码序列器。
## 指令预取
原始的8086每次可以从指令队列移动一个字节到执行端。对于386,带宽计算发生了变化。Jim Slager在ICCD 1986上的论文《Performance Optimizations of the 80386》给出了一个有用的粗略估算:平均80386指令约四个字节长,加权平均指令约需四个时钟周期,因此稳态执行需要大约每时钟一个字节的代码。
实际上,预取器需要高于平均值的突发带宽。它必须平滑变长指令、已执行的分支以及从预取窃取总线时隙的数据周期。外部总线可以支持这一点:它每两个时钟可读取四个字节,即每时钟两个字节。因此386预取单元利用完整的非复用32位总线,通过32位读取填充一个16字节的代码队列。
z386前端流水线从缓存和内存到预取、译码、译码队列和微码序列
接下来的问题是预取器与指令译码器之间的接口。8086端仍然是逐字节的。在80386上,接口更为微妙:决定结构的译码部分仍然逐字节进行,而直接量字段如位移量和立即数可以以1、2或4字节块的形式消耗。
这与8086模型相比是一个虽小但重要的区别。一条386指令可能包含前缀、操作码、ModR/M字节、SIB字节、位移量和立即数。前缀/操作码/ModR/M部分控制指令的含义,因此逐字节读取可保持逻辑紧凑。但一旦译码器知道接下来的四个字节只是位移量,就没有架构理由花费四个独立的周期来收集它们。为了实现这一点,z386提供了代码队列的两种视图:下一个字节,以及从当前字节偏移开始的32位窗口。
## 译码
x86指令译码很困难,因为指令边界并不明显。可能有多个前缀,然后是一个操作码,可能有一个`0F`转义操作码,可能有一个ModR/M字节,可能有一个SIB字节,然后是位移量和立即数字段,其大小取决于模式位和前面的字节。译码器必须在读取指令的同时发现其结构。
译码器的输入是来自预取队列的字节流,加上当前模式状态:操作数大小默认值、地址大小默认值、保护模式状态、累积的前缀,以及指令是否在`0F`扩展操作码空间中。输出不仅仅是一个操作码。Slager的论文描述386指令单元产生一个111位的译码指令字,并将其插入一个三项的指令队列。这个译码字是前端与执行端之间的约定。
从概念上讲,译码字包含执行入口点以及微码不应从原始字节重新发现的所有内容:操作码、前缀状态、操作数/地址大小、ModR/M和SIB字节、立即数和位移量值、指令长度、选定的源和目标寄存器字段、段覆盖、内存形式位,以及特殊控制标志如栈操作或标志更新行为。z386将其表示为一个译码指令记录,并推入一个小的FIFO供微码序列器使用。
为了构建该字,译码器是一个状态机,由两个PLA风格的表支持。**控制PLA**回答结构问题:接下来是什么?它将当前字节分类为前缀、完整操作码、需要ModR/M的操作码,或具有立即数大小类的操作码。在ModR/M状态下,同一个PLA帮助决定SIB和位移数字节是否跟随。
**入口PLA**回答执行问题:微码从哪里开始?第一遍使用操作数大小、操作码、REP状态、保护模式状态和`0F`转义标志。某些操作码需要在ModR/M已知后进行第二遍,因为ModR/M的`reg`字段或内存/寄存器形式决定了最终的例程。
例如,在32位模式下译码`8B 44 24 08`不是一次查找。译码器随着处理逐步了解指令的含义:
| 字节 | 含义 | 译码器动作 |
|------|------|------------|
| `8B` | `MOV r32, r/m32` | 控制PLA表示ModR/M跟随。入口PLA第一遍表示属于`MOV r,rm`类别。 |
| `44` | ModR/M: `mod=01`, `reg=000`, `r/m=100` | 选择目标寄存器`EAX`,标记内存形式,并运行入口PLA第二遍。由于这是内存型`MOV r,rm`,最终微码入口为`0x019`。 |
| `24` | SIB: 比例1, 无索引, 基址`ESP` | 捕获SIB并选择有效地址基址形式。 |
| `08` | disp8 | 捕获位移量`+8`,计算指令长度4,并推送译码记录。 |
得到的译码条目本质上表示:操作码`8B`,ModR/M`44`,SIB`24`,位移量`8`,操作数/地址大小为32位,目标寄存器`EAX`,基于`ESP+8`的内存操作数,指令长度4,微码入口`0x019`。微码引擎随后可以从`0x019`开始,无需重新读取原始字节流。
PLA结构使译码保持紧凑:几个密集的ROM/PLA表远小于大量独立门电路。这里仍有一些未解之谜。z386使用了当前译码器所需的控制PLA线路,但许多恢复的线路仍然未使用或仅部分理解。更深入地理解PLA或许能使译码器更小更快。
## 微码序列器——控制程序
z386使用原始的Intel 386微码作为其主要控制程序。ROM决定哪些内部值移动,何时ALU运行,何时内存周期开始,何时序列器分支,以及何时可以开始下一条x86指令。RTL并未将`ADD`、`IRET`或`SGDT`指令实现为大型行为块,而是实现了微码期望控制的那部分硬件。
每条微指令宽37位。Reenigne将每个字划分为以下字段:
37位80386微码字分为源、目标、ALU源、ALU/跳转、操作、子操作和总线字段
源和目标字段选择内部寄存器和数据通路端点。ALU源和ALU/跳转字段选择ALU操作数、ALU操作或分支目标。操作/子操作字段编码特殊的序列器和大小行为。总线字段启动读、写、预取刷新、描述符缓存操作和地址流水线操作。
一个简单的寄存器到寄存器移动展示了为什么386有最小两个周期:
```
003 SRCREG PASS RNI 0
004 SIGMA -> DSTREG
```
第一条微指令选择源寄存器并通过ALU传递,同时将指令标记为`RNI`(运行下条指令)。第二条微指令是延迟槽,将`SIGMA`写入目标寄存器。在486风格的流水线上,第二个动作看起来更像是写回阶段与后续指令工作重叠。在386上,它仍然是一个串行的微码周期。因此寄存器到寄存器移动需要两个周期。
同样的单周期延迟也适用于一般的微码控制流。指令连续运行:当序列器看到`RNI`时,它开始准备下一条译码的指令,但紧随其后的微指令仍然作为当前指令的最后一个周期先执行。微码通常在那里放置有用的工作,例如寄存器写回、`DLY`等待或最终的总线操作。
微码分支也有一个延迟槽。跳转、调用或返回会改变未来的微地址,但分支之后的微指令仍然会执行。这就是为什么将386微码当作直线汇编阅读可能会产生误导:分支后面的行总是会执行。
保护PLA可以重定向序列器,但其结果在三个周期后到达,因此在重定向生效之前,可以执行三条微指令。之前的[保护文章](https://nand2mario.github.io/posts/2026/80386_protection/)涵盖了`LD_DESCRIPTOR`示例,其中选择子测试重定向远返回,而延迟槽已经在共享的描述符加载子例程中执行。这很强大,因为在PLA做出决定时,有用的设置工作正在进行,但也产生了控制流,其中子例程中的几条指令可能在重定向到别处之前执行。
`RNI`本身有三种变体,都与指令终止有关。`RNI`是正常形式:结束当前微程序,并在延迟槽之后开始下一条译码的指令。`RNi`是有条件的:仅当在延迟槽中执行时才终止。字符串和循环微码使用此形式,以便同一条微指令既可以继续循环体,也可以在退出路径上干净地终止。`RnI`终止指令,并禁止可屏蔽中断直到后续
相似文章
80386 微码反汇编
一篇博客文章,详细介绍了成功反汇编和分析 Intel 80386 微码的过程,揭示了215条指令入口点以及其复杂的内部架构。
Intel 8087浮点芯片内部的微码:寄存器交换
对Intel 8087浮点协处理器内部微码的详细逆向工程分析,聚焦于FXCH寄存器交换指令及芯片内部架构。
386处理器寄存器的异常复杂电路
对英特尔386处理器寄存器电路的详细逆向工程分析,揭示了六种不同的定制电路和交织位存储。
微软开源“迄今为止发现的最早的DOS源代码”
微软发布了已知最早的DOS源代码,包括86-DOS 1.00内核和实用程序,以及开发者文档。
逆向工程386处理器的预取队列电路
详细介绍386处理器预取队列电路的逆向工程,解释所用的增量器、对齐网络和动态逻辑。