Qemu.js amb suport JIT: encara podeu girar la picada cap enrere

Fa uns anys Fabrice Bellard escrit per jslinux és un emulador de PC escrit en JavaScript. Després d'això, almenys hi havia més Virtual x86. Però tots, pel que jo sé, eren intèrprets, mentre que Qemu, escrit molt abans pel mateix Fabrice Bellard, i, probablement, qualsevol emulador modern que es precie, utilitza la compilació JIT del codi convidat al codi del sistema amfitrió. Em va semblar que ja era hora d'implementar la tasca contrària en relació a la que resolen els navegadors: compilació JIT de codi màquina a JavaScript, per a la qual em semblava més lògic portar Qemu. Sembla que, per què Qemu, hi ha emuladors més senzills i fàcils d'utilitzar -el mateix VirtualBox, per exemple- instal·lats i funciona. Però Qemu té diverses característiques interessants

  • codi obert
  • capacitat de treballar sense un controlador del nucli
  • capacitat de treballar en mode intèrpret
  • suport per a un gran nombre d'arquitectures d'amfitrió i convidats

Pel que fa al tercer punt, ara puc explicar que, de fet, en el mode TCI, no s'interpreten les instruccions de la màquina convidada, sinó el bytecode obtingut d'elles, però això no canvia l'essència, per construir i executar. Qemu en una nova arquitectura, si teniu sort, n'hi ha prou amb un compilador C: es pot posposar l'escriptura d'un generador de codi.

I ara, després de dos anys de retoc pausat amb el codi font de Qemu durant el meu temps lliure, va aparèixer un prototip de treball, en el qual ja podeu executar, per exemple, Kolibri OS.

Què és Emscripten

Actualment, han aparegut molts compiladors, el resultat final dels quals és JavaScript. Alguns, com Type Script, originalment estaven pensats per ser la millor manera d'escriure per a la web. Al mateix temps, Emscripten és una manera d'agafar el codi C o C++ existent i compilar-lo en una forma llegible pel navegador. Encès aquesta pàgina Hem recollit molts ports de programes coneguts: aquíPer exemple, podeu mirar PyPy; per cert, diuen que ja tenen JIT. De fet, no tots els programes es poden compilar i executar simplement en un navegador: n'hi ha un nombre característiques, que has de suportar, però, ja que la inscripció de la mateixa pàgina diu "Emscripten es pot utilitzar per compilar gairebé qualsevol portàtil codi C/C++ a JavaScript". És a dir, hi ha una sèrie d'operacions que tenen un comportament no definit segons l'estàndard, però que solen funcionar amb x86, per exemple, l'accés no alineat a variables, que generalment està prohibit en algunes arquitectures. En general , Qemu és un programa multiplataforma i, m'ho volia creure, i encara no conté gaire comportament no definit: preneu-lo i compileu-lo, després feu una mica de JIT, i ja està! Però això no és el Caixa...

Primer intent

En termes generals, no sóc la primera persona a tenir la idea de portar Qemu a JavaScript. Es va fer una pregunta al fòrum de ReactOS si això era possible amb Emscripten. Fins i tot abans, hi havia rumors que en Fabrice Bellard ho feia personalment, però estàvem parlant de jslinux, que, pel que jo sé, és només un intent d'aconseguir manualment un rendiment suficient en JS, i va ser escrit des de zero. Més tard, es va escriure Virtual x86: es van publicar fonts no ofuscades i, com s'ha dit, el major "realisme" de l'emulació va permetre utilitzar SeaBIOS com a microprogramari. A més, hi va haver almenys un intent de portar Qemu amb Emscripten; vaig intentar fer-ho parell de preses, però el desenvolupament, pel que entenc, estava congelat.

