Qemu.js com suporte JIT: enchimento ainda pode ser devolvido

Alguns anos atrás, Fabrice Bellard escrito por jslinux é um emulador de PC escrito em JavaScript. Depois disso houve pelo menos mais x86 virtual. Mas todos eles, até onde eu sei, eram intérpretes, enquanto o Qemu, escrito muito antes pelo mesmo Fabrice Bellard, e provavelmente qualquer emulador moderno que se preze, usa a compilação JIT do código convidado no código do sistema host. Pareceu-me que era hora de implementar a tarefa oposta àquela que os navegadores resolvem: compilação JIT de código de máquina em JavaScript, para a qual parecia mais lógico portar o Qemu. Ao que parece, por que Qemu, existem emuladores mais simples e fáceis de usar - o mesmo VirtualBox, por exemplo - instalados e funcionando. Mas o Qemu tem vários recursos interessantes

  • Código aberto
  • capacidade de trabalhar sem um driver de kernel
  • capacidade de trabalhar no modo intérprete
  • suporte para um grande número de arquiteturas host e guest

Em relação ao terceiro ponto, posso agora explicar que na verdade, no modo TCI, não são as próprias instruções da máquina convidada que são interpretadas, mas o bytecode obtido delas, mas isso não muda a essência - para construir e executar Qemu em uma nova arquitetura, se você tiver sorte, o compilador AC é suficiente - escrever um gerador de código pode ser adiado.

E agora, depois de dois anos mexendo tranquilamente no código-fonte do Qemu em meu tempo livre, apareceu um protótipo funcional, no qual você já pode executar, por exemplo, o Kolibri OS.

O que é Emscripten

Hoje em dia surgiram muitos compiladores cujo resultado final é o JavaScript. Alguns, como o Type Script, foram originalmente concebidos para serem a melhor maneira de escrever para a web. Ao mesmo tempo, Emscripten é uma forma de pegar o código C ou C++ existente e compilá-lo em um formato legível pelo navegador. Sobre esta página Coletamos muitas versões de programas conhecidos: aquiPor exemplo, você pode olhar para o PyPy - a propósito, eles afirmam já ter JIT. Na verdade, nem todo programa pode ser simplesmente compilado e executado em um navegador - há vários recursos, que você terá que aturar, no entanto, já que a inscrição na mesma página diz “Emscripten pode ser usado para compilar quase qualquer portátil Código C/C++ para JavaScript". Ou seja, há uma série de operações que têm comportamento indefinido de acordo com o padrão, mas geralmente funcionam em x86 - por exemplo, acesso desalinhado a variáveis, que geralmente é proibido em algumas arquiteturas. Em geral , Qemu é um programa multiplataforma e, eu queria acreditar, e ele ainda não contém muitos comportamentos indefinidos - pegue-o e compile, depois mexa um pouco no JIT - e pronto! Mas isso não é o caso...

A primeira tentativa

De modo geral, não sou a primeira pessoa a ter a ideia de portar o Qemu para JavaScript. Houve uma pergunta no fórum do ReactOS se isso era possível usando o Emscripten. Ainda antes, havia rumores de que Fabrice Bellard fez isso pessoalmente, mas estávamos falando sobre jslinux, que, até onde eu sei, é apenas uma tentativa de obter manualmente desempenho suficiente em JS e foi escrito do zero. Mais tarde, o Virtual x86 foi escrito - foram postadas fontes não ofuscadas para ele e, como dito, o maior “realismo” da emulação possibilitou o uso do SeaBIOS como firmware. Além disso, houve pelo menos uma tentativa de portar o Qemu usando Emscripten - tentei fazer isso par de tomadas, mas o desenvolvimento, pelo que entendi, estava congelado.

