Portierungsbeben 3 nach Rust


Unser Team Immunant liebt Rust und arbeitet aktiv an C2Rust, einem Migrationsframework, das sich um die gesamte Routine der Migration nach Rust kĂŒmmert. Wir bemĂŒhen uns, automatisch Sicherheitsverbesserungen im konvertierten Rust-Code einzufĂŒhren und dem Programmierer zu helfen, dies selbst zu tun, wenn das Framework ausfĂ€llt. ZunĂ€chst mĂŒssen wir jedoch einen zuverlĂ€ssigen Übersetzer erstellen, mit dem Benutzer mit Rust beginnen können. Da das Testen mit kleinen CLI-Programmen langsam veraltet ist, haben wir uns entschlossen, Quake 3 auf Rust zu ĂŒbertragen. Nach ein paar Tagen waren wir höchstwahrscheinlich die Ersten, die Quake3 auf Rust spielten!

Vorbereitung: 3 Quellen beben


Nachdem wir den Quellcode des ursprĂŒnglichen Quake 3 und verschiedener Gabeln studiert hatten, entschieden wir uns fĂŒr ioquake3 . Dies ist eine von der Community erstellte Abzweigung von Quake 3, die weiterhin auf modernen Plattformen unterstĂŒtzt und aufgebaut wird.

Als Ausgangspunkt haben wir uns entschieden, sicherzustellen, dass wir das Projekt in seiner ursprĂŒnglichen Form zusammenstellen können:

$ make release 

Beim Erstellen von ioquake3 werden mehrere Bibliotheken und ausfĂŒhrbare Dateien erstellt:

 $ tree --prune -I missionpack -P "*.so|*x86_64" . └── build └── debug-linux-x86_64 ├── baseq3 │ ├── cgamex86_64.so # client │ ├── qagamex86_64.so # game server │ └── uix86_64.so # ui ├── ioq3ded.x86_64 # dedicated server binary ├── ioquake3.x86_64 # main binary ├── renderer_opengl1_x86_64.so # opengl1 renderer └── renderer_opengl2_x86_64.so # opengl2 renderer 

Unter diesen Bibliotheken können die BenutzeroberflĂ€chen-, Client- und Serverbibliotheken entweder als Quake VM- Assembly oder als native gemeinsam genutzte X86-Bibliotheken kompiliert werden. In unserem Projekt haben wir uns fĂŒr native Versionen entschieden. Das Übersetzen von VMs nach Rust und die Verwendung von QVM-Versionen wĂ€ren viel einfacher, aber wir wollten C2Rust grĂŒndlich testen.

In unserem Transferprojekt haben wir uns auf die BenutzeroberflĂ€che, das Spiel, den Client, den OpenGL1-Renderer und die ausfĂŒhrbare Hauptdatei konzentriert. Wir könnten den OpenGL2-Renderer auch ĂŒbersetzen, haben uns jedoch dafĂŒr entschieden, dies zu ĂŒberspringen, da er eine erhebliche Menge von .glsl Shader- .glsl , die das Build-System als String-Literale in den C-Quellcode einbettet GLSL-Code in Rust-Strings, aber es gibt immer noch keine gute automatisierte Möglichkeit, diese automatisch generierten temporĂ€ren Dateien zu transponieren. Stattdessen haben wir einfach die OpenGL1-Renderer-Bibliothek ĂŒbersetzt und das Spiel gezwungen, sie anstelle des Standard-Renderers zu verwenden. DarĂŒber hinaus haben wir beschlossen, die dedizierten Server- und verpackten Missionsdateien zu ĂŒberspringen, da sie nicht schwer zu ĂŒbertragen sind und fĂŒr unsere Demonstration nicht erforderlich sind.

Transponieren Sie Quake 3


Um die in Quake 3 verwendete Verzeichnisstruktur beizubehalten und den Quellcode nicht zu Ă€ndern, mussten genau die gleichen BinĂ€rdateien wie in der nativen Assembly abgerufen werden, d. H. Vier gemeinsam genutzte Bibliotheken und eine ausfĂŒhrbare Datei.