Així doncs, sembla que aquí teniu les fonts, aquí teniu Emscripten: preneu-lo i compileu. Però també hi ha biblioteques de les quals depèn Qemu, biblioteques de les quals depenen aquestes biblioteques, etc., i una d'elles és libffi, de la qual depèn la glib. Hi havia rumors a Internet que n'hi havia un a la gran col·lecció de ports de biblioteques d'Emscripten, però d'alguna manera era difícil de creure: en primer lloc, no estava pensat per ser un compilador nou, en segon lloc, era un nivell massa baix. biblioteca per recollir-lo i compilar-lo a JS. I no és només una qüestió d'insercions de muntatge: probablement, si ho retorceu, per a algunes convencions de trucada podeu generar els arguments necessaris a la pila i cridar la funció sense ells. Però Emscripten és una cosa complicada: per tal que el codi generat sembli familiar per a l'optimitzador de motors JS del navegador, s'utilitzen alguns trucs. En particular, l'anomenat relooping, un generador de codi que utilitza el LLVM IR rebut amb algunes instruccions de transició abstractes, intenta recrear ifs plausibles, bucles, etc. Bé, com es passen els arguments a la funció? Naturalment, com a arguments de les funcions JS, és a dir, si és possible, no a través de la pila.

Al principi va haver-hi la idea d'escriure simplement un reemplaçament de libffi amb JS i executar proves estàndard, però al final em vaig confondre sobre com fer els meus fitxers de capçalera perquè funcionin amb el codi existent; què puc fer, com diuen: "Les tasques són tan complexes "Som tan estúpids?" Vaig haver de portar libffi a una altra arquitectura, per dir-ho d'alguna manera, afortunadament, Emscripten té tant macros per al muntatge en línia (en Javascript, sí, bé, sigui quina sigui l'arquitectura, per tant l'assemblador) i la capacitat d'executar codi generat sobre la marxa. En general, després de jugar amb fragments de libffi dependents de la plataforma durant un temps, vaig obtenir un codi compilable i el vaig executar a la primera prova que vaig trobar. Per a la meva sorpresa, la prova va tenir èxit. Sorpresa pel meu geni -no hi ha broma, va funcionar des del primer llançament- jo, encara sense creure els meus ulls, vaig tornar a mirar el codi resultant, per avaluar on havia de cavar a continuació. Aquí em vaig tornar boig per segona vegada: l'únic que feia la meva funció va ser ffi_call - això ha informat d'una trucada satisfactòria. No hi havia cap trucada en si. Així que vaig enviar la meva primera sol·licitud d'extracció, que va corregir un error a la prova que era clar per a qualsevol estudiant de les Olimpíades: els números reals no s'han de comparar com a == b i fins i tot com a - b < EPS - També heu de recordar el mòdul, en cas contrari, 0 serà molt igual a 1/3... En general, em va ocórrer un determinat port de libffi, que passa les proves més senzilles i amb el qual és glib. compilat: vaig decidir que seria necessari, ho afegiré més tard. De cara al futur, diré que, com va resultar, el compilador ni tan sols va incloure la funció libffi al codi final.

Però, com ja he dit, hi ha algunes limitacions i, entre l'ús gratuït de diversos comportaments no definits, s'ha amagat una característica més desagradable: JavaScript per disseny no admet multithreading amb memòria compartida. En principi, fins i tot es pot anomenar una bona idea, però no per portar codi l'arquitectura del qual està lligada a fils C. En termes generals, Firefox està experimentant amb el suport dels treballadors compartits, i Emscripten té una implementació de pthread per a ells, però no volia dependre d'això. Vaig haver d'arrelar lentament el multithreading del codi Qemu, és a dir, esbrinar on s'executen els fils, moure el cos del bucle que s'executa en aquest fil a una funció separada i cridar aquestes funcions una per una des del bucle principal.

Segona prova

En algun moment, va quedar clar que el problema continuava allà i que posar crosses a l'atzar al voltant del codi no portaria a res. Conclusió: cal sistematitzar d'alguna manera el procés d'afegir crosses. Per tant, es va prendre la versió 2.4.1, que era nova en aquell moment (no la 2.5.0, perquè, qui sap, hi haurà errors a la nova versió que encara no s'han detectat, i en tinc prou dels meus propis errors). ), i el primer va ser reescriure-ho amb seguretat thread-posix.c. Bé, és a dir, tan segur: si algú intentava realitzar una operació que portava al bloqueig, la funció es cridava immediatament abort() - per descomptat, això no va resoldre tots els problemes alhora, però almenys va ser d'alguna manera més agradable que rebre dades inconsistents en silenci.

En general, les opcions d'Emscripten són molt útils per portar el codi a JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - detecten alguns tipus de comportament no definit, com ara trucades a una adreça no alineada (que no és gens coherent amb el codi de les matrius escrites com HEAP32[addr >> 2] = 1) o cridar una funció amb el nombre incorrecte d'arguments.