Então, ao que parece, aqui estão as fontes, aqui está o Emscripten - pegue e compile. Mas também existem bibliotecas das quais o Qemu depende, e bibliotecas das quais essas bibliotecas dependem, etc., e uma delas é libffi, do qual depende. Havia rumores na Internet de que havia um em uma grande coleção de portas de bibliotecas para Emscripten, mas era difícil de acreditar: em primeiro lugar, não se destinava a ser um novo compilador e, em segundo lugar, era um compilador de nível muito baixo. biblioteca para apenas pegar e compilar em JS. E não se trata apenas de inserções de assembly - provavelmente, se você distorcer, para algumas convenções de chamada você poderá gerar os argumentos necessários na pilha e chamar a função sem eles. Mas o Emscripten é uma coisa complicada: para fazer com que o código gerado pareça familiar ao otimizador do mecanismo JS do navegador, alguns truques são usados. Em particular, o chamado relooping - um gerador de código que usa o LLVM IR recebido com algumas instruções de transição abstratas tenta recriar ifs, loops, etc. Bem, como os argumentos são passados ​​para a função? Naturalmente, como argumentos para funções JS, isto é, se possível, não através da pilha.

No início, tive a ideia de simplesmente escrever um substituto para libffi por JS e executar testes padrão, mas no final fiquei confuso sobre como fazer meus arquivos de cabeçalho para que funcionassem com o código existente - o que posso fazer, como se costuma dizer: “As tarefas são tão complexas? Somos tão estúpidos? Tive que portar a libffi para outra arquitetura, por assim dizer - felizmente, o Emscripten tem macros para montagem embutida (em Javascript, sim - bem, qualquer que seja a arquitetura, então o assembler) e a capacidade de executar o código gerado em tempo real. Em geral, depois de mexer com fragmentos de libffi dependentes da plataforma por algum tempo, consegui um código compilável e executei-o no primeiro teste que encontrei. Para minha surpresa, o teste foi bem-sucedido. Atordoado com minha genialidade - não é brincadeira, funcionou desde o primeiro lançamento - eu, ainda sem acreditar no que via, fui olhar novamente o código resultante, para avaliar onde cavar a seguir. Aqui fiquei maluco pela segunda vez - a única coisa que minha função fez foi ffi_call - isso relatou uma chamada bem-sucedida. Não houve chamada em si. Então enviei meu primeiro pull request, que corrigiu um erro no teste que fica claro para qualquer aluno da Olimpíada - números reais não devem ser comparados como a == b e até como a - b < EPS - você também precisa se lembrar do módulo, caso contrário 0 será muito igual a 1/3... Em geral, eu criei uma certa porta de libffi, que passa nos testes mais simples e com a qual o glib é compilado - decidi que seria necessário, adicionarei mais tarde. Olhando para o futuro, direi que, como descobri, o compilador nem sequer incluiu a função libffi no código final.

Mas, como já disse, existem algumas limitações e, entre o uso gratuito de vários comportamentos indefinidos, um recurso mais desagradável foi ocultado - o JavaScript por design não suporta multithreading com memória compartilhada. Em princípio, isso geralmente pode até ser considerado uma boa ideia, mas não para portar código cuja arquitetura esteja vinculada a threads C. De modo geral, o Firefox está experimentando suporte a trabalhadores compartilhados, e o Emscripten tem uma implementação pthread para eles, mas eu não queria depender disso. Tive que erradicar lentamente o multithreading do código Qemu - ou seja, descobrir onde os threads estão sendo executados, mover o corpo do loop em execução neste thread para uma função separada e chamar essas funções uma por uma no loop principal.

Segunda tentativa

Em algum momento, ficou claro que o problema ainda existia e que colocar muletas ao acaso no código não levaria a nada de bom. Conclusão: precisamos sistematizar de alguma forma o processo de adição de muletas. Portanto, foi adotada a versão 2.4.1, que era recente na época (não a 2.5.0, porque, quem sabe, haverá bugs na nova versão que ainda não foram detectados, e já tenho meus próprios bugs suficientes ), e a primeira coisa foi reescrevê-lo com segurança thread-posix.c. Bem, isto é, tão seguro: se alguém tentasse realizar uma operação que levasse ao bloqueio, a função era imediatamente chamada abort() - é claro que isso não resolveu todos os problemas de uma vez, mas pelo menos foi de alguma forma mais agradável do que receber silenciosamente dados inconsistentes.

Em geral, as opções do Emscripten são muito úteis na portabilidade de código para JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - eles capturam alguns tipos de comportamento indefinido, como chamadas para um endereço não alinhado (o que não é nada consistente com o código para matrizes digitadas como HEAP32[addr >> 2] = 1) ou chamar uma função com o número errado de argumentos.

