QEMU.js : maintenant pour de vrai avec WASM

Il y a longtemps, pour rire, j'ai décidé de prouver la réversibilité du processus et d'apprendre à générer du JavaScript (ou plutôt, Asm.js) à partir de code machine. Pour l'expérience, j'ai choisi QEMU, et quelque temps plus tard, un article a été écrit sur Habr. Dans les commentaires, on m'a conseillé de refondre le projet en WebAssembly, et il était difficile de laisser tomber un projet presque terminé… Le travail avançait, mais très lentement, et récemment, un commentaire est apparu sous cet article concernant « Alors, comment ça s'est fini ? ». À ma réponse détaillée, j'ai entendu « Ça mérite un article ». Bon, puisque ça mérite, alors ce sera un article. Peut-être que cela sera utile à quelqu'un. Le lecteur y découvrira certains faits sur l'architecture des backends de génération de code avec QEMU, ainsi que sur la façon d'écrire un compilateur Just-in-Time pour une application web. commentaire Puisque j'avais déjà appris à porter QEMU sur JavaScript de manière « sommaire », cette fois, j'ai décidé de faire les choses correctement et de ne pas répéter les anciennes erreurs.

Objectifs

Erreur numéro un : se détacher d'un point release

Ma première erreur a été de dériver ma version de la version upstream 2.4.1. À l'époque, je pensais que c'était une bonne idée : si un point release existe, c'est probablement plus stable que le simple 2.4, et encore plus que la branche

. Et puisque je prévoyais d'ajouter une quantité considérable de mes propres bogues, les bogues existants ne m'étaient vraiment pas nécessaires. Cela a probablement porté ses fruits. Mais voilà le problème : QEMU n'est pas resté inactif, et à un moment donné, ils ont même annoncé une optimisation du code généré d'environ 10 %. « Ah, maintenant je vais l'intégrer », ai-je pensé, et j'ai pris un coup. Il faut faire une parenthèse : en raison de la nature monocœur de QEMU.js et du fait que l'original QEMU ne prévoit pas l'absence de multithreading (c'est-à-dire qu'il est essentiel pour lui de pouvoir travailler simultanément avec plusieurs chemins de code non liés, et pas simplement « d'utiliser tous les cœurs »), les principales fonctions de flux ont dû être « retournées » pour permettre un appel externe. Cela a créé certaines difficultés naturelles lors de la fusion. Cependant, le fait qu'une partie des changements de la branche master, d'où j'essayais de fusionner mon code, ait également été sélectionnée pour le point release (ce qui signifie que ma branche en a également bénéficié) n'a probablement pas non plus ajouté de commodité. masterEn gros, j'ai décidé qu'il valait mieux jeter le prototype, le démonter et construire une nouvelle version de zéro basée sur quelque chose de plus récent et maintenant à partir de

Erreur numéro deux : méthodologie TLP master.

Erreur numéro deux : méthode TLP

En fait, ce n'est pas une erreur, c'est simplement une particularité de la création de projet dans des conditions de totale incompréhension quant à « où et comment avancer ? », voire « allons-nous y parvenir ? ». Dans ces conditions, la programmation à l'emporte-pièce était une option justifiable, mais, bien sûr, je ne voulais pas le répéter sans nécessité. Cette fois, je voulais faire les choses correctement : des commits atomiques, des changements de code réfléchis (et pas « assembler des caractères aléatoires jusqu'à ce que ça compile (avec des warnings) », comme l'a dit un jour Linus Torvalds, si l'on en croit Wikiquote), etc.

Erreur numéro trois : ne pas connaître le terrain et entrer dans l'eau