Per cert, els errors d'alineació són un problema independent. Com ja he dit, Qemu té un backend interpretatiu "degenerat" per a la generació de codi TCI (intèrpret de codi petit), i per construir i executar Qemu en una nova arquitectura, si teniu sort, n'hi ha prou amb un compilador C. "si tens sort". Vaig tenir mala sort i va resultar que TCI utilitza accés no alineat quan analitza el seu bytecode. És a dir, en tot tipus d'arquitectures ARM i altres amb accés necessàriament anivellat, Qemu compila perquè tenen un backend normal de TCG que genera codi natiu, però si TCI hi funcionarà és una altra qüestió. Tanmateix, com va resultar, la documentació de TCI indicava clarament alguna cosa semblant. Com a resultat, es van afegir al codi trucades de funció per a una lectura no alineada, que es van descobrir en una altra part de Qemu.

Destrucció de pila

Com a resultat, es va corregir l'accés no alineat a TCI, es va crear un bucle principal que al seu torn anomenava processador, RCU i algunes altres petites coses. I així llanço Qemu amb l'opció -d exec,in_asm,out_asm, el que significa que cal dir quins blocs de codi s'estan executant, i també en el moment de la difusió per escriure quin era el codi de convidat, quin codi d'amfitrió es va convertir (en aquest cas, bytecode). S'inicia, executa diversos blocs de traducció, escriu el missatge de depuració que vaig deixar que ara s'iniciarà RCU i... es bloqueja abort() dins d'una funció free(). Remenant amb la funció free() Vam aconseguir esbrinar que a la capçalera del bloc de pila, que es troba en els vuit bytes anteriors a la memòria assignada, en comptes de la mida del bloc o alguna cosa semblant, hi havia escombraries.

Destrucció del munt: que bonic... En aquest cas, hi ha un remei útil: des (si és possible) de les mateixes fonts, munteu un binari natiu i executeu-lo sota Valgrind. Després d'un temps, el binari estava llest. El llançaré amb les mateixes opcions: es bloqueja fins i tot durant la inicialització, abans d'arribar a l'execució. És desagradable, per descomptat, aparentment, les fonts no eren exactament les mateixes, cosa que no és d'estranyar, perquè configure va buscar opcions lleugerament diferents, però tinc Valgrind; primer solucionaré aquest error i després, si tinc sort. , apareixerà l'original. Estic executant el mateix amb Valgrind... Y-y-y, y-y-y, uh-uh, va començar, va passar per la inicialització amb normalitat i va passar de l'error original sense cap avís sobre l'accés incorrecte a la memòria, per no parlar de les caigudes. La vida, com diuen, no em va preparar per a això: un programa d'estavellament deixa de fallar quan es llança sota Walgrind. El que va ser és un misteri. La meva hipòtesi és que un cop a prop de la instrucció actual després d'una fallada durant la inicialització, gdb va mostrar el treball memset-a amb un punter vàlid utilitzant qualsevol mmx, o xmm registres, llavors potser va ser algun tipus d'error d'alineació, tot i que encara és difícil de creure.

D'acord, sembla que Valgrind no ajudi aquí. I aquí va començar el més repugnant: sembla que tot comença, però s'estavella per motius absolutament desconeguts a causa d'un esdeveniment que podria haver passat fa milions d'instruccions. Durant molt de temps, ni tan sols estava clar com abordar. Al final, encara vaig haver de seure i depurar. La impressió amb què es va reescriure la capçalera va mostrar que no semblava un número, sinó una mena de dades binàries. I, vet aquí, aquesta cadena binària es va trobar al fitxer de la BIOS, és a dir, ara es podia dir amb una confiança raonable que es tractava d'un desbordament de memòria intermèdia, i fins i tot està clar que es va escriure en aquest buffer. Bé, aleshores alguna cosa així: a Emscripten, afortunadament, no hi ha una aleatorització de l'espai d'adreces, tampoc hi ha forats, de manera que podeu escriure en algun lloc al mig del codi per sortir dades mitjançant el punter de l'últim llançament, mireu les dades, mireu el punter i, si no ha canviat, feu pensar. És cert que es triga un parell de minuts a enllaçar després de qualsevol canvi, però què pots fer? Com a resultat, es va trobar una línia específica que copiava la BIOS de la memòria intermèdia temporal a la memòria del convidat i, de fet, no hi havia prou espai a la memòria intermèdia. Trobar l'origen d'aquesta estranya adreça de memòria intermèdia va donar lloc a una funció qemu_anon_ram_alloc a l'arxiu oslib-posix.c - la lògica era aquesta: de vegades pot ser útil alinear l'adreça a una pàgina enorme de 2 MB de mida, per això demanarem mmap primer una mica més, i després tornarem l'excés amb l'ajuda munmap. I si aquesta alineació no és necessària, indicarem el resultat en lloc de 2 MB getpagesize() - mmap encara donarà una adreça alineada... Així que a Emscripten mmap només trucades malloc, però per descomptat no s'alinea a la pàgina. En general, un error que em va frustrar durant un parell de mesos es va corregir amb un canvi двух línies.