A propósito, erros de alinhamento são um problema à parte. Como eu já disse, o Qemu possui um backend interpretativo “degenerado” para geração de código TCI (tiny code interpreter), e para construir e executar o Qemu em uma nova arquitetura, se você tiver sorte, um compilador C é suficiente. "se tiver sorte". Não tive sorte e descobri que o TCI usa acesso não alinhado ao analisar seu bytecode. Ou seja, em todos os tipos de ARM e outras arquiteturas com acesso necessariamente nivelado, o Qemu compila porque possui um backend TCG normal que gera código nativo, mas se o TCI funcionará neles é outra questão. No entanto, como se viu, a documentação do TCI indicava claramente algo semelhante. Como resultado, foram adicionadas ao código chamadas de função para leitura desalinhada, que foram descobertas em outra parte do Qemu.

Destruição de pilha

Como resultado, o acesso desalinhado ao TCI foi corrigido, foi criado um loop principal que por sua vez chamava o processador, RCU e algumas outras pequenas coisas. E então eu lanço o Qemu com a opção -d exec,in_asm,out_asm, o que significa que você precisa dizer quais blocos de código estão sendo executados, e também no momento da transmissão escrever qual era o código convidado, qual código host se tornou (no caso, bytecode). Ele inicia, executa vários blocos de tradução, escreve a mensagem de depuração que deixei informando que o RCU irá iniciar agora e... trava abort() dentro de uma função free(). Ao mexer na função free() Conseguimos descobrir que no cabeçalho do bloco heap, que fica nos oito bytes anteriores à memória alocada, em vez do tamanho do bloco ou algo semelhante, havia lixo.

Destruição da pilha - que fofo... Nesse caso, existe uma solução útil - a partir (se possível) das mesmas fontes, monte um binário nativo e execute-o no Valgrind. Depois de algum tempo, o binário estava pronto. Eu o inicio com as mesmas opções - ele trava mesmo durante a inicialização, antes de realmente chegar à execução. É desagradável, claro - aparentemente, as fontes não eram exatamente as mesmas, o que não é surpreendente, porque o configure explorou opções ligeiramente diferentes, mas eu tenho o Valgrind - primeiro vou consertar esse bug e depois, se tiver sorte , o original aparecerá. Estou executando a mesma coisa no Valgrind... Aaa, aaa, uh-uh, ele começou, passou pela inicialização normalmente e passou pelo bug original sem um único aviso sobre acesso incorreto à memória, sem mencionar quedas. A vida, como dizem, não me preparou para isso - um programa que trava para de travar quando iniciado no Walgrind. O que foi é um mistério. Minha hipótese é que, uma vez próximo à instrução atual após uma falha durante a inicialização, o gdb mostrou trabalho memset-a com um ponteiro válido usando mmx, seja xmm registros, então talvez tenha sido algum tipo de erro de alinhamento, embora ainda seja difícil de acreditar.

Ok, Valgrind não parece ajudar aqui. E aqui começou a coisa mais nojenta - tudo parece até começar, mas trava por motivos absolutamente desconhecidos devido a um evento que poderia ter acontecido há milhões de instruções. Por muito tempo, nem ficou claro como abordar. No final, ainda tive que sentar e depurar. Imprimir o que o cabeçalho foi reescrito mostrou que ele não parecia um número, mas sim algum tipo de dado binário. E eis que essa string binária foi encontrada no arquivo BIOS - ou seja, agora era possível dizer com razoável confiança que era um buffer overflow, e é até claro que foi gravado nesse buffer. Bem, então algo assim - no Emscripten, felizmente, não há randomização do espaço de endereço, também não há lacunas nele, então você pode escrever em algum lugar no meio do código para gerar dados por ponteiro desde o último lançamento, observe os dados, observe o ponteiro e, se ele não mudou, procure o que pensar. É verdade que leva alguns minutos para vincular após qualquer alteração, mas o que você pode fazer? Como resultado, foi encontrada uma linha específica que copiava o BIOS do buffer temporário para a memória convidada - e, de fato, não havia espaço suficiente no buffer. Encontrar a origem daquele estranho endereço de buffer resultou em uma função qemu_anon_ram_alloc no arquivo oslib-posix.c - a lógica era esta: às vezes pode ser útil alinhar o endereço a uma página enorme de 2 MB, para isso pediremos mmap primeiro um pouco mais e depois devolveremos o excesso com a ajuda munmap. E se tal alinhamento não for necessário, indicaremos o resultado em vez de 2 MB getpagesize() - mmap ainda fornecerá um endereço alinhado... Então, em Emscripten mmap apenas liga malloc, mas é claro que não se alinha na página. Em geral, um bug que me frustrou por alguns meses foi corrigido por uma mudança no двух linhas.

