2 Punkte von GN⁺ 2024-05-13 | 1 Kommentare | Auf WhatsApp teilen
  • 6o6 ist ein Projekt, das auf dem nur schwach geschützten NMOS 6502 erneut einen 6502 in Software ausführt und alten 8-Bit-Systemen damit eine kontrollierbare virtuelle Ausführungsschicht hinzufügt
  • Es kontrolliert die Befehlsausführung und Speicherzugriffe des Gastcodes zwischengeschaltet und bietet Funktionen wie Adress-Remapping, das Blockieren illegaler Lese-/Schreibzugriffe und Traps für Jam-Opcodes
  • Der Kern besteht darin, die ALU des Host-6502 direkt mitzubenutzen: Gastregister und Flags werden auf den Host geladen, derselbe Befehl wird ausgeführt, und das Ergebnis wird anschließend wieder gespeichert
  • Zur Validierung wurden Klaus Dormanns 6502 functional test suite und eine lib6502-basierte Testumgebung verwendet; die optimierte Konfiguration führte 1.602.516.769 Befehle aus und damit 36,5 % weniger als die nicht optimierte Konfiguration
  • Das ebenfalls veröffentlichte The Incredible KIMplement 1.0 sowie mehrere Beispiele zeigen den Einsatzbereich von 6o6: KIM-1-Emulation, verschachtelte Virtualisierung, Task-Switching und ein geoRAM-basiertes externes Speichersystem

Was 6o6 und KIMplement veröffentlicht haben

  • The Incredible KIMplement 1.0 emuliert den 1 KB großen, mit 1 MHz laufenden MOS/Commodore KIM-1 6502 Single-Board-Computer
    • Läuft auf einem nicht erweiterten Commodore 64
    • Unterstützt das eingebaute TTY des KIM; Zugriff über die serielle Schnittstelle eines echten Computers ist ebenfalls möglich
    • Der Adressraum wird auf 16K erweitert
  • 6o6 steht für „6502-on-6502“ und ist eine vollständige softwarebasierte virtuelle NMOS-6502-CPU, die auf einer 6502-CPU läuft
    • Steuert die Ausführung von Gastcode
    • Trappt undokumentierte Opcodes und Jam-Opcodes
    • Abstrahiert alle Speicherzugriffe
    • Unterstützt Adress-Remapping, das Abfangen illegaler Lese-/Schreibzugriffe und Ausführung auf Basis virtuellen Speichers
  • Auf Commodore 64 und Apple IIe läuft ein Gast-hello world; außerdem funktioniert verschachtelte Virtualisierung, bei der 6o6 wiederum innerhalb von 6o6 ausgeführt wird
    • Stage 1 läuft nahezu sofort
    • Stage 2 ist langsamer
    • Stage 3 ist sehr langsam, funktioniert aber

Warum der 6502 Virtualisierung braucht

  • In frühen Personal Computern kontrollierte üblicherweise ein einzelnes Programm die gesamte Maschine; lief ein Programm falsch, half ein Neustart
  • In Mehrbenutzer- oder Multitasking-Umgebungen entstehen Probleme, wenn fehlerhafter Code andere Adressräume beschädigt, gefährliche Befehle ausführt oder Ressourcen monopolisiert
  • Der NMOS 6502 ist eine einfache CPU mit weniger als etwa 4.000 Transistoren, daher sind seine Schutzfunktionen begrenzt
    • Viele historische NMOS-6502-Systeme konnten die Zero Page oder die Position des Prozessor-Stacks nur schwer frei verschieben
    • Es gab keine Funktion, Codeadressen beliebig zu remappen und ohne Fixups auszuführen
    • Der Zugriff auf bestimmte Speicherstellen konnte nicht umfassend verboten werden
    • Wenn ein undokumentierter Jam- oder KIL-Opcode ausgeführt wurde, konnte der Prozessor vollständig anhalten
  • Einige Probleme lassen sich per Hardware abmildern
    • Wird periodisch ein NMI ausgelöst, kann ein Prozess, der das System durch Setzen des Interrupt-Flags monopolisiert, von außen gestoppt werden
    • Einige 6502-Multitasking-Kernel implementieren auf diese Weise präemptives Task-Switching
    • In-Circuit-Emulatoren wie „Trap65“ von Eastern House Software konnten fehlerhafte Opcodes durch trapbare BRKs ersetzen, waren aber teuer und bei komplexer Busmanipulation begrenzt