Característiques de les funcions de trucada

I ara el processador està comptant alguna cosa, Qemu no falla, però la pantalla no s'encén i el processador entra ràpidament en bucles, a jutjar per la sortida -d exec,in_asm,out_asm. Ha sorgit una hipòtesi: les interrupcions del temporitzador (o, en general, totes les interrupcions) no arriben. I, de fet, si desenrosqueu les interrupcions de l'assemblea nativa, que per alguna raó va funcionar, obteniu una imatge similar. Però aquesta no va ser en absolut la resposta: una comparació de les traces emeses amb l'opció anterior va mostrar que les trajectòries d'execució van divergir molt aviat. Aquí cal dir que la comparació del que es va gravar amb el llançador emrun la depuració de la sortida amb la sortida del conjunt natiu no és un procés completament mecànic. No sé exactament com es connecta un programa que s'executa en un navegador emrun, però algunes línies de la sortida resulten estar reordenades, de manera que la diferència en la diferència encara no és un motiu per suposar que les trajectòries han divergit. En general, va quedar clar que segons les instruccions ljmpl hi ha una transició a diferents adreces, i el bytecode generat és fonamentalment diferent: un conté una instrucció per cridar una funció auxiliar, l'altre no. Després de buscar a Google les instruccions i d'estudiar el codi que tradueix aquestes instruccions, va quedar clar que, en primer lloc, immediatament abans en el registre cr0 es va fer una gravació -també utilitzant un ajudant- que va canviar el processador al mode protegit i, en segon lloc, que la versió js mai no va canviar al mode protegit. Però el fet és que una altra característica d'Emscripten és la seva reticència a tolerar codi com ara la implementació d'instruccions call a TCI, que qualsevol punter de funció dóna com a tipus long long f(int arg0, .. int arg9) - Les funcions s'han de cridar amb el nombre correcte d'arguments. Si s'incompleix aquesta regla, depenent de la configuració de depuració, el programa es bloquejarà (que és bo) o trucarà a la funció equivocada (que serà trist depurar). També hi ha una tercera opció: habilitar la generació d'embolcalls que afegeixen/eliminen arguments, però en total aquests embolcalls ocupen molt d'espai, malgrat que de fet només necessito una mica més d'un centenar d'embolcalls. Això només és molt trist, però va resultar que hi havia un problema més greu: al codi generat de les funcions d'embolcall, els arguments es van convertir i es van convertir, però de vegades la funció amb els arguments generats no s'anomenava, bé, igual que a la meva implementació libffi. És a dir, alguns ajudants simplement no van ser executats.

Afortunadament, Qemu té llistes d'ajudants llegibles per màquina en forma de fitxer de capçalera com

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

S'utilitzen força divertides: primer, les macros es redefinien de la manera més estranya DEF_HELPER_n, i després s'encén helper.h. En la mesura que la macro s'amplia en un inicialitzador d'estructura i una coma, i després es defineix una matriu, i en lloc d'elements - #include <helper.h> Com a resultat, finalment vaig tenir l'oportunitat de provar la biblioteca a la feina pyparsing, i es va escriure un script que genera exactament aquests embolcalls exactament per a les funcions per a les quals es necessiten.

