Qemu.js avec support JIT : vous pouvez toujours retourner le hachis à l'envers

Il y a quelques années Fabrice Bellard écrit par jslinux est un émulateur PC écrit en JavaScript. Après il y avait au moins plus x86 virtuel. Mais tous, à ma connaissance, étaient des interprètes, tandis que Qemu, écrit bien plus tôt par le même Fabrice Bellard, et, probablement, tout émulateur moderne qui se respecte, utilise la compilation JIT du code invité dans le code du système hôte. Il m'a semblé qu'il était temps de mettre en œuvre la tâche inverse de celle que résolvent les navigateurs : la compilation JIT du code machine en JavaScript, pour laquelle il semblait plus logique de porter Qemu. Il semblerait, pourquoi Qemu, il existe des émulateurs plus simples et conviviaux - le même VirtualBox, par exemple - installés et fonctionnent. Mais Qemu possède plusieurs fonctionnalités intéressantes

  • Open source
  • capacité à travailler sans pilote de noyau
  • capacité à travailler en mode interprète
  • prise en charge d'un grand nombre d'architectures hôtes et invités

Concernant le troisième point, je peux maintenant expliquer qu'en fait, en mode TCI, ce ne sont pas les instructions de la machine invitée elles-mêmes qui sont interprétées, mais le bytecode obtenu à partir d'elles, mais cela ne change pas l'essence - pour construire et exécuter Qemu sur une nouvelle architecture, si vous avez de la chance, un compilateur C suffit - l'écriture d'un générateur de code peut être reportée.

Et maintenant, après deux ans de bricolage tranquille avec le code source de Qemu pendant mon temps libre, un prototype fonctionnel est apparu, dans lequel vous pouvez déjà exécuter, par exemple, Kolibri OS.

Qu'est-ce qu'Emscripten

De nos jours, de nombreux compilateurs sont apparus, dont le résultat final est JavaScript. Certains, comme Type Script, étaient initialement destinés à être le meilleur moyen d'écrire pour le Web. Dans le même temps, Emscripten est un moyen de prendre du code C ou C++ existant et de le compiler sous une forme lisible par le navigateur. Sur cette page Nous avons rassemblé de nombreux ports de programmes bien connus : iciPar exemple, vous pouvez consulter PyPy - en passant, ils prétendent avoir déjà JIT. En fait, tous les programmes ne peuvent pas être simplement compilés et exécutés dans un navigateur. traits, qu'il faut cependant supporter, car l'inscription sur la même page dit : « Emscripten peut être utilisé pour compiler presque n'importe quel portable Code C/C++ vers JavaScript". Autrement dit, il existe un certain nombre d'opérations dont le comportement n'est pas défini selon la norme, mais qui fonctionnent généralement sur x86 - par exemple, l'accès non aligné aux variables, qui est généralement interdit sur certaines architectures. En général , Qemu est un programme multiplateforme et , je voulais le croire, et il ne contient pas déjà beaucoup de comportements indéfinis - prenez-le et compilez, puis bricolez un peu avec JIT - et vous avez terminé ! Mais ce n'est pas le cas cas...

Première tentative

D'une manière générale, je ne suis pas la première personne à avoir l'idée de porter Qemu en JavaScript. Une question a été posée sur le forum ReactOS si cela était possible avec Emscripten. Même plus tôt, il y avait des rumeurs selon lesquelles Fabrice Bellard l'avait fait personnellement, mais nous parlions de jslinux, qui, pour autant que je sache, n'est qu'une tentative d'obtenir manuellement des performances suffisantes en JS, et a été écrit à partir de zéro. Plus tard, Virtual x86 a été écrit - des sources non obscurcies ont été publiées et, comme indiqué, le plus grand « réalisme » de l'émulation a permis d'utiliser SeaBIOS comme firmware. De plus, il y a eu au moins une tentative de portage de Qemu à l'aide d'Emscripten - j'ai essayé de le faire paire de sockets, mais le développement, d'après ce que je comprends, a été gelé.

Donc, semble-t-il, voici les sources, voici Emscripten - prenez-le et compilez-le. Mais il existe également des bibliothèques dont dépend Qemu, et des bibliothèques dont dépendent ces bibliothèques, etc., et l'une d'elles est libffi, dont dépend la simplicité. Il y avait des rumeurs sur Internet selon lesquelles il y en avait un parmi la grande collection de ports de bibliothèques pour Emscripten, mais c'était difficile à croire : premièrement, il n'était pas destiné à être un nouveau compilateur, deuxièmement, il s'agissait d'un compilateur de trop bas niveau. bibliothèque à récupérer et à compiler en JS. Et ce n'est pas seulement une question d'insertions d'assembly - probablement, si vous le tordez, pour certaines conventions d'appel, vous pouvez générer les arguments nécessaires sur la pile et appeler la fonction sans eux. Mais Emscripten est une chose délicate : afin de rendre le code généré familier à l'optimiseur du moteur JS du navigateur, certaines astuces sont utilisées. En particulier, ce qu'on appelle le relooping - un générateur de code utilisant l'IR LLVM reçu avec des instructions de transition abstraites tente de recréer des ifs, des boucles, etc. plausibles. Eh bien, comment les arguments sont-ils transmis à la fonction ? Naturellement, en tant qu'arguments des fonctions JS, c'est-à-dire, si possible, pas via la pile.