Da C2Rust die Cargo-Assembly-Dateien erstellt, benötigt jede BinÀrdatei eine eigene Rust-Kiste mit der entsprechenden Cargo.toml Datei.

Damit C2Rust eine Kiste pro AusgabebinĂ€rdatei erstellen kann, wird außerdem eine Liste der BinĂ€rdateien mit den entsprechenden Objekt- oder Quelldateien sowie ein Linkaufruf zum Erstellen jeder BinĂ€rdatei benötigt (um andere Details zu bestimmen, z. B. BibliotheksabhĂ€ngigkeiten).

Wir stießen jedoch schnell auf eine EinschrĂ€nkung, die dadurch verursacht wurde, dass C2Rust den systemeigenen Erstellungsprozess abfĂ€ngt: C2Rust empfĂ€ngt am Eingang eine Kompilierungsdatenbankdatei , die eine Liste der Kompilierungsbefehle enthĂ€lt, die wĂ€hrend der Erstellung ausgefĂŒhrt werden. Diese Datenbank enthĂ€lt jedoch nur Kompilierungsbefehle ohne Linker-Aufrufe. Die meisten Tools, die diese Datenbank erstellen, haben diese absichtliche EinschrĂ€nkung, z. B. cmake with CMAKE_EXPORT_COMPILE_COMMANDS , bear und compiledb . Unserer Erfahrung nach ist das einzige Tool, das build-logger Befehle enthĂ€lt build-logger der von CodeChecker erstellte build-logger , den wir nicht verwendet haben, weil wir erst nach dem Schreiben unserer eigenen Wrapper davon erfahren haben (sie werden unten beschrieben). Dies bedeutete, dass wir zum Kompilieren eines C-Programms mit mehreren BinĂ€rdateien nicht die Datei compile_commands.json verwenden konnten, die mit einem der gĂ€ngigen Tools erstellt wurde.

Aus diesem Grund haben wir unsere eigenen Compiler- und Linker- Wrapper-Skripts geschrieben, die alle Aufrufe des Compilers und des Linkers an die Datenbank compile_commands.json und anschließend in die erweiterte compile_commands.json . Anstelle der ĂŒblichen Montage mit einem Befehl wie:

 $ make release 

Wir haben Wrapper hinzugefĂŒgt, um die Assembly abzufangen mit:

 $ make release CC=/path/to/C2Rust/scripts/cc-wrappers/cc 

Wrapper erstellen ein Verzeichnis mit mehreren JSON-Dateien, eine pro Aufruf. Das zweite Skript sammelt alle in einer neuen compile_commands.json Datei, die sowohl Kompilierungs- als auch Kompilierungsbefehle enthĂ€lt. Dann haben wir C2Rust so erweitert, dass es die Build-Befehle aus der Datenbank liest und fĂŒr jede verknĂŒpfte BinĂ€rdatei eine separate Kiste erstellt. DarĂŒber hinaus liest C2Rust jetzt auch BibliotheksabhĂ€ngigkeiten fĂŒr jede BinĂ€rdatei und fĂŒgt sie automatisch der build.rs Datei der entsprechenden Kiste hinzu.

Zur Verbesserung der Benutzerfreundlichkeit können alle BinÀrdateien gleichzeitig erfasst werden, indem sie im Arbeitsbereich abgelegt werden . C2Rust erstellt die Cargo.toml Datei des Arbeitsbereichs der obersten Ebene, sodass wir das Projekt mit dem einzigen cargo build zum quake3-rs cargo build im quake3-rs :

 $ tree -L 1 . ├── Cargo.lock ├── Cargo.toml ├── cgamex86_64 ├── ioquake3 ├── qagamex86_64 ├── renderer_opengl1_x86_64 ├── rust-toolchain └── uix86_64 $ cargo build --release 

Rauheit beseitigen