C'est de cela que je n'ai pas encore totalement réussi à me débarrasser, mais maintenant j'ai décidé de ne pas suivre le chemin de la moindre résistance et de faire les choses « comme il se doit », c'est-à-dire, d'écrire mon backend TCG de zéro, pour ne pas avoir à dire : « Oui, c'est sûr, c'est lent, mais je ne peux pas tout contrôler — c'est comme ça que le TCI est écrit… ». De plus, cela semblait à l'origine une solution évidente, car je génère du code binaire. Comme on dit, « J'ai assemblé Gentau, mais pas celui-là » : le code est certes binaire, mais il n'est pas si simple d'en avoir le contrôle — il doit être clairement inséré dans le navigateur pour être compilé, obtenant ainsi un certain objet du monde JS, qui doit encore être sauvegardé quelque part. Cependant, sur des architectures RISC normales, autant que je comprends, il est typique de devoir vider explicitement le cache d'instructions pour le code régénéré — si ce n'est pas exactement ce dont nous avons besoin, c'est en tout cas proche. De plus, j'ai appris de ma précédente tentative que le contrôle n'est pas réellement transféré au milieu d'un bloc de traduction, il n'est donc pas vraiment nécessaire d'avoir du bytecode interprété à partir de n'importe quel décalage, et on peut simplement générer par fonction sur TB.

Ils sont arrivés et ont donné un coup de pied

Bien que j'ai commencé à réécrire le code en juillet, ce coup de pouce magique est arrivé sans que je m'en rende compte : généralement, les courriels de GitHub arrivent sous forme de notifications de réponses aux problèmes et demandes de tirage, mais cette fois, soudainement il y avait une mention dans le thread Binaryen en tant que backend qemu dans le contexte, « Voici, il a fait quelque chose de similaire, peut-être qu'il dira quelque chose ». Il s'agissait de l'utilisation d'une bibliothèque apparentée à Emscripten, Binaryen pour créer un JIT WASM. Eh bien, j'ai dit que leur licence est Apache 2.0, tandis que QEMU en tant qu'ensemble est distribué sous GPLv2, et qu'ils ne sont pas très compatibles. Il s'est soudainement avéré que la licence pouvait être modifiée d'une certaine manière (Je ne sais pas : peut-être le changer, peut-être le double licenciement, peut-être autre chose…). Cela m'a bien sûr réjoui, car je m'étais déjà intéressé plusieurs fois à un format binaire WebAssembly, et j'étais un peu triste et confus à ce sujet. Ici, il y avait une bibliothèque qui pouvait traiter les blocs de base avec le graphe des transitions, fournir du bytecode, et même l'exécuter dans un interpréteur si nécessaire.

Il y avait aussi un e-mail dans la liste de diffusion QEMU, mais c'était plutôt une question de « Qui en a vraiment besoin ? ». Et en fait, soudainement, cela s'est avéré nécessaire. Au moins, il est possible de trouver des applications potentielles, si cela fonctionne plutôt bien :

  • lancer quelque chose d'éducatif sans aucune installation
  • virtualisation sur iOS, où, selon les rumeurs, la seule application autorisée à générer du code à la volée est le moteur JS (est-ce vrai ?)
  • démonstration d'un mini-OS — disquettes, embedded, divers firmwares, etc…

Caractéristiques de l'environnement d'exécution du navigateur

Comme je l'ai déjà dit, QEMU est lié aux threads, mais il n'y en a pas dans le navigateur. Enfin, disons qu'il n'y en avait pas du tout au début, puis les WebWorkers sont apparus — d'après ce que je comprends, c'est de la multithread basé sur le passage de messages sans variables changeables en commun. Évidemment, cela pose d'importants problèmes lors du portage du code existant basé sur le modèle de mémoire partagée. Ensuite, sous la pression du public, cela a été réalisé sous le nom de SharedArrayBuffers. Cela a été progressivement introduit, célébré son lancement dans différents navigateurs, puis célébré le Nouvel An, puis le Meltdown… Après quoi, il a été conclu que peu importe comment mesurer le temps, grâce à la mémoire partagée et à un thread incrémentant un compteur, cela fonctionnerait quand même assez précisément. Ainsi, la multithreading avec mémoire partagée a été désactivée. Apparemment, ils l'ont ensuite réactivée, mais comme il est devenu évident à partir du premier essai, même sans elle, il y a de la vie, et donc, essayons de le faire sans compter sur la multithreading.