I així, després d'això, el processador semblava funcionar. Sembla ser perquè la pantalla mai es va inicialitzar, tot i que memtest86+ es va poder executar a l'assemblea nativa. Aquí cal aclarir que el codi d'E/S del bloc Qemu està escrit en corrutines. Emscripten té la seva pròpia implementació molt complicada, però encara calia ser compatible amb el codi Qemu i ara podeu depurar el processador: Qemu admet opcions -kernel, -initrd, -append, amb el qual podeu arrencar Linux o, per exemple, memtest86+, sense utilitzar dispositius de bloc. Però aquí està el problema: a l'assemblea nativa es podria veure la sortida del nucli de Linux a la consola amb l'opció -nographic, i cap sortida del navegador al terminal des d'on es va llançar emrun, no va venir. És a dir, no està clar: el processador no funciona o la sortida gràfica no funciona. I llavors se'm va ocórrer esperar una mica. Va resultar que "el processador no està dormint, sinó que simplement parpelleja lentament", i després d'uns cinc minuts el nucli va llançar un munt de missatges a la consola i va continuar penjant. Va quedar clar que el processador, en general, funciona, i hem d'aprofundir en el codi per treballar amb SDL2. Malauradament, no sé com utilitzar aquesta biblioteca, així que en alguns llocs he hagut d'actuar a l'atzar. En algun moment, la línia paral·lela0 va parpellejar a la pantalla sobre un fons blau, cosa que va suggerir algunes reflexions. Al final, va resultar que el problema era que Qemu obre diverses finestres virtuals en una finestra física, entre les quals podeu canviar amb Ctrl-Alt-n: funciona a la construcció nativa, però no a Emscripten. Després de desfer-se de finestres innecessàries utilitzant opcions -monitor none -parallel none -serial none i instruccions per tornar a dibuixar amb força tota la pantalla a cada fotograma, tot va funcionar de sobte.

Corutines

Per tant, l'emulació al navegador funciona, però no hi podeu executar res interessant d'un sol disquet, perquè no hi ha cap E/S de bloc; heu d'implementar suport per a les corrutines. Qemu ja té diversos backends de corrutina, però a causa de la naturalesa de JavaScript i del generador de codi Emscripten, no podeu començar a fer malabars amb les piles. Sembla que “tot ha desaparegut, s'estan retirant el guix”, però els desenvolupadors d'Emscripten ja s'han encarregat de tot. Això s'implementa força divertit: anomenem una trucada de funció com aquesta sospitosa emscripten_sleep i diversos altres que utilitzen el mecanisme Asyncify, així com trucades de punter i trucades a qualsevol funció on es pugui produir un dels dos casos anteriors més avall de la pila. I ara, abans de cada trucada sospitosa, seleccionarem un context asíncron i, immediatament després de la trucada, comprovarem si s'ha produït una trucada asíncrona i, si és així, guardarem totes les variables locals en aquest context asíncron, indicant quina funció. per transferir el control a quan necessitem continuar amb l'execució i sortir de la funció actual. Aquí és on hi ha marge per estudiar l'efecte malbaratament — per a les necessitats de continuar amb l'execució de codi després de tornar d'una trucada asíncrona, el compilador genera "stubs" de la funció que comença després d'una trucada sospitosa, així: si hi ha n trucades sospitoses, la funció s'ampliarà en algun lloc n/2 vegades — això encara és, si no Tingueu en compte que després de cada trucada potencialment asíncrona, heu d'afegir desar algunes variables locals a la funció original. Posteriorment, fins i tot vaig haver d'escriure un script senzill en Python, que, basant-se en un conjunt determinat de funcions especialment utilitzades en excés que suposadament "no permeten que l'asincronia passi per elles mateixes" (és a dir, la promoció de la pila i tot el que acabo de descriure no ho fan. treballen en ells), indica trucades a través de punters en què les funcions han de ser ignorades pel compilador perquè aquestes funcions no es considerin asíncrones. I aleshores els fitxers JS de menys de 60 MB són clarament massa, diguem com a mínim 30. Tot i que, un cop estava configurant un script de muntatge i accidentalment vaig llançar les opcions d'enllaç, entre les quals hi havia -O3. Executo el codi generat i Chromium consumeix memòria i es bloqueja. Aleshores vaig mirar accidentalment què estava intentant descarregar... Bé, què puc dir, jo també m'hauria congelat si m'haguessin demanat que estudiés i optimitzés un Javascript de més de 500 MB.