Wie 6o6 ausführt

  • Auch ein einfacher Interpreter-Ansatz kann auf dem 6502 praktikabel sein
    • Der 6502 hat nur wenige Register
    • Es gibt 56 Befehle und nicht allzu viele Adressierungsmodi
    • Der Prozessorzustand lässt sich vergleichsweise leicht verfolgen
  • Die „Virtualisierung“ von 6o6 besteht darin, die ALU des Host-6502 für interne Gastoperationen zu verwenden
    • Der Gast-Accumulator und die Flags werden in die Host-CPU geladen
    • Derselbe Befehl, den der Gast ausführen würde, wird auf dem Host ausgeführt
    • Ergebnis und Flags werden gespeichert, und der Host-Zustand wird bereinigt
  • Der Vorteil dieses Ansatzes ist, dass Arithmetik und Flag-Behandlung nicht direkt neu implementiert werden müssen
    • Decimal Mode, also BCD-Arithmetik, funktioniert ebenfalls natürlich
    • Da der echte 6502 rechnet, entsprechen die Ergebnisse denen eines 6502
  • Auch beim Lesen von Speicherwerten oder beim Übertragen von Registern wird derselbe Ansatz genutzt, um Negative- und Zero-Flags zu behandeln
  • Die Implementierung verwendet selbstmodifizierenden Code, daher ist für den Einsatz in ROM besondere Vorsicht nötig

Struktur von VM, Harness und Kernel

  • Die 6o6-VM übernimmt nicht die Rolle eines vollständigen Systems, sondern die einer Engine
  • Die Laufzeitumgebung ist in drei Teile gegliedert
    • VM: eine hardwareunabhängige virtuelle CPU, die auf dem echten 6502 läuft
    • Harness: Schnittstelle zu Gastspeicher und verwalteter Hardware
    • Kernel: Control Loop, der die VM aufruft und Ausnahmen sowie Gastzustand verarbeitet
  • Das Harness stellt über eine standardisierte Jump Table ein binäres Interface bereit
    • Implementiert load/store für bestimmte Adressen
    • Verarbeitet instruction fetch
    • Erhält Hardware-Stack und Stack Pointer
    • Die VM setzt keine Page-Größe und kein vorhandenes Memory Paging voraus
  • Das Harness kann alles von einfacher Adressübersetzung per Addition und Bit-Shift bis zu paged virtual memory implementieren
    • Page Faults müssen nicht nach außen gegeben werden; das Harness kann beim load/store paging in/out durchführen
    • Auch Schutz-Ausnahmen kann das Harness auslösen
  • Der Kernel startet die VM-Ausführung und interpretiert die von der VM zurückgegebenen Statuscodes
    • Verarbeitet Ausnahmen aus dem Harness oder aus 6o6 selbst
    • Prüft oder verändert Gastregister und PC
    • Kann bestimmte Service-Routinen nativ behandeln
    • Zwischen VM-Aufrufen ist die Gast-CPU „angehalten“, sodass State Capture oder Context Switching möglich ist
  • Die VM löst virtuelle IRQs, NMIs oder reset nicht direkt aus
    • Den Zeitpunkt solcher Events bestimmt der Kernel
    • BRK wird unterstützt, aber die VM setzt nicht den Stack und springt dann zu einem neuen PC, sondern gibt eine Ausnahme zurück

Einsatz von 6o6 in KIMplement

  • Das Harness von KIMplement virtualisiert den standardmäßigen 6502-Stack und die KIM-4-Erweiterung
    • $0000-$17ff: Lese-/Schreibspeicher
    • $1800-$1fff: ROM
    • $2000-$3fff: RAM
    • $4000-$fff7: nicht gemappter, nicht beschreibbarer Bereich
    • $1ff8-$1fff wird für Vektoren nach $fff8-$ffff gespiegelt
  • Der Host-Commodore 64 speichert die unteren 16K bei $4000-$7fff; der Rest wird vom Harness synthetisiert
  • Der KIMplement-Kernel behandelt RRIOT-Emulation, LED-Anzeige, TTY-Dienste, NMI-Injektion für Stop und Single-Step Switch sowie einige Traps des KIM-1-ROM-Monitors
  • Nach der VM-Ausführung prüft der KIMplement-Kernel den PC von 6o6, um zu entscheiden, ob die aktuelle Routine abgefangen werden soll
    • TTY und einige Funktionen werden auf diese Weise implementiert