La seconde caractéristique réside dans l'impossibilité de manipulations de bas niveau avec la pile : il n'est pas possible de simplement prendre, sauvegarder le contexte actuel et de basculer sur un nouveau avec une nouvelle pile. La pile d'appels est gérée par la machine virtuelle JS. On pourrait se demander quel est le problème, puisque nous avons décidé de gérer entièrement les anciens fils nous-mêmes ? Le fait est que l'entrée/sortie de blocs dans QEMU est réalisée via des coroutines, et c'est là que nous aurions eu besoin de manipulations de bas niveau avec la pile. Heureusement, Emscripten contient déjà un mécanisme pour les opérations asynchrones, même deux : Asyncify et Emterpreter. Le premier fonctionne grâce à un gonflement considérable du code JavaScript généré et n'est plus maintenu. Le second est la "bonne manière" actuelle et fonctionne via la génération de bytecode pour son propre interpréteur. Cela fonctionne, bien sûr, lentement, mais n'alourdit pas le code. En vérité, le support des coroutines pour ce mécanisme a dû être ajouté par la communauté (il existait déjà des coroutines écrites pour Asyncify et une implémentation d'une API à peu près similaire pour Emterpreter, il suffisait de les connecter).

Pour le moment, je n'ai pas encore eu le temps de séparer le code en version compilable en WASM et celui interprété via Emterpreter, donc les dispositifs de blocs ne fonctionnent pas encore (restez à l'écoute pour les prochains épisodes, comme on dit…). En fin de compte, cela devrait donner un drôle d'objet multicouche :

  • une entrée/sortie de blocs interprétée. Mais alors, vous vous attendiez vraiment à un NVMe émulé avec des performances natives ? 🙂
  • code principal QEMU statiquement compilé (transcodeur, autres dispositifs émulés, etc.)
  • code invité dynamiquement compilé en WASM

Caractéristiques des sources QEMU

Comme vous l'avez probablement deviné, le code d'émulation des architectures invitées et le code de génération d'instructions machine hôte dans QEMU sont séparés. En réalité, c'est même un peu plus astucieux :

  • il existe des architectures invitées
  • Il existe accélérateurs, à savoir, KVM pour la virtualisation matérielle sous Linux (pour des systèmes invités et hôtes compatibles entre eux), TCG pour la génération de code JIT partout. À partir de QEMU 2.9, un support du standard de virtualisation matérielle HAXM sous Windows a été ajouté (les détails)
  • si TCG est utilisé au lieu de la virtualisation matérielle, il dispose d'un support distinct pour la génération de code pour chaque architecture d'hôte, ainsi que pour un interpréteur universel.
  • … et tout autour de cela — périphériques émulés, interface utilisateur, migration, enregistrement-relecture, etc.

Au fait, saviez-vous que : QEMU peut émuler non seulement un ordinateur entier, mais aussi un processeur pour un processus utilisateur distinct dans le noyau hôte, comme l'utilise par exemple le fuzzing AFL pour l'instrumentation des binaires. Peut-être que quelqu'un voudra porter ce mode de fonctionnement de QEMU sur JS ? 😉

Comme la plupart des programmes libres existant depuis longtemps, QEMU est compilé via un appel. configure et makeSupposons que vous ayez décidé d'ajouter quelque chose : un backend TCG, une implémentation de threads, quelque chose d'autre. Ne vous réjouissez pas / ne vous effrayez pas (cochez ce qui convient) à l'idée de devoir interagir avec Autoconf — en fait, configure QEMU a apparemment un code écrit maison et n'est pas généré à partir de rien.

WebAssembly

Qu'est-ce donc — WebAssembly (ou WASM) ? C'est un remplacant d'Asm.js, qui n'essaie plus d'être un code JavaScript valide. Au contraire, c'est purement binaire et optimisé, et même simplement écrire un entier dedans n'est pas si simple : il est stocké au format LEB128.

