Notre Ă©quipe Immunant aime Rust et travaille activement sur C2Rust, un cadre de migration qui prend en charge toute la routine de migration vers Rust. Nous nous efforçons d'introduire automatiquement des amĂ©liorations de sĂ©curitĂ© dans le code Rust converti et d'aider le programmeur Ă le faire lui-mĂȘme lorsque le framework Ă©choue. Cependant, tout d'abord, nous devons crĂ©er un traducteur fiable qui permet aux utilisateurs de dĂ©marrer avec Rust. Les tests sur les petits programmes CLI deviennent lentement obsolĂštes, nous avons donc dĂ©cidĂ© de transfĂ©rer Quake 3. vers Rust. AprĂšs quelques jours, nous Ă©tions probablement les premiers Ă jouer Ă Quake3 sur Rust!
Préparation: Quake 3 sources
AprÚs avoir étudié le code source du Quake 3 original et diverses fourches, nous nous sommes installés sur
ioquake3 . Il s'agit d'un fork créé par la communauté de Quake 3, qui est toujours pris en charge et construit sur des plateformes modernes.
Comme point de départ, nous avons décidé de nous assurer que nous pouvons assembler le projet dans sa forme originale:
$ make release
Lors de la construction d'ioquake3, plusieurs bibliothÚques et fichiers exécutables sont créés:
$ tree --prune -I missionpack -P "*.so|*x86_64" . âââ build âââ debug-linux-x86_64 âââ baseq3 â âââ cgamex86_64.so
Parmi ces bibliothĂšques, l'interface utilisateur, les bibliothĂšques client et serveur peuvent ĂȘtre compilĂ©es soit en tant qu'assembly
VM Quake , soit en tant que bibliothÚques partagées natives X86. Dans notre projet, nous avons décidé d'utiliser des versions natives. Traduire des machines virtuelles en Rust et utiliser des versions QVM serait beaucoup plus simple, mais nous voulions tester en profondeur C2Rust.
Dans notre projet de transfert, nous nous sommes concentrés sur l'interface utilisateur, le jeu, le client, le moteur de rendu OpenGL1 et le principal exécutable. Nous pourrions également traduire le moteur de rendu OpenGL2, mais nous avons décidé de l'ignorer car il utilise une quantité importante de
.glsl
shader
.glsl
, que le systÚme de construction incorpore sous forme de littéraux de chaßne dans le code source C. AprÚs la compilation, nous ajouterons la prise en charge des scripts de construction pour l'intégration Code GLSL en chaßnes Rust, mais il n'y a toujours pas de bon moyen automatisé de transposer ces fichiers temporaires générés automatiquement. Donc, à la place, nous venons de traduire la bibliothÚque de rendu OpenGL1 et avons forcé le jeu à l'utiliser à la place du rendu par défaut. De plus, nous avons décidé d'ignorer le serveur dédié et les fichiers de mission packagés, car ils ne seront pas difficiles à transférer et ils ne sont pas nécessaires pour notre démonstration.
Transposer le séisme 3
Afin de conserver la structure de rĂ©pertoires utilisĂ©e dans Quake 3 et de ne pas changer le code source, nous avions besoin d'obtenir exactement les mĂȘmes fichiers binaires que dans l'assembly natif, c'est-Ă -dire quatre bibliothĂšques partagĂ©es et un exĂ©cutable.
Ătant donnĂ© que C2Rust crĂ©e les fichiers d'assemblage Cargo, chaque binaire nĂ©cessite sa propre caisse Rust avec le fichier
Cargo.toml
correspondant.
Pour que C2Rust crée une caisse par fichier binaire de sortie, il aura également besoin d'une liste de fichiers binaires avec l'objet ou les fichiers source correspondants, ainsi qu'un appel de l'éditeur de liens utilisé pour créer chaque fichier binaire (utilisé pour déterminer d'autres détails, par exemple, les dépendances de bibliothÚque).
Cependant, nous avons rapidement rencontré une limitation causée par la façon dont C2Rust intercepte le processus de génération natif: C2Rust reçoit en entrée un fichier de
base de données de compilation qui contient une liste de commandes de compilation qui sont exécutées pendant la génération. Cependant, cette base de données contient
uniquement des commandes de compilation sans appels de l'éditeur de liens. La plupart des outils créant cette base de données ont cette limitation intentionnelle, par exemple
cmake
avec
CMAKE_EXPORT_COMPILE_COMMANDS
,
bear
et
compiledb
. D'aprÚs notre expérience, le seul outil qui inclut des commandes de
build-logger
est le
build-logger
créé par
CodeChecker
, que nous n'avons pas utilisé car nous ne l'avons appris qu'aprÚs avoir écrit nos propres wrappers (ils sont décrits ci-dessous). Cela signifiait que pour compiler un programme C avec plusieurs fichiers binaires, nous ne pouvions pas utiliser le fichier
compile_commands.json
créé par l'un des outils courants.
Par conséquent, nous avons écrit nos propres scripts de wrapper de
compilateur et de
lieur qui transfÚrent tous les appels vers le compilateur et le lieur vers la base de données, puis les convertissons en
compile_commands.json
étendu. Au lieu de l'assemblage habituel, utilisez une commande comme:
$ make release
nous avons ajouté des wrappers pour intercepter l'assembly avec:
$ make release CC=/path/to/C2Rust/scripts/cc-wrappers/cc
Les wrappers créent un répertoire de plusieurs fichiers JSON, un par appel. Le deuxiÚme
script les collecte tous dans un nouveau fichier
compile_commands.json
, qui contient à la fois les commandes de compilation et de compilation. Ensuite, nous avons étendu C2Rust pour qu'il lise les commandes de construction de la base de données et crée une caisse séparée pour chaque binaire lié. De plus, C2Rust lit désormais les dépendances de bibliothÚque pour chaque fichier binaire et les ajoute automatiquement au fichier
build.rs
de la caisse correspondante.
Pour amĂ©liorer la commoditĂ©, tous les fichiers binaires peuvent ĂȘtre collectĂ©s Ă la fois en les plaçant dans l'
espace de travail . C2Rust crée le fichier d'espace de travail de niveau supérieur
Cargo.toml
, afin que nous puissions créer le projet avec la seule
cargo build
dans le
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
Ălimine la rugositĂ©
Lorsque nous avons essayé de compiler le code traduit pour la premiÚre fois, nous avons rencontré quelques problÚmes avec les sources de Quake 3: il y avait des cas limites que C2Rust ne pouvait pas gérer (ni correctement, ni du tout en quelque sorte).
Pointeurs de tableau
Plusieurs emplacements dans le code source d'origine contiennent des expressions qui pointent vers l'élément suivant aprÚs le dernier élément du tableau. Voici un exemple de code C simplifié:
int array[1024]; int *p;
La norme C (voir, par exemple,
C11, Section 6.5.6 ) permet aux pointeurs vers un Ă©lĂ©ment d'aller au-delĂ de la fin d'un tableau. Cependant, Rust l'interdit, mĂȘme si nous ne prenons que l'adresse de l'Ă©lĂ©ment. Nous avons trouvĂ© des exemples d'un tel modĂšle dans la fonction
AAS_TraceClientBBox
.
Le compilateur Rust a également signalé un exemple similaire, mais en fait
G_TryPushingEntity
dans
G_TryPushingEntity
, oĂč l'instruction conditionnelle est de la forme
>
, pas
>=
. Un pointeur qui sort des limites est ensuite déréférencé aprÚs la construction conditionnelle, qui est un bogue de sécurité de la mémoire.
Pour éviter ce problÚme à l'avenir, nous avons corrigé le transpileur C2Rust afin qu'il utilise l'arithmétique du pointeur pour calculer l'adresse d'un élément de tableau, plutÎt que d'utiliser l'opération d'indexation du tableau. Grùce à ce correctif, le code qui utilise le modÚle similaire «adresse d'élément à la fin du tableau» est maintenant correctement traduit et exécuté sans modifications.
ĂlĂ©ments de tableau Ă longueur variable
Nous avons lancé le jeu pour tout tester et avons immédiatement eu la panique de Rust:
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
En
cm_polylib.c
un Ćil Ă
cm_polylib.c
, nous avons remarqué qu'il déréférence le champ
p
dans la structure suivante:
typedef struct { int numpoints; vec3_t p[4];
Le champ
p
dans la structure est une version du membre du tableau flexible qui n'est pas prise en charge par la norme C99, mais qui est toujours acceptée par
gcc
. C2Rust reconnaßt les éléments des tableaux de longueur variable avec la syntaxe C99 (
vec3_t p[]
) et implémente une
heuristique simple pour identifier également les versions de ce modÚle avant C99 (tableaux de tailles 0 et 1 à la fin des structures; nous avons également trouvé plusieurs exemples de ce type dans le code source ioquake3).
Changer la structure ci-dessus en syntaxe C99 a éliminé la panique:
typedef struct { int numpoints; vec3_t p[];
Une tentative de corriger automatiquement ce modĂšle dans le cas gĂ©nĂ©ral (avec des tailles de tableau diffĂ©rentes de 0 et 1) sera extrĂȘmement difficile, car nous devrons faire la distinction entre les tableaux ordinaires et les Ă©lĂ©ments de tableaux de longueur variable de tailles arbitraires. Par consĂ©quent, nous vous recommandons plutĂŽt de corriger manuellement le code C d'origine, comme nous l'avons fait avec ioquake3.
Opérandes liés dans le code assembleur en ligne
Une autre source de plantages Ă©tait ce code assembleur C-assembler de l'en-tĂȘte systĂšme
/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)
définir la version interne de la macro
__FD_ZERO
. Cette définition soulÚve un cas limite rare d'
E / S d'opérandes liés gcc
: avec différentes tailles. L'opérateur de sortie
"=D" (__d1)
lie le registre
edi
Ă la variable
__d1
tant que valeur 32 bits et
"1" (&__FDS_BITS (fdsp)[0])
lie le mĂȘme registre Ă l'adresse
fdsp->fds_bits
tant que pointeur 64 bits.
gcc
et
clang
résolvent ce décalage. en utilisant le registre
rdi
64 bits et en tronquant sa valeur avant d'affecter la valeur
__d1
, et Rust utilise la sĂ©mantique LLVM par dĂ©faut, dans laquelle un tel cas reste indĂ©fini. Dans les versions de dĂ©bogage (pas dans les versions de version, qui se comportaient bien), nous avons vu que les deux opĂ©randes peuvent ĂȘtre affectĂ©s au registre
edi
, à cause de quoi le pointeur est tronqué à 32 bits avant le code assembleur intégré, ce qui provoque des échecs.
Ătant donnĂ© que
rustc
transmet le code assembleur Rust intégré à LLVM avec trÚs peu de modifications, nous avons décidé de corriger ce cas particulier dans C2Rust. Nous avons implémenté une nouvelle caisse
c2rust-asm-casts
qui
c2rust-asm-casts
ce problĂšme grĂące au systĂšme de type Rust utilisant des fonctions de
trait et d'assistance qui développent et tronquent automatiquement les opérandes liés à une taille interne suffisamment grande pour contenir les deux opérandes. Le code ci-dessus se traduit correctement comme suit:
let mut __d0: c_int = 0; let mut __d1: c_int = 0;
Il convient de noter que ce code ne nécessite aucun type pour les valeurs d'entrée et de sortie dans l'assembly du code assembleur; lors de la résolution des conflits de types, comptez plutÎt sur eux pour générer les types Rust (principalement les types
fresh6
et
fresh8
).
Variables globales alignées
La derniÚre source de l'échec était la variable globale suivante stockant la constante SSE:
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 prend actuellement en charge l'attribut d'alignement pour les types de structure, mais pas pour les variables globales, c'est-à -dire éléments
static
. Nous avons envisagé des moyens de résoudre ce problÚme dans le cas général, soit dans Rust soit dans C2Rust, mais pour l'instant dans ioquake3, nous avons décidé de le corriger manuellement avec un court fichier de
patch . Ce fichier correctif remplace l'équivalent
ssemask
Rust
ssemask
suit:
#[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, ]);
Exécution de quake3-rs
Lorsque
cargo build --release
, des fichiers binaires sont créés, mais ils sont créés sous
target/release
avec une structure de répertoires que le binaire
ioquake3
ne reconnaßt pas. Nous avons écrit un
script qui crée des liens symboliques dans le répertoire courant pour recréer la structure de répertoire correcte (y compris des liens vers des fichiers
.pk3
contenant des ressources de jeu):
$ /path/to/make_quake3_rs_links.sh /path/to/quake3-rs/target/release /path/to/paks
Le chemin
/path/to/paks
doit pointer vers le répertoire contenant les fichiers
.pk3
.
Lançons maintenant le jeu! Nous devons passer
+set vm_game 0
, etc., nous chargeons donc ces modules en tant que bibliothÚques partagées Rust, et non en tant qu'assemblage QVM, ainsi que
cl_renderer
pour utiliser le moteur de rendu OpenGL1.
$ ./ioquake3 +set sv_pure 0 +set vm_game 0 +set vm_cgame 0 +set vm_ui 0 +set cl_renderer "opengl1"
Et ...
Nous avons lancé Quake3 sur Rust!
Voici une vidéo de la façon dont nous transposons Quake 3, téléchargeons le jeu et jouons-en un peu:
Vous pouvez étudier les
sources transpilées dans la branche
transpiled
de notre référentiel. Il existe également une branche
refactored
contenant les mĂȘmes
sources avec plusieurs
commandes de refactorisation pré-appliquées.
Comment transposer
Si vous voulez essayer de transposer Quake 3 et l'exĂ©cuter vous-mĂȘme, notez que vous aurez besoin de vos propres ressources de jeu Quake 3 ou ressources de dĂ©monstration sur Internet. Vous devrez Ă©galement installer C2Rust (au moment de la rĂ©daction, la version nocturne requise est
nightly-2019-12-05
, mais nous vous recommandons de consulter
le référentiel C2Rust ou dans
crates.io pour trouver la derniĂšre version):
$ cargo +nightly-2019-12-05 install c2rust
et des copies de nos référentiels C2Rust et ioquake3:
$ 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
Au lieu d'installer
c2rust
à l'aide de la commande ci-dessus, vous pouvez créer C2Rust manuellement à l'aide de
cargo build --release
. Dans tous les cas, le référentiel C2Rust sera toujours nécessaire, car il contient les scripts d'encapsulation du compilateur nécessaires pour transposer ioquake3.
Nous avons publié un
script qui transporte automatiquement le code C et applique le correctif
ssemask
. Pour l'utiliser, exécutez la commande suivante à partir du niveau supérieur du référentiel
ioq3
:
$ ./transpile.sh </path/to/C2Rust repository> </path/to/c2rust binary>
Cette commande doit créer un sous
quake3-rs
répertoire
quake3-rs
contenant du code Rust, pour lequel vous pouvez ensuite exécuter la
cargo build --release
et les étapes restantes décrites ci-dessus.