Recursos de chamadas de funções

E agora o processador está contando alguma coisa, o Qemu não trava, mas a tela não liga e o processador entra rapidamente em loop, a julgar pela saída -d exec,in_asm,out_asm. Surgiu uma hipótese: as interrupções do temporizador (ou, em geral, todas as interrupções) não chegam. E, de fato, se você desparafusar as interrupções da montagem nativa, que por algum motivo funcionou, você terá uma imagem semelhante. Mas esta não foi a resposta: uma comparação dos traços emitidos com a opção acima mostrou que as trajetórias de execução divergiram muito cedo. Aqui é preciso dizer que comparação do que foi gravado no launcher emrun depurar a saída com a saída do assembly nativo não é um processo completamente mecânico. Não sei exatamente como um programa executado em um navegador se conecta emrun, mas algumas linhas na saída foram reorganizadas, então a diferença na diferença ainda não é uma razão para supor que as trajetórias divergiram. Em geral, ficou claro que de acordo com as instruções ljmpl há uma transição para endereços diferentes e o bytecode gerado é fundamentalmente diferente: um contém uma instrução para chamar uma função auxiliar, o outro não. Depois de pesquisar as instruções no Google e estudar o código que traduz essas instruções, ficou claro que, em primeiro lugar, imediatamente antes dela no cadastro cr0 foi feita uma gravação - também usando um auxiliar - que colocou o processador em modo protegido e, em segundo lugar, que a versão js nunca mudou para modo protegido. Mas o fato é que outra característica do Emscripten é sua relutância em tolerar códigos como a implementação de instruções call no TCI, onde qualquer ponteiro de função resulta no tipo long long f(int arg0, .. int arg9) - as funções devem ser chamadas com o número correto de argumentos. Se esta regra for violada, dependendo das configurações de depuração, o programa irá travar (o que é bom) ou chamar a função errada (o que será triste para depurar). Há também uma terceira opção - habilitar a geração de wrappers que adicionam/removem argumentos, mas no total esses wrappers ocupam muito espaço, apesar de na verdade eu precisar apenas de um pouco mais de cem wrappers. Isso por si só é muito triste, mas acabou sendo um problema mais sério: no código gerado das funções wrapper, os argumentos foram convertidos e convertidos, mas às vezes a função com os argumentos gerados não foi chamada - bem, assim como em minha implementação libffi. Ou seja, alguns ajudantes simplesmente não foram executados.

Felizmente, o Qemu possui listas de auxiliares legíveis por máquina na forma de um arquivo de cabeçalho como

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

Eles são usados ​​de forma bastante engraçada: primeiro, as macros são redefinidas da maneira mais bizarra DEF_HELPER_ne, em seguida, liga helper.h. Na medida em que a macro é expandida em um inicializador de estrutura e uma vírgula, e então um array é definido, e em vez de elementos - #include <helper.h> Como resultado, finalmente tive a oportunidade de experimentar a biblioteca no trabalho análise de py, e foi escrito um script que gera exatamente esses wrappers para exatamente as funções para as quais eles são necessários.