Vous avez peut-être entendu parler de l'algorithme de relooping pour Asm.js — il restaure les instructions de contrôle de flux « haut niveau » (c'est-à-dire if-then-else, boucles, etc.) pour lesquelles les moteurs JS sont optimisés, à partir de l'IR LLVM 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. On pourrait penser, voilà le bytecode, la fin des souffrances… Et là, les blocs, if-then-else et boucles !

Et c'est une autre raison pour laquelle Binaryen est utile : il peut naturellement accepter des blocs de haut niveau, proches de ce qui sera sauvegardé dans WASM. Mais il peut également produire du code à partir de graphes de blocs de base et des transitions entre eux. Quant à ce qu'il cache derrière une API C/C++ conviviale pour le format de stockage de WebAssembly, je l'ai déjà dit.

TCG (Tiny Code Generator)

TCG était à l'origine backend pour le compilateur C. Ensuite, il semble qu'il n'ait pas pu rivaliser avec GCC, mais il a finalement trouvé sa place dans QEMU en tant que mécanisme de génération de code pour la plateforme hôte. Il y a aussi un backend TCG qui génère un bytecode abstrait, que l'interpréteur exécute immédiatement, mais j'ai décidé de renoncer à son utilisation cette fois. Cependant, le fait que QEMU dispose déjà d'une fonctionnalité pour passer au TB généré via la fonction tcg_qemu_tb_exec, m'a été très utile.

Pour ajouter un nouveau backend TCG dans QEMU, il faut créer un sous-répertoire tcg/ (dans ce cas, tcg/binaryen), et à l'intérieur, deux fichiers : tcg-target.h et tcg-target.inc.c et inscrire tout cela dans configure. On peut aussi y mettre d'autres fichiers, mais, comme on peut le deviner par les noms de ces deux fichiers, ils seront tous deux inclus quelque part : l'un comme un fichier d'en-tête classique (il est inclus dans tcg/tcg.h, et celui-ci dans d'autres fichiers dans les répertoires tcg, accel et pas seulement), l'autre — uniquement comme un extrait de code dans tcg/tcg.c, mais il a accès à ses fonctions statiques.

Ayant décidé que je passerais trop de temps à examiner en détail son fonctionnement, j'ai simplement copié les « squelettes » de ces deux fichiers d'une autre implémentation de backend, en indiquant honnêtement cela dans l'en-tête de la licence.

Le fichier tcg-target.h contient principalement des configurations sous forme de #define-s :

  • combien de registres et quelle largeur sont disponibles sur l'architecture cible (chez nous — autant que nous voulons, la question est plus de ce qui sera généré en un code plus efficace par le navigateur sur l'architecture « complètement cible »…)
  • alignement des instructions hôtes : sur x86, et même dans TCI, les instructions ne sont pas vraiment alignées, je prévois de mettre dans le tampon de code non pas des instructions mais des pointeurs sur les structures de la bibliothèque Binaryen, donc je dirai : 4 octets
  • quelles instructions optionnelles peut générer le backend — incluons tout ce que nous trouvons dans Binaryen, le reste peut être décomposé par l'accélérateur en plus simple.
  • Quelle est la taille approximative du cache TLB demandée par le backend ? En fait, dans QEMU, tout est pris au sérieux : même s'il existe des fonctions d'aide qui effectuent des opérations de chargement/stockage en tenant compte du MMU invité (et où irions-nous sans cela ?), le cache de traduction est conservé sous forme de structure, dont le traitement peut facilement être intégré directement dans les blocs de traduction. La question est donc de savoir quel décalage dans cette structure est le plus efficacement traité par une petite et rapide séquence d'instructions.
  • On peut également modifier l'affectation d'un ou deux registres réservés, activer l'appel TB via une fonction et éventuellement décrire quelques petites fonctions. inline-fonctions comme flush_icache_range (mais ce n'est pas notre cas).

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

  • initialisation, qui indique également les restrictions sur les instructions qui peuvent travailler avec quels opérandes. J'ai honteusement copié cela d'un autre backend.
  • fonction qui prend une instruction de bytecode interne.
  • On peut aussi y placer des fonctions d'assistance, et ici on peut utiliser des fonctions statiques de tcg/tcg.c