Au début, il y avait l'idée d'écrire simplement un remplacement pour libffi avec JS et d'exécuter des tests standard, mais à la fin, je ne savais pas comment créer mes fichiers d'en-tête pour qu'ils fonctionnent avec le code existant - que puis-je faire, comme on dit : « Les tâches sont-elles si complexes ? Sommes-nous si stupides ? J'ai dû porter libffi sur une autre architecture, pour ainsi dire - heureusement, Emscripten a à la fois des macros pour l'assemblage en ligne (en Javascript, ouais - enfin, quelle que soit l'architecture, donc l'assembleur), et la possibilité d'exécuter du code généré à la volée. En général, après avoir bricolé pendant un certain temps des fragments libffi dépendants de la plate-forme, j'ai obtenu du code compilable et je l'ai exécuté lors du premier test que j'ai rencontré. À ma grande surprise, le test a réussi. Abasourdi par mon génie - sans blague, cela a fonctionné dès le premier lancement -, n'en croyant toujours pas mes yeux, je suis allé revoir le code résultant, pour évaluer où creuser ensuite. Ici, je suis devenu fou pour la deuxième fois - la seule chose que ma fonction faisait était ffi_call - cela a signalé un appel réussi. Il n'y a pas eu d'appel lui-même. J'ai donc envoyé ma première pull request, qui corrigeait une erreur dans le test qui est claire pour tout étudiant de l'Olympiade : les nombres réels ne doivent pas être comparés comme a == b et même comment a - b < EPS - il faut aussi se souvenir du module, sinon 0 s'avérera être très égal à 1/3... En général, j'ai trouvé un certain port de libffi, qui passe les tests les plus simples, et avec lequel glib est compilé - j'ai décidé que ce serait nécessaire, je l'ajouterai plus tard. Pour l'avenir, je dirai qu'il s'est avéré que le compilateur n'a même pas inclus la fonction libffi dans le code final.

Mais, comme je l'ai déjà dit, il existe certaines limitations, et parmi l'utilisation gratuite de divers comportements non définis, une fonctionnalité plus désagréable a été cachée : JavaScript, de par sa conception, ne prend pas en charge le multithreading avec mémoire partagée. En principe, cela peut même être considéré comme une bonne idée, mais pas pour le portage de code dont l'architecture est liée aux threads C. De manière générale, Firefox expérimente la prise en charge des travailleurs partagés et Emscripten propose une implémentation pthread pour eux, mais je ne voulais pas en dépendre. J'ai dû éliminer lentement le multithreading du code Qemu - c'est-à-dire découvrir où les threads s'exécutent, déplacer le corps de la boucle exécutée dans ce thread dans une fonction distincte et appeler ces fonctions une par une à partir de la boucle principale.

Deuxième tentative

À un moment donné, il est devenu clair que le problème était toujours là et que déplacer le code au hasard avec des béquilles ne mènerait à rien de bon. Conclusion : nous devons en quelque sorte systématiser le processus d'ajout de béquilles. Par conséquent, la version 2.4.1, qui était récente à l'époque, a été prise (pas la 2.5.0, car, qui sait, il y aura des bugs dans la nouvelle version qui n'ont pas encore été détectés, et j'en ai assez de mes propres bugs ), et la première chose était de le réécrire en toute sécurité thread-posix.c. Eh bien, c'est tout aussi sûr : si quelqu'un essayait d'effectuer une opération conduisant à un blocage, la fonction était immédiatement appelée abort() - bien sûr, cela n'a pas résolu tous les problèmes à la fois, mais au moins c'était en quelque sorte plus agréable que de recevoir tranquillement des données incohérentes.

En général, les options d'Emscripten sont très utiles pour porter du code vers JS. -s ASSERTIONS=1 -s SAFE_HEAP=1 - ils détectent certains types de comportements non définis, comme les appels à une adresse non alignée (ce qui n'est pas du tout cohérent avec le code des tableaux typés comme HEAP32[addr >> 2] = 1) ou en appelant une fonction avec un nombre incorrect d'arguments.