E então, depois disso, o processador pareceu funcionar. Parece que a tela nunca foi inicializada, embora o memtest86+ tenha conseguido rodar no assembly nativo. Aqui é necessário esclarecer que o código de E/S do bloco Qemu é escrito em corrotinas. O Emscripten tem sua própria implementação muito complicada, mas ainda precisava de suporte no código Qemu, e você pode depurar o processador agora: Qemu oferece suporte a opções -kernel, -initrd, -append, com o qual você pode inicializar o Linux ou, por exemplo, o memtest86+, sem usar nenhum dispositivo de bloco. Mas aqui está o problema: no assembly nativo pode-se ver a saída do kernel Linux para o console com a opção -nographic, e nenhuma saída do navegador para o terminal de onde foi iniciado emrun, não veio. Ou seja, não está claro: o processador não está funcionando ou a saída gráfica não está funcionando. E então me ocorreu esperar um pouco. Descobriu-se que “o processador não está dormindo, apenas piscando lentamente”, e depois de cerca de cinco minutos o kernel jogou um monte de mensagens no console e continuou travando. Ficou claro que o processador, em geral, funciona, e precisamos nos aprofundar no código para trabalhar com SDL2. Infelizmente, não sei como usar esta biblioteca, então em alguns lugares tive que agir de forma aleatória. Em algum momento, a linha paralela0 brilhou na tela em um fundo azul, o que sugeriu algumas reflexões. No final, descobriu-se que o problema é que o Qemu abre várias janelas virtuais em uma janela física, entre as quais você pode alternar usando Ctrl-Alt-n: funciona na compilação nativa, mas não no Emscripten. Depois de se livrar de janelas desnecessárias usando opções -monitor none -parallel none -serial none e instruções para redesenhar à força a tela inteira em cada quadro, tudo funcionou de repente.

Cor-rotinas

Portanto, a emulação no navegador funciona, mas você não pode executar nada interessante de disquete único nele, porque não há E/S de bloco - você precisa implementar suporte para corrotinas. O Qemu já possui vários back-ends de corrotinas, mas devido à natureza do JavaScript e do gerador de código Emscripten, você não pode simplesmente começar a fazer malabarismos com pilhas. Parece que “acabou tudo, o gesso está sendo retirado”, mas os desenvolvedores do Emscripten já cuidaram de tudo. Isso é implementado de forma bastante engraçada: vamos chamar uma chamada de função como essa de suspeita emscripten_sleep e vários outros que usam o mecanismo Asyncify, bem como chamadas de ponteiro e chamadas para qualquer função onde um dos dois casos anteriores possa ocorrer mais abaixo na pilha. E agora, antes de cada chamada suspeita, selecionaremos um contexto assíncrono, e imediatamente após a chamada, verificaremos se ocorreu uma chamada assíncrona, e se ocorreu, salvaremos todas as variáveis ​​locais neste contexto assíncrono, indicaremos qual função para transferir o controle para quando precisarmos continuar a execução e sair da função atual. É aqui que há espaço para estudar o efeito desperdiçando — para as necessidades de continuar a execução do código após retornar de uma chamada assíncrona, o compilador gera “stubs” da função começando após uma chamada suspeita — assim: se houver n chamadas suspeitas, então a função será expandida em algum lugar n/2 vezes - isso ainda é, se não. Lembre-se de que após cada chamada potencialmente assíncrona, você precisa adicionar o salvamento de algumas variáveis ​​locais à função original. Posteriormente, tive até que escrever um script simples em Python, que, baseado em um determinado conjunto de funções particularmente usadas em demasia que supostamente “não permitem que a assincronia passe por si mesmas” (ou seja, promoção de pilha e tudo o que acabei de descrever, não trabalhar neles), indica chamadas através de ponteiros nas quais funções devem ser ignoradas pelo compilador para que estas funções não sejam consideradas assíncronas. E então os arquivos JS com menos de 60 MB são claramente demais - digamos pelo menos 30. Embora, uma vez que eu estava configurando um script assembly, e acidentalmente joguei fora as opções do vinculador, entre as quais estava -O3. Eu executo o código gerado e o Chromium consome memória e trava. Então, acidentalmente, olhei para o que ele estava tentando baixar... Bem, o que posso dizer, eu também teria congelado se me pedissem para estudar cuidadosamente e otimizar um Javascript com mais de 500 MB.

