Il y a quelques annĂ©es, Fabrice Bellard â un Ă©mulateur de PC Ă©crit en JavaScript. AprĂšs cela, il y a eu au moins . Mais tous, autant que je sache, Ă©taient des interprĂ©teurs, tandis que Qemu, Ă©crit beaucoup plus tĂŽt par le mĂȘme Fabrice Bellard, et probablement tout Ă©mulateur moderne de bonne rĂ©putation, utilise la compilation JIT du code invitĂ© en code de systĂšme hĂŽte. Il m'a semblĂ© qu'il Ă©tait temps de rĂ©aliser la tĂąche inverse de celle que les navigateurs rĂ©solvent : la compilation JIT du code machine en JavaScript, pour laquelle il Ă©tait logique de porter Qemu. On pourrait se demander pourquoi Qemu, il existe des Ă©mulateurs plus simples et conviviaux â comme VirtualBox, par exemple â qu'il suffit d'installer et de faire fonctionner. Mais Qemu a plusieurs caractĂ©ristiques intĂ©ressantes
- les sources ouvertes
- la possibilité de fonctionner sans pilote de noyau
- la possibilité de fonctionner en mode interpréteur
- le support d'un grand nombre d'architectures, tant hÎtes qu'invitées
Pour le troisiĂšme point, je peux maintenant expliquer que, en rĂ©alitĂ©, en mode TCI, ce ne sont pas les instructions machine invitĂ©es qui sont interprĂ©tĂ©es, mais le bytecode qui en est dĂ©rivĂ©, mais cela ne change rien Ă la situation â pour compiler et exĂ©cuter Qemu sur une nouvelle architecture, si tout va bien, un compilateur C suffit â la rĂ©daction d'un gĂ©nĂ©rateur de code peut ĂȘtre remise Ă plus tard.
Et voilà , aprÚs deux ans de plongée tranquille dans les sources de Qemu, un prototype fonctionnel est apparu, sur lequel il est déjà possible de faire tourner, par exemple, Kolibri OS.
Qu'est-ce qu'Emscripten
De nos jours, de nombreux compilateurs existent dont le rĂ©sultat final est du JavaScript. Certains, comme TypeScript, ont Ă©tĂ© conçus dĂšs le dĂ©part comme un meilleur moyen d'Ă©crire pour le web. En mĂȘme temps, Emscripten est un moyen de prendre du code existant en C ou C++ et de le compiler dans un format comprĂ©hensible par le navigateur. Sur de nombreux ports de programmes connus ont Ă©tĂ© rĂ©alisĂ©s : , par exemple, on peut jeter un Ćil Ă PyPy â d'ailleurs, il semble qu'ils aient dĂ©jĂ JIT. En rĂ©alitĂ©, il n'est pas possible de simplement compiler et exĂ©cuter n'importe quel programme dans un navigateur â il existe une sĂ©rie , avec laquelle il faut composer, cependant, comme le dit l'inscription sur cette mĂȘme page "Emscripten peut ĂȘtre utilisĂ© pour compiler presque n'importe quel portable Code C/C++ vers JavaScript. Cela signifie qu'il existe un certain nombre d'opĂ©rations qui sont un comportement indĂ©fini selon la norme, mais qui fonctionnent gĂ©nĂ©ralement sur x86 â par exemple, l'accĂšs non alignĂ© aux variables, qui est complĂštement interdit sur certaines architectures. En gĂ©nĂ©ral, Qemu est un programme multiplateforme et, espĂ©rons-le, ne contient pas trop de comportements indĂ©finis â prends et compile, puis un peu de manipulation avec JIT â et c'est prĂȘt ! Mais ce n'est pas si simple...
PremiĂšre tentative
En fait, je ne suis pas le premier Ă avoir eu l'idĂ©e de porter Qemu sur JavaScript. Sur le forum ReactOS, la question a Ă©tĂ© posĂ©e, est-ce possible avec Emscripten ? Plus tĂŽt, des rumeurs circulaient selon lesquelles Fabrice Bellard l'avait fait personnellement, mais il s'agissait de jslinux, qui, autant que je sache, est en fait une tentative manuelle d'atteindre des performances suffisantes sur JS, et a Ă©tĂ© Ă©crit Ă partir de zĂ©ro. Plus tard, Virtual x86 a Ă©tĂ© Ă©crit â ses sources non obfusquĂ©es ont Ă©tĂ© publiĂ©es, et il a Ă©tĂ© affirmĂ© qu'une plus grande "rĂ©alisme" d'Ă©mulation permettait d'utiliser SeaBIOS comme firmware. De plus, il y a eu au moins une tentative de porter Qemu avec Emscripten â cela a Ă©tĂ© tentĂ© par , mais le dĂ©veloppement, autant que j'ai compris, a Ă©tĂ© gelĂ©.
Ainsi, on pourrait penser que voilĂ les sources, voilĂ Emscripten â prends et compile. Mais il y a aussi des bibliothĂšques dont Qemu dĂ©pend, et des bibliothĂšques dont ces bibliothĂšques dĂ©pendent, etc., et l'une d'elles est â , dont lequel dĂ©pend glib. Sur Internet, il y avait des rumeurs selon lesquelles une grande collection de ports de bibliothĂšques pour Emscripten la contenait, mais il Ă©tait difficile d'y croire : d'une part, elle ne se compilait pas avec le nouveau compilateur, et d'autre part, c'est une bibliothĂšque trop bas niveau pour simplement ĂȘtre compilĂ©e en JS. Ce n'est mĂȘme pas seulement une question d'instructions assembleur â probablement, si on cherche Ă torturer un peu le systĂšme, pour certaines conventions d'appel, on peut former les arguments nĂ©cessaires sur la pile sans elles et appeler la fonction. Mais Emscripten est une chose dĂ©licate : pour que le code gĂ©nĂ©rĂ© ait l'air familier pour l'optimiseur du moteur JS du navigateur, quelques trucs sont utilisĂ©s. En particulier, ce qu'on appelle le relooping â le gĂ©nĂ©rateur de code, Ă partir de l'IR LLVM reçu, essaie de recrĂ©er des if plausibles, des boucles, etc. Et les arguments de la fonction, comment sont-ils passĂ©s ? Ăvidemment, comme les arguments des fonctions JS, c'est-Ă -dire autant que possible, pas par la pile.
Au dĂ©part, j'avais l'idĂ©e d'Ă©crire simplement un remplacement de libffi en JS et de faire tourner les tests standards, mais en fin de compte, je me suis embrouillĂ© sur la façon de crĂ©er mes fichiers d'en-tĂȘte pour qu'ils fonctionnent avec le code existant â que voulez-vous, comme on dit, "Soit les tĂąches sont si complexes, soit nous sommes si stupides". J'ai donc dĂ» porter libffi sur une autre architecture, si l'on peut dire â heureusement, dans Emscripten, il y a des macros pour l'assembly inline (en JavaScript, oui â quelle architecture, quel assembleur), ainsi que la possibilitĂ© d'exĂ©cuter le code gĂ©nĂ©rĂ© Ă la volĂ©e. En somme, aprĂšs avoir passĂ© quelque temps avec les fragments dĂ©pendants de la plateforme de libffi, j'ai obtenu un code compilable et l'ai fait passer au premier test venu. Ă ma grande surprise, le test a Ă©tĂ© rĂ©ussi. ĂtonnĂ© par ma gĂ©nie â pas une blague, ça a fonctionnĂ© du premier coup â je, ne croyant toujours pas mes yeux, suis allĂ© vĂ©rifier Ă nouveau le code obtenu, Ă©valuer oĂč approfondir mes recherches. LĂ , j'ai de nouveau Ă©tĂ© surpris â la seule chose que ma fonction faisait ffi_call â c'Ă©tait rapporter un appel rĂ©ussi. Il n'y avait pas eu d'appel. Ainsi, j'ai soumis ma premiĂšre demande de tirage, corrigeant une erreur Ă©vidente pour tout olympien dans le test â les nombres Ă virgule flottante ne doivent pas ĂȘtre comparĂ©s comme a == b et mĂȘme comme a - b < EPS â il ne faut pas oublier le module, sinon 0 se rĂ©vĂ©lera en rĂ©alitĂ© Ă©gal Ă 1/3⊠En gros, j'ai obtenu une sorte de port de libffi, qui passe les tests les plus simples, et avec lequel glib se compile â j'ai dĂ©cidĂ© que je l'amĂ©liorerais plus tard si besoin. Pour anticiper, je dirai que, comme il s'avĂšre, le compilateur n'a mĂȘme pas inclus le code final de la fonction libffi.
Mais, comme je l'ai dĂ©jĂ dit, il y a certaines limitations, et parmi l'utilisation libre de divers comportements indĂ©finis, il y a une particularitĂ© plutĂŽt dĂ©sagrĂ©able â JavaScript ne supporte pas par conception le multithreading avec mĂ©moire partagĂ©e. En principe, on peut mĂȘme appeler cela une bonne idĂ©e, mais pas pour le portage de code dont l'architecture est basĂ©e sur des threads C. En fait, des expĂ©riences sont en cours dans Firefox pour supporter les workers partagĂ©s, et l'implĂ©mentation de pthread pour eux existe dans Emscripten, mais je n'ai pas voulu en dĂ©pendre. J'ai donc dĂ» progressivement retirer le multithreading du code Qemu â c'est-Ă -dire localiser oĂč les threads sont lancĂ©s, extraire le corps de la boucle qui s'exĂ©cute dans ce thread dans une fonction distincte, et appeler successivement ces fonctions depuis la boucle principale.
DeuxiĂšme tentative
Ă un certain moment, il est devenu Ă©vident que le problĂšme Ă©tait toujours prĂ©sent, et que la rĂ©partition dĂ©sordonnĂ©e de solutions de contournement dans le code n'allait pas mener Ă de bons rĂ©sultats. Conclusion : il faut systĂ©matiser le processus d'ajout des solutions de contournement. Par consĂ©quent, j'ai pris la version 2.4.1 qui Ă©tait rĂ©cente Ă l'Ă©poque (pas 2.5.0, parce qu'on ne sait jamais, il pourrait y avoir d'autres bugs non rĂ©glĂ©s dans la nouvelle version, et j'ai dĂ©jĂ assez de mes propres bugs), et en premier lieu, j'ai réécrit thread-posix.c. Donc, de maniĂšre sĂ©curisĂ©e : si quelqu'un tentait d'exĂ©cuter une opĂ©ration entraĂźnant un blocage, la fonction abort() Ă©tait immĂ©diatement appelĂ©e â bien sĂ»r, cela ne rĂ©glait pas tous les problĂšmes d'un coup, mais au moins, c'Ă©tait plus agrĂ©able que de recevoir silencieusement des incohĂ©rences de donnĂ©es.
En fait, lors du portage de code vers JS, les options d'Emscripten aident beaucoup -s ASSERTIONS=1 -s SAFE_HEAP=1 â elles dĂ©tectent certains types de comportements indĂ©finis comme des accĂšs Ă des adresses non alignĂ©es (ce qui ne correspond pas du tout au code pour les typed arrays comme HEAP32[addr >> 2] = 1) ou l'appel d'une fonction avec un nombre incorrect d'arguments.
En fait, les erreurs d'alignement sont un sujet à part. Comme je l'ai déjà mentionné, Qemu dispose d'un backend d'interprétation codée appelé TCI (tiny code interpreter), et pour compiler et exécuter Qemu sur une nouvelle architecture, si on a de la chance, un compilateur C suffit. Mots-clés "si on a de la chance". Je n'ai pas eu de chance, et il s'avÚre que TCI utilise un accÚs non aligné lors de l'analyse de son bytecode. Cela signifie que sur des architectures comme ARM et d'autres nécessitant un accÚs aligné, Qemu se compile parce qu'il existe un backend TCG normal qui génÚre du code natif, mais savoir si TCI fonctionnera sur ces architectures est une autre question. Cependant, il s'avÚre que la documentation sur TCI mentionnait clairement quelque chose de similaire. En conséquence, des appels de fonctions pour la lecture non alignée ont été ajoutés dans le code, qui se sont révélés dans une autre partie de Qemu.
Destruction de tas
En fin de compte, l'accĂšs non alignĂ© dans TCI a Ă©tĂ© corrigĂ©, et une boucle principale a Ă©tĂ© construite, appelant Ă tour de rĂŽle le processeur, RCU et quelques autres petites choses. Et maintenant, je lance Qemu avec l'option -d exec,in_asm,out_asm, ce qui signifie qu'il faut indiquer quels blocs de code sont exĂ©cutĂ©s, tout en Ă©crivant, au moment de la translation, quel Ă©tait le code invitĂ© et quel est devenu le code hĂŽte (dans ce cas, le bytecode). Cela dĂ©marre, exĂ©cute plusieurs blocs de traduction, Ă©crit le message de dĂ©bogage que j'ai laissĂ©, que RCU va dĂ©marrer et... ça plante Ă abort() l'intĂ©rieur de la fonction free(). En fouillant dans la fonction free() , il a Ă©tĂ© possible de 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 d'une autre information similaire, se trouvait des dĂ©chets.
La destruction de la pile â comme c'est mignon... Dans ce cas, il existe un moyen utile : assembler un binaire natif Ă partir des mĂȘmes sources (si possible) et le faire passer sous Valgrind. AprĂšs un certain temps, le binaire Ă©tait prĂȘt. Je le lance avec les mĂȘmes options â il plante encore Ă l'initialisation, sans atteindre l'exĂ©cution. Pas agrĂ©able, bien sĂ»r â il semble que les sources n'Ă©taient pas tout Ă fait les mĂȘmes, ce qui n'est pas surprenant, car configure a dĂ©tectĂ© quelques options diffĂ©rentes, mais j'ai Valgrind â je vais d'abord corriger ce bogue, puis, si j'ai de la chance, l'original apparaĂźtra. Je relance tout cela sous Valgrind⊠Ouh-ouh, il a dĂ©marrĂ©, a bien passĂ© l'initialisation et est passĂ© Ă cĂŽtĂ© du bogue original sans un seul avertissement d'accĂšs mĂ©moire incorrect, sans parler des plantages. La vie ne m'a pas prĂ©parĂ© à ça â un programme qui plantait cesse de planter lorsqu'il est exĂ©cutĂ© sous Valgrind. Qu'est-ce que ça voulait dire â un mystĂšre. Ma thĂ©orie est que puisque dans les environs de l'instruction actuelle, aprĂšs le plantage Ă l'initialisation, gdb montrait une activitĂ© memset-a avec un pointeur valide en utilisant soit mmx, soit xmm registre, alors c'Ă©tait peut-ĂȘtre une sorte d'erreur d'alignement, bien que cela reste peu crĂ©dible.
D'accord, Valgrind ne semble pas ĂȘtre d'une grande aide ici. Et c'est Ă ce moment-lĂ que les choses se sont gĂątĂ©es â tout semble dĂ©marrer, mais ça plante pour des raisons totalement inconnues Ă cause d'un Ă©vĂ©nement qui aurait pu se produire des millions d'instructions auparavant. Pendant un long moment, c'Ă©tait mĂȘme flou sur la façon d'aborder le problĂšme. Finalement, j'ai dĂ» m'asseoir et dĂ©boguer. L'impression du contenu de l'en-tĂȘte a montrĂ© que ce n'Ă©tait pas un nombre, mais plutĂŽt des donnĂ©es binaires. Et, ĂŽ miracle, cette chaĂźne binaire a Ă©tĂ© trouvĂ©e dans le fichier du BIOS â donc, on pouvait dĂšs lors affirmer avec une certaine confiance qu'il s'agissait d'un dĂ©bordement de tampon, et mĂȘme comprendre ce qui Ă©tait Ă©crit dans ce tampon. Eh bien, ensuite, comme cela, dans Emscripten, heureusement, il n'y a pas de randomisation de l'espace d'adresses, il n'y a pas de failles non plus, donc on peut Ă©crire quelque part au milieu du code une sortie des donnĂ©es basĂ©es sur un pointeur de la derniĂšre exĂ©cution, observer les donnĂ©es, examiner le pointeur, et si celui-ci n'a pas changĂ©, obtenir des informations Ă mĂ©diter. Certes, il faut quelques minutes pour relier aprĂšs chaque modification, mais que peut-on y faire. En fin de compte, une chaĂźne spĂ©cifique a Ă©tĂ© trouvĂ©e, copiant le BIOS du tampon temporaire vers la mĂ©moire invitĂ©e â et, en effet, il n'y avait pas assez de place dans le tampon. La recherche de la source de cette adresse de tampon Ă©trange a conduit Ă la fonction qemu_anon_ram_alloc dans le fichier oslib-posix.c â la logique Ă©tait la suivante : il peut parfois ĂȘtre utile d'aligner l'adresse sur une Ă©norme page de 2 Mo, donc nous demanderons d'abord un peu plus, et ensuite nous renverrons l'excĂ©dent grĂące Ă mmap d'abord un peu plus, puis nous renverrons l'excĂ©dent grĂące Ă munmap. Et si cet alignement n'est pas nĂ©cessaire, alors nous indiquerons au lieu de 2 Mo le rĂ©sultat de getpagesize() â mmap qui retournera de toute façon une adresse alignĂ©e... Donc, dans Emscripten mmap il appelle simplement malloc, qui, bien sĂ»r, n'aligne pas par page. En gros, le bug qui me dĂ©rangeait depuis quelques mois a Ă©tĂ© corrigĂ© par un changement dans deux les lignes.
Caractéristiques de l'appel de fonctions
Et lĂ , le processeur commence Ă calculer quelque chose, Qemu ne plante pas, mais l'Ă©cran ne s'allume pas, et le processeur se boucle rapidement, d'aprĂšs la sortie. -d exec,in_asm,out_asm. Une hypothĂšse est apparue : les interruptions de minuterie (ou en fait toutes les interruptions) n'arrivent pas. Et en effet, si l'on dĂ©connecte les interruptions de la build native, qui fonctionnait pour une raison quelconque, on obtient une image similaire. Mais le mystĂšre ne rĂ©sidait pas lĂ : la comparaison des traces gĂ©nĂ©rĂ©es avec l'option mentionnĂ©e ci-dessus a montrĂ© que les trajectoires d'exĂ©cution divergent trĂšs tĂŽt. Il faut dire que la comparaison de la sortie enregistrĂ©e avec celle gĂ©nĂ©rĂ©e par la build native n'est pas un processus entiĂšrement mĂ©canique. emrun La sortie de dĂ©bogage avec la sortie de la build native â ce n'est pas tout Ă fait un processus mĂ©canique. Je ne sais pas exactement comment un programme exĂ©cutĂ© dans le navigateur se connecte Ă emrun, mais certaines lignes de la sortie se retrouvent Ă©changĂ©es, donc la diffĂ©rence dans le diff n'est pas nĂ©cessairement une raison pour considĂ©rer que les trajectoires ont divergĂ©. En gĂ©nĂ©ral, il est devenu clair qu'en suivant les instructions ljmpl on change d'adresses diffĂ©rentes, et le bytecode est gĂ©nĂ©rĂ© de maniĂšre fondamentalement diffĂ©rente : l'un contient une instruction d'appel d'une fonction helper C, tandis que l'autre n'en a pas. AprĂšs avoir googlĂ© des instructions et Ă©tudiĂ© le code qui les traduit, il est devenu clair que, premiĂšrement, directement avant celle-ci, un enregistrement dans le registre cr0 a Ă©tĂ© effectuĂ© â Ă©galement Ă l'aide d'une fonction helper â pour faire passer le processeur en mode protĂ©gĂ©, et deuxiĂšmement, que la version js n'est jamais passĂ©e en mode protĂ©gĂ©. En rĂ©alitĂ©, l'une des caractĂ©ristiques d'Emscripten est son incapacitĂ© Ă gĂ©rer un code similaire Ă celui de l'instruction call dans TCI, qui transforme tout pointeur vers une fonction en type long long f(int arg0, .. int arg9) â les fonctions doivent ĂȘtre appelĂ©es avec le bon nombre d'arguments. En cas de violation de cette rĂšgle, selon les paramĂštres de dĂ©bogage, le programme peut soit planter (ce qui est bon), soit appeler une fonction complĂštement diffĂ©rente (ce qui sera triste Ă dĂ©boguer). Il y a aussi une troisiĂšme option â activer la gĂ©nĂ©ration de wrappers qui ajoutent / suppriment des arguments, mais en total, ces wrappers prennent beaucoup de place, alors que j'ai en fait juste besoin de lĂ©gĂšrement plus d'une centaine de wrappers. Cela seul est dĂ©jĂ assez triste, mais il s'est avĂ©rĂ© qu'il y avait un problĂšme plus sĂ©rieux : dans le code gĂ©nĂ©rĂ© des fonctions wrapper, les arguments Ă©taient convertis, mais il se trouvait que la fonction avec les arguments gĂ©nĂ©rĂ©s Ă©tait parfois non exĂ©cutĂ©e â comme dans ma mise en Ćuvre de libffi. Cela signifie que certains helpers ne s'exĂ©cutaient tout simplement pas.
Heureusement, dans Qemu, il existe des listes de helpers lisibles par machine sous forme de fichiers 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 est inclus helper.h. Jusqu'Ă ce que la macro soit dĂ©veloppĂ©e dans un initialiseur de structure et une virgule, et qu'un tableau soit ensuite dĂ©fini, au lieu des Ă©lĂ©ments â #include <helper.h> En rĂ©sultat, il y avait enfin une raison d'essayer la bibliothĂšque , et un script a Ă©tĂ© Ă©crit pour gĂ©nĂ©rer exactement les wrappers nĂ©cessaires et pour les fonctions requises.
Et voilĂ , aprĂšs cela, le processeur semblait fonctionner. Semblait, parce que l'Ă©cran n'a pas Ă©tĂ© initialisĂ©, bien que dans la compilation native, il ait Ă©tĂ© possible de lancer memtest86+. Il convient de prĂ©ciser que le code des entrĂ©es/sorties bloquĂ©es de Qemu est Ă©crit en coroutines. Emscripten a sa propre rĂ©alisation trĂšs complexe, mais il fallait aussi la supporter dans le code de Qemu, et le dĂ©bogage du processeur peut ĂȘtre fait maintenant : Qemu supporte les options -kernel, -initrd, -append, grĂące auxquelles il est possible de charger Linux ou, par exemple, memtest86+, sans utiliser du tout de pĂ©riphĂ©riques de blocs. Mais voici le hic : dans la compilation native, on pouvait observer la sortie du noyau Linux sur la console avec l'option -nographic, mais depuis le navigateur, aucune sortie n'arrivait dans le terminal Ă partir duquel il a Ă©tĂ© lancĂ© emrun, donc c'Ă©tait incomprĂ©hensible : le processeur ne fonctionne pas ou la sortie graphique. Puis, il m'est venu Ă l'esprit d'attendre un peu. Il s'est avĂ©rĂ© que "le processeur ne dort pas, mais clignote lentement", et aprĂšs environ cinq minutes, le cĆur a balancĂ© une sĂ©rie de messages sur la console et a continuĂ© Ă se bloquer. Il est devenu clair que le processeur fonctionne, en gĂ©nĂ©ral, et il fallait explorer le code de travail avec SDL2. Malheureusement, je ne sais pas comment utiliser cette bibliothĂšque, donc j'ai dĂ» agir au hasard par endroits. Ă un moment donnĂ©, une ligne parallel0 a clignotĂ© sur un fond bleu, ce qui m'a amenĂ© Ă quelques rĂ©flexions. Finalement, il s'est avĂ©rĂ© que le problĂšme Ă©tait que Qemu ouvre plusieurs fenĂȘtres virtuelles dans une mĂȘme fenĂȘtre physique, entre lesquelles on peut basculer avec Ctrl-Alt-n : dans la compilation native, cela fonctionne, dans Emscripten â non. AprĂšs avoir Ă©liminĂ© les fenĂȘtres superflues avec les options -monitor none -parallel none -serial none et en forçant le rafraĂźchissement de tout l'Ă©cran Ă chaque frame, tout a soudainement bien fonctionnĂ©.
Coroutines
Ainsi, l'Ă©mulation dans le navigateur fonctionne, mais il n'y a rien d'intĂ©ressant de type disquette que l'on puisse lancer, car il n'y a pas d'entrĂ©e-sortie par blocs â il est nĂ©cessaire de mettre en Ćuvre un support pour les coroutines. Qemu possĂšde dĂ©jĂ plusieurs backends de coroutine, mais en raison des particularitĂ©s de JavaScript et du gĂ©nĂ©rateur de code Emscripten, il n'est pas possible de jongler simplement avec les piles. On pourrait penser que "tout est perdu, le plĂątre est retirĂ©", mais les dĂ©veloppeurs d'Emscripten ont dĂ©jĂ pris tout en charge. C'est rĂ©alisĂ© de maniĂšre assez amusante : pourquoi ne pas appeler des fonctions suspectes comme emscripten_sleep et quelques autres utilisant le mĂ©canisme Asyncify, ainsi que les appels par pointeur et les appels de n'importe quelle 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 allons allouer un contexte asynchrone, et juste aprĂšs l'appel â vĂ©rifier s'il y a eu un appel asynchrone, et si c'est le cas, nous sauvegarderons toutes les variables locales dans ce contexte asynchrone, indiquerons quelle fonction doit reprendre le contrĂŽle lorsque nous devrons continuer l'exĂ©cution, et sortirons de la fonction actuelle. VoilĂ oĂč il y a un champ d'Ă©tude pour l'effet â pour les besoins de la continuation de l'exĂ©cution du code aprĂšs le retour d'un appel asynchrone, le compilateur gĂ©nĂšre des "morceaux" de fonction, commençant aprĂšs l'appel suspect â ainsi, s'il y a n appels suspects, la fonction sera dĂ©peçée environ n/2 fois â cela sans compter qu'il faut Ă©galement ajouter des sauvegardes de certaines variables locales aprĂšs chaque appel potentiellement asynchrone. Par la suite, j'ai mĂȘme dĂ» Ă©crire un script simple en Python qui, pour un ensemble donnĂ© de fonctions particuliĂšrement dĂ©peçées, qui, selon les suppositions, "ne laissent pas passer l'asynchronicitĂ© Ă travers elles" (c'est-Ă -dire qu'elles ne dĂ©clenchent pas le dĂ©roulement de la pile et tout ce que je viens de dĂ©crire), indique quelles fonctions doivent ĂȘtre ignorĂ©es par le compilateur concernant les appels par pointeur, afin que ces fonctions ne soient pas considĂ©rĂ©es comme asynchrones. Car des fichiers JS de prĂšs de 60 Mo, c'est clairement trop â au moins 30. Pourtant, une fois, j'ai configurĂ© un script de construction et j'ai accidentellement supprimĂ© les options de l'Ă©diteur de liens, parmi lesquelles se trouvait -O3J'exĂ©cute le code gĂ©nĂ©rĂ©, et Chromium consomme toute la mĂ©moire et plante. Par la suite, j'ai regardĂ© par accident ce qu'il essayait de charger... Eh bien, que puis-je dire, je serais Ă©galement bloquĂ© si on me demandait d'analyser et d'optimiser du JavaScript de plus de 500 Mo.
Malheureusement, les vĂ©rifications dans le code de la bibliothĂšque de support Asyncify n'Ă©taient pas tout Ă fait compatibles avec longjmp- qui sont utilisĂ©s dans le code du processeur virtuel, mais aprĂšs un petit patch dĂ©sactivant ces vĂ©rifications et restaurant de force les contextes comme si tout allait bien, le code a fonctionnĂ©. Et lĂ , les choses sont devenues Ă©tranges : parfois, des vĂ©rifications dans le code de synchronisation se dĂ©clenchaient â celles qui arrĂȘtent brutalement le code si, selon la logique d'exĂ©cution, il doit se bloquer â quelqu'un essayait de capturer un mutex dĂ©jĂ acquis. Heureusement, ce n'Ă©tait pas un problĂšme logique dans le code sĂ©rialisĂ© â j'avais simplement utilisĂ© la fonctionnalitĂ© standard de la boucle principale fournie par Emscripten, mais parfois un appel asynchrone dĂ©roulait complĂštement la pile, et Ă ce moment-lĂ , setTimeout de la boucle principale se dĂ©clenchait â ainsi, le code entrait dans une itĂ©ration de la boucle principale sans sortir de l'itĂ©ration prĂ©cĂ©dente. J'ai réécrit avec 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, je n'ai pas de code qui prĂ©pare une nouvelle image d'animation â le processeur fait simplement des calculs et l'Ă©cran se met Ă jour pĂ©riodiquement. Cependant, les problĂšmes ne se sont pas arrĂȘtĂ©s lĂ : parfois, l'exĂ©cution de Qemu se terminait tout simplement sans aucune exception ni erreur. Ă ce moment-lĂ , j'ai laissĂ© tomber, mais pour aller de l'avant, je dirai que le problĂšme Ă©tait le suivant : le code des coroutines, en rĂ©alitĂ©, n'utilise pas vraiment setTimeout (ou, du moins, pas aussi souvent qu'on pourrait le penser) : la fonction emscripten_yield se contente de dĂ©finir un drapeau d'appel asynchrone. En fait, ce qui est important, c'est que emscripten_coroutine_next n'est pas une fonction asynchrone : elle vĂ©rifie le drapeau, le rĂ©initialise et transmet le contrĂŽle lĂ oĂč il faut. Cela signifie que c'est lĂ que le dĂ©roulement de la pile s'arrĂȘte. Le problĂšme Ă©tait qu'Ă cause d'un use-after-free, qui se manifestait lorsque le pool de coroutines Ă©tait dĂ©sactivĂ©, parce que je n'avais pas copiĂ© la ligne de code importante Ă partir du backend de coroutine existant, la fonction qemu_in_coroutine retournait true alors qu'elle aurait dĂ» retourner false. Cela entraĂźnait un appel emscripten_yield, au-dessus duquel la pile ne montait pas emscripten_coroutine_next, la pile s'Ă©tendait jusqu'au sommet, mais aucune setTimeout, comme je l'ai dĂ©jĂ dit, n'Ă©tait affichĂ©.
Génération de code JavaScript
Et voici, en fait, le "revers de la viande hachĂ©e" promis. En rĂ©alitĂ©, non. Bien sĂ»r, si vous lancez Qemu dans le navigateur, et en lui â Node.js, alors, aprĂšs la gĂ©nĂ©ration de code dans Qemu, nous obtiendrons un JavaScript complĂštement diffĂ©rent. Mais tout de mĂȘme, quel que soit le cas, il y a une certaine rĂ©tro-conversion.
Pour commencer, un peu sur le fonctionnement de Qemu. Je vous demande immĂ©diatement de me pardonner : je ne suis pas un dĂ©veloppeur professionnel de Qemu et mes conclusions peuvent ĂȘtre parfois erronĂ©es. Comme on dit, "l'avis d'un Ă©tudiant ne doit pas nĂ©cessairement coĂŻncider avec celui de l'enseignant, l'axiomatique de Peano et le bon sens". Qemu a un certain nombre d'architectures invitĂ©es prises en charge, et pour chacune, il existe un rĂ©pertoire comme target-i386. Lors de la compilation, il est possible d'indiquer le support de plusieurs architectures invitĂ©es, mais le rĂ©sultat sera simplement plusieurs binaires. Le code pour le support de l'architecture invitĂ©e, Ă son tour, gĂ©nĂšre certaines opĂ©rations internes de Qemu, que TCG (Tiny Code Generator) transforme ensuite en code machine de l'architecture hĂŽte. Comme indiquĂ© dans le fichier readme se trouvant dans le rĂ©pertoire tcg, Ă l'origine, cela faisait partie d'un compilateur C normal, qui a ensuite Ă©tĂ© adaptĂ© au JIT. Par consĂ©quent, par exemple, l'architecture cible dans les termes de ce document â ce n'est plus l'architecture invitĂ©e, mais celle de l'hĂŽte. Ă un moment donnĂ©, un autre composant est apparu â l'InterprĂšte de Petite Code (TCI), qui est censĂ© exĂ©cuter le code (pratiquement les mĂȘmes opĂ©rations internes) en l'absence de gĂ©nĂ©rateur de code pour une architecture hĂŽte spĂ©cifique. En fait, comme indiquĂ© dans sa documentation, cet interprĂšte peut ne pas toujours fonctionner aussi bien que le gĂ©nĂ©rateur de code JIT, non seulement quantitativement en termes de vitesse, mais aussi qualitativement. Bien que je ne sois pas sĂ»r que sa description soit entiĂšrement actuelle.
Au départ, j'ai essayé de créer un backend TCG complet, mais je me suis rapidement perdu dans le code source et la documentation des instructions de bytecode pas toujours compréhensible, donc j'ai décidé d'envelopper l'interprÚte TCI. Cela a immédiatement apporté plusieurs avantages :
- lors de la mise en Ćuvre du gĂ©nĂ©rateur de code, on pouvait se rĂ©fĂ©rer non pas Ă la description des instructions, mais au code de l'interprĂšte
- Il est possible de générer des fonctions non pas pour chaque bloc de traduction rencontré, mais seulement aprÚs la centiÚme exécution, par exemple.
- En cas de modification du code généré (ce qui semble possible, à en juger par les fonctions dont les noms contiennent le mot patch), je devrai invalider le code JavaScript généré, mais au moins j'aurai de quoi le régénérer.
Quant au troisiÚme point, je ne suis pas sûr que le patching soit possible aprÚs la premiÚre exécution du code, mais les deux premiers points suffisent.
Ă l'origine, le code Ă©tait gĂ©nĂ©rĂ© sous forme d'un gros switch basĂ© sur l'adresse de l'instruction bytecode source, mais aprĂšs avoir rappelĂ© un article sur Emscripten, l'optimisation du JS gĂ©nĂ©rĂ© et la relooping, j'ai dĂ©cidĂ© de gĂ©nĂ©rer un code plus humain, d'autant plus qu'expĂ©rimentalement, il s'est avĂ©rĂ© que le seul point d'entrĂ©e dans le bloc de traduction est son dĂ©but. Cela a Ă©tĂ© fait, et aprĂšs un certain temps, un gĂ©nĂ©rateur de code a Ă©tĂ© créé, gĂ©nĂ©rant du code avec des if (bien que sans boucles). Mais voilĂ le problĂšme, il Ă©chouait en donnant un message indiquant que l'instruction avait une longueur incorrecte. Par ailleurs, la derniĂšre instruction Ă ce niveau de rĂ©cursion Ă©tait brcond. Bien, j'ajouterai une vĂ©rification identique lors de la gĂ©nĂ©ration de cette instruction avant l'appel rĂ©cursif et aprĂšs, et... aucune d'entre elles ne s'est exĂ©cutĂ©e, mais aprĂšs le switch sur l'assert, tout de mĂȘme, cela a Ă©chouĂ©. Finalement, aprĂšs avoir Ă©tudiĂ© le code gĂ©nĂ©rĂ©, j'ai compris qu'aprĂšs le switch, le pointeur sur l'instruction actuelle se rechargeait Ă partir de la pile et Ă©tait probablement Ă©crasĂ© par le code JavaScript gĂ©nĂ©rĂ©. Et c'Ă©tait le cas. L'augmentation du tampon d'un mĂ©gaoctet Ă dix n'a rien rĂ©solu, et il est devenu Ă©vident que le gĂ©nĂ©rateur de code tournait en rond. J'ai dĂ» vĂ©rifier que nous ne dĂ©passions pas les limites du TB actuel, et si nous le dĂ©passions, alors donner l'adresse du prochain TB avec un signe moins, afin que nous puissions continuer l'exĂ©cution. D'ailleurs, cela rĂ©sout le problĂšme de "quelles fonctions gĂ©nĂ©rĂ©es invalider si ce morceau de bytecode a changĂ© ?" - il faut invalider uniquement la fonction qui correspond Ă ce bloc de traduction. Au fait, bien que j'ai tout dĂ©boguĂ© dans Chromium (puisque j'utilise Firefox et qu'il est plus facile pour moi d'utiliser un autre navigateur pour les expĂ©riences), Firefox m'a aidĂ© Ă rĂ©soudre des incompatibilitĂ©s avec la norme asm.js, aprĂšs quoi le code a commencĂ© Ă fonctionner plus rapidement dans Chromium.
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
Donc, le travail n'est toujours pas terminé, mais j'en ai assez de perfectionner ce long projet en secret. J'ai donc décidé de publier pour l'instant ce qui existe. Le code est parfois un peu brut, car c'est une expérience, et il n'est pas clair à l'avance ce qu'il faut faire. Il est probablement judicieux de préparer des commits atomiques normaux au-dessus d'une version plus moderne de Qemu. Pour l'instant, il y a une branche dans Git au format blog : à chaque "niveau" un peu franchi, un commentaire détaillé en russe a été ajouté. En fait, cet article est en grande partie un résumé des résultats. git log.
Vous pouvez essayer tout ça (attention, trafic).
Ce qui fonctionne déjà :
- Un processeur virtuel x86 fonctionne
- Il y a un prototype fonctionnel de générateur de code JIT qui convertit le code machine en JavaScript
- Il y a une préparation pour la compilation d'autres architectures invitées 32 bits : vous pouvez actuellement admirer un Linux qui se fige dans le navigateur au stade de chargement pour l'architecture MIPS.
Que peut-on encore faire
- AccĂ©lĂ©rer l'Ă©mulation. MĂȘme en mode JIT, cela semble fonctionner plus lentement que Virtual x86 (mais potentiellement, il existe tout un Qemu avec beaucoup de matĂ©riel et d'architectures Ă©mulĂ©s)
- CrĂ©er une interface correcte â je ne suis pas vraiment un dĂ©veloppeur web, donc pour l'instant j'ai refait le shell standard d'Emscripten, comme j'ai pu.
- Essayer d'exĂ©cuter des fonctions plus complexes de Qemu â rĂ©seau, migration de VM, etc.
- Mise à jour : Il faudra soumettre dans l'upstream d'Emscripten mes quelques contributions et rapports de bugs, comme l'ont fait les porteurs précédents de Qemu et d'autres projets. Merci à eux pour la possibilité d'utiliser implicitement leur contribution à Emscripten dans le cadre de ma tùche.
Source : habr.com