Soit dit en passant, les erreurs d'alignement sont un problème distinct. Comme je l'ai déjà dit, Qemu dispose d'un backend interprétatif « dégénéré » pour la génération de code TCI (tiny code interpréteur), et pour construire et exécuter Qemu sur une nouvelle architecture, si vous avez de la chance, un compilateur C suffit. "si tu es chanceux". Je n'ai pas eu de chance et il s'est avéré que TCI utilise un accès non aligné lors de l'analyse de son bytecode. Autrement dit, sur toutes sortes d'architectures ARM et autres avec un accès nécessairement nivelé, Qemu compile parce qu'ils ont un backend TCG normal qui génère du code natif, mais la question de savoir si TCI fonctionnera dessus est une autre question. Cependant, il s'est avéré que la documentation de TCI indiquait clairement quelque chose de similaire. En conséquence, des appels de fonction pour une lecture non alignée ont été ajoutés au code, qui ont été trouvés dans une autre partie de Qemu.

Destruction de tas

En conséquence, l'accès non aligné au TCI a été corrigé, une boucle principale a été créée, qui à son tour appelle le processeur, le RCU et quelques autres petites choses. Et donc je lance Qemu avec l'option -d exec,in_asm,out_asm, ce qui signifie que vous devez indiquer quels blocs de code sont en cours d'exécution, et également au moment de la diffusion, écrire quel était le code invité, quel est le code hôte (dans ce cas, le bytecode). Il démarre, exécute plusieurs blocs de traduction, écrit le message de débogage que j'ai laissé indiquant que RCU va maintenant démarrer et... plante abort() à l'intérieur d'une fonction free(). En bricolant la fonction free() Nous avons réussi à découvrir que dans l'en-tête du bloc de tas, qui se trouve dans les huit octets précédant la mémoire allouée, au lieu de la taille du bloc ou quelque chose de similaire, il y avait des déchets.

Destruction du tas - comme c'est mignon... Dans un tel cas, il existe un remède utile - à partir (si possible) des mêmes sources, assemblez un binaire natif et exécutez-le sous Valgrind. Après un certain temps, le binaire était prêt. Je le lance avec les mêmes options : il plante même lors de l'initialisation, avant d'atteindre réellement l'exécution. C'est désagréable, bien sûr - apparemment, les sources n'étaient pas exactement les mêmes, ce qui n'est pas surprenant, car la configuration a repéré des options légèrement différentes, mais j'ai Valgrind - je vais d'abord corriger ce bug, et ensuite, si j'ai de la chance , l'original apparaîtra. Je lance la même chose sous Valgrind... Y-y-y, y-y-y, euh-euh, ça a démarré, s'est initialisé normalement et a dépassé le bug d'origine sans un seul avertissement concernant un accès mémoire incorrect, sans parler des chutes. La vie, comme on dit, ne m'a pas préparé à cela - un programme qui plante cesse de planter lorsqu'il est lancé sous Walgrind. Ce que c'était est un mystère. Mon hypothèse est qu'une fois à proximité de l'instruction en cours après un crash lors de l'initialisation, gdb a montré du travail memset-a avec un pointeur valide utilisant soit mmxou xmm registres, alors c'était peut-être une sorte d'erreur d'alignement, même si c'est encore difficile à croire.

D'accord, Valgrind ne semble pas aider ici. Et ici, la chose la plus dégoûtante a commencé - tout semble même commencer, mais se bloque pour des raisons absolument inconnues en raison d'un événement qui aurait pu se produire il y a des millions d'instructions. Pendant longtemps, on ne savait même pas comment s'y prendre. En fin de compte, j'ai encore dû m'asseoir et déboguer. L'impression avec laquelle l'en-tête a été réécrit a montré qu'il ne ressemblait pas à un nombre, mais plutôt à une sorte de données binaires. Et voilà, cette chaîne binaire a été trouvée dans le fichier BIOS - c'est-à-dire qu'il était maintenant possible de dire avec une confiance raisonnable qu'il s'agissait d'un débordement de tampon, et il est même clair qu'elle a été écrite dans ce tampon. Eh bien, alors quelque chose comme ça - dans Emscripten, heureusement, il n'y a pas de randomisation de l'espace d'adressage, il n'y a pas non plus de trous, vous pouvez donc écrire quelque part au milieu du code pour afficher les données par pointeur du dernier lancement, regardez les données, regardez le pointeur et, si elles n'ont pas changé, trouvez matière à réflexion. Il est vrai que la création d'un lien prend quelques minutes après tout changement, mais que pouvez-vous faire ? En conséquence, une ligne spécifique a été trouvée qui copiait le BIOS du tampon temporaire vers la mémoire invité - et, en effet, il n'y avait pas assez d'espace dans le tampon. Trouver la source de cette étrange adresse de tampon a abouti à une fonction qemu_anon_ram_alloc dans le fichier oslib-posix.c - la logique était la suivante : parfois il peut être utile d'aligner l'adresse sur une énorme page de 2 Mo, pour cela nous demanderons mmap d'abord un peu plus, puis nous rendrons l'excédent avec l'aide munmap. Et si un tel alignement n'est pas requis, alors nous indiquerons le résultat au lieu de 2 Mo getpagesize() - mmap il donnera toujours une adresse alignée... Donc en Emscripten mmap il suffit d'appeler malloc, mais bien sûr, il ne s'aligne pas sur la page. En général, un bug qui m'a frustré pendant quelques mois a été corrigé par un changement de deux lignes.

