QEMU.js : désormais sérieux et avec WASM

Il était une fois j'ai décidé pour m'amuser prouver la réversibilité du processus et apprenez à générer du JavaScript (plus précisément, Asm.js) à partir du code machine. QEMU a été choisi pour l'expérience et, quelque temps plus tard, un article a été écrit sur Habr. Dans les commentaires, on m'a conseillé de refaire le projet dans WebAssembly, et même de m'arrêter presque fini D'une manière ou d'une autre, je ne voulais pas du projet... Le travail avançait, mais très lentement, et maintenant, récemment, dans cet article est apparu commenter sur le thème « Alors, comment tout cela s'est-il terminé ? » En réponse à ma réponse détaillée, j'ai entendu « Cela ressemble à un article ». Eh bien, si vous le pouvez, il y aura un article. Peut-être que quelqu'un le trouvera utile. Le lecteur y apprendra quelques faits sur la conception des backends de génération de code QEMU, ainsi que sur la façon d'écrire un compilateur juste à temps pour une application Web.

Tâches

Comme j'avais déjà appris à porter « d'une manière ou d'une autre » QEMU vers JavaScript, cette fois, il a été décidé de le faire judicieusement et de ne pas répéter les vieilles erreurs.

Erreur numéro un : branchement à partir de la version intermédiaire

