NEC V20 マイクロコードのデコード
Decoding the NEC V20 Microcode (martypc.blogspot.com)
要約
この記事では、NEC V20 CPUのマイクロコードを抽出・解析するプロセスについて詳述しています。高解像度のCPUダイ写真とMaskRomToolなどのツールを使用し、最終的には畳み込みニューラルネットワーク(CNN)を用いてマイクロコードのビットを識別・抽出することに成功しました。これにより、NEC V20の正確なエミュレーションへの道が開かれました。
全文翻訳
reenigneによる2020年の8088マイクロコードの解読は、8088 CPUの極めて正確なエミュレーションへの道を開きました。
MartyPCにNEC V20のサポートを追加していましたが、V20コアは実際のタイミングに関してサイクル精度ではありませんでした。それはV20の服を着た8088であり、私の8088コアにV20命令を追加したコピー&ペーストでした。
これは理想的な状況ではありませんでしたが、マイクロコードなしでV20コアをサイクル精度にするという見通しは、試行錯誤の退屈な作業になる可能性がありました。
では、なぜマイクロコードを入手しないのでしょうか?
最近、InfoSecDJにNEC V20 CPU(実際にはSharpが製造したセカンドソースのV20ですが、V20であることに変わりはありません)のダイ写真撮影を依頼しました。彼は素晴らしい仕事をしてくれました。
このフォトモザイクは非常に高解像度です。具体的には5.6ギガピクセル、驚異的な70478x80672ピクセルで、JPEG画像フォーマットに収まらないほど大きいです!
完全な解像度で全体を見ることができます。こちら。
ダイの中央よりやや下の長方形の領域が、メインのマイクロコードROMです。
ROMアレイは258x116で、29,928ビットを含んでいます。これは29で均等に割り切れます。V20のマイクロコードワード長は29ビットであることがわかっているので、これは良い兆候です。しかし、1024ワードを期待していたのに1032ワードになるということは、少し奇妙ではありませんか?その理由は後で解明します。
ROMアレイをクローズアップしたものです。
明るく水平に走るトレースは、チップのメタル層の一部です。黄色い点は、メタル層とそれより下のポリシリコン層を接続するインターコネクトです。メタル層の後ろにある垂直バーに注目してください。また、各インターコネクトのどちらか一方の側にポリシリコンの隙間が時々あることに注意してください。
これらの隙間がトランジスタを形成しており、トランジスタの存在は1ビットを示します。1ビットを視覚的にわかりやすくするためにハイライトします。
これを見て非常に興奮しました。ROM内のビットを視覚的に識別できるなら、ROMの内容を抽出できます。ただ一つの小さな問題があります。アレイには29,928ビットがあります。手作業で抽出するのは少し面倒です。
幸いなことに、このタスクのためのツールがあります。Travis Goodspeed氏のMaskRomToolにROMイメージをロードしました。
MaskRomToolを使用すると、ビット位置を定義する行と列を描画できました。残念ながら、そのビット検出方法はしきい値処理に基づいていることがわかり、ビットと非ビットのコントラストに十分な差がなく、効果的な検出メカニズムになりませんでした。ビットヒストグラムが遠い軸に向かって非常に圧縮されていることに注意してください。これはうまくいきませんでした。おそらく、明るいメタル層がなければ、しきい値処理技術はより良く機能したかもしれませんが、InfoSec DJにそれを削除するように依頼したくはありませんでした。別のアプローチが必要でした。
幸いなことに、MaskRomToolを使用して定義したビット位置をJSON形式でエクスポートできました。このエクスポートされたJSONファイルを使用して、各ビット位置を中心に正方形のビットマップを抽出するPythonスクリプトを作成し、ビットの論理的な列と行番号をファイル名に含めて保存しました。
これにより、105MBのZIPファイルに、ビットまたは非ビットを含む小さな42x42ピクセルのPNGファイルが格納されました。このアイデアは、畳み込みニューラルネットワークをトレーニングして、ビットを0または1として識別させることでした。
これが難しすぎると感じるかもしれませんが、私はこのプロジェクト以前はCNNのトレーニング経験が全くなく、午後中に完了しました。PyTorchのような最新のPythonフレームワークは、それを非常に簡単にします。
優れたCNNチュートリアルがいくつかありますが、その多くは手書き文字認識という古典的な問題に焦点を当てています。しかし、私たちのタスクはさらに単純です。画像を0または1の2つのバケットに分類するだけです。問題は文字通り「ホットドッグかそうでないか」です。
CNNをトレーニングする前に、トレーニングデータセットが必要です。そこで、素早く簡単なPython/tkinterスクリプトを作成し、目で見てビットを0または1のバケットに素早く分類できるようにしました。
ここに表示されているのは「1」ビットです。メタル層の後ろにあるトランジスタで見分けられますか?ボタンは結局不要になりました。分類するにはキーボードの1または0を押すだけで済みます。理論的には、これを29,928回繰り返せば、数時間で完了します。当初は、CNNトレーニングがうまくいかなかった場合のバックアップ方法としてこれを意図していました。友人たちが協力してくれることになっており、「Bit Voter」が生成するJSONログは、分散作業をサポートするためにマージできます。幸いなことに、これは必要ありませんでした。
最終的に、この方法で1,000ビット強を手動で分類しました。それらを2つのディレクトリに分類した後、CNNモデルのトレーニングを試みることができます。入力としてソートされた画像を使用します。
ここではCNNの書き方について詳しく説明しません。ソースコードはGitHubにあります。私は優れたPyTorchライブラリを使用しました。これにより、プロセス全体が想像以上に簡単になりました。
トレーニング実行の様子です。
[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に到達することは不可能かもしれませんし、望ましくないかもしれません(過学習というものがあります)。時には長く続けると事態が悪化することもあるので、安定した改善が見られない場合はトレーニングを終了します。
トレーニングの出力はニューラルネットワークモデルです。このモデルを使用して、入力データセット全体に対して推論を実行できます。推論とは、モデルを適用して、トレーニングしたとおりに、画像が0ビットまたは1ビットを含んでいるかどうかを予測することです。
先に進む前に、潜在的な論争を避けるための簡単な注意点です。CNNはコンピュータサイエンスの観点からAIの広範な範囲に loosely に含まれますが、ここでは通常、大規模言語モデル(LLM)を指す現代的で論争の多い意味での「AI」は使用していません。
推論パスを実行すると、各ピクセルに対して信頼度スコアが得られます。この信頼度スコアを使用して、モデルが確信度の低いビットを、特定のしきい値(ここでは99%未満を使用)でマークすることができます。最初の実行結果を、曖昧なビットを赤で色付けして示します。
曖昧なビットをすべて取り込み、トレーニングフォルダに手動で再度分類し、この結果が得られるまでトレーニングを再実行しました。
これはかなり良好でした。曖昧なビットは4つだけで、モデルを再度トレーニングするよりも、それらを手動で確認する方が速かったです。
これで29kのマイクロコードビットが得られ、数時間の面倒な手作業を節約できました(Pythonでトレーニングスクリプトを書く数時間と引き換えですが、少なくとも再利用可能です!)。
この長方形のビットの塊を、29ビットのマイクロコードワードのリストに変換する必要があります。つまり、ビットマップを258x116ではなく29x1032に再編成する必要があります。それを正確に行う方法は明らかではありませんが、対応するデコーダーPLAをデコードするまで、一時的に保留できます。
デコードまたは「アクティベーション」PLAは、メインのマイクロコードROMブロックの上にあり、その間にいくつかの回路が挟まれています。
このPLAの仕事は、左側から13の論理入力(各入力には双子の反転信号があり、合計26の入力線があります)を受け取り、入力信号がPLAのその列に一致した場合に、下のマイクロコード配列の1つの列をアクティブにすることです。
一致するとはどういう意味でしょうか?
A closer zoom might be informative:
垂直金属配線の同様の配置があり、インターコネクトで区切られ、基板の隙間がトランジスタを形成しています。この場合、これらのトランジスタは論理ANDゲートを形成します。マイクロコードROMとは異なり、インターコネクトには常に1つのトランジスタのみが接続されており、入力の正規信号または反転信号のいずれかに向き合っています。これにより、PLAは0、1をテストしたり、その入力信号を全く気にしない(インターコネクトが存在しない場合)ことができます。これにより、マスク可能なブール論理が作成されます。すべてのゲートは一緒に結ばれているため、すべてのゲートが一致した場合にのみ、対応するマイクロコードの列がアクティブになります。
このマッチングロジックは非常に巧妙です。おそらく13の入力線のうち8つは、特定の命令のオペコードバイト自体でしょう。「Don't care」ビットをPLAに設定することで、命令の範囲全体が同じマイクロコード実装を共有できます。これは、マイクロコードROMがCPUダイ全体を占有しないようにするために非常に重要です。
ここにはビットのセットではなく、実際にはロジックが格納されています。単純なANDロジックはビットのペアとして表すことができ、マイクロコードROMを抽出したのと同じ方法でこれらのビットを抽出できます。MaskRomToolで行と列を描画し、ビットを画像としてエクスポートし、1000個を手動でソートしてから、それをホットドッグCNNに供給します。
これが結果です。
デコードPLAの最初の数列を手動でデコードするのは有望です。基本的に次のようになります。
00?00???0??00
00?00???10?00
00?000??11100
ここで「?」は、その位置のビットの値は「気にしない」ことを意味し、オペコードの範囲が一致することを可能にします。
幸いなことに、命令オペコードの8ビットは、4番目の入力から始まる8つの入力で表されているようです。
最初の行は、00、01、02、03、08、09、0a、0b、10、11、12、13などから始まる64種類のオペコードに一致します。これは、8088 ISAの一般的なALUオペコードに対応しており、すべて8088では同じマイクロコードを共有しています。これは良い兆候です。
2番目の行は、04、05、0c、0d、14、15、1c、1dなどに対応し、即値オペランドを取る一般的なALUオペコードに対応します。これは間違いなく偶然以上のものです。実際、ほとんどの場合、マッチングロジックは非常に数値順に配置されています。
注意点として、1032マイクロコードワードに対してアクティベーションラインは257本しかありません。これは、特定の命令に対するマイクロコードROMへの「エントリポイント」が4で割り切れるアドレスである必要があることを意味します。これは8088でも同様でしたので、奇妙なことではありません。
マイクロコードの各列には4つのワードが含まれています。
マイクロコードの2つのブロックの間にある4ウェイマルチプレクサにより、2つの論理入力がデコードPLAによって現在アクティブになっている4つのワードのうちの1つを選択できます。これらの入力は、マイクロコードプログラムカウンタの2つの下位ビットから取得されます。
長方形のマイクロコードROMビットのブロックが与えられた場合、そのブロックがどのようにアドレス指定されて線形なマイクロコードワードの配列を生成するかという問題があります。ROMアレイ自体から、上または下から読み取ることができ、各マルチプレクサ行内でも同様の選択肢があります。線が入れ替えられたりインターリーブされたりする可能性もあるため、合理的に見えるものを生成するには試行錯誤が必要になる場合があります。実際には、各列からマイクロコードワードをボトムアップで読み取ります。
結果のワードビットマップのパターンが既知のフィールドと一致する場合、正しい方向に向かっていることがわかります。これは、それらの既知のフィールドが何であるかを知っている場合に非常に役立ちます。幸いなことに、NECとIntelの悪名高い訴訟に関する裁判書類から、ある程度の手がかりを得ていました。
ここで、Discordで特定のワード抽出の妥当性について考察している様子が見られます。
幸いなことに、正しいワード抽出を見つけるのに時間はかかりませんでした。
抽出されたすべてのマイクロコードワードが、5つの列に分割された様子です(各列は左の列の下から続きます)。視覚的な興味のために、さまざまなセクションを色分けしました。色覚異常に優しいパレット(Okabe & Ito)を使用しようとしましたが、この情報がすべての人に見えるわけではない場合は申し訳ありません。
V20マイクロコードの約3分の2は、Intel 80186 ISAの実装に費やされています。残りは、0Fhプレフィックス付きオペコード空間のNEC独自の拡張命令セットの実装、またはNECの8080エミュレーションモードで使用される8080命令セットの実装に費やされています。後者は、ROM領域全体の約5分の1を占めます。
命令がどこにあるかを知っており、マイクロコードワードの形式の概略図があるため、ソースおよびデスティネーションオペランドのさまざまな値が何を意味するかを、推論によって決定し始めることができます。これはクロスワードパズルを解くのに非常に似ています。特定の論理アンカーから始めます(たとえば、特定のレジスタを操作する命令)、推測と仮定を記録しながら作業を進め、それらを検証または却下します。
最初のソースフィールドの最初の4つの値は、セグメントレジスタ(ES、CS、SS、DS)であることが判明しました。最後の8つの値は、AX、CX、DX、BX、SP、BP、SI、DIであることが判明しました。
フィールドのデコードとさまざまなフィールド値のルックアップを実行するために、Excelスプレッドシートを使用して作業を追跡し始めました。追加のシートにそれらを保持しました。
主なデコード作業
この時点で、いくつかの助けが必要だと判断しました。reenigneは自身の80386マイクロコードの解読で忙しかったため、Vintage Computer Federationフォーラムのメンバーに協力を依頼しました。
私が投稿したスレッドは、ベテランVCFユーザーであるdreNorterR(以前に80186マイクロコードを解読しており、そのような問題に多くの経験を持っていました)との生産的な協力につながりました。スレッド全体を再現するつもりはありません。2人のオタクがV20の秘密を解き明かす様子に興味があるなら、読む価値があります。さまざまなフィールドがどのようにリバースエンジニアリングされ、どのようにアイデアを交換してパズルピースを組み合わせたかを見ることができます。
グループデコードROMの解明
マイクロコード自体を見ているだけでは、一部の命令がどのように機能するのか、なぜなら多くの場合、マイクロコード自体には命令が実際にどのように動作しなければならないかを制御するのに十分なコンテキストがないため、当惑することがあります。
8088では、別のグループデコードROM(GDR)PLAが追加の信号を提供し、さまざまなオペコードによってアクティブになります。8088 GDRはオペコードごとに15個の信号を発行し、マイクロコード命令の欠けているコンテキストを提供します。Ken Shirriff氏の8088グループデコードROMに関する優れたブログ投稿を読むことをお勧めします。
8088に精通していた私は、V20にもGDRがあるはずだと確信していました。それは、PLA回路の大きなブロックであったため、見つけるのは難しくありませんでした。V20のGDRが発行する信号の多くは、8088の信号と同じ目的を持っていたため、デコードは多かれ少なかれ簡単でした。
GDRの最初の数行をデコードすると、いくつかの馴染みのあるパターンが見られます。
01 111100?? 00100000001000 PREFIXES f0,f1,f2,f3
01 1111010? 00100000001000 HLT,CMC f4,f5
01 1111?0?? 00100000000100 f0,f1,f2,f3,f8,f9,fa,fb
01 1111??0? 00100000000010 f0,f1,f4,f5,f8,f9,fc,fd
01 1111?00 00100000000001 f0,f2,f8,fa
01 1111?100 00100000000001 HLT,STD f4,fc
01 00001111 00100000000001 EXT PFX 0f
01 001??110 00100000000000 26,2e,36,3e
01 01100100 00100000000001 REPNC 64
01 0110010? 00100000001000 REPX 64,65
01 0100???? 00000000011100 INC/DEC 40,41,42,43,44,45,46,47,48,49,4a,4b,4c,4d,4e,4f
01 1111?11? 01000100011011 GRP f6,f7,fe,ff
最初のマスク列の一致は、オペコードF0、F1、F2、F3に一致します。これらはすべて命令プレフィックスです。後で0Fオペコード拡張も明示的に一致していることがわかります。
真のV20マイクロコードワードフォーマット
VCFフォーラムの協力中に興味深い進展がありました。dreNorteRは、有名な裁判書類には記載されていないマイクロコードワードフォーマットを発見しました。
実際、頻繁に再版される図は不完全でした。マイクロコードワードの左側は2つの形式を取り、そのうちの1つはインライン定数値をエンコードします。マイクロコードワードの右側には、3つではなく合計4つの形式があります。
この分割はダイ写真で非常に明確に見ることができます。マイクロコードROMの17の出力は一方向にROMから出ており、残りの線は反対方向に抜けています。したがって、それらは明らかに異なる機能的な分割を持っていました。
古い図で説明されていなかった「F」、「W」、「E」のようなフィールドには、今や意味がわかっています。「F」が8088のFフィールドと関連して「フラグ更新」であると仮定するのはもっともらしいかもしれませんが、実際には全く異なる意味、つまりフェッチ(Fetch)を持っています。
V30
NECはIntelに対する利点があり、非常に巧妙な最適化を行うことができました。NEC V20は、8088と同様に8ビットバスを持っています。完全に16ビットの8086に対応するチップはNECのV30 CPUです。
Intelは最初に8086を設計し、その後8088を派生させました。これは、8088と8086が同じマイクロコードマスクで設計できなかったことを意味します。NECはV20とV30を同時に設計することができ、メインのマイクロコードROMの上に配置されたメタル層を使用して、マイクロコードの2本の線を切り替えるだけで済みました。
下の画像では、示された円内にある金属構造の一方の側またはアームが、製造されているCPUタイプに応じて切断されていました。これにより、両方のCPUが同じマイクロコードマスクを共有できるようになりました。これは、最初に気づいたマイクロコードワード数の不一致も説明しています!
ズームインすると、このV20ダイでは、上部中央の太いポストに接続されている左側のトレースが切断されていることがはっきりとわかります。V30では、反対側が代わりに切断されます。
まだ終わっていない作業
V20のマイクロコードは100%解読されていません。まだ特定されていないソース値がいくつかあります。しかし、私のエミュレータMartyPCでNEC V20 CPUのマイクロコードベースの実装作業を開始するのに十分なほど解読が進んだと感じています。残りの謎は、既知のものを利用して実際のCPUの動作を再現しようとする際に明らかになるだろうと推測しています。私のハードウェア生成NEC V20テストスイートに対してです。
アーティファクト
デコードに使用したすべてのリソースと、私の最新のマイクロコードスプレッドシートはGitHubでこちらで入手できます。