- Elevator traduit statiquement un exécutable x86-64 complet vers AArch64, sans informations de débogage, code source ni hypothèses sur la disposition du binaire
- Au lieu d’heuristiques pour distinguer code et données, il construit un CFG sur-ensemble contenant toutes les interprétations possibles de chaque octet, puis n’élimine que les chemins menant à une terminaison exceptionnelle du programme
- Il mappe l’état x64 en correspondance un-à-un avec des registres AArch64 et gère les branchements indirects via une table de correspondance entre adresses d’origine et code traduit
- La banque de tuiles hors ligne décrit la sémantique des instructions x64 sous forme de modèles C, puis les compile avec LLVM 20 en séquences d’octets AArch64
- Le résultat est un binaire AArch64 autonome, sans traduction à l’exécution, offrant sur SPECint 2006 des performances équivalentes ou supérieures au JIT en mode utilisateur de QEMU
Objectif d’Elevator
- Elevator est un traducteur binaire entièrement statique qui porte un exécutable x86-64 complet vers AArch64
- Il n’utilise ni informations de débogage, ni code source, ni motifs de code du binaire d’origine, ni hypothèses sur la disposition du binaire
- Les traducteurs statiques existants s’appuient sur des heuristiques ou des solutions de repli à l’exécution pour distinguer le code des données, tandis qu’Elevator traduit à l’avance tous les octets de l’exécutable source selon toutes les interprétations possibles
- Comme n’importe quel octet peut être une donnée, une partie d’opcode ou une partie d’opérande, il construit un CFG sur-ensemble qui contient tous les flux de contrôle possibles, puis ne retire que les chemins menant à une terminaison exceptionnelle du programme
- La sortie est un binaire AArch64 autonome comprenant le code traduit, le binaire x64 d’origine, une table de correspondance d’adresses et un pilote d’exécution
- Une fois la traduction terminée, l’exécution est possible sans JIT ni support de traduction à l’exécution
- Traduire deux fois le même binaire d’entrée produit exactement la même séquence de bits en sortie, ce qui garantit que l’objet testé, vérifié, certifié ou signé cryptographiquement correspond au code réellement déployé
- Le principal coût est une augmentation de la taille du code, en échange d’une meilleure vérifiabilité avant déploiement par rapport à un émulateur ou à un compilateur JIT
- L’évaluation inclut l’ensemble du benchmark SPECint 2006 ainsi que des binaires écrits à la main, avec des performances équivalentes ou supérieures à l’émulation QEMU en mode utilisateur avec accélération JIT
- Les chercheurs indiquent qu’ils publieront l’ensemble du projet en open source à la fin de celui-ci
Pourquoi la traduction statique est nécessaire et quelles sont les limites existantes
- Lorsqu’un matériel passe d’une ISA à une autre, il faut porter les logiciels existants vers la nouvelle plateforme, et recompiler le code source encore disponible peut ne pas suffire
- Pour du code legacy vérifié ou certifié, ce n’est souvent pas le code source mais un exécutable binaire de référence précis et largement testé qui fait foi pour la certification
- Reproduire plus tard à l’identique, bit à bit, le même binaire depuis les sources peut nécessiter les versions exactes du compilateur, de l’éditeur de liens et du système de build de l’époque, ce qui est difficile en pratique
- Si un constructeur a appliqué directement des correctifs au binaire sans repasser par les sources, reconstruire à partir des sources archivées peut réintroduire des bugs déjà corrigés
- Les approches existantes qui travaillent directement sur le binaire combinent émulation, traduction statique et traduction dynamique, mais elles ajoutent au programme traduit des composants système supplémentaires faisant partie du code de confiance
- Le comportement dynamique peut varier selon l’ordre des tests ou les entrées, ce qui complique la vérification de la fiabilité globale
- Horspool et Marovac ont montré en 1980 que réinterpréter un exécutable exige de distinguer avec certitude code et données, et que, sur la plupart des architectures, ce problème est équivalent au problème de l’arrêt, donc insoluble en général
- Les lifters binaires statiques existants approchent la distinction code/données par heuristiques, avec des difficultés particulièrement importantes lorsqu’il faut prédire les cibles des transferts de contrôle indirects
- LLBT relève les instructions ARM en LLVM IR puis les recompile vers l’architecture cible, mais il utilise des heuristiques pour détecter les cibles des branchements indirects et fait plusieurs hypothèses sur le binaire d’entrée
- Même de bonnes heuristiques échouent sur certains cas, et comme relever correctement un binaire complet impose d’avoir raison sur toutes les distinctions code/données, le risque d’échec augmente avec la taille du binaire
- Les approches dynamiques peuvent suivre le flux d’instructions réellement exécuté et ainsi traiter la récupération d’instructions et le contrôle indirect, mais elles ne relèvent pas les instructions jamais atteintes dans une exécution concrète
- Avec une ISA à instructions de longueur variable comme x64, une séquence d’instructions peut en contenir une autre, et un branchement au milieu d’une instruction multi-octets peut faire décoder un ancien opérande comme une instruction distincte
- Les attaques ROP et l’obfuscation de code peuvent exploiter cette propriété
- Rosetta II d’Apple et Prism de Microsoft combinent traduction préalable et composants de traduction dynamique
- WYTIWYG et Polynima relèvent statiquement les chemins de contrôle identifiés par profilage dynamique, puis utilisent un mécanisme de repli qui collecte dynamiquement les informations de flux de contrôle lorsqu’une adresse cible non observée est atteinte
- Elevator ne décide pas quels octets sont du code ou des données, ni s’il s’agit de mots d’instruction ou d’opérandes, et inclut chaque octet de l’exécutable dans tous ses rôles possibles sous forme de chemins de contrôle distincts
- Cette approche applique la désassemblage sur-ensemble à la recompilation statique et à la compilation croisée inter-ISA, en échangeant la précision du décodage contre une augmentation du code
Flux de contrôle et préservation de l’état
- Elevator fonctionne selon le principe de préserver intégralement l’état x64 à l’intérieur du code AArch64 traduit
- Il établit une correspondance un-à-un entre registres x64 et registres AArch64, de sorte que l’état de chaque registre x64 est émulé dans le registre AArch64 correspondant
- La pile x64 est émulée directement sur la pile AArch64, et l’extension habituelle de pile en cours d’exécution est gérée par le système d’exploitation
- Sans analyser l’ABI du binaire x64 d’entrée, il n’effectue la traduction d’ABI qu’aux points où l’exécution passe vers du code externe ou en revient, conformément à l’ABI x64 System V et à l’AArch64 Procedure Call Standard
- Grâce à la préservation complète de l’état et à la correspondance un-à-un des registres, chaque instruction x64 peut être traduite indépendamment, sans connaître les instructions précédentes ou suivantes
- Chaque décalage d’octet exécutable du binaire d’origine est interprété à la fois comme donnée et comme point de départ potentiel d’une séquence d’instructions
- Pour toutes les cibles potentielles impossibles à analyser statiquement, comme les sauts indirects, les callbacks ou le dispatch à l’exécution, le binaire réécrit contient un point d’atterrissage correspondant
- À l’exécution, une table de correspondance incluse dans le binaire final résout les cibles en traduisant les adresses d’instructions d’origine vers les adresses du code traduit
-
Exemple d’instructions imbriquées
Listing 1montre qu’en commençant le décodage à.byte 0xB0, on obtientMOV AL, 0xC3puisRET, alors qu’en commençant un octet plus loin àReturnC2, on n’obtient queRET- Les deux décodages sont atteignables depuis le
jzprécédent, et si le traducteur ne choisit qu’une seule interprétation pour ces deux octets, il manque l’un des chemins
-
Exemple de branchement indirect calculé
Listing 2montre quecall Labelconstruit une adresse de base relative à une table, quepop rsirécupère ensuite, puis qu’un décalage dépendant de l’entrée est ajouté pour former la cible dejmp rsi- Le branchement peut atterrir sur l’une des quatre instructions
inc eaxdisposées tous les 2 octets dans le flux encodé - Un traducteur qui ne réécrit que les cibles de saut interprétables statiquement n’a aucun endroit où faire atterrir ce type de branchement
-
Appels, retours et branchements
- Les instructions
call,returnetbranchne peuvent pas être exprimées comme tuiles C, car la position de l’adresse de retour, le compteur ordinal et la disposition des drapeaux conditionnels diffèrent entre x64 et AArch64 - Un appel direct pousse l’adresse de retour x64 d’origine sur la pile émulée, puis branche vers la tuile traduite du callee
- Un appel indirect vérifie si la cible se trouve à l’intérieur du binaire traduit ou dans une bibliothèque externe ; les cibles internes sont traduites via la table x64 offset-vers-tuile, puis l’exécution branche vers cette tuile
- Pour une cible externe, l’adresse du gadget de rétro-traduction ABI est placée dans
X30, où la bibliothèque AArch64 reviendra, puis la traduction ABI de sortie est effectuée avant le branchement vers la cible externe - Un retour retire une adresse de retour de 8 octets de la pile émulée, la compare à l’intervalle du binaire x64 embarqué et, si le retour est interne, traduit l’adresse via la table de correspondance avant de brancher vers la tuile correspondante
- Un branchement direct a une cible connue au moment de la traduction, et un branchement conditionnel est traduit en branchement conditionnel AArch64 qui teste les bits de drapeaux x64 stockés dans
X14 - Un branchement indirect émet la même vérification de bornes que les appels indirects et les retours, et effectue une traduction ABI de sortie si la cible est externe
- Les instructions
Pipeline de traduction à base de tuiles
- La traduction d’Elevator se divise en trois étapes : génération hors ligne de la banque de tuiles, réécriture spécifique au binaire d’entrée, puis empaquetage final
- L’étape hors ligne exprime la sémantique des instructions x64 sous forme de fonctions C, les spécialise pour chaque combinaison d’opérandes sous une correspondance fixe x64-vers-AArch64 des registres, puis les compile avec un LLVM 20 modifié pour produire des séquences d’octets AArch64 réutilisables
- L’étape spécifique au binaire d’entrée effectue un désassemblage sur-ensemble, puis pour chaque instruction candidate trouvée, recherche les tuiles par nom et concatène les séquences d’octets AArch64 correspondantes
- Les catégories d’instructions difficiles à exprimer sous forme de tuiles C, comme les transferts de contrôle ou les frontières ABI, sont traitées par de petits modèles écrits à la main
- L’étape d’empaquetage combine le code traduit, le binaire x64 d’origine, la table de correspondance d’adresses et le pilote d’exécution pour produire un binaire AArch64 exécutable de manière autonome
-
Banque de tuiles hors ligne
- Écrire à la main une séquence d’instructions AArch64 équivalente pour chaque instruction x64 n’est pas réaliste
- Un seul modèle comme
ADD Reg8, Reg8s’étend déjà à 256 combinaisons concrètes de registres, et l’ensemble d’instructions x64 comporte de nombreuses variantes d’adressage pour les registres, les opérandes mémoire et les immédiats - Elevator écrit la sémantique de chaque instruction x64 sous forme d’une petite fonction C, la spécialise selon chaque combinaison concrète d’opérandes, puis laisse LLVM la compiler vers AArch64
- Dans l’exemple
ADD Reg8, Reg8, le modèle met à jour les 8 bits de poids faible du registre destination avec une somme sur 8 bits tout en préservant les 56 bits supérieurs, afin de respecter la sémantique x64 d’écriture partielle de registre - L’instruction x64
ADD Reg8, Reg8modifie aussi les drapeaux Carry, Parity, Auxiliary Carry, Zero, Sign et Overflow deRFLAGS; en raison de la contrainte des fonctions C à valeur de retour unique, la mise à jour des drapeaux est capturée dans une tuile de drapeaux distincte - Une instruction x64 peut correspondre à une ou plusieurs tuiles, qui sont réassemblées de façon contiguë à l’émission pour reconstruire la sémantique complète
- L’attribut
aarch64_custom_regdéclare dans quels registres AArch64 LLVM doit placer la valeur de retour et chacun des arguments - La correspondance fixe est choisie pour aligner les propriétés callee-saved et caller-saved de System V x64 et d’AAPCS64, réduire les réordonnancements de registres d’arguments entiers et conserver des registres AArch64 callee-saved libres pour un éventuel état shadow futur
- Les bits de
RFLAGSet le fichier de registresXMMde x64 sont eux aussi stockés dans des registres AArch64 dédiés selon le même principe un-à-un - Le LLVM 20 modifié traite l’attribut
aarch64_custom_regau niveau des fonctions et reclassifie comme callee-saved, dans l’allocateur de registres, les registres AArch64 qui contiennent l’état x64 émulé TileGenparcourt les modèles C pour créer une copie spécialisée pour chaque combinaison d’opérandes autorisée, puis synthétise mécaniquement les attributs à partir des positions des paramètres et de la correspondance des registres
-
Réécriture spécifique au binaire d’entrée
- Une fois le binaire x64 d’entrée fourni, l’étape par-binaire effectue un désassemblage sur-ensemble et parcourt le CFG résultant
- À chaque nœud, le formateur construit un nom de tuile à partir de l’opcode et des opérandes de l’instruction décodée, et combine plusieurs noms lorsqu’une instruction requiert plusieurs tuiles
- x64 n’impose aucune contrainte d’alignement du pointeur de pile, mais AArch64 exige un alignement sur 16 octets lorsque le pointeur de pile est utilisé dans un opérande mémoire
- Si
RSPétait mappé directement surSP, des motifs x64 courants comme une série dePUSHdans un prologue de fonction pourraient provoquer des exceptions d’alignement sur AArch64 - Elevator fait donc accéder les tuiles à la pile via un registre séparé
X25, et ne matérialiseSPà l’intérieur que lorsque la tuile en a réellement besoin - Comme les tuiles compilées par LLVM supposent un
SPaligné sur 16 octets à l’entrée, Elevator aligneSPvers le bas avant d’exécuter une tuile détectée comme allouant de l’espace de spill, puis le restaure ensuite - Les tuiles de calcul des drapeaux étant relativement coûteuses, le calcul des drapeaux d’un nœud est supprimé s’ils sont écrasés avant d’être lus par une instruction post-dominante
- Les instructions actuellement non prises en charge relèvent principalement des extensions vectorielles larges AVX2 et ultérieures de x64 ; à ces emplacements, une instruction d’interruption est insérée à la place d’une tuile
- Dans l’évaluation complète sur SPECint 2006, l’intégralité de l’ISA entière x86-64 ainsi que le sous-ensemble SSE utilisé par SPECint ont suffi pour exécuter tous les benchmarks
- La prise en charge d’instructions supplémentaires peut être étendue en ajoutant de nouvelles tuiles, mais les auteurs estiment qu’un travail d’ingénierie supplémentaire apporterait peu de nouvelles connaissances scientifiques
Gestion des frontières ABI
- Elevator ne prend en charge que les binaires à liaison dynamique
- Les binaires statiquement liés peuvent inclure directement des instructions spécifiques à l’architecture comme
CPUID, alors que les binaires à liaison dynamique les délèguent àlibc, ce qui réduit le besoin de traduction - Lors de l’interaction avec des bibliothèques dynamiques, il prend en charge les transitions entre l’environnement x64 émulé et le code natif des bibliothèques AArch64, en convertissant entre l’ABI Linux x64 et l’ABI Linux AArch64
- Les éléments clés nécessitant une traduction d’ABI sont la disposition des arguments et la position de l’adresse de retour
- L’ABI System V x64 utilise six registres d’arguments :
RDI,RSI,RDX,RCX,R8,R9, et passe les arguments supplémentaires sur la pile à partir de[RSP+8] - L’instruction x64
CALLenregistre l’adresse de retour dans[RSP] - L’AArch64 Procedure Call Standard utilise huit registres d’arguments
X0-X7, place les arguments restants sur la pile à[SP]et stocke l’adresse de retour dansX30 -
Appel à une bibliothèque externe
- Si un appel x64 traduit cible une bibliothèque externe, la disposition des arguments doit être adaptée à la convention d’appel AArch64
- D’abord, on soustrait 8 à
SPpour réaligner sur une frontière de 16 octets, et l’adresse de retour x64 déjà présente sur la pile est placée à[SP+0x8] - Les valeurs situées à
[SP+0x10]et[SP+0x18]sont chargées dansX6etX7, afin que la bibliothèque AArch64 puisse voir les 7e et 8e arguments potentiels que le code x64 avait placés sur la pile - Les autres arguments potentiels sur pile restent à partir de
[SP+0x20], ce qui ne correspond pas à l’emplacement attendu par AArch64 - Supprimer de la pile l’adresse de retour x64 et les valeurs déplacées vers
X6etX7n’est pas sûr, car ces valeurs peuvent ne pas être de vrais arguments mais du caller spill space ou une partie d’une structure allouée sur la pile de l’appelant - Elevator ne touche donc pas à la disposition de pile de l’appelant : il alloue
n×8octets d’espace de pile supplémentaire, puis copie depuis la position courantenarguments potentiels de 8 octets - La valeur par défaut de
nest 10, et elle peut être augmentée par configuration si le binaire d’entrée passe au total plus de 16 arguments à une fonction de bibliothèque externe - Enfin, l’adresse du gadget auquel la bibliothèque externe devra revenir est stockée dans
X30
-
Retour depuis une bibliothèque externe
- Quand le contrôle revient via le gadget stocké dans
X30avant l’appel à la bibliothèque externe, le pointeur de pile est incrémenté den×8pour libérer les arguments de pile copiés précédemment - La valeur de retour de la bibliothèque externe est déplacée de
X0versX9, qui correspond à l’emplacement attendu par le code x64 émulé pourRAX - L’adresse de retour x64 d’origine et le padding associé sont retirés de la pile, l’adresse est traduite, puis l’exécution branche vers cette cible pour reprendre après le
CALLd’origine
- Quand le contrôle revient via le gadget stocké dans
-
Callbacks entrants dans le code traduit
- Si du code natif AArch64 appelle le binaire traduit, la convention d’appel AArch64 doit être convertie en convention d’appel x64
- Le code x64 émulé attend les 7e et 8e arguments sur la pile et non dans
X6etX7;X7est donc poussé en premier, puisX6, afin de les placer aux positions de pile attendues par x64 - Si le callee n’attend pas réellement de 7e et 8e arguments, ces valeurs poussées n’ont aucun effet
- L’adresse de retour que l’instruction AArch64 branch-and-link de la bibliothèque externe a placée dans
X30est poussée à l’emplacement de pile où l’instruction de retour x64 s’attend à la trouver
-
Retour d’un callback vers une bibliothèque externe
- Lorsque le code traduit retourne d’un callback vers une bibliothèque externe, il effectue l’inverse du processus d’entrée
- L’adresse de retour est retirée de la pile,
X6etX7sont poussés, et l’espace de pile alloué est libéré en ajoutant0x10au pointeur de pile
1 commentaires
Commentaires sur Hacker News
Je ne sais pas exactement ce que fait le JIT en mode utilisateur de QEMU, mais il semble y avoir une marge d'amélioration assez importante
En 2013, j'ai créé un moteur JIT qui convertissait de x86-64 vers aarch64, et à l'époque il pouvait exécuter des binaires Fedora bêta aarch64, ce qui permettait de recompiler une grande partie du portage aarch64 de Fedora sur un Linux x86_64
J'ai aussi créé le JIT dans l'autre sens, aarch64 → x86-64, et, pour le plaisir, j'ai même montré que les deux JIT pouvaient s'exécuter mutuellement en boucle dans le même processus, du style x86-64 → aarch64 → x86_64
Le JIT que j'ai conçu faisait une correspondance un-vers-plusieurs entre les instructions et l'état CPU, et il n'était qu'environ 2 à 5 fois plus lent que du code recompilé nativement
Plus tard, en le comparant au JIT de QEMU, QEMU semblait plutôt se situer dans une fourchette 10 à 50 fois plus lente
Malheureusement, comme la licence n'était pas open source, je ne pouvais pas publier le code pour le prouver
Surtout si l'on peut spécialiser la conception uniquement pour « x86 vers aarch64 » et « mode utilisateur seulement », il y a beaucoup de gains de performance à aller chercher
Le support du mode utilisateur de QEMU ressemble davantage à un appendice « qui fonctionne tant bien que mal » greffé au support d'émulation système, et toute l'architecture du JIT repose sur un schéma « invité → représentation intermédiaire → hôte », ce qui est très bien pour prendre en charge plusieurs architectures invitées et plusieurs architectures hôtes, mais rend difficile l'exploitation des propriétés propres à une combinaison invité/hôte précise, comme « x86 a peu de registres entiers, donc on peut faire une allocation fixe » ou « si l'on place un CPU aarch64 dans le bon mode, la sémantique complexe des flottants tombe toujours juste »
En plus, dans le développement de QEMU, on passe plus de temps à « émuler la nouvelle fonctionnalité d'architecture X » qu'à chercher des opportunités d'optimisation des performances, parce que c'est ce que les financeurs jugent plus important
Le fait que la section
.textdevienne 50 fois plus grosse est énorme, mais cela semble rester un prix acceptable à payer pour obtenir une traduction totalement déterministeDans beaucoup de cas, le gain de performance par rapport à l'émulation l'emportera sur l'inconvénient de la taille
Il est aussi intéressant de voir que le multithreading et la gestion des exceptions ne sont pas impossibles, mais simplement hors du périmètre de ce projet
Je me demande si l'étape suivante ne serait pas d'utiliser des heuristiques pour réduire l'espace des possibilités et diminuer la taille du binaire
On perdrait alors la garantie de traduction, mais la portabilité du binaire pourrait devenir plus réaliste en pratique
Ce traducteur est bien plus lent que Box64 ou FEX, et sauf si l'on ne peut vraiment pas utiliser de JIT pour une raison quelconque, c'est simplement une moins bonne option
Je me suis toujours demandé comment le traducteur gère les sauts indirects
Lorsqu'on analyse un binaire, on ne peut découvrir que les segments de code reliés par des sauts directs dont l'adresse de destination est connue
Cela signifie donc qu'à chaque saut indirect, il faut retrouver la fonction cible, la traduire si nécessaire, puis revenir au code traduit ; n'est-ce pas lent ?
Je me demande s'il existe une méthode plus rapide, si l'on peut faire correspondre l'adresse de la fonction traduite à celle de la fonction d'origine, ou s'il faut insérer à l'adresse d'origine un saut vers le code traduit
jmpindirect va à l'adresse X, alors le bloc correspondant est à l'emplacement Y »C'est plus lent qu'un
jmpdirect sans table, mais, dans le programme d'origine aussi, les sauts indirects sont de toute façon plus lents au départ, et ils apparaissent généralement rarement dans les boucles critiques pour les performancesJ'aime beaucoup l'idée du graphe de flot de contrôle sur-approximé, mais pour ceux qui veulent lire l'article, voici ce qu'il faut savoir
Le temps d'exécution est environ 4,75 fois plus rapide que QEMU, mais reste nettement plus lent que Box64 ; le nombre d'instructions exécutées augmente d'un facteur 7 et la taille du binaire d'un facteur 50
L'ABI x86 est émulée jusqu'aux appels externes
Une grande partie de l'état CPU x86, comme EFLAGS, doit être émulée, et même des
movcomplexes doivent être calculés individuellementSeuls les binaires mono-thread sont pris en charge
Il n'y a ni gestion des exceptions ni déroulage de pile (
unwinding)L'ensemble complet des instructions n'est pas pris en charge
Travail intéressant
Je n'ai pas regardé en détail, mais les offsets relatifs me semblent pouvoir rester problématiques
Comme la taille du code généré sera de toute façon différente, il faudrait sans doute une sorte de couche de traduction ou de MMU, ce qui affecterait surtout les tables de saut et les branchements internes
Je travaille surtout sur des choses des années 90, et les désassembleurs font beaucoup d'hypothèses sur le début et la fin du code
Mais il arrive parfois qu'on ne puisse même pas repérer un morceau de binaire sans connaissance préalable, comme un pointeur de point d'entrée à un emplacement fixe
Avec quelques passes, on pourrait sans doute raffiner le binaire en « zones qui sont certainement du code »
Si « Elevator considère toutes les interprétations possibles de chaque octet et génère à l'avance une traduction distincte pour chacune [...] en n'élaguant que les cas qui mènent à une terminaison anormale », alors tous les programmes réels susceptibles de collision sont-ils tous élagués ?
Il y aurait donc toujours collision, mais ce ne serait pas la même chose qu'un plantage provoqué par l'exécution directe d'un code erroné
Pour moi, l'aspect le plus intéressant est celui de la certification
Dans les secteurs réglementés comme l'aéronautique ou les dispositifs médicaux, le code exécuté doit être du code certifié, et c'est précisément pour cette raison qu'on ne peut souvent pas utiliser de JIT
Une traduction statique qui produit un binaire signable pourrait constituer une véritable avancée pratique, même au prix d'un gonflement du code
Il est probable qu'on ne puisse pas non plus y appliquer les LLM à grande échelle, mais ce type de sujet est presque absent du grand discours sur « l'IA au travail »
50 fois, ce n'est pas raisonnable, et c'est une catastrophe pour le cache
Tous les gains de performance obtenus en évitant le JIT pourraient être entièrement absorbés
En regroupant le code chaud au même endroit, on peut faire en sorte que le code inutilisé ne soit jamais chargé
Les instructions ne sont de toute façon pas si volumineuses, et le CPU fait aussi des optimisations pendant l'exécution
Est-ce que cela peut gérer du code auto-modifiant ?
Je me demande aussi pourquoi seulement x86_64
Traduire des programmes 32 bits, comme d'anciens jeux, semblerait plus utile
« Code auto-modifiant et code compilé par JIT. Elevator, comme tout réécrivain de binaire totalement statique, ne prend pas en charge le code auto-modifiant ni le code compilé par JIT »
De nos jours, la section
.textest généralement en lecture seule, et il est peu probable que les exigences de sécurité diminuentC'est fondamentalement contradictoire
Cela ruine les performances des lignes de cache et de la prédiction de branchement du pipeline
En outre, cela viole W^X, donc cela devrait en général se limiter à des pages mémoire compatibles JIT
C'est pourquoi il faut presque toujours l'éviter
À l'époque du 486 ou du P5, on en voyait un peu, par exemple en utilisant des valeurs immédiates comme variables de boucle internes, mais ce n'est plus vraiment le cas aujourd'hui
Il existe de très nombreux cas limites sales du x86 qu'il faut gérer pour parvenir à une émulation ou une traduction proche de la perfection
Où est le code source ?