Malauradament, les comprovacions del codi de la biblioteca de suport d'Asyncify no eren del tot amigables longjmp-s que s'utilitzen en el codi del processador virtual, però després d'un petit pedaç que desactiva aquestes comprovacions i restableix amb força els contextos com si tot estigués bé, el codi va funcionar. I aleshores va començar una cosa estranya: de vegades es desencadenaven comprovacions en el codi de sincronització -les mateixes que fan fallar el codi si, segons la lògica d'execució, s'hauria de bloquejar- algú intentava agafar un mutex ja capturat. Afortunadament, això va resultar no ser un problema lògic en el codi serialitzat: simplement estava utilitzant la funcionalitat estàndard de bucle principal proporcionada per Emscripten, però de vegades la trucada asíncrona desembolicava completament la pila i, en aquell moment, fallaria. setTimeout des del bucle principal: per tant, el codi va entrar a la iteració del bucle principal sense sortir de la iteració anterior. Reescrit en un bucle infinit i emscripten_sleep, i els problemes amb els mutex es van aturar. El codi fins i tot s'ha tornat més lògic; després de tot, de fet, no tinc cap codi que prepari el següent fotograma d'animació; el processador només calcula alguna cosa i la pantalla s'actualitza periòdicament. Tanmateix, els problemes no es van aturar aquí: de vegades l'execució de Qemu acabava simplement en silenci sense cap excepció ni error. En aquell moment hi vaig renunciar, però, mirant endavant, diré que el problema era aquest: el codi de corrutina, de fet, no utilitza setTimeout (o almenys no tan sovint com podríeu pensar): funció emscripten_yield simplement estableix la marca de trucada asíncrona. La qüestió és que emscripten_coroutine_next no és una funció asíncrona: internament comprova la bandera, la reinicia i transfereix el control allà on calgui. És a dir, la promoció de la pila acaba aquí. El problema va ser que a causa de l'ús després de la lliure, que va aparèixer quan es va desactivar el grup de corrutines a causa del fet que no vaig copiar una línia important de codi del backend de corrutina existent, la funció qemu_in_coroutine va tornar true quan de fet hauria d'haver tornat fals. Això va provocar una trucada emscripten_yield, per sobre del qual no hi havia ningú a la pila emscripten_coroutine_next, la pila es va desplegar fins a dalt, però no setTimeout, com ja he dit, no es va exposar.

Generació de codi JavaScript

I aquí, de fet, el promès de "tornar la carn picada enrere". No realment. Per descomptat, si executem Qemu al navegador i Node.js en ell, aleshores, naturalment, després de la generació de codi a Qemu tindrem JavaScript completament equivocat. Però tot i així, una mena de transformació inversa.

Primer, una mica sobre com funciona Qemu. Si us plau, perdoneu-me de seguida: no sóc un desenvolupador professional de Qemu i les meves conclusions poden ser errònies en alguns llocs. Com diuen, "l'opinió de l'alumne no ha de coincidir amb l'opinió del professor, l'axiomàtica i el sentit comú de Peano". Qemu té un cert nombre d'arquitectures convidades suportades i per a cadascuna hi ha un directori com target-i386. Quan es construeix, podeu especificar suport per a diverses arquitectures convidades, però el resultat només serà diversos binaris. El codi per donar suport a l'arquitectura convidada, al seu torn, genera algunes operacions internes de Qemu, que el TCG (Tiny Code Generator) ja converteix en codi màquina per a l'arquitectura host. Com s'indica al fitxer readme situat al directori tcg, originalment formava part d'un compilador C normal, que més tard es va adaptar per a JIT. Per tant, per exemple, l'arquitectura de destinació en termes d'aquest document ja no és una arquitectura convidada, sinó una arquitectura amfitrió. En algun moment, va aparèixer un altre component: Tiny Code Interpreter (TCI), que hauria d'executar codi (gairebé les mateixes operacions internes) en absència d'un generador de codi per a una arquitectura d'amfitrió específica. De fet, com indica la seva documentació, és possible que aquest intèrpret no sempre funcioni tan bé com un generador de codi JIT, no només quantitativament en termes de velocitat, sinó també qualitativament. Encara que no estic segur que la seva descripció sigui completament rellevant.

Al principi vaig intentar fer un backend de TCG complet, però ràpidament em vaig confondre amb el codi font i una descripció no del tot clara de les instruccions del bytecode, així que vaig decidir embolicar l'intèrpret TCI. Això va donar diversos avantatges:

  • en implementar un generador de codi, no podeu mirar la descripció de les instruccions, sinó el codi de l'intèrpret
  • podeu generar funcions no per a cada bloc de traducció que trobeu, sinó, per exemple, només després de la centèsima execució
  • si el codi generat canvia (i això sembla ser possible, a jutjar per les funcions amb noms que contenen la paraula pedaç), hauré d'invalidar el codi JS generat, però almenys tindré alguna cosa per regenerar-lo.