Performance-Optimierung

  • Aufrufe zwischen 6o6 und Harness können ein großer Flaschenhals sein
    • Selbst einfache Befehle brauchen mindestens einen Fetch
    • Indirekte Adressierung kann mehr Speicherzugriffe erzeugen
  • In KIMplement 0.2 wurden einige Memory Loads per Präprozessor-Makro inline gesetzt
    • Load-Routinen für beliebige virtuelle Adressen und für Zero-Page-optimierte Loads werden direkt mit der VM verbunden
    • Die Geschwindigkeit verbesserte sich stark, aber die VM wurde größer
    • Stores bleiben wegen geringerer Häufigkeit und höherer Komplexität als Subroutinenaufrufe erhalten
  • In der neuesten Iteration wurde auch Ineffizienz beim Zugriff der Inline-Makros auf den Program Counter bereinigt, was den instruction fetch weiter verbessert
  • KIMplement 0.3 ergänzt eine primitive instruction fusion namens „extra helpings
    • Befehle, die keinen Speicher berühren, müssen nicht sofort zum Kernel zurückkehren
    • Ziel sind Immediate-Befehle, accumulator-zentrierte Befehle, die meisten implied-Befehle und nicht verzweigte Branches
    • Bei load/store, nicht sequenziellen PC-Änderungen oder Ausnahmen stoppt die VM den Versuch, Befehle zu bündeln
  • Extra helpings machen die VM selbst nicht schneller
    • Bei Systemen wie KIMplement, die Funktionen je nach PC-Position einschränken, kann es sogar etwas langsamer sein
    • Stattdessen werden andere Systemteile schneller, weil der Kernel nicht bei jedem Befehl ohne beobachtbare Änderung unnötig ausgeführt wird
    • Für Anwendungen, die den PC präzise steuern müssen, kann das stören; daher gibt es Optionen zur stufenweisen oder vollständigen Deaktivierung

Validierung und Testergebnisse

  • Zur Validierung wurde Klaus Dormanns functional test suite verwendet
    • Das bereitgestellte Binary trifft keine Annahmen über die Hardware
    • Ein erfolgreicher Abschluss wird durch eine Endlosschleife an einer bestimmten Stelle signalisiert
  • Die Tests wurden so eingerichtet, dass sie mit Ian Piumartas lib6502 CPU emulator direkt in der Shell laufen
  • lib6502 scheiterte anfangs wegen eines Edge Case im Decimal Mode und bestand nach einem Patch
  • Das von Klaus bereitgestellte Binary ist volle 64K groß und konnte daher nicht zusammen mit 6o6 im normalen 6502-Adressraum untergebracht werden
    • Zu lib6502 wurde ein Patch für ein minimales System mit 32K Bank Switching hinzugefügt
    • Der Bereich $7000-$efff wird genutzt; die vorderen 32K und hinteren 32K des Test-Binaries liegen in unterschiedlichen Banks
  • Getestet wurde mit drei Konfigurationen
    • Keine extra helpings, kein inline fetch macro
    • Mit inline fetch macro, keine extra helpings
    • Sowohl inline fetch macro als auch extra helpings
  • Alle drei Konfigurationen bestanden die Klaus-Suite
  • Die Befehlszahlen lauten wie folgt
    • lib6502 ohne 6o6: 30.646.178 Befehle
    • 6o6 ohne Optimierungen: 2.188.322.914 Befehle
    • Mit inline fetch macro: 1.713.350.225 Befehle
    • Mit inline fetch macro und extra helpings: 1.602.516.769 Befehle
  • Die schnellste 6o6-Konfiguration führt 36,5 % weniger Befehle aus als die am wenigsten optimierte Konfiguration
  • Die schnellste Konfiguration führt im Mittel 52,3 Befehle pro Gastbefehl aus
    • Diese Zahl umfasst Harness, Kernel und 6o6-Ausführung
    • Da Befehle unterschiedliche Cycle Counts haben, sollte sie nicht als Geschwindigkeitsfaktor interpretiert werden

Enthaltene Beispiele

  • Das Beispiel hello world führt dasselbe Programm zuerst auf der nativen CPU und danach über 6o6 aus
    • Auf dem Commodore 64 wird es auf die Zeichenausgaberoutine bei $ffd2 gemappt, auf dem Apple II auf $fded
    • Erkennt der Kernel, dass der PC auf die Zeichenausgaberoutine zeigt, holt er den Gast-Accumulator, ruft die native ROM-Routine auf, entnimmt die return address vom Stack und kehrt zur Schleife zurück
  • Das Beispiel inception lässt 6o6 mit demselben Harness und Kernel sich selbst als Payload ausführen
    • Jede Stage hat eine eigene Zero Page und einen eigenen Stack
    • Da 6o6 derzeit selbstmodifizierenden Code verwendet, braucht jede Stage eine eigene VM-Kopie
    • In Stage 3 wird der Großteil des Speichers für drei VM-Kopien verwendet; die VM mit inline fetch macro ist jeweils über 10 KB groß
  • Bei verschachtelter Ausführung wird ein CHROUT-Aufruf von Stage 3 über Stage 2 und Stage 1 schließlich an die native Routine weitergereicht
  • Das Beenden des Payloads nutzt den RTS-Befehl wie einen „Kick“
    • Da beim Start keine return address auf dem Stack liegt, verursacht RTS einen Stack Underflow
    • Meldet das Harness dies als Ausnahme, behandelt der Kernel es als normales Ende
    • Auch in tieferen Stages wird es auf dieselbe Weise bis zum übergeordneten Kernel propagiert
  • Auf dem Apple II kann man es nach der Ausführung mit CALL 2051, auf dem Commodore 64 mit RUN erneut starten
    • Die Apple-II-Version nutzt auch den residenten DOS-Bereich oberhalb von $9000, daher wird nach der Ausführung ein Neustart empfohlen

Task-Switching-Beispiel

  • Das Beispiel tasks ist ein kleiner Task-Switching-Kernel, der zwischen zwei unabhängigen Tasks wechselt
  • Jeder Task hat eine eigene Zero Page, einen eigenen Stack und einen kleinen Code-Adressbereich und weiß nichts voneinander oder von der Existenz der VM
  • Ein Task zeigt das Alphabet an, der andere Zahlen
    • Die Zahlen werden zur visuellen Unterscheidung in Reverse Video angezeigt
    • Bei jedem Tastendruck wird der Task gewechselt
  • Beide Tasks verwenden dieselbe Position in der Zero Page zur Zustandsspeicherung, aber da ihre Zero Pages getrennt sind, läuft jeder ab seiner letzten Position weiter
  • Für Context Switching nötig sind die aktuelle Task-Information und Speicherbereiche für A, X, Y, P, S und PC jedes Tasks
    • Das Harness wählt anhand des „on CPU“-Tasks die physischen Adressen von Zero Page, Stack und Ausführungscode
    • Beim Umschalten speichert und lädt der Kernel anderen Zustand und markiert einen anderen Task als „on processor“