Infelizmente, as verificações no código da biblioteca de suporte do Asyncify não eram totalmente compatíveis com longjmp-s que são usados ​​no código do processador virtual, mas depois de um pequeno patch que desativa essas verificações e restaura contextos à força como se tudo estivesse bem, o código funcionou. E então começou uma coisa estranha: às vezes eram acionadas verificações no código de sincronização - as mesmas que travam o código se, de acordo com a lógica de execução, ele deveria ser bloqueado - alguém tentava capturar um mutex já capturado. Felizmente, isso acabou não sendo um problema lógico no código serializado - eu estava simplesmente usando a funcionalidade de loop principal padrão fornecida pelo Emscripten, mas às vezes a chamada assíncrona desembrulhava completamente a pilha e, naquele momento, falhava setTimeout do loop principal - assim, o código entrou na iteração do loop principal sem sair da iteração anterior. Reescrito em um loop infinito e emscripten_sleep, e os problemas com mutexes pararam. O código ficou ainda mais lógico - afinal, na verdade, não tenho nenhum código que prepare o próximo quadro de animação - o processador apenas calcula algo e a tela é atualizada periodicamente. No entanto, os problemas não pararam por aí: às vezes a execução do Qemu simplesmente terminava silenciosamente, sem quaisquer exceções ou erros. Naquele momento desisti, mas, olhando para frente, direi que o problema foi esse: o código da corrotina, na verdade, não usa setTimeout (ou pelo menos não com a frequência que você imagina): function emscripten_yield simplesmente define o sinalizador de chamada assíncrona. A questão toda é que emscripten_coroutine_next não é uma função assíncrona: internamente verifica o sinalizador, reinicia-o e transfere o controle para onde for necessário. Ou seja, a promoção da pilha termina aí. O problema foi que devido ao use-after-free, que apareceu quando o pool de corrotinas foi desabilitado devido ao fato de eu não ter copiado uma linha importante de código do backend de corrotina existente, a função qemu_in_coroutine retornou verdadeiro quando na verdade deveria ter retornado falso. Isto levou a uma chamada emscripten_yield, acima do qual não havia ninguém na pilha emscripten_coroutine_next, a pilha se desdobrou até o topo, mas não setTimeout, como já disse, não foi exibido.

Geração de código JavaScript

E aqui, de fato, está o prometido “devolver a carne picada”. Na verdade. Claro, se executarmos o Qemu no navegador e o Node.js nele, então, naturalmente, após a geração do código no Qemu, obteremos um JavaScript completamente errado. Mas ainda assim, algum tipo de transformação reversa.

Primeiro, um pouco sobre como funciona o Qemu. Por favor, perdoe-me imediatamente: não sou um desenvolvedor profissional do Qemu e minhas conclusões podem estar erradas em alguns lugares. Como se costuma dizer, “a opinião do aluno não precisa coincidir com a opinião do professor, a axiomática e o bom senso de Peano”. Qemu tem um certo número de arquiteturas convidadas suportadas e para cada uma existe um diretório como target-i386. Ao compilar, você pode especificar suporte para diversas arquiteturas convidadas, mas o resultado serão apenas vários binários. O código de suporte à arquitetura guest, por sua vez, gera algumas operações internas do Qemu, que o TCG (Tiny Code Generator) já transforma em código de máquina para a arquitetura host. Conforme declarado no arquivo leia-me localizado no diretório tcg, originalmente fazia parte de um compilador C regular, que mais tarde foi adaptado para JIT. Portanto, por exemplo, a arquitetura alvo nos termos deste documento não é mais uma arquitetura convidada, mas uma arquitetura hospedeira. Em algum momento, outro componente apareceu - Tiny Code Interpreter (TCI), que deveria executar código (quase as mesmas operações internas) na ausência de um gerador de código para uma arquitetura de host específica. Na verdade, como afirma a sua documentação, este intérprete pode nem sempre ter um desempenho tão bom quanto um gerador de código JIT, não apenas quantitativamente em termos de velocidade, mas também qualitativamente. Embora eu não tenha certeza de que sua descrição seja totalmente relevante.

