NEC V20 微码解码
摘要
本文描述了利用高分辨率芯片摄影解码NEC V20 CPU微码的过程,旨在改进MartyPC中的周期精确模拟,基于先前的8088微码工作。
暂无内容
查看缓存全文
缓存时间: 2026/09/07 18:32
# 解码NEC V20微码
来源:https://martypc.blogspot.com/2026/09/decoding-nec-v20-microcode.html
reenigne在2020年对8088微码的解码(https://www.reenigne.org/blog/8086-microcode-disassembled/)开启了对8088 CPU进行极其精确仿真的大门。
尽管我之前在MartyPC中增加了对NEC V20的支持,但V20核心在时序方面并非周期精确。它本质上是一个披着V20外衣的8088——只是在我的8088核心基础上粘贴了V20指令。
这并非理想情况,但在没有微码的情况下让V20核心实现周期精确,看起来可能是一项令人气馁的试错苦差。
那么,为什么不直接获取微码呢?
我最近委托InfoSecDJ(https://siliconpr0n.org/archive/doku.php?id=infosecdj:start#commissioning_work)对一颗NEC V20 CPU进行芯片摄影(实际上是由Sharp代工的第二来源V20,但依然是V20)。他做得非常出色。
[芯片图片]
*NEC V20 CPU芯片 - InfoSecDJ*
这张照片马赛克分辨率极高——准确说是5.6吉像素,达到了惊人的70478x80672分辨率——大到甚至无法用JPEG格式存储!
您可以在[此处](https://siliconprawn.org/archive/doku.php?id=infosecdj:sharp:lh70108)以完整分辨率查看全部内容。
芯片中心下方偏下的矩形区域就是主微码ROM。
[ROM区域图片]
V20微码ROM模块
ROM阵列大小为258x116,包含29,928比特。它能被29整除,而我们知道这正是V20的微码字长,这是个好迹象。但这意味着它包含1032个微码字,而我们原本只预期1024个。这有点奇怪,对吧?我们稍后会弄清原因。
这是ROM阵列的特写:
[微码比特放大图]
微码比特,放大视图
水平方向明亮的走线是芯片金属层的一部分。黄色的点是连接金属层与下方多晶硅层的互连点。注意金属层后面的垂直条——并注意每个互连点两侧的多晶硅上偶尔存在间隙。
这些间隙构成了晶体管——晶体管的存在表示1比特。我将突出显示1比特以便于观察。
[1比特可视化图]
看到这个让我非常兴奋——如果我们能通过视觉识别ROM中的比特,就能提取ROM内容。但有一个小问题——阵列中有29,928个比特。手动提取会有点繁琐。
幸运的是,有工具可以完成这项任务。我使用了Travis Goodspeed的MaskRomTool(https://github.com/travisgoodspeed/maskromtool)加载ROM图像。
[MaskRomTool定义比特位置图]
使用MaskRomTool定义比特位置
MaskRomTool允许我绘制定义比特位置的行和列。但不幸的是,我发现它的比特检测方法基于阈值处理,而比特与非比特之间的对比度差异不足,无法使该机制有效工作。注意比特直方图非常集中在远端轴。这样行不通。也许没有明亮的金属层时,阈值技术会更有效,但我不想请InfoSecDJ尝试移除它。需要另一种方法。
幸运的是,我们可以使用MaskRomTool将定义的比特位置导出为JSON格式。我用这个导出的JSON文件编写了一个Python脚本,提取每个比特位置为中心的方形位图,并以比特的逻辑列号和行号命名保存。
这给了我一个105MB的ZIP文件,里面是许多42x42像素的PNG文件,每个文件包含一个比特或一个非比特。这样做的想法是训练一个卷积神经网络(https://en.wikipedia.org/wiki/Convolutional_neural_network)来识别比特是0还是1。
如果这听起来超出了你的能力范围,其实在这个项目之前我没有任何训练CNN的经验,而且我在一个下午就完成了。像PyTorch(https://pytorch.org/)这样的现代Python框架让这变得如此简单。
网上有一些很好的CNN教程(https://www.datacamp.com/tutorial/pytorch-cnn-tutorial),许多都专注于手写识别这类经典问题,而我们的任务更简单。我们只需将图像分为两类:0或1。这个问题实际上就是“热狗还是非热狗(https://www.youtube.com/watch?v=ACmydtFDTGs)”。
在训练CNN之前,我们需要训练数据集。所以我编写了一个快速简单的Python/tkinter脚本,以便我可以通过目视快速将比特分类到0或1的文件夹中。
[分类工具图]
快速简单的分类工具
这里显示的是一个“1”比特。你能通过金属层后面的晶体管发现它吗?那些按钮最终是多余的——你只需要按键盘上的1或0来分类比特。理论上,你可以这样做29,928次,几个小时就能完成工作。我原本打算将其作为CNN训练失败时的备用方法——有几个朋友愿意志愿帮忙,而“比特投票器”产生的JSON日志可以合并以支持分布式协作并达成共识。幸运的是,不需要这样做。
最终,我手动分类了超过1,000个比特。一旦它们被分到两个目录中,我们就可以尝试使用分类后的图像作为输入来训练CNN模型。
我不会详细讲解如何编写CNN——如果你感兴趣,源代码会在我的GitHub上。我使用了出色的PyTorch(https://pytorch.org/)库,它使整个过程比我想象的简单得多。
这是一次训练运行的样子:
```
[Epoch 01] train: loss=0.6945 acc=0.7273 f1=0.0164 | val: loss=0.6924 acc=0.7876 f1=0.0000
val precision=0.0000 recall=0.0000 cm=[[178, 0], [48, 0]]
[Epoch 02] train: loss=0.6860 acc=0.7151 f1=0.3826 | val: loss=0.6257 acc=0.7965 f1=0.0729
val precision=0.5000 recall=0.0394 cm=[[178, 0], [46, 2]]
[Epoch 03] train: loss=0.3751 acc=0.8914 f1=0.7213 | val: loss=0.3900 acc=0.7655 f1=0.6327
val precision=0.4661 recall=1.0000 cm=[[125, 53], [0, 48]]
[Epoch 04] train: loss=0.1159 acc=0.9523 f1=0.9139 | val: loss=0.0495 acc=0.9912 f1=0.9773
val precision=0.9773 recall=0.9773 cm=[[177, 1], [1, 47]]
[Epoch 05] train: loss=0.0251 acc=0.9945 f1=0.9888 | val: loss=0.0460 acc=0.9867 f1=0.9744
val precision=0.9514 recall=1.0000 cm=[[175, 3], [0, 48]]
[Epoch 06] train: loss=0.0319 acc=0.9933 f1=0.9802 | val: loss=0.0438 acc=0.9823 f1=0.9659
val precision=0.9350 recall=1.0000 cm=[[174, 4], [0, 48]]
[Epoch 07] train: loss=0.0185 acc=0.9945 f1=0.9212 | val: loss=0.0274 acc=0.9956 f1=0.9891
val precision=0.9792 recall=1.0000 cm=[[177, 1], [0, 48]]
[Epoch 08] train: loss=0.0141 acc=0.9956 f1=0.9913 | val: loss=0.0271 acc=0.9956 f1=0.9891
val precision=0.9792 recall=1.0000 cm=[[177, 1], [0, 48]]
[Epoch 09] train: loss=0.0101 acc=0.9978 f1=0.9940 | val: loss=0.0447 acc=0.9867 f1=0.9735
val precision=0.9488 recall=1.0000 cm=[[175, 3], [0, 48]]
[Epoch 10] train: loss=0.0110 acc=0.9967 f1=0.9907 | val: loss=0.0437 acc=0.9912 f1=0.9773
val precision=0.9773 recall=0.9773 cm=[[177, 1], [1, 47]]
Early stopping: no val F1 improvement >= 0.0 for 3 epoch(s).
Best val F1: 0.9891
```
如果你有支持CUDA的GPU,训练会相当快——这只花了几分钟。
我们的想法是最大化准确率——但达到1.0可能不可行,甚至可能不可取(存在过拟合的问题)。有时训练更久反而会使情况变糟,所以如果我们看不到持续改善,就会停止训练。
训练的输出是一个神经网络模型——然后我们可以使用这个模型对整个输入数据集进行[推理](https://en.wikipedia.org/wiki/Inference_engine)。推理只是应用我们训练好的模型去执行它被训练任务的流行说法——预测给定图像是包含0比特还是1比特。
在我们继续之前——简要说明一下以避免潜在的争议。从计算机科学角度看,CNN大致属于AI的更广泛范畴,但我们并未使用现代通常指大型语言模型(LLM)的那种有争议的“AI”含义。
当我们运行推理时,每个像素都会得到一个置信度分数。我们可以使用这个置信度分数标记模型不太确定的比特,在某个特定阈值下(我这里使用<99%)。以下是第一次运行的结果,模糊的比特标记为红色:
[首次推理结果图]
比特分类CNN的第一次输出
我将所有模糊比特手动分类回训练文件夹,然后重新运行训练,重复此过程直到获得以下结果:
[最终推理结果图]
最终的“足够好”的推理运行
这效果相当好——只有4个比特仍然模糊,直接手动验证它们比再训练一个模型更快。
太好了,我们得到了29k个微码比特,并节省了数小时的繁琐手动劳动(换取了数小时编写Python训练脚本的时间,但至少这是可复用的!)
我们仍需将这个矩形的比特块转换为29位微码字的列表。换句话说,我们需要重组位图,使其变为29x1032,而不是258x116。具体如何操作并不明显,但我们可以暂时搁置,直到解码出[匹配解码器PLA](https://en.wikipedia.org/wiki/Programmable_logic_array)。
解码或“激活”PLA位于主微码ROM块的上方,中间夹着一些中间电路。
[解码器标注图]
解码PLA
这个PLA的任务是接收来自左侧的13个逻辑输入(每个输入都有一个反相信号线,总共26条输入线),如果输入信号匹配PLA的某一列,则激活其下方微码阵列的对应列。
“匹配”是什么意思?
特写一下可能更有信息量:
[PLA放大图]
放大查看的解码PLA
我们看到类似的垂直金属走线排列,被互连点隔开,基底上的间隙形成晶体管。在这种情况下,这些晶体管构成逻辑与门。与微码ROM不同,这里只有一个晶体管连接到一个互连点,面向输入对的正常或反相信号。这使得PLA可以测试0、1或完全不关心该输入信号(即没有互连点的情况)。这创建了一种可屏蔽的布尔逻辑。由于所有门都连接在一起,只有当所有门都匹配时,相应的微码列才会被激活。
这种匹配逻辑非常巧妙——13个输入线中几乎可以肯定有8条是指令本身的指令操作码字节。在PLA中设置“无关”位允许整个指令范围共享相同的微码实现,这对于效率至关重要,这样微码ROM就不会占据整个CPU芯片。
这里存储的不是一组比特,而是逻辑——简单的与逻辑可以表示为比特对,我们可以用提取微码ROM相同的方式提取这些比特——在MaskRomTool中绘制行和列,将比特导出为图像,手动分类一千个左右,然后将其输入我们的“热狗”CNN。
这就是我们的结果:
[PLA提取最终结果图]
最终的PLA提取结果
手动解码解码PLA的前几列结果很有希望。我们基本上得到:
00?00???0??00
00?00???10?00
00?000??11100
其中?表示我们“不关心”该位置的比特值,允许一系列操作码匹配。
幸运的是,指令操作码的8位似乎由从第四个输入开始的8个输入表示。
第一行将匹配64种不同的操作码,从00,01,02,03,08,09,0a,0b,10,11,12,13等开始。这恰好对应8088 ISA的一般ALU操作码,它们在8088上共享相同的微码。这是个好迹象。
第二行将匹配04,05,0c,0d,14,15,1c,1d等,对应使用立即操作数的一般ALU操作码。这肯定不只是巧合。事实上,匹配逻辑在大部分情况下都以令人安心的数值顺序排列。
需要注意的是,对于1032个微码字,只有257条激活线。这意味着对于给定指令,进入微码ROM的“入口点”必须在能被4整除的地址上——8088也是如此,所以看到这点并不奇怪。
微码的每一列包含4个字。
[列包含4个字的图片]
(图片说明:V20微码ROM,列包含4个字。请注意,金属层走线是水平方向的,并且有一个间隙模式,这个模式可以识别位。)
*注意:您提供的文章内容在此处不完整。*
相似文章
80386 微码反汇编
一篇博客文章,详细介绍了成功反汇编和分析 Intel 80386 微码的过程,揭示了215条指令入口点以及其复杂的内部架构。
Intel 8087浮点芯片内部的微码:寄存器交换
对Intel 8087浮点协处理器内部微码的详细逆向工程分析,聚焦于FXCH寄存器交换指令及芯片内部架构。
Intel 8087浮点芯片的指令解码
对Intel 8087浮点协处理器指令解码的详细逆向工程分析,解释主CPU与协处理器之间的交互、微码ROM的使用以及总线接口单元。
关于奔腾处理器微码电路的笔记
对奔腾处理器微码ROM的详细剖析,描述其结构、容量以及微码如何在硬件层面实现机器指令。
Intel 8087浮点芯片中的微码:FSCALE指令
本文详细介绍了Intel 8087浮点芯片中微码的逆向工程,重点阐述了FSCALE指令的复杂实现以及该芯片在建立浮点标准中的历史作用。