Pour moi, j'ai adopté la stratégie suivante : dans les premiers mots d'un bloc de traduction, j'enregistrais quatre pointeurs : un marqueur de début (une certaine valeur dans les alentours de 0xFFFFFFFF, qui déterminait l'état actuel du TB), le contexte, le module généré et un nombre magique pour le débogage. Au départ, le marqueur était réglé à 0xFFFFFFFF - n, où n — un petit nombre positif, et à chaque exécution via l'interprète, il augmentait de 1. Lorsqu'il atteignait 0xFFFFFFFE, une compilation se produisait, le module était enregistré dans la table des fonctions importées dans un petit « lanceur », vers lequel l'exécution était dirigée depuis tcg_qemu_tb_exec, et le module était supprimé de la mémoire QEMU.

Pour reformuler un classique, « Bricolage, combien de choses cette sonorité évoque-t-elle pour le cœur d'un développeur… ». Pourtant, la mémoire s'échappait quelque part. De fait, c'était de la mémoire gérée par QEMU ! J'avais un code qui, lors de l'écriture d'une instruction (c'est-à-dire, d'un pointeur), supprimait celle à laquelle ce dernier faisait référence, mais cela n'aidait pas. En l'occurrence, 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 buffer se remplit, le code est éjecté, et à sa place commence à être enregistré le suivant.

En analysant le code, j'ai compris que le patch avec le nombre magique permettait d'éviter une chute lors de la destruction de la pile, en libérant quelque chose d'inapproprié dans un tampon non initialisé lors du premier passage. Mais qui réécrit le tampon en contournant ma fonction ensuite ? Comme le recommandent les développeurs d'Emscripten, confronté au problème, j'ai porté le code résultant à une application native, et je l'ai soumis à Mozilla Record-Replay… En fin de compte, j'ai compris une chose simple : pour chaque bloc, il y a un struct TranslationBlock avec sa description. Devinez où… Correct, juste avant le bloc dans le tampon. En réalisant cela, j'ai décidé de mettre fin aux patchs (au moins certains), et j'ai simplement supprimé le nombre magique, et j'ai transféré les mots restants dans struct TranslationBlock, en établissant une liste chaînée qui permet de parcourir rapidement lors de la réinitialisation du cache de traduction et de libérer de la mémoire.

Certains patchs sont restés : par exemple, les pointeurs marqués dans le tampon de code - une partie d'entre eux représentent simplement des BinaryenExpressionRef, c'est-à-dire qu'ils pointent vers des expressions qui doivent être placées linéairement dans le bloc de base généré, une partie constitut l'instruction de transition entre les blocs de base, et une autre - vers où aller. De plus, il y a des blocs déjà préparés pour le Relooper, qui doivent être connectés selon certaines conditions. Pour les distinguer, on utilise l'hypothèse que tous sont alignés sur au moins quatre octets, donc on peut utiliser tranquillement les deux bits de poids faible pour l'étiquette, il suffit de ne pas oublier de l'enlever si nécessaire. D'ailleurs, de telles étiquettes sont déjà utilisées dans QEMU pour indiquer la raison de sortie de la boucle TCG.

L'utilisation de Binaryen

Les modules dans WebAssembly contiennent des fonctions, chacune ayant un corps qui est une expression. Les expressions sont des opérations unaires et binaires, des blocs constitués de listes d'autres expressions, du contrôle de flux, etc. Comme je l'ai déjà dit, le contrôle de flux ici est organisé comme des bifurcations de haut niveau, des boucles, des appels de fonctions, etc. Les arguments sont passés aux fonctions non pas sur la pile, mais explicitement, comme en JS. Il y a aussi des variables globales, mais je ne les ai pas utilisées, donc je n'en parlerai pas.

Les fonctions ont également des variables locales numérotées à partir de zéro, qui ont les types : int32 / int64 / float / double. Dans ce cas, les premières n variables locales sont les arguments passés à la fonction. Notez que, bien que tout cela ne soit pas vraiment de bas niveau en termes de flux de contrôle, les entiers ne portent pas en eux-mêmes l'indicateur « signé/non signé » : le comportement du nombre dépend du code d'opération.