Pel que fa al tercer punt, no estic segur que el pegat sigui possible després d'executar el codi per primera vegada, però els dos primers punts són suficients.

Inicialment, el codi es generava en forma d'un interruptor gran a l'adreça de la instrucció bytecode original, però després, recordant l'article sobre Emscripten, optimització de JS generat i relooping, vaig decidir generar més codi humà, sobretot perquè empíricament va resultar que l'únic punt d'entrada al bloc de traducció és el seu Inici. Tan aviat com dit i fet, després d'un temps teníem un generador de codi que generava codi amb ifs (encara que sense bucles). Però mala sort, es va estavellar, donant un missatge que les instruccions eren d'una longitud incorrecta. A més, l'última instrucció en aquest nivell de recursivitat va ser brcond. D'acord, afegiré una comprovació idèntica a la generació d'aquesta instrucció abans i després de la trucada recursiva i... no s'ha executat cap d'elles, però després del canvi d'assert encara han fallat. Al final, després d'estudiar el codi generat, em vaig adonar que després del canvi, el punter a la instrucció actual es torna a carregar de la pila i probablement es sobreescriu pel codi JavaScript generat. I així va resultar. Augmentar la memòria intermèdia d'un megabyte a deu no va donar lloc a res, i va quedar clar que el generador de codi funcionava en cercles. Vam haver de comprovar que no vam anar més enllà dels límits de la TB actual i, si ho vam fer, emetre l'adreça de la següent TB amb un signe menys per poder continuar amb l'execució. A més, això resol el problema "Quines funcions generades s'haurien d'invalidar si aquest fragment de bytecode ha canviat?" — només cal invalidar la funció que correspon a aquest bloc de traducció. Per cert, tot i que ho he depurat tot a Chromium (ja que faig servir Firefox i em resulta més fàcil utilitzar un navegador separat per als experiments), Firefox em va ajudar a corregir les incompatibilitats amb l'estàndard asm.js, després de la qual cosa el codi va començar a funcionar més ràpidament en Crom.

Exemple de codi generat

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"]

Conclusió

Per tant, l'obra encara no s'ha acabat, però estic cansat de portar a la perfecció aquesta construcció a llarg termini. Per això, vaig decidir publicar el que tinc ara per ara. El codi fa una mica de por en alguns llocs, perquè es tracta d'un experiment i no està clar per endavant què s'ha de fer. Probablement, val la pena emetre compromisos atòmics normals a més d'alguna versió més moderna de Qemu. Mentrestant, hi ha un fil al Gita en format de bloc: per a cada "nivell" que s'ha superat almenys d'alguna manera, s'ha afegit un comentari detallat en rus. De fet, aquest article és en gran mesura una revisió de la conclusió git log.

Pots provar-ho tot aquí (Compte amb el trànsit).

Què ja funciona:

  • processador virtual x86 en funcionament
  • Hi ha un prototip de funcionament d'un generador de codi JIT des del codi màquina fins a JavaScript
  • Hi ha una plantilla per muntar altres arquitectures convidades de 32 bits: ara mateix podeu admirar Linux per a l'arquitectura MIPS que es congela al navegador en l'etapa de càrrega.

Què més pots fer

  • Accelera l'emulació. Fins i tot en mode JIT sembla que funciona més lent que Virtual x86 (però hi ha potencialment un Qemu sencer amb molt de maquinari i arquitectures emulades)
  • Per fer una interfície normal, francament, no sóc un bon desenvolupador web, així que de moment he refet l'intèrpret d'ordres estàndard d'Emscripten com he pogut
  • Intenteu llançar funcions Qemu més complexes: xarxes, migració de VM, etc.
  • ACTUALITZACIÓ: haureu d'enviar els vostres pocs desenvolupaments i informes d'errors a Emscripten aigües amunt, com van fer els anteriors portadors de Qemu i altres projectes. Gràcies a ells per poder utilitzar implícitament la seva contribució a Emscripten com a part de la meva tasca.

Font: www.habr.com

Afegeix comentari