Caractéristiques des fonctions d'appel

Et maintenant, le processeur compte quelque chose, Qemu ne plante pas, mais l'écran ne s'allume pas et le processeur entre rapidement en boucle, à en juger par la sortie -d exec,in_asm,out_asm. Une hypothèse a émergé : les interruptions de la minuterie (ou, en général, toutes les interruptions) n'arrivent pas. Et en effet, si vous dévissez les interruptions de l'assemblage natif, qui, pour une raison quelconque, ont fonctionné, vous obtenez une image similaire. Mais ce n’était pas du tout la réponse : une comparaison des traces émises avec l’option ci-dessus a montré que les trajectoires d’exécution divergeaient très tôt. Ici il faut dire que la comparaison de ce qui a été enregistré à l'aide du lanceur emrun le débogage de la sortie avec la sortie de l’assembly natif n’est pas un processus complètement mécanique. Je ne sais pas exactement comment un programme exécuté dans un navigateur se connecte à emrun, mais certaines lignes de la sortie s'avèrent être réorganisées, donc la différence de différence n'est pas encore une raison pour supposer que les trajectoires ont divergé. En général, il est devenu clair que selon les instructions ljmpl il y a une transition vers différentes adresses, et le bytecode généré est fondamentalement différent : l'un contient une instruction pour appeler une fonction d'assistance, l'autre non. Après avoir recherché les instructions sur Google et étudié le code qui traduit ces instructions, il est devenu clair que, premièrement, juste avant dans le registre cr0 un enregistrement a été effectué - également à l'aide d'un assistant - qui a fait passer le processeur en mode protégé, et deuxièmement, que la version js n'est jamais passée en mode protégé. Mais le fait est qu’une autre caractéristique d’Emscripten est sa réticence à tolérer du code tel que l’implémentation d’instructions. call dans TCI, quel type de pointeur de fonction donne long long f(int arg0, .. int arg9) - les fonctions doivent être appelées avec le bon nombre d'arguments. Si cette règle n'est pas respectée, en fonction des paramètres de débogage, le programme plantera (ce qui est bien) ou appellera la mauvaise fonction (ce qui sera triste à déboguer). Il existe également une troisième option : activer la génération de wrappers qui ajoutent/suppriment des arguments, mais au total, ces wrappers prennent beaucoup de place, malgré le fait qu'en fait je n'ai besoin que d'un peu plus d'une centaine de wrappers. Cela en soi est très triste, mais il s'est avéré qu'il y avait un problème plus grave : dans le code généré des fonctions wrapper, les arguments étaient convertis et convertis, mais parfois la fonction avec les arguments générés n'était pas appelée - enfin, tout comme dans mon implémentation libffi. Autrement dit, certains assistants n'ont tout simplement pas été exécutés.

Heureusement, Qemu dispose de listes d'assistants lisibles par machine sous la forme d'un fichier d'en-tête comme

DEF_HELPER_0(lock, void)
DEF_HELPER_0(unlock, void)
DEF_HELPER_3(write_eflags, void, env, tl, i32)

Ils sont utilisés de manière assez amusante : d'abord, les macros sont redéfinies de la manière la plus bizarre DEF_HELPER_n, puis s'allume helper.h. Dans la mesure où la macro est développée en un initialiseur de structure et une virgule, puis un tableau est défini, et au lieu d'éléments - #include <helper.h> En conséquence, j'ai enfin eu la chance d'essayer la bibliothèque au travail pyanalyse, et un script a été écrit qui génère exactement ces wrappers pour exactement les fonctions pour lesquelles ils sont nécessaires.

