Qemu.js con compatibilidade con JIT: aínda podes virar o picado cara atrás

Hai uns anos Fabrice Bellard escrito por jslinux é un emulador de PC escrito en JavaScript. Despois diso houbo polo menos máis Virtual x86. Pero todos eles, que eu saiba, eran intérpretes, mentres que Qemu, escrito moito antes polo mesmo Fabrice Bellard e, probablemente, calquera emulador moderno que se precie, utiliza a compilación JIT de código de convidado no código do sistema host. Pareceume que era hora de implementar a tarefa contraria en relación á que solucionan os navegadores: compilación JIT de código máquina en JavaScript, para o que parecía máis lóxico portar Qemu. Parece que, por que Qemu, hai emuladores máis sinxelos e fáciles de usar -o mesmo VirtualBox, por exemplo- instalados e funcionan. Pero Qemu ten varias características interesantes

  • código aberto
  • capacidade de traballar sen un controlador do núcleo
  • capacidade de traballar en modo intérprete
  • soporte para un gran número de arquitecturas de host e convidados

Respecto do terceiro punto, agora podo explicar que, de feito, no modo TCI, non se interpretan as propias instrucións da máquina convidada, senón o bytecode obtido delas, pero isto non cambia a esencia, para poder construír e executar. Qemu nunha nova arquitectura, se tes sorte, un compilador C é suficiente - a escritura dun xerador de código pode pospoñerse.

E agora, despois de dous anos de xogar tranquilamente co código fonte de Qemu no meu tempo libre, apareceu un prototipo de traballo, no que xa se pode executar, por exemplo, Kolibri OS.

Que é Emscripten

Hoxe en día apareceron moitos compiladores, cuxo resultado final é JavaScript. Algúns, como Type Script, tiñan a intención orixinal de ser a mellor forma de escribir para a web. Ao mesmo tempo, Emscripten é unha forma de tomar o código C ou C++ existente e compilalo nun formato lexible polo navegador. Activado Esta páxina Recollemos moitos portos de programas coñecidos: aquíPor exemplo, podes mirar PyPy; por certo, afirman que xa teñen JIT. De feito, non todos os programas poden simplemente compilarse e executarse nun navegador: hai un número características, que hai que soportar, porén, xa que a inscrición da mesma páxina di "Emscripten pode ser usado para compilar case calquera portátil código C/C++ a JavaScript". É dicir, hai unha serie de operacións que teñen un comportamento indefinido segundo o estándar, pero que normalmente funcionan en x86; por exemplo, o acceso non aliñado a variables, que xeralmente está prohibido nalgunhas arquitecturas. En xeral. , Qemu é un programa multiplataforma e , quería crer, e aínda non contén moito comportamento indefinido - tómao e compíllao, despois xoga un pouco con JIT - e xa estás! Pero iso non é o caso...

Primeiro intento

En xeral, non son a primeira persoa que se lle ocorreu a idea de portar Qemu a JavaScript. Houbo unha pregunta no foro de ReactOS se isto era posible usando Emscripten. Mesmo antes, había rumores de que Fabrice Bellard facía isto persoalmente, pero estabamos a falar de jslinux, que, polo que eu sei, é só un intento de conseguir manualmente un rendemento suficiente en JS, e foi escrito desde cero. Máis tarde, escribiuse Virtual x86: publicáronse fontes sen ocultar para iso e, como se dixo, o maior "realismo" da emulación permitiu usar SeaBIOS como firmware. Ademais, houbo polo menos un intento de portar Qemu usando Emscripten: tentei facelo par de tomas, pero o desenvolvemento, polo que eu entendo, estaba conxelado.