No começo, tentei fazer um back-end TCG completo, mas rapidamente fiquei confuso com o código-fonte e com uma descrição não totalmente clara das instruções do bytecode, então decidi envolver o interpretador TCI. Isso deu várias vantagens:

  • ao implementar um gerador de código, você não pode olhar para a descrição das instruções, mas para o código do interpretador
  • você pode gerar funções não para cada bloco de tradução encontrado, mas, por exemplo, somente após a centésima execução
  • se o código gerado mudar (e isso parece possível, a julgar pelas funções com nomes contendo a palavra patch), precisarei invalidar o código JS gerado, mas pelo menos terei algo para regenerá-lo

Em relação ao terceiro ponto, não tenho certeza se o patch será possível após a execução do código pela primeira vez, mas os dois primeiros pontos são suficientes.

Inicialmente, o código foi gerado na forma de um grande switch no endereço da instrução de bytecode original, mas depois, lembrando do artigo sobre Emscripten, otimização do JS gerado e relooping, resolvi gerar mais código humano, principalmente porque empiricamente é descobriu-se que o único ponto de entrada no bloco de tradução é o seu início. Mal dito e feito, depois de um tempo tínhamos um gerador de código que gerava código com ifs (embora sem loops). Mas, azar, ele travou, dando uma mensagem de que as instruções tinham comprimento incorreto. Além disso, a última instrução neste nível de recursão foi brcond. Ok, adicionarei uma verificação idêntica à geração desta instrução antes e depois da chamada recursiva e... nenhuma delas foi executada, mas após a troca de afirmação elas ainda falharam. No final, após estudar o código gerado, percebi que após a troca, o ponteiro para a instrução atual é recarregado da pilha e provavelmente sobrescrito pelo código JavaScript gerado. E assim aconteceu. Aumentar o buffer de um megabyte para dez não levou a nada e ficou claro que o gerador de código estava funcionando em círculos. Tivemos que verificar se não ultrapassamos os limites do TB atual e, caso o fizéssemos, emitir o endereço do próximo TB com um sinal de menos para que pudéssemos continuar a execução. Além disso, isso resolve o problema “quais funções geradas devem ser invalidadas se este pedaço de bytecode for alterado?” — apenas a função que corresponde a este bloco de tradução precisa ser invalidada. A propósito, embora eu tenha depurado tudo no Chromium (já que uso o Firefox e é mais fácil usar um navegador separado para experimentos), o Firefox me ajudou a corrigir incompatibilidades com o padrão asm.js, após o que o código começou a funcionar mais rápido em Cromo.

Exemplo de código gerado

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

Conclusão

Portanto, a obra ainda não está concluída, mas estou cansado de levar secretamente à perfeição esta construção de longo prazo. Portanto, decidi publicar o que tenho por enquanto. O código é um pouco assustador em alguns lugares, porque é um experimento e não está claro de antemão o que precisa ser feito. Provavelmente, vale a pena emitir commits atômicos normais além de alguma versão mais moderna do Qemu. Enquanto isso, há um tópico no Gita em formato de blog: para cada “nível” que foi de alguma forma ultrapassado, um comentário detalhado em russo foi adicionado. Na verdade, este artigo é, em grande medida, uma recontagem da conclusão git log.

Você pode tentar tudo aqui (cuidado com o trânsito).

O que já está funcionando:

  • processador virtual x86 em execução
  • Existe um protótipo funcional de um gerador de código JIT de código de máquina para JavaScript
  • Existe um modelo para montar outras arquiteturas convidadas de 32 bits: agora você pode admirar o Linux pela arquitetura MIPS congelando no navegador na fase de carregamento

O que mais você pode fazer

  • Acelere a emulação. Mesmo no modo JIT, ele parece rodar mais devagar que o Virtual x86 (mas há potencialmente um Qemu inteiro com muitos hardwares e arquiteturas emuladas)
  • Para fazer uma interface normal - francamente, não sou um bom desenvolvedor web, então, por enquanto, refiz o shell Emscripten padrão da melhor maneira que posso
  • Tente lançar funções mais complexas do Qemu - rede, migração de VM, etc.
  • UPD: você precisará enviar seus poucos desenvolvimentos e relatórios de bugs para o upstream do Emscripten, como fizeram os carregadores anteriores do Qemu e de outros projetos. Obrigado a eles por poderem usar implicitamente sua contribuição para o Emscripten como parte de minha tarefa.

Fonte: habr.com

Adicionar um comentário