Als wir zum ersten Mal versuchten, den ĂŒbersetzten Code zu kompilieren, hatten wir einige Probleme mit den Quake 3-Quellen: Es gab GrenzfĂ€lle, die C2Rust nicht handhaben konnte (weder richtig noch irgendwie).

Array-Zeiger


Mehrere Stellen im ursprĂŒnglichen Quellcode enthalten AusdrĂŒcke, die auf das nĂ€chste Element nach dem letzten Array-Element verweisen. Hier ist ein vereinfachtes C-Codebeispiel:

 int array[1024]; int *p; // ... if (p >= &array[1024]) { // error... } 

Der C-Standard (siehe z. B. C11, Abschnitt 6.5.6 ) ermöglicht es Zeigern auf ein Element, ĂŒber das Ende eines Arrays hinauszugehen. Rust verbietet dies jedoch, auch wenn wir nur die Adresse des Elements nehmen. Beispiele fĂŒr ein solches Muster haben wir in der Funktion AAS_TraceClientBBox .

Der Rust-Compiler signalisierte auch ein Ă€hnliches, aber tatsĂ€chlich G_TryPushingEntity Beispiel in G_TryPushingEntity , wo die bedingte Anweisung die Form > , nicht >= . Ein außerhalb der Grenzen liegender Zeiger wird dann nach dem bedingten Konstrukt dereferenziert, bei dem es sich um einen Speicher-Sicherheitsfehler handelt.

Um dieses Problem in Zukunft zu vermeiden, haben wir den C2Rust-Transpiler so korrigiert, dass er Zeigerarithmetik verwendet, um die Adresse eines Array-Elements zu berechnen, anstatt die Array-Indizierungsoperation zu verwenden. Dank dieses Fixes wird Code, der das Ă€hnliche Muster "Elementadresse am Ende des Arrays" verwendet, jetzt korrekt ĂŒbersetzt und ohne Änderungen ausgefĂŒhrt.

Array-Elemente mit variabler LĂ€nge


Wir haben das Spiel gestartet, um alles zu testen und haben sofort Panik von Rust bekommen:

 thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 4', quake3-client/src/cm_polylib.rs:973:17 