Entón, ao parecer, aquí están as fontes, aquí está Emscripten: tómao e compila. Pero tamén hai bibliotecas das que depende Qemu, e bibliotecas das que dependen esas bibliotecas, etc., e unha delas é libffi, da que depende a glib. Había rumores en Internet de que había un na gran colección de portos de bibliotecas para Emscripten, pero era difícil de crer dalgún xeito: en primeiro lugar, non estaba pensado para ser un compilador novo, en segundo lugar, era un compilador de nivel demasiado baixo. biblioteca para recoller e compilar en JS. E non é só unha cuestión de insercións de conxunto; probablemente, se o retorce, para algunhas convencións de chamada pode xerar os argumentos necesarios na pila e chamar á función sen eles. Pero Emscripten é algo complicado: para que o código xerado pareza familiar ao optimizador de motores JS do navegador, utilízanse algúns trucos. En particular, o chamado relooping - un xerador de código que usa o LLVM IR recibido con algunhas instrucións de transición abstractas tenta recrear ifs, bucles, etc. Ben, como se pasan os argumentos á función? Por suposto, como argumentos para as funcións JS, é dicir, se é posible, non a través da pila.

Ao principio xurdiu a idea de simplemente escribir un substituto para libffi con JS e executar probas estándar, pero ao final confundínme sobre como facer os meus ficheiros de cabeceira para que funcionasen co código existente, que podo facer? como din: "Son tan complexas as tarefas "Somos tan estúpidos?" Tiven que levar libffi a outra arquitectura, por así dicilo; afortunadamente, Emscripten ten tanto macros para a montaxe en liña (en Javascript, si, ben, sexa cal sexa a arquitectura, polo que o ensamblador) e a capacidade de executar código xerado sobre a marcha. En xeral, despois de xogar con fragmentos de libffi dependentes da plataforma durante algún tempo, conseguín algún código compilable e executei na primeira proba que atopei. Para a miña sorpresa, a proba foi exitosa. Atónito pola miña xenialidade -non é broma, funcionou desde o primeiro lanzamento- eu, aínda sen crer aos meus ollos, fun mirar de novo o código resultante, para avaliar onde cavar a continuación. Aquí quedei loco por segunda vez - o único que facía a miña función foi ffi_call - isto informou dunha chamada exitosa. Non houbo ningunha chamada en si. Así que enviei a miña primeira solicitude de extracción, que corrixiu un erro na proba que está claro para calquera estudante das Olimpiadas: os números reais non deben compararse como a == b e mesmo como a - b < EPS - tamén cómpre lembrar o módulo, se non, 0 resultará ser moi igual a 1/3... En xeral, ocorréuseme un certo porto de libffi, que pasa as probas máis sinxelas e co que glib é compilado - decidín que sería necesario, engadirei máis tarde. Mirando cara adiante, direi que, como resultou, o compilador nin sequera incluíu a función libffi no código final.

Pero, como xa dixen, hai algunhas limitacións e, entre o uso gratuíto de varios comportamentos indefinidos, ocultouse unha característica máis desagradable: o deseño de JavaScript non admite o multithreading con memoria compartida. En principio, isto pode ser considerado unha boa idea, pero non para portar código cuxa arquitectura está ligada a fíos C. En xeral, Firefox está experimentando co soporte de traballadores compartidos e Emscripten ten unha implementación de pthread para eles, pero non quería depender diso. Tiven que eliminar lentamente o multithreading do código Qemu, é dicir, descubrir onde se executan os fíos, mover o corpo do bucle que se executa neste fío a unha función separada e chamar a tales funcións unha por unha desde o bucle principal.

Segundo intento

Nalgún momento, quedou claro que o problema seguía aí e que meter muletas ao azar polo código non levaría a nada. Conclusión: hai que sistematizar dalgún xeito o proceso de engadir muletas. Polo tanto, tomouse a versión 2.4.1, que era nova naquel momento (non a 2.5.0, porque, quen sabe, haberá erros na nova versión que aínda non foron detectados, e teño bastantes dos meus propios erros). ), e o primeiro foi reescribilo con seguridade thread-posix.c. Ben, é dicir, como seguro: se alguén tentaba realizar unha operación que levaba ao bloqueo, a función chamábase inmediatamente abort() - por suposto, isto non resolveu todos os problemas á vez, pero polo menos foi dalgún xeito máis agradable que recibir silenciosamente datos inconsistentes.

En xeral, as opcións de Emscripten son moi útiles para portar código a JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - captan algúns tipos de comportamento indefinido, como chamadas a un enderezo non aliñado (que non é en absoluto coherente co código das matrices escritas como HEAP32[addr >> 2] = 1) ou chamar a unha función cun número incorrecto de argumentos.

Por certo, os erros de aliñamento son un problema aparte. Como xa dixen, Qemu ten un backend interpretativo "dexenerado" para a xeración de código TCI (pequeno intérprete de código), e para construír e executar Qemu nunha nova arquitectura, se tes sorte, un compilador C é suficiente. "se tes sorte". Tiven mala sorte e resultou que TCI usa acceso non aliñado ao analizar o seu bytecode. É dicir, en todo tipo de arquitecturas ARM e outras con acceso necesariamente nivelado, Qemu compila porque teñen un backend normal de TCG que xera código nativo, pero se TCI funcionará neles é outra cuestión. Non obstante, como se viu, a documentación do TCI indicaba claramente algo semellante. Como resultado, engadíronse ao código chamadas de función para lectura non aliñada, que foron descubertas noutra parte de Qemu.

Destrución do montón

Como resultado, corrixiuse o acceso non aliñado a TCI, creouse un bucle principal que á súa vez chamou procesador, RCU e algunhas outras pequenas cousas. E así lanzo Qemu coa opción -d exec,in_asm,out_asm, o que significa que cómpre dicir que bloques de código se están a executar, e tamén no momento da emisión escribir cal era o código de convidado, en que se converteu o código de host (neste caso, bytecode). Comeza, executa varios bloques de tradución, escribe a mensaxe de depuración que deixei que agora se iniciará RCU e... falla abort() dentro dunha función free(). Ao xogar coa función free() Conseguimos descubrir que na cabeceira do bloque do montón, que se atopa nos oito bytes anteriores á memoria asignada, en lugar do tamaño do bloque ou algo semellante, había lixo.

Destrución do montón - que bonito... Neste caso, hai un remedio útil - a partir (se é posible) das mesmas fontes, montar un binario nativo e executalo baixo Valgrind. Despois dun tempo, o binario estaba listo. Lanzoo coas mesmas opcións: falla mesmo durante a inicialización, antes de chegar á execución. É desagradable, por suposto - ao parecer, as fontes non eran exactamente as mesmas, o que non é de estrañar, porque a configuración buscaba opcións lixeiramente diferentes, pero teño Valgrind - primeiro solucionarei este erro e despois, se teño sorte , aparecerá o orixinal. Estou executando o mesmo con Valgrind... Y-y-y, y-y-y, uh-uh, comezou, pasou pola inicialización normalmente e pasou por encima do erro orixinal sen un só aviso sobre o acceso incorrecto á memoria, sen esquecer as caídas. A vida, como din, non me preparou para iso: un programa de falla deixa de fallar cando se lanzou baixo Walgrind. O que foi é un misterio. A miña hipótese é que unha vez nas proximidades da instrución actual despois dun fallo durante a inicialización, gdb mostrou traballo memset-a cun punteiro válido usando calquera mmx, ou xmm rexistros, entón quizais foi algún tipo de erro de aliñamento, aínda que aínda é difícil de crer.

Vale, Valgrind non parece axudar aquí. E aquí comezou o máis noxento: todo parece comezar, pero falla por razóns absolutamente descoñecidas debido a un evento que puido ocorrer hai millóns de instrucións. Durante moito tempo, nin sequera estaba claro como abordar. Ao final, aínda tiven que sentarme e depurar. Imprimir co que se reescribía a cabeceira mostrou que non parecía un número, senón un tipo de datos binarios. E, velaquí, esta cadea binaria atopouse no ficheiro da BIOS, é dicir, agora era posible dicir con razoable confianza que se trataba dun desbordamento do búfer, e mesmo está claro que se escribiu neste búfer. Ben, entón algo así: en Emscripten, afortunadamente, non hai unha aleatorización do espazo de enderezos, tampouco hai buratos nel, polo que podes escribir nalgún lugar no medio do código para sacar os datos mediante o punteiro do último lanzamento. mire os datos, mire o punteiro e, se non cambiou, faga pensar. É certo que leva un par de minutos enlazar despois de calquera cambio, pero que podes facer? Como resultado, atopouse unha liña específica que copiaba a BIOS do búfer temporal á memoria do convidado e, de feito, non había espazo suficiente no búfer. Ao atopar a orixe desa estraña dirección do búfer resultou unha función qemu_anon_ram_alloc en arquivo oslib-posix.c - a lóxica era esta: ás veces pode ser útil aliñar o enderezo a unha páxina enorme de 2 MB de tamaño, para iso preguntarémolo mmap primeiro un pouco máis, e despois devolveremos o exceso coa axuda munmap. E se tal aliñamento non é necesario, indicaremos o resultado en lugar de 2 MB getpagesize() - mmap aínda dará un enderezo aliñado... Entón, en Emscripten mmap só chamadas malloc, pero por suposto non se aliña na páxina. En xeral, un erro que me frustrou durante un par de meses foi corrixido mediante un cambio двух liñas.

Características das funcións de chamada

E agora o procesador está contando algo, Qemu non falla, pero a pantalla non se acende e o procesador entra rapidamente en bucles, a xulgar pola saída -d exec,in_asm,out_asm. Xurdiu unha hipótese: as interrupcións do temporizador (ou, en xeral, todas as interrupcións) non chegan. E de feito, se desenroscas as interrupcións da montaxe nativa, que por algún motivo funcionou, obtén unha imaxe similar. Pero esta non foi a resposta en absoluto: unha comparación dos trazos emitidos coa opción anterior mostrou que as traxectorias de execución diverxeron moi cedo. Aquí hai que dicir que a comparación do que se gravou usando o lanzador emrun a saída de depuración coa saída da montaxe nativa non é un proceso completamente mecánico. Non sei exactamente como se conecta un programa que se executa nun navegador emrun, pero algunhas liñas na saída resultan reorganizadas, polo que a diferenza na diferenza aínda non é un motivo para asumir que as traxectorias diverxiron. En xeral, quedou claro que segundo as instrucións ljmpl hai unha transición a diferentes enderezos, e o bytecode xerado é fundamentalmente diferente: un contén unha instrución para chamar a unha función auxiliar, o outro non. Despois de buscar en Google as instrucións e de estudar o código que traduce estas instrucións, quedou claro que, en primeiro lugar, inmediatamente antes no rexistro cr0 fíxose unha gravación -utilizando tamén un axudante- que cambiou o procesador ao modo protexido e, en segundo lugar, que a versión js nunca pasou ao modo protexido. Pero o feito é que outra característica de Emscripten é a súa reticencia a tolerar códigos como a implementación de instrucións call en TCI, que resulta en tipo calquera punteiro de función long long f(int arg0, .. int arg9) - As funcións deben chamarse co número correcto de argumentos. Se se infrinxe esta regra, dependendo da configuración de depuración, o programa fallará (o que é bo) ou chamará a función incorrecta (o que será triste depurar). Tamén hai unha terceira opción: activar a xeración de envoltorios que engaden/eliminan argumentos, pero en total estes envoltorios ocupan moito espazo, a pesar de que, de feito, só necesito un pouco máis de cen envoltorios. Só isto é moi triste, pero resultou ser un problema máis grave: no código xerado das funcións do envoltorio, os argumentos convertéronse e convertéronse, pero ás veces non se chamaba a función cos argumentos xerados, ben, igual que en a miña implementación de libffi. É dicir, algúns axudantes simplemente non foron executados.

Afortunadamente, Qemu ten listas de axudantes lexibles pola máquina en forma de ficheiro de cabeceira como

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

Utilízanse de xeito bastante divertido: primeiro, as macros defínense da forma máis estraña DEF_HELPER_n, e despois acende helper.h. Na medida en que a macro se expande nun inicializador de estrutura e unha coma, e despois defínese unha matriz, e no canto de elementos - #include <helper.h> Como resultado, por fin tiven a oportunidade de probar a biblioteca no traballo piparseando, e escribiuse un script que xera exactamente eses envoltorios para exactamente as funcións para as que son necesarios.

E así, despois diso, o procesador parecía funcionar. Parece que a pantalla nunca se inicializou, aínda que memtest86+ puido executarse na montaxe nativa. Aquí é necesario aclarar que o código de E/S do bloque Qemu está escrito en corrutinas. Emscripten ten a súa propia implementación moi complicada, pero aínda necesitaba ser compatible co código Qemu, e agora pode depurar o procesador: Qemu admite opcións -kernel, -initrd, -append, co que podes arrincar Linux ou, por exemplo, memtest86+, sen usar dispositivos de bloqueo. Pero aquí está o problema: na asemblea nativa pódese ver a saída do núcleo de Linux á consola coa opción -nographic, e sen saída do navegador ao terminal desde onde se lanzou emrun, non veu. É dicir, non está claro: o procesador non funciona ou a saída gráfica non funciona. E entón ocorréuseme esperar un pouco. Resultou que "o procesador non está durmindo, senón que simplemente parpadea lentamente", e despois duns cinco minutos o núcleo lanzou unha morea de mensaxes na consola e continuou colgando. Quedou claro que o procesador, en xeral, funciona e necesitamos investigar o código para traballar con SDL2. Desafortunadamente, non sei como usar esta biblioteca, polo que nalgúns lugares tiven que actuar ao chou. Nalgún momento, a liña paralela0 brillou na pantalla sobre un fondo azul, o que suxeriu algunhas reflexións. Ao final, resultou que o problema era que Qemu abre varias fiestras virtuais nunha ventá física, entre as que pode cambiar usando Ctrl-Alt-n: funciona na compilación nativa, pero non en Emscripten. Despois de desfacerse das fiestras innecesarias usando opcións -monitor none -parallel none -serial none e instrucións para redeseñar con forza toda a pantalla en cada fotograma, todo funcionou de súpeto.

Corrutinas

Entón, a emulación no navegador funciona, pero non podes executar nada interesante nun único disquete, porque non hai E/S de bloque: cómpre implementar soporte para as rutinas. Qemu xa ten varios backends de rutina, pero debido á natureza de JavaScript e do xerador de código Emscripten, non podes comezar a facer malabares coas pilas. Parece que "todo pasou, quítanse o xeso", pero os desenvolvedores de Emscripten xa se ocuparon de todo. Esta implementación é bastante divertida: chamemos unha chamada de función como esta sospeitosa emscripten_sleep e varios outros que usan o mecanismo Asyncify, así como chamadas de punteiro e chamadas a calquera función na que un dos dous casos anteriores poida ocorrer máis abaixo na pila. E agora, antes de cada chamada sospeitosa, seleccionaremos un contexto asíncrono e, inmediatamente despois da chamada, comprobaremos se se produciu unha chamada asíncrona e, se é así, gardaremos todas as variables locais neste contexto asíncrono, indicando que función para transferir o control a cando necesitemos continuar coa execución e saír da función actual. Aquí é onde hai espazo para estudar o efecto despilfarro — para as necesidades de continuar coa execución de código despois de regresar dunha chamada asíncrona, o compilador xera "stubs" da función que comeza despois dunha chamada sospeitosa, así: se hai n chamadas sospeitosas, entón a función expandirase nalgún lugar n/2 veces: isto aínda é, se non Teña en conta que despois de cada chamada potencialmente asíncrona, cómpre engadir gardando algunhas variables locais á función orixinal. Posteriormente, incluso tiven que escribir un script sinxelo en Python, que, baseado nun determinado conxunto de funcións especialmente usadas en exceso que supostamente "non permiten que a asincronía pase por si mesmas" (é dicir, a promoción da pila e todo o que acabo de describir non traballar neles), indica chamadas a través de punteiros nas que as funcións deben ser ignoradas polo compilador para que estas funcións non se consideren asíncronas. E entón os ficheiros JS de menos de 60 MB son claramente demasiado, digamos polo menos 30. Aínda que, unha vez que estaba configurando un script de montaxe, e accidentalmente tirei as opcións de ligazón, entre as que estaba -O3. Executo o código xerado e Chromium consume memoria e falla. Despois mirei accidentalmente o que estaba tentando descargar... Ben, que podo dicir, eu tamén me quedaría conxelado se me pediran que estudase e optimizase coidadosamente un Javascript de máis de 500 MB.

Desafortunadamente, as comprobacións do código da biblioteca de soporte de Asyncify non foron totalmente amigables longjmp-s que se usan no código do procesador virtual, pero despois dun pequeno parche que desactiva estas comprobacións e restaura con forza os contextos coma se todo estivese ben, o código funcionou. E entón comezou unha cousa estraña: ás veces desencadeaban comprobacións no código de sincronización -as mesmas que bloquean o código se, segundo a lóxica de execución, debería bloquearse- alguén tentaba coller un mutex xa capturado. Afortunadamente, isto resultou non ser un problema lóxico no código serializado: simplemente estaba usando a funcionalidade estándar do bucle principal proporcionada por Emscripten, pero ás veces a chamada asíncrona desenrolaba completamente a pila e, nese momento, fallaría. setTimeout desde o bucle principal: así, o código entrou na iteración do bucle principal sen saír da iteración anterior. Reescribiu nun bucle infinito e emscripten_sleep, e os problemas cos mutex pararon. O código aínda se volveu máis lóxico - despois de todo, de feito, non teño algún código que prepare o seguinte fotograma de animación - o procesador só calcula algo e a pantalla actualízase periodicamente. Non obstante, os problemas non quedaron aí: ás veces a execución de Qemu simplemente remataba silenciosamente sen ningunha excepción ou erro. Nese momento renunciei a iso, pero, mirando cara adiante, direi que o problema era este: o código de corrutina, de feito, non usa setTimeout (ou polo menos non tantas veces como se podería pensar): función emscripten_yield simplemente establece a marca de chamada asíncrona. O asunto é iso emscripten_coroutine_next non é unha función asíncrona: internamente comproba a bandeira, reiniciala e transfire o control onde sexa necesario. É dicir, a promoción da pila remata aí. O problema foi que debido a use-after-free, que apareceu cando o grupo de corrutinas foi desactivado debido ao feito de que non copiei unha liña importante de código do backend de corrutina existente, a función qemu_in_coroutine devolveu verdadeiro cando en realidade debería ser falso. Isto levou a unha chamada emscripten_yield, por riba do cal non había ninguén na pila emscripten_coroutine_next, a pila despregouse ata arriba, pero non setTimeout, como xa dixen, non se expuxo.

Xeración de código JavaScript

E aquí, de feito, está o prometido "darlle a volta á carne picada". En realidade non. Por suposto, se executamos Qemu no navegador e Node.js nel, entón, naturalmente, despois da xeración de código en Qemu teremos JavaScript completamente incorrecto. Pero aínda así, algún tipo de transformación inversa.

En primeiro lugar, un pouco sobre como funciona Qemu. Perdóname de inmediato: non son un desenvolvedor profesional de Qemu e as miñas conclusións poden ser erróneas nalgúns lugares. Como din, "a opinión do alumno non ten que coincidir coa opinión do profesor, a axiomática e o sentido común de Peano". Qemu ten un certo número de arquitecturas convidadas soportadas e para cada unha hai un directorio como target-i386. Ao construír, pode especificar soporte para varias arquitecturas convidadas, pero o resultado será só varios binarios. O código para soportar a arquitectura convidada, á súa vez, xera algunhas operacións internas de Qemu, que o TCG (Tiny Code Generator) xa converte en código máquina para a arquitectura host. Como se indica no ficheiro Léame situado no directorio tcg, este era orixinalmente parte dun compilador C normal, que máis tarde foi adaptado para JIT. Polo tanto, por exemplo, a arquitectura de destino en termos deste documento xa non é unha arquitectura convidada, senón unha arquitectura host. Nalgún momento, apareceu outro compoñente: Tiny Code Interpreter (TCI), que debería executar código (case as mesmas operacións internas) en ausencia dun xerador de código para unha arquitectura de host específica. De feito, como indica a súa documentación, é posible que este intérprete non sempre funcione tan ben como un xerador de código JIT, non só cuantitativamente en termos de velocidade, senón tamén cualitativamente. Aínda que non estou seguro de que a súa descrición sexa completamente relevante.

Ao principio tentei facer un backend de TCG completo, pero axiña confundínme no código fonte e nunha descrición non totalmente clara das instrucións do bytecode, polo que decidín envolver o intérprete TCI. Isto deu varias vantaxes:

  • ao implementar un xerador de código, podes mirar non a descrición das instrucións, senón o código do intérprete
  • pode xerar funcións non para cada bloque de tradución atopado, senón, por exemplo, só despois da centésima execución
  • se o código xerado cambia (e isto parece ser posible, a xulgar polas funcións con nomes que conteñen a palabra parche), terei que invalidar o código JS xerado, pero polo menos terei algo para rexeneralo.

Respecto ao terceiro punto, non estou seguro de que sexa posible aplicar parches despois de que se execute o código por primeira vez, pero os dous primeiros puntos son suficientes.

Inicialmente, o código foi xerado en forma de interruptor grande no enderezo da instrución de bytecode orixinal, pero despois, lembrando o artigo sobre Emscripten, optimización do JS xerado e relooping, decidín xerar máis código humano, sobre todo porque empíricamente resultou que o único punto de entrada ao bloque de tradución é o seu Inicio. Nada máis dicir que feito, despois dun tempo tivemos un xerador de código que xeraba código con ifs (aínda que sen bucles). Pero a mala sorte, estrelouse, dando unha mensaxe de que as instrucións eran dunha lonxitude incorrecta. Ademais, a última instrución neste nivel de recursión foi brcond. Vale, engadirei unha comprobación idéntica á xeración desta instrución antes e despois da chamada recursiva e... non se executou ningunha delas, pero despois do cambio de afirmación aínda fallaron. Ao final, despois de estudar o código xerado, decateime de que despois do cambio, o punteiro á instrución actual cárgase de novo desde a pila e probablemente se sobrescriba polo código JavaScript xerado. E así resultou. Aumentar o búfer dun megabyte a dez non levou a nada, e quedou claro que o xerador de código funcionaba en círculos. Tivemos que comprobar que non superamos os límites da actual TB e, se o fixemos, emitir o enderezo da seguinte TB cun signo menos para poder continuar coa execución. Ademais, isto resolve o problema "que funcións xeradas deberían ser invalidadas se este fragmento de bytecode cambiou?" — só hai que invalidar a función que corresponde a este bloque de tradución. Por certo, aínda que depurei todo en Chromium (xa que uso Firefox e é máis fácil para min usar un navegador separado para os experimentos), Firefox axudoume a corrixir incompatibilidades co estándar asm.js, despois de que o código comezou a funcionar máis rápido en Cromo.

Exemplo de código xerado

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ón

Entón, o traballo aínda non está rematado, pero estou canso de levar á perfección esta construción a longo prazo. Por iso, decidín publicar o que teño polo momento. O código dá un pouco de medo nalgúns lugares, porque este é un experimento e non está claro de antemán o que hai que facer. Probablemente, paga a pena emitir compromisos atómicos normais enriba dalgunha versión máis moderna de Qemu. Mentres tanto, hai un fío no Gita en formato de blog: para cada "nivel" que se superou polo menos dalgún xeito, engadiuse un comentario detallado en ruso. En realidade, este artigo é en gran medida un relato da conclusión git log.

Podes probalo todo aquí (coidado co tráfico).

O que xa está funcionando:

  • Procesador virtual x86 en execución
  • Hai un prototipo funcional dun xerador de código JIT desde código máquina ata JavaScript
  • Hai un modelo para montar outras arquitecturas convidadas de 32 bits: agora mesmo podes admirar Linux para a arquitectura MIPS que se conxela no navegador na fase de carga.

Que máis podes facer

  • Acelera a emulación. Mesmo no modo JIT parece funcionar máis lento que Virtual x86 (pero potencialmente hai un Qemu completo con moito hardware e arquitecturas emuladas)
  • Para facer unha interface normal, francamente, non son un bo programador web, polo que de momento refixei o shell estándar de Emscripten o mellor que puiden
  • Tenta lanzar funcións Qemu máis complexas: redes, migración de máquinas virtuales, etc.
  • ACTUALIZACIÓN: terás que enviar os teus poucos desenvolvementos e informes de erros a Emscripten río arriba, como fixeron os porteadores anteriores de Qemu e outros proxectos. Grazas a eles por poder utilizar implicitamente a súa contribución a Emscripten como parte da miña tarefa.

Fonte: www.habr.com

Engadir un comentario