Et donc, après cela, le processeur a semblé fonctionner. Cela semble être dû au fait que l'écran n'a jamais été initialisé, bien que memtest86+ ait pu s'exécuter dans l'assembly natif. Ici, il est nécessaire de préciser que le code d'E/S du bloc Qemu est écrit en coroutines. Emscripten a sa propre implémentation très délicate, mais elle devait encore être prise en charge dans le code Qemu, et vous pouvez déboguer le processeur maintenant : Qemu prend en charge les options -kernel, -initrd, -append, avec lequel vous pouvez démarrer Linux ou, par exemple, memtest86+, sans utiliser de périphérique bloc. Mais voici le problème : dans l'assembly natif, on pouvait voir la sortie du noyau Linux sur la console avec l'option -nographic, et aucune sortie du navigateur vers le terminal à partir duquel il a été lancé emrun, n'est pas venu. Autrement dit, ce n'est pas clair : le processeur ne fonctionne pas ou la sortie graphique ne fonctionne pas. Et puis j’ai pensé à attendre un peu. Il s'est avéré que "le processeur ne dort pas, mais clignote simplement lentement", et après environ cinq minutes, le noyau a envoyé un tas de messages sur la console et a continué à se bloquer. Il est devenu clair que le processeur, en général, fonctionne et que nous devons approfondir le code pour travailler avec SDL2. Malheureusement, je ne sais pas comment utiliser cette bibliothèque, j'ai donc dû agir au hasard à certains endroits. À un moment donné, la ligne parallèle0 a clignoté sur l'écran sur fond bleu, ce qui a suggéré quelques réflexions. En fin de compte, il s'est avéré que le problème était que Qemu ouvre plusieurs fenêtres virtuelles dans une fenêtre physique, entre lesquelles vous pouvez basculer en utilisant Ctrl-Alt-n : cela fonctionne dans la version native, mais pas dans Emscripten. Après vous être débarrassé des fenêtres inutiles à l'aide des options -monitor none -parallel none -serial none et des instructions pour redessiner de force tout l'écran sur chaque image, tout a soudainement fonctionné.

Coroutines

Ainsi, l'émulation dans le navigateur fonctionne, mais vous ne pouvez rien y exécuter d'intéressant sur une seule disquette, car il n'y a pas d'E/S de bloc - vous devez implémenter la prise en charge des coroutines. Qemu dispose déjà de plusieurs backends de coroutines, mais en raison de la nature de JavaScript et du générateur de code Emscripten, vous ne pouvez pas simplement commencer à jongler avec les piles. Il semblerait que « tout soit parti, le plâtre est en train d'être enlevé », mais les développeurs d'Emscripten se sont déjà occupés de tout. Ceci est implémenté de manière assez amusante : appelons un appel de fonction comme celui-ci suspect emscripten_sleep et plusieurs autres utilisant le mécanisme Asyncify, ainsi que des appels de pointeur et des appels à toute fonction où l'un des deux cas précédents peut se produire plus bas dans la pile. Et maintenant, avant chaque appel suspect, nous sélectionnerons un contexte asynchrone, et immédiatement après l'appel, nous vérifierons si un appel asynchrone a eu lieu, et si c'est le cas, nous enregistrerons toutes les variables locales dans ce contexte asynchrone, indiquerons quelle fonction pour transférer le contrôle au moment où nous devons continuer l'exécution et quitter la fonction actuelle. C'est là qu'il est possible d'étudier l'effet gaspillage — pour les besoins de poursuite de l'exécution du code après le retour d'un appel asynchrone, le compilateur génère des « stubs » de la fonction commençant après un appel suspect — comme ceci : s'il y a n appels suspects, alors la fonction sera développée quelque part n/2 fois - c'est toujours le cas, sinon Gardez à l'esprit qu'après chaque appel potentiellement asynchrone, vous devez ajouter la sauvegarde de certaines variables locales à la fonction d'origine. Par la suite, j'ai même dû écrire un simple script en Python, qui, basé sur un ensemble donné de fonctions particulièrement galvaudées qui sont censées « ne pas laisser passer l'asynchronie » (c'est-à-dire la promotion de la pile et tout ce que je viens de décrire ne le font pas). y travailler), indique les appels via des pointeurs dans lesquels les fonctions doivent être ignorées par le compilateur afin que ces fonctions ne soient pas considérées comme asynchrones. Et puis les fichiers JS de moins de 60 Mo, c'est clairement trop - disons au moins 30. Bien que, une fois que j'ai configuré un script d'assemblage, j'ai accidentellement jeté les options de l'éditeur de liens, parmi lesquelles -O3. J'exécute le code généré et Chromium consomme de la mémoire et plante. J'ai ensuite accidentellement regardé ce qu'il essayait de télécharger... Eh bien, que puis-je dire, j'aurais aussi gelé si on m'avait demandé d'étudier et d'optimiser attentivement un Javascript de plus de 500 Mo.