En général, Binaryen fournit une API C simple: vous créez un module, dans lequel vous créez des expressions — unaires, binaires, des blocs d'autres expressions, un flux de contrôle, etc. Ensuite, vous créez une fonction dont le corps doit spécifier l'expression. Si vous avez, comme moi, un graphe de transitions de bas niveau, le composant relooper vous sera utile. D'après ce que je comprends, il est possible d'utiliser un contrôle de flux de haut niveau dans un bloc tant qu'il ne sort pas du bloc — c'est-à-dire que l'on peut faire un branchement interne fast path / slow path dans le code intégré de gestion du cache TLB, mais il n'est pas possible d'interférer avec le flux de contrôle « externe ». Lorsque vous libérez le relooper, ses blocs sont libérés, et lorsque vous libérez le module, les expressions, fonctions, etc., qui y sont allouées disparaissent. de son arène..

Cependant, si vous souhaitez interpréter le code à la volée sans créer et supprimer des instances de l'interpréteur, il peut avoir du sens de déplacer cette logique dans un fichier en C++, et de gérer directement toute l'API C++ de la bibliothèque, en contournant les wrappers prêts à l'emploi.

Ainsi, pour générer du code, il faut

// настроить глобальные параметры (можно поменять потом)
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, je suis désolé, c'est juste pour représenter l'échelle, et les détails se trouvent dans la documentation.

Et maintenant commence le kréks-feks-peks, à peu près 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,
          // ...
      }
  );
  // et voilà, vous avez déjà une instance !
}, buf, sz);

Pour relier le monde de QEMU et de JS tout en accédant rapidement aux fonctions compilées, un tableau (table de fonctions pour l'importation dans le lanceur) a été créé, et y ont été placées les fonctions générées. Pour calculer rapidement l'indice, l'indice du premier mot du bloc de traduction a d'abord été utilisé, mais ensuite, l'indice calculé selon cette formule a été simplement inscrit dans le champ. struct TranslationBlock.

Au fait, une démo (pour l'instant avec une licence floue) ne fonctionne correctement que sur Firefox. Les développeurs de Chrome étaient plutôt pas prêts à l'idée que quelqu'un voudrait créer plus de mille instances de modules WebAssembly, ils attribuaient donc simplement un gigaoctet d'espace d'adressage virtuel par instance...

Pour l'instant, c'est tout. Peut-être qu'il y aura un autre article si cela intéresse quelqu'un. En particulier, il reste au moins uniquement à faire fonctionner les dispositifs bloc. Il pourrait également être judicieux de rendre la compilation des modules WebAssembly asynchrone, comme c'est habituel dans le monde de JS, puisque nous disposons d'un interpréteur capable d'exécuter tout cela pendant que le module natif n'est pas prêt.

Pour finir, une énigme : vous avez compilé un binaire sur une architecture 32 bits, mais le code via des opérations mémoire déborde 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, cet accès à une adresse résultante trop élevée est problématique. Comment contourner cela ?

Du point de vue de l'administration

Je ne l'ai finalement pas testé, mais ma première idée a été : « Que se passerait-il si l'on installait Linux 32 bits ? » Dans ce cas, la partie supérieure de l'espace d'adressage serait occupée par le noyau. La seule question est de savoir combien serait occupé : 1 ou 2 Go.

Du point de vue du programmeur (option pour les praticiens)

Nous allons créer une bulle dans la partie supérieure de l'espace d'adressage. Je ne comprends pas vraiment pourquoi cela fonctionne — il devrait y avoir déjà une pile. Mais « nous, les 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));
}

… ce n'est pas compatible avec Valgrind, mais heureusement, Valgrind réussit très bien à évincer tout le monde de là 🙂

Peut-être que quelqu'un proposera une meilleure explication sur le fonctionnement de ce code que j'ai écrit…

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster