1 Punkte von GN⁺ 2024-08-26 | 1 Kommentare | Auf WhatsApp teilen
  • Dozer ist ein auf reinem C basierender Rust-Compiler, der Rust in einer früheren Bootstrap-Phase nutzbar machen soll, und wird ohne C++, flex, yacc oder Makefile geschrieben
  • Der offizielle Compiler rustc ist in Rust geschrieben, daher wird eine neue Version mit einer früheren rustc-Version gebaut; diese Kette reicht zurück bis zum ersten Rust-Compiler, der in OCaml geschrieben war, und weiter bis zu Schichten aus Guile und C
  • Der Linux-Bootstrap von Bootstrappable Builds startet mit einem 512-Byte-Binary-Seed und erweitert sich über einfache Compiler, Shell, ein C-Subset, TinyCC, yacc, coreutils, Bash, autotools, GCC bis hin zu Linux
  • Derzeit taucht Rust erst in einer späten Phase über mrustc auf, das in C++ geschrieben ist und rustc 1.56 kompiliert; vor der Einführung von C++ ist Rust daher schwer nutzbar
  • Dozer zielt auf einen mit TinyCC bootstrappbaren Rust-Compiler, der später zu libcore, dem Cranelift-Backend von rustc, einem cargo-Ersatzwerkzeug und schließlich zum Neuaufbau des kanonischen rustc/cargo führen soll

Ziele und Einschränkungen von Dozer

  • Dozer ist ein Rust-Compiler, der in reinem C geschrieben wird
  • Er verwendet kein C++ und auch nicht flex, yacc oder Makefile
  • Das Hauptziel ist, einen Compiler zu bauen, mit dem sich Rust aus C heraus bootstrappen lässt
  • Insbesondere soll er mit TinyCC bootstrappbar sein; dabei wird angenommen, dass das System außer einem C-Compiler und einer sehr einfachen Shell kaum nützliche Werkzeuge besitzt

Das Problem, dass der Rust-Compiler sich selbst baut

  • Um Rust-Code auszuführen, muss er kompiliert werden, und cargo build ruft intern normalerweise rustc auf
  • rustc selbst ist ebenfalls ein in Rust geschriebener Rust-Compiler, daher wird ein neues rustc mit einer älteren rustc-Version kompiliert
    • rustc 1.80.0 wird mit rustc 1.79.0 kompiliert
    • Diese Kette setzt sich mit noch älteren Versionen wie rustc 1.78.0 fort
  • Die frühen Stufen reichen bis zu Rust 0.7 zurück; der Compiler zu diesem Zeitpunkt war in OCaml geschrieben
  • Da auch ein OCaml-Compiler benötigt wird, führt die Bootstrap-Kette weiter zu Implementierungen in anderen Sprachen
    • camlboot kann mithilfe von Guile einen OCaml-Compiler kompilieren
    • Der Guile-Interpreter ist in C geschrieben

Die unteren Stufen der Bootstrappable-Builds-Kette

  • Bootstrappable Builds beschreibt einen Ablauf, bei dem ein vollständiges System aus einem kleinen Binary-Seed heraus gebootstrapped wird
  • Der Linux-Bootstrap-Prozess beginnt mit einem 512-Byte-Binary-Seed
    • Dieser Seed enthält einen extrem einfachen Compiler, der Hexadezimalzahlen entgegennimmt und die entsprechenden Rohbytes ausgibt
    • Eine Folge von Hex-Bytes gilt technisch ebenfalls als analysierbarer Quellcode, solange Kommentare und Leerraum außen vor bleiben
  • Die folgenden Stufen bauen schrittweise Werkzeuge auf höherem Niveau
    • ein sehr einfaches Betriebssystem
    • eine grundlegende Shell
    • ein etwas weiterentwickelter Compiler
    • eine Stufe, die wie Assemblercode aussieht
    • ein sehr grundlegendes C-Subset
    • ein fortgeschrittenerer C-Compiler, der in diesem C-Subset geschrieben ist
  • Nach einigen Schritten lässt sich TinyCC kompilieren; danach folgen yacc, grundlegende coreutils, Bash, autotools, GCC und Linux
  • Alle Stufen sind in live-bootstrap parts.rst aufgeführt

Rust erscheint in der Bootstrap-Kette viel zu spät

  • Aktuell erscheint Rust in diesem Prozess erst sehr spät
  • Verwendet wird mrustc, eine alternative Rust-Implementierung in C++
  • mrustc kann rustc 1.56 kompilieren und von dort aus weiter bis zu modernem Rust-Code
  • Wenn C++ in die Bootstrap-Kette eingeführt wird, ist der Bootstrap allerdings praktisch schon fast abgeschlossen
  • Wenn Rust schon vor der Einführung von C++ nutzbar sein soll, wird ein aus C heraus bootstrappbarer Rust-Compiler benötigt

Aktueller Implementierungsstand von Dozer

  • An Dozer wird seit etwa zwei Monaten gearbeitet, ohne Erweiterungen zu verwenden
  • Der Compiler lässt sich derzeit sowohl mit TinyCC als auch mit cproc problemlos kompilieren
  • Als Backend wird QBE verwendet
  • Die Implementierung befindet sich noch in einer frühen Phase
    • Der Lexer ist fertig
    • Der Parser ist zu großen Teilen implementiert
    • Makro-/Modulerweiterung wird so weit wie möglich nach hinten verschoben
    • Die Typprüfung unterstützt derzeit nur i32
    • Die Codegenerierung ist noch grob
  • Der folgende Rust-Code lässt sich derzeit erfolgreich kompilieren
fn rust_main() -> i32 {
    (2 - 1) * 6 + 3
}

Der Plan bis hin zu rustc

  • Ziel ist, Dozer schrittweise so weiterzuentwickeln, dass zunächst grundlegende Beispiele mit libc und danach libcore sowie schließlich rustc selbst kompiliert werden können
  • Für die Kompilierung von rustc soll das Cranelift-Backend verwendet werden
    • Das Cranelift-Backend ist vollständig in Rust geschrieben
    • Da kein C++ vorausgesetzt wird, kann LLVM nicht kompiliert werden
  • Geplant ist außerdem ein cargo-Ersatzwerkzeug, mit dem sich Rust-Pakete über Dozer kompilieren lassen
  • In den rustc-Quellen müssen automatisch generierte Dateien gefunden und entfernt werden
    • Nach den Regeln des Bootstrappable-Projekts ist automatisch generierter Code nicht zulässig
  • Das Endziel ist, rustc und cargo zu kompilieren und anschließend mit dem selbst kompilierten rustc/cargo das kanonische rustc/cargo erneut zu kompilieren
  • Das Projekt sei die bisher schwierigste Aufgabe; auch wenn Zweifel an der Fertigstellbarkeit bestehen, wolle man es weiter versuchen