Malheureusement, les vérifications dans le code de la bibliothèque de support Asyncify n'étaient pas entièrement compatibles avec longjmp-s qui sont utilisés dans le code du processeur virtuel, mais après un petit patch qui désactive ces vérifications et restaure de force les contextes comme si tout allait bien, le code a fonctionné. Et puis une chose étrange a commencé : parfois des contrôles dans le code de synchronisation étaient déclenchés - les mêmes qui plantent le code si, selon la logique d'exécution, il devait être bloqué - quelqu'un a essayé de récupérer un mutex déjà capturé. Heureusement, cela ne s'est pas avéré être un problème logique dans le code sérialisé - j'utilisais simplement la fonctionnalité de boucle principale standard fournie par Emscripten, mais parfois l'appel asynchrone déballait complètement la pile, et à ce moment-là, il échouait. setTimeout de la boucle principale - ainsi, le code est entré dans l'itération de la boucle principale sans quitter l'itération précédente. Réécrit sur une boucle infinie et emscripten_sleep, et les problèmes avec les mutex ont cessé. Le code est même devenu plus logique - après tout, en fait, je n'ai pas de code qui prépare la prochaine image d'animation - le processeur calcule simplement quelque chose et l'écran est périodiquement mis à jour. Cependant, les problèmes ne s'arrêtaient pas là : parfois, l'exécution de Qemu se terminait simplement silencieusement, sans aucune exception ni erreur. À ce moment-là, j'ai abandonné, mais, pour l'avenir, je dirai que le problème était le suivant : le code de la coroutine, en fait, n'utilise pas setTimeout (ou du moins pas aussi souvent qu'on pourrait le penser) : fonction emscripten_yield définit simplement l'indicateur d'appel asynchrone. Le tout est que emscripten_coroutine_next n'est pas une fonction asynchrone : en interne, elle vérifie le drapeau, le réinitialise et transfère le contrôle là où il est nécessaire. C'est-à-dire que la promotion de la pile s'arrête là. Le problème était qu'en raison de l'utilisation après libération, qui apparaissait lorsque le pool de coroutines était désactivé en raison du fait que je n'avais pas copié une ligne de code importante du backend de coroutine existant, la fonction qemu_in_coroutine est retourné vrai alors qu'en fait il aurait dû retourner faux. Cela a conduit à un appel emscripten_yield, au-dessus duquel il n'y avait personne sur la pile emscripten_coroutine_next, la pile s'est dépliée tout en haut, mais non setTimeout, comme je l'ai déjà dit, n'a pas été exposé.

Génération de code JavaScript

Et voici, en fait, la promesse de « retourner la viande hachée ». Pas vraiment. Bien sûr, si nous exécutons Qemu dans le navigateur et Node.js dedans, alors, naturellement, après la génération de code dans Qemu, nous nous tromperons complètement en JavaScript. Mais quand même, une sorte de transformation inverse.

Tout d’abord, un peu sur le fonctionnement de Qemu. Veuillez me pardonner tout de suite : je ne suis pas un développeur Qemu professionnel et mes conclusions peuvent être erronées à certains endroits. Comme on dit, « l’opinion de l’élève ne doit pas nécessairement coïncider avec l’opinion de l’enseignant, avec les axiomatiques et le bon sens de Peano ». Qemu dispose d'un certain nombre d'architectures invitées supportées et pour chacune il existe un répertoire comme target-i386. Lors de la construction, vous pouvez spécifier la prise en charge de plusieurs architectures invitées, mais le résultat ne sera que plusieurs binaires. Le code destiné à prendre en charge l'architecture invitée génère à son tour certaines opérations internes de Qemu, que le TCG (Tiny Code Generator) transforme déjà en code machine pour l'architecture hôte. Comme indiqué dans le fichier Lisez-moi situé dans le répertoire tcg, celui-ci faisait à l'origine partie d'un compilateur C standard, qui a ensuite été adapté pour JIT. Ainsi, par exemple, l'architecture cible au sens de ce document n'est plus une architecture invitée, mais une architecture hôte. À un moment donné, un autre composant est apparu - Tiny Code Interpreter (TCI), qui devrait exécuter du code (presque les mêmes opérations internes) en l'absence d'un générateur de code pour une architecture hôte spécifique. En fait, comme l'indique sa documentation, cet interpréteur ne fonctionne pas toujours aussi bien qu'un générateur de code JIT, non seulement quantitativement en termes de vitesse, mais aussi qualitativement. Même si je ne suis pas sûr que sa description soit tout à fait pertinente.

Au début, j'ai essayé de créer un backend TCG à part entière, mais je me suis rapidement perdu dans le code source et une description pas tout à fait claire des instructions du bytecode, j'ai donc décidé d'emballer l'interpréteur TCI. Cela présentait plusieurs avantages :

  • lors de l'implémentation d'un générateur de code, vous ne pouvez pas regarder la description des instructions, mais le code de l'interprète
  • vous pouvez générer des fonctions non pas pour chaque bloc de traduction rencontré, mais, par exemple, seulement après la centième exécution
  • si le code généré change (et cela semble possible, à en juger par les fonctions dont les noms contiennent le mot patch), je devrai invalider le code JS généré, mais au moins j'aurai quelque chose pour le régénérer

Concernant le troisième point, je ne suis pas sûr que le patch soit possible après la première exécution du code, mais les deux premiers points suffisent.

Initialement, le code était généré sous la forme d'un gros commutateur à l'adresse de l'instruction de bytecode d'origine, mais ensuite, en me souvenant de l'article sur Emscripten, de l'optimisation du JS généré et du relooping, j'ai décidé de générer plus de code humain, d'autant plus qu'empiriquement il Il s'est avéré que le seul point d'entrée dans le bloc de traduction est son début. Aussitôt dit, aussitôt fait, au bout d'un moment, nous avions un générateur de code qui générait du code avec des if (bien que sans boucles). Mais pas de chance, il s'est écrasé, donnant un message indiquant que les instructions étaient d'une longueur incorrecte. De plus, la dernière instruction à ce niveau de récursion était brcond. D'accord, je vais ajouter une vérification identique à la génération de cette instruction avant et après l'appel récursif et... aucun d'entre eux n'a été exécuté, mais après le commutateur d'assertion, ils ont toujours échoué. Au final, après avoir étudié le code généré, j'ai réalisé qu'après le basculement, le pointeur vers l'instruction en cours est rechargé depuis la pile et est probablement écrasé par le code JavaScript généré. Et c’est ce qui s’est passé. L'augmentation du tampon d'un mégaoctet à dix n'a mené à rien et il est devenu clair que le générateur de code tournait en rond. Nous devions vérifier que nous n'avions pas dépassé les limites du TB actuel, et si nous le faisions, alors émettre l'adresse du prochain TB avec un signe moins afin que nous puissions continuer l'exécution. De plus, cela résout le problème « quelles fonctions générées doivent être invalidées si ce morceau de bytecode a changé ? » — seule la fonction qui correspond à ce bloc de traduction doit être invalidée. À propos, bien que j'aie tout débogué dans Chromium (puisque j'utilise Firefox et qu'il m'est plus facile d'utiliser un navigateur séparé pour les expériences), Firefox m'a aidé à corriger les incompatibilités avec la norme asm.js, après quoi le code a commencé à fonctionner plus rapidement dans Chrome.

Exemple de code généré

Compiling 0x15b46d0:
CompiledTB[0x015b46d0] = function(stdlib, ffi, heap) {
"use asm";
var HEAP8 = new stdlib.Int8Array(heap);
var HEAP16 = new stdlib.Int16Array(heap);
var HEAP32 = new stdlib.Int32Array(heap);
var HEAPU8 = new stdlib.Uint8Array(heap);
var HEAPU16 = new stdlib.Uint16Array(heap);
var HEAPU32 = new stdlib.Uint32Array(heap);

var dynCall_iiiiiiiiiii = ffi.dynCall_iiiiiiiiiii;
var getTempRet0 = ffi.getTempRet0;
var badAlignment = ffi.badAlignment;
var _i64Add = ffi._i64Add;
var _i64Subtract = ffi._i64Subtract;
var Math_imul = ffi.Math_imul;
var _mul_unsigned_long_long = ffi._mul_unsigned_long_long;
var execute_if_compiled = ffi.execute_if_compiled;
var getThrew = ffi.getThrew;
var abort = ffi.abort;
var qemu_ld_ub = ffi.qemu_ld_ub;
var qemu_ld_leuw = ffi.qemu_ld_leuw;
var qemu_ld_leul = ffi.qemu_ld_leul;
var qemu_ld_beuw = ffi.qemu_ld_beuw;
var qemu_ld_beul = ffi.qemu_ld_beul;
var qemu_ld_beq = ffi.qemu_ld_beq;
var qemu_ld_leq = ffi.qemu_ld_leq;
var qemu_st_b = ffi.qemu_st_b;
var qemu_st_lew = ffi.qemu_st_lew;
var qemu_st_lel = ffi.qemu_st_lel;
var qemu_st_bew = ffi.qemu_st_bew;
var qemu_st_bel = ffi.qemu_st_bel;
var qemu_st_leq = ffi.qemu_st_leq;
var qemu_st_beq = ffi.qemu_st_beq;

function tb_fun(tb_ptr, env, sp_value, depth) {
  tb_ptr = tb_ptr|0;
  env = env|0;
  sp_value = sp_value|0;
  depth = depth|0;
  var u0 = 0, u1 = 0, u2 = 0, u3 = 0, result = 0;
  var r0 = 0, r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0, r7 = 0, r8 = 0, r9 = 0;
  var r10 = 0, r11 = 0, r12 = 0, r13 = 0, r14 = 0, r15 = 0, r16 = 0, r17 = 0, r18 = 0, r19 = 0;
  var r20 = 0, r21 = 0, r22 = 0, r23 = 0, r24 = 0, r25 = 0, r26 = 0, r27 = 0, r28 = 0, r29 = 0;
  var r30 = 0, r31 = 0, r41 = 0, r42 = 0, r43 = 0, r44 = 0;
    r14 = env|0;
    r15 = sp_value|0;
  START: do {
    r0 = HEAPU32[((r14 + (-4))|0) >> 2] | 0;
    r42 = 0;
    result = ((r0|0) != (r42|0))|0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445321] = r14;
    if(result|0) {
    HEAPU32[1445322] = r15;
    return 0x0345bf93|0;
    }
    r0 = HEAPU32[((r14 + (16))|0) >> 2] | 0;
    r42 = 8;
    r0 = ((r0|0) - (r42|0))|0;
    HEAPU32[(r14 + (16)) >> 2] = r0;
    r1 = 8;
    HEAPU32[(r14 + (44)) >> 2] = r1;
    r1 = r0|0;
    HEAPU32[(r14 + (40)) >> 2] = r1;
    r42 = 4;
    r0 = ((r0|0) + (r42|0))|0;
    r2 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    HEAPU32[1445309] = r2;
    HEAPU32[1445321] = r14;
    HEAPU32[1445322] = r15;
    qemu_st_lel(env|0, r0|0, r2|0, 34, 22759218);
if(getThrew() | 0) abort();
    r0 = 3241038392;
    HEAPU32[1445307] = r0;
    r0 = qemu_ld_leul(env|0, r0|0, 34, 22759233)|0;
if(getThrew() | 0) abort();
    HEAPU32[(r14 + (24)) >> 2] = r0;
    r1 = HEAPU32[((r14 + (12))|0) >> 2] | 0;
    r2 = HEAPU32[((r14 + (40))|0) >> 2] | 0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    HEAPU32[1445309] = r2;
    qemu_st_lel(env|0, r2|0, r1|0, 34, 22759265);
if(getThrew() | 0) abort();
    r0 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
    HEAPU32[(r14 + (40)) >> 2] = r0;
    r1 = 24;
    HEAPU32[(r14 + (52)) >> 2] = r1;
    r42 = 0;
    result = ((r0|0) == (r42|0))|0;
    if(result|0) {
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    }
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    return execute_if_compiled(22759392|0, env|0, sp_value|0, depth|0) | 0;
    return execute_if_compiled(23164080|0, env|0, sp_value|0, depth|0) | 0;
    break;
  } while(1); abort(); return 0|0;
}
return {tb_fun: tb_fun};
}(window, CompilerFFI, Module.buffer)["tb_fun"]

