Het ontcijferen van de NEC V20-microcode

Dit was niet ideaal, maar het vooruitzicht om mijn V20-core cycle-exact te maken zonder de microcode leek een ontmoedigend proces van vallen en opstaan. Daarom besloot ik de microcode te bemachtigen.

Die-fotografie van de NEC V20

Ik heb onlangs InfoSecDJ gevraagd om die-fotografie te maken van een NEC V20 CPU (eigenlijk een V20 geproduceerd door Sharp, maar desondanks een V20). Het resultaat is een fotomozaïek met een extreem hoge resolutie: exact 5,6 Gigapixels (70.478 x 80.672), wat te groot is voor het JPEG-formaat.

Het rechthoekige gebied net onder het centrum van de die is de hoofd-microcode ROM.

De Microcode ROM

De ROM-array is 258x116 en bevat 29.928 bits. Dit is precies deelbaar door 29, wat de microcode-woordlengte van de V20 is. Dit is een goed teken, maar het impliceert 1032 microcode-woorden, terwijl er slechts 1024 werden verwacht. De reden hiervoor wordt later duidelijk.

Bij het inzoomen op de ROM-array zijn heldere, horizontaal lopende sporen zichtbaar die deel uitmaken van de metallagen van de chip. De gelige stippen zijn interconnects die de metaallaag verbinden met de onderliggende polysiliconlaag. Achter de metaallaag bevinden zich verticale balken, waarbij er af en toe een gat in het polysilicon aan weerszijden van elke interconnect zit. Deze gaten vormen een transistor; de aanwezigheid van een transistor duidt op een bit '1'.

Extractie via Machine Learning (CNN)

Omdat het handmatig extraheren van 29.928 bits te tijdrovend zou zijn, heb ik gebruikgemaakt van tools. Eerst probeerde ik MaskRomTool van Travis Goodspeed om de rij- en kolomposities van de bits te definiëren. De bit-detectiemethoden van deze tool zijn echter gebaseerd op drempelwaarden (thresholding), en het contrastverschil tussen een bit en een niet-bit was te gering om dit effectief te laten werken.

Een andere aanpak was nodig. Ik gebruikte MaskRomTool om de gedefinieerde bitposities te exporteren naar JSON-formaat. Met dit bestand schreef ik een Python-script dat een vierkant bitmap-fragment centreerde op elke bitpositie en dit opsloeg als een PNG-bestand van 42x42 pixels.

Het doel was om een convolutional neural network (CNN) te trainen om de bits te identificeren als 0 of 1. Hoewel ik geen ervaring had met het trainen van CNN's, maakte het gebruik van moderne Python-frameworks zoals PyTorch dit proces zeer toegankelijk.

Training van het model

Voor de training was een dataset nodig. Ik maakte een eenvoudig Python/tkinter-script om handmatig bits te sorteren in mappen voor 0-en en 1-en. Uiteindelijk classificeerde ik iets meer dan 1.000 bits handmatig voordat ik het CNN-model begon te trainen.

Een trainingsrun zag er als volgt uit:

[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

Met een CUDA-compatibele GPU duurde deze training slechts enkele minuten. Het doel was om de nauwkeurigheid te maximaliseren zonder overfitting te veroorzaken. Na de training werd het model gebruikt voor inference: het voorspellen of een afbeelding een 0-bit of 1-bit bevat.

Bij de eerste run werden bits met een betrouwbaarheidsscore van minder dan 99% als ambigu gemarkeerd (rood). Deze werden handmatig gecorrigeerd en opnieuw toegevoegd aan de trainingsset, totdat er slechts 4 ambigue bits overbleven, die vervolgens handmatig zijn geverifieerd.

De Decode PLA

Na de extractie van de bits moest de rechthoekige blob van bits worden omgezet in een lijst van 29-bit microcode-woorden. Dit vereist inzicht in de decode of activation PLA, die zich boven het microcode ROM-blok bevindt.

De taak van deze PLA is om 13 logische inputs (met elk een complementair gesignaleerd signaal, dus 26 lijnen in totaal) te verwerken en één kolom van de onderliggende microcode-array te activeren als de inputsignalen overeenkomen met die kolom van de PLA.

De transistors in de PLA vormen logische AND-poorten. In tegenstelling tot de microcode ROM is er slechts één transistor verbonden aan een interconnect, gericht op ofwel het normale of het geïnverteerde signaal van een inputpaar. Dit stelt de PLA in staat om te testen op een 0, een 1, of het signaal volledig te negeren (een "don't care"-situatie). Hierdoor ontstaat een vorm van maskeerbare Booleaanse logica.

Resultaten van de PLA-extractie

Door dezelfde CNN-methode toe te passen op de PLA, kon de logica worden geëxtraheerd. De resultaten tonen patronen zoals:

  • 00?00???0??00
  • 00?00???10?00
  • 00?000??11100

Hierbij staat een ? voor een "don't care". De 8 bits van de instructie-opcode worden gerepresenteerd in de 8 inputs beginnend bij de vierde input. De eerste rij komt overeen met de algemene ALU-opcodes van de 8088 ISA, wat een bevestiging is dat de analyse juist is.

Er zijn slechts 257 activatielijnen voor 1032 microcode-woorden. Dit betekent dat de toegangspunten in de microcode ROM op adressen moeten liggen die deelbaar zijn door 4. Elke kolom van de microcode bevat dus 4 woorden.

Het extraheren van de microcode-woorden

Tussen de microcode-blokken bevindt zich een 4-weg multiplexer die twee logische inputs gebruikt (afgeleid van de twee laagste bits van de microcode program counter) om één van de vier geactiveerde woorden te selecteren.

Om de bits in een lineaire volgorde van microcode-woorden te zetten, was wat trial-and-error nodig. Uiteindelijk bleek dat de microcode-woorden in elke kolom van onder naar boven worden gelezen. Ter verificatie werden patronen vergeleken met bekende velden uit gerechtelijke documenten van een rechtszaak tussen NEC en Intel over de V20-microcode.

Indeling van de microcode

Zodra de woorden waren geëxtraheerd, kon de indeling in kaart worden gebracht:

  • Intel 80186 ISA: Ongeveer twee derde van de microcode is gewijd aan de implementatie van de 80186 ISA.
  • NEC Extended Instruction Set: Een deel is gewijd aan de uitgebreide instructieset van NEC in de 0Fh-geprefixte opcode-ruimte.
  • 8080 Emulatie: Bijna een vijfde van de totale ROM-oppervlakte is bestemd voor de 8080-instructieset, gebruikt voor de 8080-emulatiemodus van NEC.

De decodering van de velden gebeurde via deductie (vergelijkbaar met het oplossen van een kruiswoordpuzzel). De eerste vier waarden van het eerste bronveld bleken segmentregisters te zijn (ES, CS, SS en DS), en de laatste acht waarden registers zoals AX, CX, DX, BX, SP, BP, SI en DI.

De Group Decode ROM (GDR)

Sommige instructies vereisen meer context dan de microcode alleen kan bieden. Bij de 8088 wordt dit opgelost door een aparte Group Decode ROM (GDR) PLA. De V20 heeft een vergelijkbare GDR, die signalen uitzendt die de ontbrekende context leveren.

Uit de decodering van de GDR komen bekende patronen naar voren, zoals matches voor prefixen (F0, F1, F2, F3) en specifieke instructies zoals HLT, CMC en de 0F-opcode extensie.

Het definitieve microcode-woordformaat

Door samenwerking met dreNorteR op de Vintage Computer Federation (VCF) forums werd ontdekt dat de veelgeciteerde diagrammen uit de gerechtelijke documenten onvolledig waren.

Het bleek dat de linkerkant van een microcode-woord twee vormen kan aannemen, waarvan één een inline constante waarde codeert. De rechterkant heeft vier mogelijke vormen in plaats van drie. Dit is ook zichtbaar in de die-fotografie: 17 outputs van de microcode ROM verlaten de ROM in de ene richting, en de overige lijnen in de tegenovergestelde richting.

Veldnamen zoals 'F', 'W' en 'E' kregen hierdoor hun ware betekenis. Zo bleek 'F' niet te staan voor "Update Flags" (zoals bij de 8088), maar voor Fetch.

V20 versus V30: Een slimme optimalisatie

NEC maakte gebruik van een slimme optimalisatie voor de V20 (8-bit bus) en de V30 (16-bit bus). In plaats van twee verschillende microcode-masks te ontwerpen, konden ze dezelfde mask gebruiken en slechts twee lijnen wisselen via de metallaag bovenop de ROM.

Door in de metallaag een specifieke arm door te snijden of te laten zitten, kon worden bepaald of de chip een V20 of een V30 werd. Dit verklaart ook de eerder opgemerkte discrepantie in het aantal microcode-woorden.

Nog te doen

De microcode van de V20 is nog niet voor 100% ontcijferd; er zijn nog enkele onbekende bronwaarden. Er is echter genoeg bekend om te beginnen met een microcode-gebaseerde implementatie van de NEC V20 CPU in de emulator MartyPC. De resterende mysteries zullen waarschijnlijk worden opgelost door het gedrag van de emulator te toetsen aan een hardware-gegenereerde testsuite van de NEC V20.