64K externes Speicherbeispiel auf geoRAM-Basis

  • Das Beispiel vmgr ist Commodore-64-spezifisch und stellt einen 64K-Adressraum als externen Speicher bereit, ohne den RAM des Systems selbst zu verwenden
  • geoRAM ist ein paged RAM-Gerät, das sich von Commodores offizieller REU unterscheidet
    • Die REU ist DMA-zentriert und verwendet den MOS 8726 REC für Lese-, Schreib- und Austauschoperationen mit dem main memory
    • geoRAM mappt Speicher über ein 256-Byte-Window-Page im I/O-Bereich $de00
    • Die Control Registers liegen bei $dffe, $dfff
    • Moderne kompatible Clones bieten auch Kapazitäten bis 4 MB
    • VICE unterstützt geoRAM-Emulation
  • Das Beispiel verwendet das ROM des 6502 processor module, das für den RC2014 Z80 kit computer bereitgestellt wurde
    • Das ROM enthält einen Monitor und Lee Davisons EhBASIC
    • Verwendet wird ein auf GitHub verfügbares pre-built ROM in der 6551-Version
  • Das Harness ignoriert Schreibzugriffe ab $c100, dem Gast-ROM-Bereich
    • Schreibzugriffe unter 16K nutzen den schnellen Pfad
    • Darüber hinaus wird die geoRAM-Bank per Mask und Shift angepasst
    • Die aktuelle geoRAM-Page wird gecacht, sodass bei Zugriffen auf dieselbe Page die Konfiguration übersprungen wird
  • Kernel und main program sind in diesem Beispiel zusammengeführt
    • Prüft Vorhandensein und Funktion von geoRAM
    • Kopiert das ROM-Image nach geoRAM
    • BRK führt zurück zum Monitor
    • Illegal instruction, user-defined instruction trap und Ähnliches werden wie BRK behandelt
  • Der serial vector des RC2014-ROM wird abgefangen, um ein einfaches Terminal zu emulieren
    • Führt Konvertierung zwischen PETSCII und Terminal-Zeichen durch
    • Verwaltet einen kleinen Cursor
    • Passt Gastregister und Flags entsprechend dem Ergebnis an
    • Mit CTRL-SHIFT-Commodore lässt sich das emulierte System zurücksetzen, wobei der Speicher erhalten bleibt
  • Wenn man beim EhBASIC Cold Start die Speichergröße nicht direkt eingibt, dauert es auf der Kombination aus C64 und geoRAM etwa eine Minute, bis 32768 Byte free gefunden werden
    • Das ROM wurde mit einem Hard Cap bei $8000 gebaut und ist daher tatsächlich auf 32768 Byte begrenzt, selbst wenn mehr vorhanden wäre
    • In $8000-$c0ff kann anderes abgelegt werden
    • EhBASIC akzeptiert keine lowercase commands oder keywords, daher muss alles in Großbuchstaben eingegeben werden
  • Es läuft auch auf einem echten Commodore 128DCR mit 512K-geoRAM-Cartridge
    • Floating-Point-Operationen funktionieren ebenfalls korrekt
    • Bad instructions werden sofort kontrolliert abgefangen
    • Abgesehen vom 256-Byte-Window läuft das System auf dem Bildschirm nicht im 6502-eigenen Adressraum
    • Selbst mit 512K geoRAM lassen sich acht separate 64K-6502-System-Tasks unterbringen

Künftige Verbesserungen und Einsatzmöglichkeiten

  • Verbesserungen, um 6o6 aus ROM laufen zu lassen, sind möglich, erfordern aber Refactoring und könnten langsamer werden; es ist daher eher eine Option
  • 65816-Emulation wird als außerhalb des Scopes betrachtet, aber CMOS-Befehle auf einem NMOS-System zu emulieren, könnte möglich sein
    • Da die ALU verwendet wird, setzt ein NMOS 6502 die Flags im NMOS-Stil, auch wenn er einen CMOS 65C02 emuliert
    • Umgekehrt gilt dasselbe
    • Die Adressierung ist derzeit für die NMOS-CPU-Arbeitsweise geschrieben
  • Beim inline memory macro-Ansatz gibt es Chancen für Peephole Optimization
    • Vor der eigentlichen Assembly könnte ein „post-preprocessor“-Pass eingefügt werden
    • Da die Toolchain dadurch komplexer wird, müsste der allgemeine Nutzen bestätigt werden
  • Einer der ausdrücklich genannten Einsatzzwecke von 6o6 ist es, heruntergeladenen Code auszuführen, ohne die aktuelle Arbeit zu beschädigen
    • Es gibt die Idee, es als Teil eines Gopher-Clients zu nutzen, um Heruntergeladenes dynamisch auszuführen
  • Wenn man ein neues 6502-System selbst entwirft, dürfte es schneller sein, die benötigten Funktionen in Hardware umzusetzen
  • Auf NMOS-CPUs mit wenigen Schutzmechanismen oder wenn zusätzliches Silizium minimiert werden soll, ist 6o6 eine flexible und anpassungsfähige Alternative

Distribution und Lizenz

  • The Incredible KIMplement ist über die Homepage und auf GitHub verfügbar
  • 6o6 ist auf GitHub verfügbar und enthält auch die vier im Artikel behandelten Beispiele
  • Das Update KIMplement 1.0 konzentriert sich auf Aufräumarbeiten für die Veröffentlichung und kleine Bugfixes
  • KIMplement enthält auch Tiny PILOT, bereitgestellt von Dave Hassler
    • Tiny PILOT basiert auf einer Implementierung von Nicholas Vrtis aus dem Jahr 1979 im MICRO magazine, ergänzt um Patches von Bob Applegate und Dave Hassler
    • Dave Hassler hat auch ELIZA aus der Atari-PILOT-Implementierung von Carol Shaw und Harry Stewart aus dem Jahr 1980 portiert
  • KIMplement und 6o6 werden beide unter der Floodgap Free Software License veröffentlicht