Ma première erreur a été de dériver ma version de la version 2.4.1 en amont. Ensuite cela m'a semblé une bonne idée : si la version ponctuelle existe, alors elle est probablement plus stable que la simple 2.4, et encore plus la branche master. Et comme j’avais prévu d’ajouter une bonne quantité de mes propres bugs, je n’avais pas du tout besoin de ceux de quelqu’un d’autre. C’est probablement comme ça que ça s’est passé. Mais voici le problème : QEMU ne reste pas immobile et, à un moment donné, ils ont même annoncé une optimisation de 10 % du code généré. "Ouais, maintenant je vais geler", ai-je pensé et je suis tombé en panne. Ici, nous devons faire une digression : en raison de la nature monothread de QEMU.js et du fait que le QEMU d'origine n'implique pas l'absence de multi-threading (c'est-à-dire la possibilité d'exploiter simultanément plusieurs chemins de code non liés, et non seulement « utiliser tous les noyaux » est essentiel pour cela, mais les principales fonctions des threads que je devais « éteindre » pour pouvoir appeler de l'extérieur. Cela a créé des problèmes naturels lors de la fusion. Cependant, le fait que certains des changements apportés par la branche master, avec lesquels j'ai essayé de fusionner mon code, ont également été sélectionnés dans la version intermédiaire (et donc dans ma branche) et n'auraient probablement pas non plus ajouté de commodité.

En général, j'ai décidé qu'il était toujours logique de jeter le prototype, de le démonter pour les pièces et de construire une nouvelle version à partir de zéro, basée sur quelque chose de plus récent et maintenant de master.

Erreur numéro deux : la méthodologie TLP

En substance, ce n'est pas une erreur, en général, c'est juste une caractéristique de la création d'un projet dans des conditions d'incompréhension totale à la fois de « où et comment se déplacer ? » et en général « y arriverons-nous ? » Dans ces conditions programmation maladroite C’était une option justifiée, mais, naturellement, je ne voulais pas la répéter inutilement. Cette fois, je voulais le faire judicieusement : des commits atomiques, des changements de code conscients (et non "enchaîner des caractères aléatoires jusqu'à ce qu'ils soient compilés (avec des avertissements)", comme Linus Torvalds l'a dit un jour à propos de quelqu'un, selon Wikiquote), etc.

Erreur numéro trois : se mettre à l'eau sans connaître le gué

Je ne m'en suis pas encore complètement débarrassé, mais maintenant j'ai décidé de ne pas suivre du tout la voie de la moindre résistance, et de le faire « en tant qu'adulte », c'est-à-dire d'écrire mon backend TCG à partir de zéro, pour ne pas devoir dire plus tard : « Oui, bien sûr, cela se fait lentement, mais je ne peux pas tout contrôler - c'est comme ça que TCI est écrit... » De plus, cela semblait au premier abord une solution évidente, puisque Je génère du code binaire. Comme on dit : « Gand a rassembléу, mais pas celui-là » : le code est, bien sûr, binaire, mais le contrôle ne peut pas simplement lui être transféré - il doit être explicitement poussé dans le navigateur pour la compilation, ce qui donne un certain objet du monde JS, qui doit encore être sauvé quelque part. Cependant, sur les architectures RISC normales, autant que je sache, une situation typique est la nécessité de réinitialiser explicitement le cache d'instructions pour le code régénéré - si ce n'est pas ce dont nous avons besoin, alors, de toute façon, c'est proche. De plus, lors de ma dernière tentative, j'ai appris que le contrôle ne semble pas être transféré au milieu du bloc de traduction, nous n'avons donc pas vraiment besoin d'interpréter le bytecode à partir d'un décalage quelconque, et nous pouvons simplement le générer à partir de la fonction sur TB .

Ils sont venus et ont donné des coups de pied

Bien que j'aie commencé à réécrire le code en juillet, un coup de magie s'est glissé inaperçu : généralement les lettres de GitHub arrivent sous forme de notifications concernant les réponses aux problèmes et aux demandes d'extraction, mais ici, brusquement mentionner dans le fil de discussion Binaryen comme backend qemu dans le contexte, "Il a fait quelque chose comme ça, peut-être qu'il dira quelque chose." Nous parlions d'utiliser la bibliothèque associée d'Emscripten binaire pour créer WASM JIT. Eh bien, j'ai dit que vous aviez là une licence Apache 2.0, et QEMU dans son ensemble est distribué sous GPLv2, et ils ne sont pas très compatibles. Soudain, il s'est avéré qu'une licence pouvait être répare-le d'une manière ou d'une autre (Je ne sais pas : peut-être le changer, peut-être la double licence, peut-être autre chose...). Bien sûr, cela m'a rendu heureux, car à ce moment-là, j'avais déjà regardé de près format binaire WebAssembly, et j'étais en quelque sorte triste et incompréhensible. Il existait également une bibliothèque qui dévorerait les blocs de base avec le graphe de transition, produirait le bytecode et même l'exécuterait dans l'interpréteur lui-même, si nécessaire.

Puis il y avait plus la lettre sur la liste de diffusion QEMU, mais il s'agit plutôt de la question « Qui en a besoin de toute façon ? » Et c'est brusquement, il s'est avéré que c'était nécessaire. Au minimum, vous pouvez regrouper les possibilités d'utilisation suivantes, si cela fonctionne plus ou moins vite :

  • lancer quelque chose d'éducatif sans aucune installation
  • virtualisation sur iOS, où, selon les rumeurs, la seule application qui a le droit de générer du code à la volée est un moteur JS (est-ce vrai ?)
  • démonstration de mini-OS - disquette unique, intégré, toutes sortes de firmware, etc...

Fonctionnalités d'exécution du navigateur

Comme je l'ai déjà dit, QEMU est lié au multithreading, mais le navigateur ne l'a pas. Eh bien, non... Au début, cela n'existait pas du tout, puis les WebWorkers sont apparus - d'après ce que j'ai compris, il s'agit d'un multithreading basé sur la transmission de messages. sans variables partagées. Naturellement, cela crée des problèmes importants lors du portage du code existant basé sur le modèle de mémoire partagée. Puis, sous la pression du public, il a également été mis en œuvre sous le nom SharedArrayBuffers. Il a été introduit progressivement, ils ont célébré son lancement dans différents navigateurs, puis ils ont célébré le Nouvel An, et enfin Meltdown... Après quoi ils sont arrivés à la conclusion que la mesure du temps était grossière ou grossière, mais avec l'aide de la mémoire partagée et d'un thread qui incrémente le compteur, c'est pareil ça fonctionnera assez précisément. Nous avons donc désactivé le multithreading avec mémoire partagée. Il semble qu'ils l'aient rallumé plus tard, mais, comme il est devenu clair dès la première expérience, il y a une vie sans cela, et si c'est le cas, nous essaierons de le faire sans compter sur le multithreading.

La deuxième caractéristique est l'impossibilité de manipulations de bas niveau avec la pile : vous ne pouvez pas simplement prendre, sauvegarder le contexte actuel et passer à un nouveau avec une nouvelle pile. La pile d'appels est gérée par la machine virtuelle JS. Il semblerait, quel est le problème, puisque nous avons quand même décidé de gérer les anciens flux de manière entièrement manuelle ? Le fait est que les E/S de bloc dans QEMU sont implémentées via des coroutines, et c'est là que les manipulations de pile de bas niveau seraient utiles. Heureusement, Emscipten contient déjà un mécanisme pour les opérations asynchrones, voire deux : Asyncifier и Interprète. Le premier fonctionne grâce à une surcharge importante du code JavaScript généré et n'est plus pris en charge. La seconde est la « méthode correcte » actuelle et fonctionne via la génération de bytecode pour l’interpréteur natif. Bien sûr, cela fonctionne lentement, mais cela ne gonfle pas le code. Certes, la prise en charge des coroutines pour ce mécanisme devait être apportée indépendamment (il y avait déjà des coroutines écrites pour Asyncify et il y avait une implémentation à peu près de la même API pour Emterpreter, il suffisait de les connecter).

Pour le moment, je n'ai pas encore réussi à diviser le code en un code compilé dans WASM et interprété à l'aide d'Emterpreter, donc les périphériques bloc ne fonctionnent pas encore (voir dans la prochaine série, comme on dit...). Autrement dit, à la fin, vous devriez obtenir quelque chose comme cette drôle de chose en couches :

  • bloc interprété E/S. Eh bien, vous attendiez-vous vraiment à un NVMe émulé avec des performances natives ? 🙂
  • Code QEMU principal compilé statiquement (traducteur, autres appareils émulés, etc.)
  • code invité compilé dynamiquement dans WASM

Caractéristiques des sources QEMU

Comme vous l'avez probablement déjà deviné, le code pour émuler les architectures invitées et le code pour générer les instructions de la machine hôte sont séparés dans QEMU. En fait, c’est encore un peu plus délicat :

  • il y a des architectures invitées
  • il est accélérateurs, à savoir KVM pour la virtualisation matérielle sous Linux (pour les systèmes invités et hôtes compatibles entre eux), TCG pour la génération de code JIT n'importe où. À partir de QEMU 2.9, la prise en charge de la norme de virtualisation matérielle HAXM sous Windows est apparue (Détails)
  • si TCG est utilisé et non la virtualisation matérielle, alors il prend en charge la génération de code distincte pour chaque architecture hôte, ainsi que pour l'interpréteur universel
  • ... et autour de tout cela - périphériques émulés, interface utilisateur, migration, enregistrement-relecture, etc.

Au fait, saviez-vous : QEMU peut émuler non seulement l'ordinateur entier, mais également le processeur d'un processus utilisateur distinct dans le noyau hôte, qui est utilisé, par exemple, par le fuzzer AFL pour l'instrumentation binaire. Peut-être que quelqu'un aimerait porter ce mode de fonctionnement de QEMU vers JS ? 😉

Comme la plupart des logiciels libres de longue date, QEMU est construit grâce à l'appel configure и make. Disons que vous décidez d'ajouter quelque chose : un backend TCG, une implémentation de thread, autre chose. Ne vous précipitez pas pour être heureux/horrifié (souligner si nécessaire) à l'idée de communiquer avec Autoconf - en fait, configure Celui de QEMU est apparemment auto-écrit et n'est généré à partir de rien.

WebAssembly

Alors, qu'est-ce que cette chose appelée WebAssembly (alias WASM) ? Il s'agit d'un remplacement d'Asm.js, ne prétendant plus être du code JavaScript valide. Au contraire, il est purement binaire et optimisé, et même simplement y écrire un entier n'est pas très simple : par souci de compacité, il est stocké au format LEB128.

Vous avez peut-être entendu parler de l'algorithme de rebouclage pour Asm.js - il s'agit de la restauration d'instructions de contrôle de flux « de haut niveau » (c'est-à-dire si-alors-sinon, boucles, etc.), pour lesquelles les moteurs JS sont conçus, à partir de le LLVM IR de bas niveau, plus proche du code machine exécuté par le processeur. Naturellement, la représentation intermédiaire de QEMU est plus proche de la seconde. Il semblerait que voilà, le bytecode, la fin du tourment... Et puis il y a les blocs, les if-then-else et les boucles !..

Et c'est une autre raison pour laquelle Binaryen est utile : il peut naturellement accepter des blocs de haut niveau proches de ceux qui seraient stockés dans WASM. Mais il peut également produire du code à partir d’un graphique de blocs de base et de transitions entre eux. Eh bien, j'ai déjà dit qu'il cache le format de stockage WebAssembly derrière l'API C/C++ pratique.

TCG (petit générateur de code)

TCG était à l'origine backend pour le compilateur C. Ensuite, apparemment, il n'a pas pu résister à la concurrence de GCC, mais il a finalement trouvé sa place dans QEMU en tant que mécanisme de génération de code pour la plate-forme hôte. Il existe également un backend TCG qui génère du bytecode abstrait, qui est immédiatement exécuté par l'interpréteur, mais j'ai décidé d'éviter de l'utiliser cette fois. Cependant, le fait que dans QEMU il soit déjà possible d'activer la transition vers le TB généré via la fonction tcg_qemu_tb_exec, cela s'est avéré très utile pour moi.

Pour ajouter un nouveau backend TCG à QEMU, vous devez créer un sous-répertoire tcg/<имя архитектуры> (dans ce cas, tcg/binaryen), et il contient deux fichiers : tcg-target.h и tcg-target.inc.c и prescrire c'est a propos de configure. Vous pouvez y placer d'autres fichiers, mais, comme vous pouvez le deviner d'après les noms de ces deux fichiers, ils seront tous deux inclus quelque part : l'un en tant que fichier d'en-tête normal (il est inclus dans tcg/tcg.h, et celui-là est déjà dans d'autres fichiers dans les répertoires tcg, accel et pas seulement), l'autre - uniquement sous forme d'extrait de code dans tcg/tcg.c, mais il a accès à ses fonctions statiques.

Décidant que je consacrerais trop de temps à des recherches détaillées sur son fonctionnement, j'ai simplement copié les « squelettes » de ces deux fichiers à partir d'une autre implémentation backend, en l'indiquant honnêtement dans l'en-tête de la licence.

Dossier tcg-target.h contient principalement des paramètres sous la forme #define-ov :

  • combien de registres et quelle largeur y a-t-il sur l'architecture cible (nous en avons autant que nous voulons, autant que nous voulons - la question est plutôt de savoir ce qui sera généré en code plus efficace par le navigateur sur l'architecture « complètement cible » ...)
  • alignement des instructions de l'hôte : sur x86, et même en TCI, les instructions ne sont pas du tout alignées, mais je vais mettre dans le tampon de code non pas du tout des instructions, mais des pointeurs vers les structures de la bibliothèque Binaryen, donc je dirai : 4 octets
  • quelles instructions facultatives le backend peut générer - nous incluons tout ce que nous trouvons dans Binaryen, laissons l'accélérateur diviser le reste en instructions plus simples
  • Quelle est la taille approximative du cache TLB demandée par le backend. Le fait est que dans QEMU tout est sérieux : bien qu'il existe des fonctions d'assistance qui effectuent le chargement/stockage en tenant compte de la MMU invitée (où en serions-nous sans elle maintenant ?), elles sauvegardent leur cache de traduction sous la forme d'une structure, le dont le traitement est pratique à intégrer directement dans les blocs de diffusion. La question est de savoir quel décalage dans cette structure est traité le plus efficacement par une séquence de commandes petite et rapide ?
  • ici, vous pouvez modifier le but d'un ou deux registres réservés, activer l'appel de TB via une fonction et éventuellement décrire quelques petits inline-fonctionne comme flush_icache_range (mais ce n'est pas notre cas)

Dossier tcg-target.inc.c, bien sûr, est généralement beaucoup plus grand et contient plusieurs fonctions obligatoires :

  • initialisation, y compris les restrictions sur les instructions qui peuvent fonctionner sur quels opérandes. Copié de manière flagrante par moi depuis un autre backend
  • fonction qui prend une instruction de bytecode interne
  • Vous pouvez également mettre des fonctions auxiliaires ici, et vous pouvez également utiliser des fonctions statiques de tcg/tcg.c

Pour ma part, j'ai choisi la stratégie suivante : dans les premiers mots du prochain bloc de traduction, j'ai noté quatre indicateurs : une marque de départ (une certaine valeur à proximité 0xFFFFFFFF, qui déterminait l'état actuel du TB), le contexte, le module généré et le nombre magique pour le débogage. Au début, la marque était placée dans 0xFFFFFFFF - nn - un petit nombre positif, et chaque fois qu'il était exécuté via l'interprète, il augmentait de 1. Lorsqu'il atteignait 0xFFFFFFFE, la compilation a eu lieu, le module a été enregistré dans la table des fonctions, importé dans un petit "lanceur", dans lequel l'exécution est passée de tcg_qemu_tb_exec, et le module a été supprimé de la mémoire QEMU.

Pour paraphraser les classiques : « Béquille, combien de choses s’entremêlent dans ce son pour le cœur du proger… ». Cependant, la mémoire fuyait quelque part. De plus, c'était de la mémoire gérée par QEMU ! J'avais un code qui, lors de l'écriture de l'instruction suivante (enfin, c'est-à-dire un pointeur), supprimait celui dont le lien se trouvait à cet endroit plus tôt, mais cela n'a pas aidé. En fait, dans le cas le plus simple, QEMU alloue de la mémoire au démarrage et y écrit le code généré. Lorsque le tampon est épuisé, le code est supprimé et le suivant commence à être écrit à sa place.

Après avoir étudié le code, j'ai réalisé que l'astuce du nombre magique me permettait de ne pas échouer lors de la destruction du tas en libérant un élément erroné sur un tampon non initialisé lors du premier passage. Mais qui réécrit le tampon pour contourner ma fonction plus tard ? Comme le conseillent les développeurs d'Emscripten, lorsque je rencontrais un problème, je portais le code résultant vers l'application native, y installais Mozilla Record-Replay... En général, au final j'ai réalisé une chose simple : pour chaque bloc, un struct TranslationBlock avec son descriptif. Devinez où... C'est vrai, juste avant le bloc, juste dans le tampon. Conscient de cela, j'ai décidé d'arrêter d'utiliser des béquilles (au moins certaines), j'ai simplement jeté le nombre magique et j'ai transféré les mots restants sur struct TranslationBlock, créant une liste à chaînage unique qui peut être rapidement parcourue lorsque le cache de traduction est réinitialisé et libérer de la mémoire.

Certaines béquilles subsistent : par exemple, des pointeurs marqués dans le tampon de code - certains d'entre eux sont simplement BinaryenExpressionRef, c'est-à-dire qu'ils examinent les expressions qui doivent être placées linéairement dans le bloc de base généré, la partie est la condition de transition entre les BB, la partie est l'endroit où aller. Eh bien, il existe déjà des blocs préparés pour Relooper qui doivent être connectés selon les conditions. Pour les distinguer, on suppose qu'ils sont tous alignés sur au moins quatre octets, vous pouvez donc utiliser en toute sécurité les deux bits les moins significatifs pour l'étiquette, il vous suffit de penser à la supprimer si nécessaire. À propos, de telles étiquettes sont déjà utilisées dans QEMU pour indiquer la raison de la sortie de la boucle TCG.

Utiliser Binaryen

Les modules de WebAssembly contiennent des fonctions, chacune contenant un corps, qui est une expression. Les expressions sont des opérations unaires et binaires, des blocs constitués de listes d'autres expressions, de flux de contrôle, etc. Comme je l'ai déjà dit, le flux de contrôle ici est organisé précisément en branches de haut niveau, boucles, appels de fonction, etc. Les arguments des fonctions ne sont pas transmis sur la pile, mais explicitement, tout comme en JS. Il existe également des variables globales, mais je ne les ai pas utilisées, donc je ne vous en parlerai pas.

Les fonctions possèdent également des variables locales, numérotées à partir de zéro, de type : int32 / int64 / float / double. Dans ce cas, les n premières variables locales sont les arguments passés à la fonction. Veuillez noter que même si tout ici n'est pas entièrement de bas niveau en termes de flux de contrôle, les entiers ne portent toujours pas l'attribut « signé/non signé » : le comportement du nombre dépend du code d'opération.

D'une manière générale, Binaryen fournit API C simple: vous créez un module, dedans créer des expressions - unaires, binaires, des blocs d'autres expressions, un flux de contrôle, etc. Ensuite, vous créez une fonction avec une expression comme corps. Si, comme moi, vous disposez d'un graphique de transition de bas niveau, le composant relooper vous aidera. Autant que je sache, il est possible d'utiliser un contrôle de haut niveau du flux d'exécution dans un bloc, à condition qu'il ne dépasse pas les limites du bloc - c'est-à-dire qu'il est possible de créer un chemin rapide/lent interne branchement de chemin à l'intérieur du code de traitement du cache TLB intégré, mais pour ne pas interférer avec le flux de contrôle « externe ». Lorsque vous libérez un relooper, ses blocs sont libérés ; lorsque vous libérez un module, les expressions, fonctions, etc. qui lui sont allouées disparaissent arène.

Cependant, si vous souhaitez interpréter du code à la volée sans création ni suppression inutiles d'une instance d'interpréteur, il peut être judicieux de mettre cette logique dans un fichier C++, et à partir de là de gérer directement l'intégralité de l'API C++ de la bibliothèque, en contournant le ready- fait des emballages.

Donc pour générer le code dont vous avez besoin

// настроить глобальные параметры (можно поменять потом)
BinaryenSetAPITracing(0);

BinaryenSetOptimizeLevel(3);
BinaryenSetShrinkLevel(2);

// создать модуль
BinaryenModuleRef MODULE = BinaryenModuleCreate();

// описать типы функций (как создаваемых, так и вызываемых)
helper_type  BinaryenAddFunctionType(MODULE, "helper-func", BinaryenTypeInt32(), int32_helper_args, ARRAY_SIZE(int32_helper_args));
// (int23_helper_args приоб^Wсоздаются отдельно)

// сконструировать супер-мега выражение
// ... ну тут уж вы как-нибудь сами :)

// потом создать функцию
BinaryenAddFunction(MODULE, "tb_fun", tb_func_type, func_locals, FUNC_LOCALS_COUNT, expr);
BinaryenAddFunctionExport(MODULE, "tb_fun", "tb_fun");
...
BinaryenSetMemory(MODULE, (1 << 15) - 1, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
BinaryenAddMemoryImport(MODULE, NULL, "env", "memory", 0);
BinaryenAddTableImport(MODULE, NULL, "env", "tb_funcs");

// запросить валидацию и оптимизацию при желании
assert (BinaryenModuleValidate(MODULE));
BinaryenModuleOptimize(MODULE);

... si j'ai oublié quelque chose, désolé, c'est juste pour représenter l'échelle, et les détails sont dans la documentation.

Et maintenant le crack-fex-pex commence, quelque chose comme ceci :

static char buf[1 << 20];
BinaryenModuleOptimize(MODULE);
BinaryenSetMemory(MODULE, 0, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
int sz = BinaryenModuleWrite(MODULE, buf, sizeof(buf));
BinaryenModuleDispose(MODULE);
EM_ASM({
  var module = new WebAssembly.Module(new Uint8Array(wasmMemory.buffer, $0, $1));
  var fptr = $2;
  var instance = new WebAssembly.Instance(module, {
      'env': {
          'memory': wasmMemory,
          // ...
      }
  );
  // и вот уже у вас есть instance!
}, buf, sz);

Afin de connecter d'une manière ou d'une autre les mondes de QEMU et JS et en même temps d'accéder rapidement aux fonctions compilées, un tableau a été créé (une table de fonctions à importer dans le lanceur), et les fonctions générées y ont été placées. Pour calculer rapidement l'index, l'index du bloc de traduction de mot zéro a été initialement utilisé, mais ensuite l'index calculé à l'aide de cette formule a commencé à simplement s'insérer dans le champ de struct TranslationBlock.

Soit dit en passant, démo (actuellement avec une licence trouble) ne fonctionne bien que dans Firefox. Les développeurs de Chrome étaient d'une manière ou d'une autre, pas prêt au fait que quelqu'un voudrait créer plus d'un millier d'instances de modules WebAssembly, alors il a simplement alloué un gigaoctet d'espace d'adressage virtuel pour chacun...

C'est tout pour le moment. Il y aura peut-être un autre article si quelqu'un est intéressé. A savoir, il reste au moins juste faire fonctionner les périphériques de blocage. Il peut également être judicieux de rendre la compilation des modules WebAssembly asynchrone, comme c'est l'habitude dans le monde JS, puisqu'il existe toujours un interpréteur qui peut faire tout cela jusqu'à ce que le module natif soit prêt.

Enfin une énigme : vous avez compilé un binaire sur une architecture 32 bits, mais le code, via des opérations de mémoire, grimpe de Binaryen, quelque part sur la pile, ou ailleurs dans les 2 Go supérieurs de l'espace d'adressage 32 bits. Le problème est que du point de vue de Binaryen, cela revient à accéder à une adresse résultante trop grande. Comment contourner ce problème ?

À la manière de l'administrateur

Je n’ai pas fini par tester cela, mais ma première pensée a été « Et si j’installais Linux 32 bits ? » La partie supérieure de l’espace d’adressage sera alors occupée par le noyau. La seule question est de savoir combien sera occupé : 1 ou 2 Go.

À la manière d'un programmeur (option pour les praticiens)

Soufflons une bulle en haut de l'espace d'adressage. Je ne comprends pas moi-même pourquoi ça marche - là déjà il doit y avoir une pile. Mais « nous sommes des praticiens : tout fonctionne pour nous, mais personne ne sait pourquoi… »

// 2gbubble.c
// Usage: LD_PRELOAD=2gbubble.so <program>

#include <sys/mman.h>
#include <assert.h>

void __attribute__((constructor)) constr(void)
{
  assert(MAP_FAILED != mmap(1u >> 31, (1u >> 31) - (1u >> 20), PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
}

... c'est vrai qu'il n'est pas compatible avec Valgrind, mais, heureusement, Valgrind lui-même pousse très efficacement tout le monde hors de là :)

Peut-être que quelqu'un donnera une meilleure explication du fonctionnement de mon code...

Source: habr.com

Ajouter un commentaire