1 Kommentare

 
GN⁺ 2024-08-26
Meinungen auf Hacker News
  • Wenn man Rust bootstrappen wollte, würde man vermutlich ein Proto-Rust in C bauen, das weniger Funktionen hat als vollständiges Rust, und mit diesem Proto-Rust dann einen vollständigen Rust-Compiler schreiben
    Zum Beispiel hätte Proto-Rust keinen Borrow Checker, nur eingeschränkte oder gar keine Makro-Unterstützung, würde Speicher vielleicht nicht freigeben und müsste auch keinen guten Code erzeugen
    Im Grunde wäre es eher C mit Rust-Syntax, aber aus Sicht von Rust-Fans wirkt das besser, als einen Rust-Compiler in „C mit C-Syntax“ zu schreiben, worauf dieses Projekt abzielt
    Ich frage mich, warum man nicht diesen Weg gewählt hat

    • Nebenbei: Auch mrustc, der bekannte bestehende Nicht-Rust-Rust-Compiler, hat bereits keinen Borrow Checker
      Wenn man den Borrow Checker entfernt, gehen korrekte Programme nicht kaputt; es lassen sich nur massenhaft falsche Programme kompilieren
      Der Haupteinsatzzweck von mrustc ist, rustc zu kompilieren, und da man bereits weiß, dass rustc sich selbst ohne Borrow-Checker-Fehler kompilieren kann, ist das in Ordnung
    • Bei Mozart/Oz hat man das tatsächlich so gemacht. Es gibt einen in Scala geschriebenen Proto-Oz-Compiler, mit dem der eigentliche, in Oz geschriebene Compiler kompiliert wird
      Da der Scala-Compiler ineffizienten Code erzeugt, kompiliert man den echten Compiler anschließend noch einmal mit sich selbst
      So erhält man am Ende einen effizienten echten Compiler, der guten Code erzeugt, und dieser Prozess ist Teil des Standard-Builds der Sprache
      https://github.com/mozart/mozart2
    • Dann verwendet man am Ende aber zwei Compiler, und mir ist nicht klar, was man außer zusätzlicher Arbeit tatsächlich gewinnt
  • Ich baue als Hobby einen C-Compiler in Rust und nenne ihn scherzhaft Small C Compiler, weil Rust natürlich schwergewichtiger ist als C. Das ist eine Parodie auf „Tiny C Compiler“
    Als Backend verwende ich Cranelift, aber die gesamte Compiler-Architektur soll über viele Traits austauschbar und leicht zu hacken sein
    Ich habe nicht vor, ihn als Open Source zu veröffentlichen, bevor er zumindest so weit funktioniert, dass er printf("%s", "Hello World!") verarbeitet
    Ich wollte den Präprozessor und den Parser implementieren und habe mich wegen des berüchtigten typedef-Problems auch bei rust-peg und HimeCC eingebracht
    Ich weiß, dass man in der Industrie Symboltabellen verwendet, um den typedef-Kontext beizubehalten, aber es gab die Einschränkung, dass man weiter unten liegende Typen nicht lesen kann. Ich frage mich, wie die akademische Lösung aussieht; mir fällt nur transaktionaler Speicher ein
    Wenn etwas Hilfreiches dabei herauskommt, werde ich es wohl irgendwann veröffentlichen

  • Wirklich cool; interessant ist, dass es dieselbe Art von Bootstrapping-Problem auch bei Hardware gibt
    Wodurch werden Computer gebaut? Durch zuvor gebaute Computer und die Software, die darauf läuft. Je mehr man darüber nachdenkt, desto spannender wird es

    • Dasselbe Bootstrapping-Problem gibt es bei allem. Wodurch werden Straßen gebaut? Durch Baumaschinen. Aber wenn es noch keine Straße gibt, wie bringt man diese Baumaschinen dann zur Baustelle?
      Vor ein paar Monaten habe ich jemanden getroffen, der bei einem Startup arbeitet, das Materiallieferung/Fulfillment für Bauprojekte macht
      Dafür braucht man andere Expertise als bei normalem Versand à la Amazon, nicht nur weil Materialien oft ungewöhnliche oder gefährliche physikalische Eigenschaften haben, sondern auch weil der Lieferort häufig noch gar keine Adresse hat
      Das ist lösbar, scheint aber Expertise zu erfordern, die über die üblichen Fähigkeiten moderner Zustelldienste hinausgeht
    • Ich habe einmal bei einem Unternehmen gearbeitet, das Rechenzentren baut, und dort wollte man Software so weit entwickeln, dass man ein gesamtes Rechenzentrum mit einem einzigen Laptop hochfahren kann
      Der Grund war die Zusammenarbeit mit europäischen Unternehmen und der Nachweis gegenüber Regulierungsbehörden, dass es keine Backdoors gibt
      Das war ein sehr interessantes, aber extrem schwieriges Problem; unser Team war nur indirekt beteiligt, arbeitete aber daran, Daten über einen Proxy zu leiten, der sicherstellt, dass alle Daten auditierbar sind und nichts gesendet wird, was nicht gesendet werden darf
      Ich verließ das Unternehmen, bevor es fertig war, und hörte später, dass es verworfen wurde, weil es zu schwierig war
    • Wenn man sich die alten oktalen Assembly-Opcodes der Cray-1 oder die Word-Opcodes des IBM System/360 ansieht, erkennt man, dass sie erstaunlich einfach gestaltet waren, sodass Menschen Opcode-Bytes direkt schreiben und von Hand assemblieren konnten
      Später erschien x86 ohne riesiges Budget oder große Abnehmer, und man entwarf das Assembly möglichst effizient und dicht
      Dadurch gingen Eigenschaften verloren, die man auf anderen Maschinen bequem haben konnte
    • Das ist einer der schönsten Aspekte solcher Bootstrapping-Projekte und von reproduzierbaren Builds
      Theoretisch kann man aus Einzelbauteilen selbst einen sehr einfachen Computer bauen
      Er wäre groß, ineffizient und unglaublich langsam, aber man könnte ihn dazu bringen, einer bestimmten Instruction Set Architecture zu folgen, und darauf ein Bootstrap-Programm bauen
      Dann könnte man argumentieren, dass das Ergebnis eines vollständig verständlichen schlechten Computers mit dem Ergebnis moderner Hardware übereinstimmt, der man nicht vollständig vertraut
    • Auch auf der Ebene der menschlichen Zivilisation ist das ein interessanter Gedanke. Wenn die Menschheit somehow vom heutigen Stand aus in die Steinzeit zurückversetzt würde, könnten wir wieder auf den heutigen Stand kommen?
      Das ist eine Art Bootstrapping-Problem. Zum Beispiel sind die heutigen Ölvorkommen schwerer zu erschließen als vor 100 Jahren, und ich frage mich, ob man sich bis dorthin wieder hochbootstrappen könnte
  • Es war etwas nervig, dass ich viermal Links folgen musste, um eine übergeordnete Begründung zu finden, die die Vorteile von Bootstrapping erklärt.
    Ich hatte erwartet, dass der „Why“-Teil des Titels genau das behandelt.
    https://bootstrappable.org/benefits.html

    • Es kann schwierig sein zu erklären, warum Bootstrapping wichtig ist. Deshalb habe ich auch in das README meines Bootstrapping-Compilers einen „Why?“-Abschnitt aufgenommen.
      Sicherheit ist ein wichtiger Grund und der Punkt, den das bootstrappable-Team hauptsächlich betont.
      Um das trusting-trust-Problem und Angriffe wie die jüngste xz-Backdoor zu vermeiden, muss man alles aus reinem Quellcode bootstrappen können.
      Sie entfernen sogar alle vorab generierten Dateien, sodass nur noch von Hand Geschriebenes und Überprüfbares als Abhängigkeit bleibt. Beim Python-Bootstrapping wird es zum Beispiel ziemlich kompliziert, weil der Quellcode Code enthält, der von Python-Skripten generiert wurde.
      Mich interessiert dagegen eher der Aspekt der kulturellen Bewahrung. Ich möchte moderne Medien an Orten wie dem Arctic World Archive für künftige Archäologen bewahren, aber das ist bedeutungslos, wenn es keine Möglichkeit gibt, sie zu dekodieren.
      Man kann Spezifikationen bewahren, aber man kann nicht erwarten, dass sie x265 und alles Nötige von Grund auf implementieren. Wenn man Binärdateien bewahrt, müsste man tausend Jahre alte Hardware betreiben oder eine tausend Jahre alte CPU virtualisieren.
      Man könnte ihnen eine einfache Lisp-Definition und darauf laufenden Code geben, aber wer implementiert schon x265 in einfachem Lisp? Das ist nicht realistisch.
      Deshalb habe ich in meinem Projekt eine einfache virtuelle Maschine gebaut und darauf C gebootstrappt.
      Sie lässt sich nicht nur auf heutige Architekturen, sondern auch auf künftige oder außerirdische Architekturen sehr einfach portieren. Künftige Archäologen oder außerirdische Zivilisationen könnten an einem Tag die VM implementieren, darauf den C-Bootstrap laufen lassen und anschließend ffmpeg und Ähnliches kompilieren, um unsere Medien zu dekodieren.
      Es gibt keine Blackboxes; alles ist debugbar, überprüfbar und offener, von Hand geschriebener Quellcode.
      https://github.com/ludocode/onramp?tab=readme-ov-file#why-bo...
      https://en.wikipedia.org/wiki/Arctic_World_Archive
  • Ich bin etwas verwirrt. Erst in der Mitte des Artikels wird klar, warum die im Titel beschriebene Reise begonnen wurde: Der Kern ist, dass das Bootstrapping praktisch schon abgeschlossen ist, sobald C++ in die Bootstrap-Chain kommt, sodass es keine Möglichkeit gibt, vorher Rust zu verwenden, selbst wenn man das möchte.
    Daher scheint die Absicht zu sein, einen Rust-Compiler zu haben, der in C, genauer gesagt von TinyCC aus, auf einem System gebootstrappt werden kann, von dem man annimmt, dass es noch keine nützlichen Werkzeuge hat.
    Das widerspricht aber den Annahmen am Anfang. rustc wird von 1.80.0 mit 1.79.0 kompiliert, 1.79.0 mit 1.78.0 und so weiter zurück bis 0.7; der Compiler zu diesem Zeitpunkt war in OCaml geschrieben.
    Außerdem wurde erwähnt, dass es ein Projekt gibt, das erfolgreich einen OCaml-Compiler mit Guile kompiliert, und dass der Guile-Interpreter in C geschrieben ist.
    Dann gibt es den vom Autor gewünschten C++-freien Pfad im Grunde bereits; er ist nur nicht der Pfad, den das rustc-Team im Alltag nutzt.
    Am Ende ist die Motivation nicht klar. Geht es darum, einen besseren C-basierten Bootstrap-Prozess zu schaffen, ihn zum alltäglichen Bootstrap-Verfahren von rustc zu machen, warum die C++-Stufe entfernt werden soll, oder warum eine C-Stufe bevorzugt wird?
    Wenn es einfach darum geht, es machen zu wollen, ist das in Ordnung, aber selbst nach einem ziemlich langen Artikel ist mir kein anderer Zweck wirklich klar.

    • Rust von Guile und dem Rust-0.7-Compiler aus zu bootstrappen, ist technisch zwar möglich, aber dafür muss man den Rust-Compiler etwa 100-mal neu kompilieren.
      Jeder Schritt dauert mehrere Stunden, und da 1.80 1.79 benötigt, 1.79 1.78 und so weiter, kann man bis 0.7 keinen Schritt überspringen.
      Selbst vollständig automatisiert könnte dieses Bootstrapping Monate dauern.
      Außerdem haben die frühen rustc-Versionen meines Wissens nur LLVM ausgegeben, sodass man ohnehin einen C++-Compiler bootstrappen müsste, um LLVM zu kompilieren.
      Wenn man einen C++-Compiler hat, kann man einfach mrustc kompilieren. Derzeit unterstützt mrustc nur bis rustc 1.54, man müsste also trotzdem durch etwa 35 Versionen hindurch kompilieren.
      Dieser gesamte Prozess ist unpraktisch. Das Ziel von Dozer ist, einen kleinen C-Compiler zu bootstrappen, Dozer zu kompilieren und dann direkt ein aktuelles rustc zu kompilieren.
      So bekommt man Rust unmittelbar, ohne C++ oder Zwischenschritte zu bootstrappen.
  • Wenn man GCC 4 und binutils von den ursprünglichen Build-Skripten trennen könnte, ließe sich vermutlich etwa die Hälfte der Liste streichen.
    Ein beträchtlicher Teil der Einträge dort besteht nur darin, autoconf-artige Werkzeuge und deren Abhängigkeiten immer wieder neu zu bauen.
    https://github.com/fosslinux/live-bootstrap/blob/master/part...

  • Ich verstehe den Punkt nicht ganz. Um neue Binärdateien zu erzeugen, die auf der Zielmaschine laufen, muss rustc die Zielarchitektur unterstützen.
    Wenn man diese Unterstützung zu rustc hinzugefügt hat, kann man rustc einfach sich selbst bauen lassen.

    • Es geht weniger um die Unterstützung neuer Architekturen als darum, einen viel kürzeren und auditierbaren Bootstrap-Prozess zu haben.
  • Manchmal stelle ich mir vor, einen C++-Interpreter oder -Compiler in Scheme zu schreiben.
    Direkt von Scheme zum aktuellen GCC zu kommen, könnte eine enorme Abkürzung sein.
    Aber der gängigen Auffassung nach gilt das Schreiben eines C++-Compilers als nahezu unmöglich. Zum Lernen wäre es trotzdem wohl hilfreich.

  • Wenn man den gesamten Stack ab dem Sub-Assembler betrachtet: Könnte das ein Weg sein, das trusting trust-Problem zu umgehen?
    https://www.cs.cmu.edu/~rdriley/487/papers/Thompson_1984_Ref...

    • Das geht nur, wenn man alles auditiert und den gesamten Prozess selbst ausführt.
      Trotzdem gibt es Dinge wie https://en.m.wikipedia.org/wiki/Underhanded_C_Contest, und bei einigen der Einreichungen hätte ich sie wohl übersehen, selbst wenn ich sie auditiert hätte.
    • War das nicht der Kernpunkt?
  • Als ich ein wenig C gelernt habe, habe ich nachgeschaut, wie Leute in C Dinge im Stil von C++ machen, und Implementierungen von Objekten, Exceptions und Concurrency gesehen.
    Wenn mrustc in C++ geschrieben ist, wäre es dann nicht einfacher, den funktionierenden C++-Code nach C zu portieren, indem man solche C++-artigen C-Primitiven verwendet?
    Es scheint auch möglich, die starke Interoperabilität zwischen C++ und C zu nutzen und den Code schrittweise zu migrieren.
    Natürlich weiß ich, dass das eine schwierige Portierung mit vielen Fallstricken ist. Man muss aber bedenken, dass der Vergleichsmaßstab darin besteht, einen Rust-Compiler in C neu zu schreiben.
    Mir fallen auch frühere C++-to-C-Compiler ein. Ich weiß nicht, ob es die noch gibt.
    Auch heute wären Rust-to-C/C++ und ein C++-to-menschlich-lesbares-C-Compiler wohl nützlich, weil man damit die Sicherheitsvorteile der einen Seite mit dem Tool-Ökosystem der anderen kombinieren könnte.