1 Kommentare

 
GN⁺ 2024-05-13
Hacker-News-Kommentare
  • Es ist immer faszinierend zu sehen, wie selbst die einfache und eingeschränkte 6502-Architektur auch nach fast 50 Jahren noch immer an neue Grenzen getrieben wird
    In einigen SoCs für extrem preisgünstige Massenmärkte steckt noch immer ein 6502-Kern.

    • Woran liegt das? Vermutlich daran, dass man ein bereits gut funktionierendes Design beibehält und so F&E-Kosten spart.
      Einen 10-Cent-RISC-V-Kern zu schlagen, dürfte nicht leicht sein.
      Zuerst musste ich lachen bei der Vorstellung eines Multicore-SoC mit 6502 6502-Kernen, aber als FPGA-Projekt wäre das wohl ziemlich unterhaltsam.
    • Es gibt eine enorme Menge an Code, den man für fast jede gewünschte Aufgabe einsetzen kann.
      Ich verwende in einem Apple 2 eine 65816 als Erweiterungskarte mit variabler Geschwindigkeit, also den 16-Bit-Nachfolger, aber meistens läuft sie im 8-Bit-Modus. Das funktioniert gut, und auch der Großteil des Bibliothekscodes ist 8-Bit.
      Bei diesen Geschwindigkeiten ist der Chip schnell, besonders wenn man an ein einfaches Modell denkt, bei dem RAM und CPU im 1:1-Takt laufen. In meinem Fall kann ich Code über einen 1-MHz-Bus ausführen, sodass die meisten Mehrzyklus-Operationen pro Speicher-Fetch wie ein einzelner Buszyklus wirken.
      Oder die Karte hat 1 MB RAM, und dieser RAM läuft mit CPU-Geschwindigkeit (0,15–16 MHz). Dadurch ist sie schnell genug, dass auch große Programme in höheren Programmiersprachen mit brauchbarer Geschwindigkeit laufen. Assembler ist natürlich absurd schnell.
      Eine ziemlich spaßige Umgebung, um alles Mögliche auszuprobieren.
  • Kann man GEOS in einem GEOS-Fenster ausführen?

  • Dieser Beitrag stand eine ganze Weile ohne Kommentare herum, und ich dachte, ich würde ihn mir später noch einmal ansehen, aber der Kernpunkt liegt darin, wie ein Commodore 64 ein komplett anderes 6502-basiertes System emuliert.
    „6o6“, also „6502-on-6502“, ist eine vollständige Virtualisierungssoftware für eine NMOS-6502-CPU, die auf einer 6502-CPU läuft, die Ausführung von Gastcode vollständig kontrolliert, einschließlich nicht dokumentierter Opcodes und Jam-Opcode-Traps, und zudem sämtliche Speicherzugriffe abstrahiert.
    Dadurch sind Adress-Remapping, das Abfangen unzulässiger Lese- und Schreibzugriffe und sogar die Ausführung mit vollständigem virtuellem Speicher möglich. Sie besteht nicht nur den vollständigen Funktionstest, sondern virtualisiert sogar sich selbst, das wiederum sich selbst virtualisiert — eine beeindruckende Arbeit nicht nur aus 6502-Sicht, sondern aus jeder Perspektive.
    Es erinnerte mich auch an ein Video darüber, dass der Zilog Z80 einen Protected Mode hat: https://www.youtube.com/watch?v=DLSUAVPKeYk

  • Das erinnerte mich an die Zeit, als ich mir 6502-Assembler zum ersten Mal selbst beigebracht habe. Es gab ein Buch namens „The Visual Computer“, und auf der Diskette war ein Emulator dabei — das war wirklich eine augenöffnende Erfahrung.
    Das PDF des Buchs habe ich gefunden [1], aber ich weiß nicht, ob die Software von der Diskette irgendwo erhalten geblieben ist.
    [1] https://files.commodore.software/reference-material/books/c6...

  • https://archive.fo/2u3Y8