Conclusion

Les travaux ne sont donc toujours pas terminés, mais j’en ai marre de perfectionner en secret cette construction au long cours. Par conséquent, j'ai décidé de publier ce que j'ai pour l'instant. Le code fait un peu peur par endroits, car il s’agit d’une expérience et il n’est pas clair à l’avance ce qui doit être fait. Cela vaut probablement la peine d'émettre des commits atomiques normaux en plus d'une version plus moderne de Qemu. Entre-temps, il existe un fil de discussion dans la Gita sous forme de blog : pour chaque « niveau » qui a été franchi au moins d'une manière ou d'une autre, un commentaire détaillé en russe a été ajouté. En fait, cet article est dans une large mesure un récit de la conclusion git log.

Vous pouvez tout essayer ici (attention à la circulation).

Ce qui fonctionne déjà :

  • Processeur virtuel x86 en cours d'exécution
  • Il existe un prototype fonctionnel d'un générateur de code JIT du code machine au JavaScript
  • Il existe un modèle pour assembler d'autres architectures invitées 32 bits : vous pouvez dès maintenant admirer Linux pour l'architecture MIPS qui se fige dans le navigateur au moment du chargement

Que pouvez vous faire d'autre

  • Accélérez l'émulation. Même en mode JIT, il semble fonctionner plus lentement que Virtual x86 (mais il existe potentiellement tout un Qemu avec beaucoup de matériel et d'architectures émulés)
  • Pour créer une interface normale - franchement, je ne suis pas un bon développeur Web, donc pour l'instant j'ai refait le shell Emscripten standard du mieux que je peux
  • Essayez de lancer des fonctions Qemu plus complexes - mise en réseau, migration de VM, etc.
  • UPD: vous devrez soumettre vos quelques développements et rapports de bugs à Emscripten en amont, comme l'ont fait les précédents porteurs de Qemu et d'autres projets. Merci à eux de pouvoir utiliser implicitement leur contribution à Emscripten dans le cadre de ma tâche.

Source: habr.com

Ajouter un commentaire