Bei einem Blick auf cm_polylib.c haben wir festgestellt, dass das Feld p in der folgenden Struktur dereferenziert wird:

 typedef struct { int numpoints; vec3_t p[4]; // variable sized } winding_t; 

Das p Feld in der Struktur ist eine Version des flexiblen Array-Members, die vom C99-Standard nicht unterstĂŒtzt wird, aber dennoch von gcc akzeptiert wird. C2Rust erkennt Elemente von Arrays variabler LĂ€nge mit der Syntax C99 ( vec3_t p[] ) und implementiert eine einfache Heuristik, um auch Versionen dieses Musters vor C99 zu identifizieren (Arrays der GrĂ¶ĂŸen 0 und 1 am Ende von Strukturen; wir haben auch mehrere solche Beispiele im Quellcode von ioquake3 gefunden).

Durch Ändern der obigen Struktur in C99-Syntax wurde die Panik beseitigt:

 typedef struct { int numpoints; vec3_t p[]; // variable sized } winding_t; 

Ein Versuch, dieses Muster im allgemeinen Fall (mit von 0 und 1 verschiedenen ArraygrĂ¶ĂŸen) automatisch zu korrigieren, ist Ă€ußerst schwierig, da zwischen gewöhnlichen Arrays und Elementen von Arrays variabler LĂ€nge beliebiger GrĂ¶ĂŸen unterschieden werden muss. Aus diesem Grund empfehlen wir Ihnen, den ursprĂŒnglichen C-Code manuell zu korrigieren, wie wir es mit ioquake3 getan haben.

Gebundene Operanden im Inline-Assembler-Code


Eine weitere /usr/include/bits/select.h war der folgende C-Assembler-Assembler-Code aus dem /usr/include/bits/select.h :

 # define __FD_ZERO(fdsp) \ do { \ int __d0, __d1; \ __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS \ : "=c" (__d0), "=D" (__d1) \ : "a" (0), "0" (sizeof (fd_set) \ / sizeof (__fd_mask)), \ "1" (&__FDS_BITS (fdsp)[0]) \ : "memory"); \ } while (0) 

Definieren der internen Version des __FD_ZERO . Diese Definition wirft einen seltenen Grenzfall fĂŒr gcc : tied-Operanden-E / A mit unterschiedlichen GrĂ¶ĂŸen auf. Der Ausgabeoperator "=D" (__d1) bindet das edi Register als 32-Bit-Wert an die Variable __d1 , und "1" (&__FDS_BITS (fdsp)[0]) bindet dasselbe Register an die Adresse fdsp->fds_bits als 64-Bit-Zeiger. gcc und clang beheben dieses MissverhĂ€ltnis. rdi das 64-Bit- rdi Register verwenden und dessen Wert __d1 , bevor Sie den Wert __d1 , verwendet Rust standardmĂ€ĂŸig die LLVM-Semantik, in der ein solcher Fall nicht definiert ist. In den Debug-Builds (nicht in den Release-Builds, die sich gut verhalten haben) haben wir gesehen, dass beide Operanden dem edi Register zugewiesen werden können, wodurch der Zeiger vor dem eingebauten Assembler-Code auf 32 Bit gekĂŒrzt wird, was zu Fehlern fĂŒhrt.

Da rustc den eingebauten Rust-Assembler-Code mit nur geringen Änderungen an LLVM weitergibt, haben wir uns entschlossen, diesen speziellen Fall in C2Rust zu beheben. Wir haben eine neue Kiste c2rust-asm-casts implementiert, die dieses Problem dank des Rust-Typ-Systems c2rust-asm-casts Dabei werden Eigenschaften- und Hilfsfunktionen verwendet, die gebundene Operanden automatisch erweitern und auf eine interne GrĂ¶ĂŸe c2rust-asm-casts , die groß genug ist, um beide Operanden aufzunehmen. Der obige Code wird korrekt in Folgendes ĂŒbersetzt:

 let mut __d0: c_int = 0; let mut __d1: c_int = 0; // Reference to the output value of the first operand let fresh5 = &mut __d0; // The internal storage for the first tied operand let fresh6; // Reference to the output value of the second operand let fresh7 = &mut __d1; // The internal storage for the second tied operand let fresh8; // Input value of the first operand let fresh9 = (::std::mem::size_of::<fd_set>() as c_ulong).wrapping_div(::std::mem::size_of::<__fd_mask>() as c_ulong); // Input value of the second operand let fresh10 = &mut *fdset.__fds_bits.as_mut_ptr().offset(0) as *mut __fd_mask; asm!("cld; rep; stosq" : "={cx}" (fresh6), "={di}" (fresh8) : "{ax}" (0), // Cast the input operands into the internal storage type // with optional zero- or sign-extension "0" (AsmCast::cast_in(fresh5, fresh9)), "1" (AsmCast::cast_in(fresh7, fresh10)) : "memory" : "volatile"); // Cast the operands out (types are inferred) with truncation AsmCast::cast_out(fresh5, fresh9, fresh6); AsmCast::cast_out(fresh7, fresh10, fresh8); 

Es ist anzumerken, dass fĂŒr diesen Code keine Typen fĂŒr Eingabe- und Ausgabewerte in der Assembly des Assembler-Codes fresh8 . fresh6 fresh8 lösen, verlassen Sie sich stattdessen auf diese, um Rust-Typen (hauptsĂ€chlich fresh6 und fresh8 ) fresh8 .

Ausgerichtete globale Variablen


Die letzte Fehlerquelle war die folgende globale Variable, in der die SSE-Konstante gespeichert ist:

 static unsigned char ssemask[16] __attribute__((aligned(16))) = { "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00" }; 

Rust unterstĂŒtzt derzeit das Ausrichtungsattribut fĂŒr Strukturtypen, jedoch nicht fĂŒr globale Variablen, d. H. static Elemente. Wir haben ĂŒberlegt, wie wir dieses Problem im allgemeinen Fall lösen können, entweder in Rust oder in C2Rust. In ioquake3 haben wir uns jedoch entschieden, es manuell mit einer kurzen Patch- Datei zu beheben. Diese Patch-Datei ersetzt die Entsprechung von Rust ssemask Folgendes:

 #[repr(C, align(16))] struct SseMask([u8; 16]); static mut ssemask: SseMask = SseMask([ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, ]); 

Laufen quake3-rs


Beim cargo build --release werden BinĂ€rdateien erstellt, die jedoch unter target/release mit einer Verzeichnisstruktur erstellt werden, die von der ioquake3 BinĂ€rdatei nicht erkannt wird. Wir haben ein Skript geschrieben , das symbolische Links im aktuellen Verzeichnis erstellt, um die korrekte Verzeichnisstruktur wiederherzustellen (einschließlich Links zu .pk3 Dateien, die .pk3 enthalten):

 $ /path/to/make_quake3_rs_links.sh /path/to/quake3-rs/target/release /path/to/paks 

Der Pfad /path/to/paks sollte auf das Verzeichnis verweisen, das die .pk3 Dateien enthÀlt.

Jetzt lass uns das Spiel starten! Wir mĂŒssen +set vm_game 0 usw. ĂŒbergeben, damit wir diese Module als gemeinsam genutzte Rust-Bibliotheken laden und nicht als QVM-Assembly sowie als cl_renderer , um den OpenGL1-Renderer zu verwenden.

 $ ./ioquake3 +set sv_pure 0 +set vm_game 0 +set vm_cgame 0 +set vm_ui 0 +set cl_renderer "opengl1" 

Und ...


Wir haben Quake3 auf Rust gestartet!


Hier ist ein Video, wie wir Quake 3 transponieren, das Spiel herunterladen und ein bisschen davon spielen:


Sie können die transpilierten Quellen in der transpiled Filiale unseres transpiled studieren. Es gibt auch einen refactored Zweig, der dieselben Quellen mit mehreren vorab angewendeten Umgestaltungsbefehlen enthÀlt .

Wie transponieren


Wenn Sie versuchen möchten, Quake 3 zu transponieren und selbst auszufĂŒhren, mĂŒssen Sie Ihre eigenen Quake 3-Spiel- oder Demo-Ressourcen aus dem Internet bereitstellen. Sie mĂŒssen auch C2Rust installieren (zum Zeitpunkt des Schreibens ist die erforderliche nĂ€chtliche Version nightly-2019-12-05 , es wird jedoch empfohlen, im C2Rust- Repository oder in crates.io nach der neuesten Version zu suchen):

 $ cargo +nightly-2019-12-05 install c2rust 

und Kopien unserer C2Rust- und ioquake3-Repositorys:

 $ git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dcbbb5a89cbbb5a8b4a9bef2bfb3b1">[email protected]</a>:immunant/c2rust.git $ git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dcbbb5a89cbbb5a8b4a9bef2bfb3b1">[email protected]</a>:immunant/ioq3.git 

Alternativ zur Installation von c2rust mit dem obigen Befehl können Sie C2Rust manuell mit cargo build --release . In jedem Fall wird das C2Rust-Repository weiterhin benötigt, da es die Compiler-Wrapper-Skripte enthÀlt, die zur Transponierung von ioquake3 erforderlich sind.

Wir haben ein Skript veröffentlicht , das automatisch C-Code transportiert und den ssemask Patch ssemask . FĂŒhren Sie dazu den folgenden Befehl auf der obersten Ebene des ioq3 Repositorys aus:

 $ ./transpile.sh </path/to/C2Rust repository> </path/to/c2rust binary> 

Dieser Befehl sollte ein Unterverzeichnis quake3-rs mit Rust-Code erstellen, fĂŒr das Sie dann den cargo build --release und die ĂŒbrigen oben beschriebenen Schritte ausfĂŒhren können.

Source: https://habr.com/ru/post/de483142/


All Articles