1 pontos por GN⁺ 1 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • Elevator traduz estaticamente um executável x86-64 inteiro para AArch64 sem informações de depuração, código-fonte nem suposições sobre o layout do binário
  • Em vez de heurísticas para distinguir código e dados, ele cria um CFG superset que contém todas as interpretações possíveis de cada byte e remove apenas os caminhos que terminam em encerramento do programa
  • Ele mapeia o estado x64 um a um para registradores AArch64 e trata desvios indiretos com uma tabela de consulta que leva do endereço original ao código traduzido
  • O banco de tiles offline escreve a semântica das instruções x64 em templates C e depois os compila com LLVM 20 em sequências de bytes AArch64
  • O resultado é um binário AArch64 autônomo, sem tradução em tempo de execução, com desempenho igual ou melhor que o JIT em modo usuário do QEMU no SPECint 2006

Objetivo do Elevator

  • Elevator é um tradutor binário totalmente estático que leva um executável x86-64 inteiro para AArch64
  • Não usa informações de depuração, código-fonte, padrões de código do binário original nem suposições sobre o layout do binário
  • Tradutores estáticos existentes dependem de heurísticas ou fallback em tempo de execução para distinguir código de dados, mas o Elevator traduz previamente todos os bytes do executável original segundo cada interpretação possível
  • Como qualquer byte pode ser dado, parte de opcode ou parte de argumento de opcode, ele constrói um CFG superset que inclui todos os fluxos de controle possíveis e remove apenas os caminhos que levam a encerramento excepcional do programa
  • A saída é composta por um binário AArch64 autônomo que inclui o código traduzido, o binário x64 original, a tabela de consulta de endereços e um driver de runtime
  • Depois que a tradução termina, ele pode executar sem JIT nem suporte de tradução em tempo de execução
  • Traduzir o mesmo binário de entrada duas vezes gera a mesma sequência de bits na saída, e o alvo de teste, validação, certificação e assinatura criptográfica coincide com o código realmente distribuído
  • O principal custo é o aumento do tamanho do código e, em troca, há maior possibilidade de validação antes da distribuição do que em emuladores ou compiladores JIT
  • A avaliação incluiu todo o benchmark SPECint 2006 e binários feitos manualmente, e o desempenho ficou no mesmo nível ou acima da emulação em modo usuário do QEMU com aceleração JIT
  • Os pesquisadores afirmam que vão liberar o projeto inteiro como open source quando o trabalho for concluído

Por que a tradução estática é necessária e quais são os limites atuais

  • Quando o hardware muda de uma ISA para outra, software legado precisa ser levado à nova plataforma, e recompilar o código-fonte remanescente pode não ser suficiente
  • Em código legado validado ou certificado, muitas vezes o alvo da certificação não é o código-fonte, mas um executável binário autoritativo específico e amplamente testado
  • Reproduzir mais tarde, bit a bit, o mesmo binário a partir do código-fonte pode exigir as versões exatas do compilador, linker e sistema de build da época, o que é pouco realista
  • Se o fabricante aplicou patches diretamente no binário sem passar pelo código-fonte, recompilar a partir do código arquivado pode reintroduzir bugs que já haviam sido corrigidos
  • Abordagens existentes que tratam diretamente o binário combinam emulação, tradução estática e tradução dinâmica, mas componentes adicionais do sistema executados junto com o programa traduzido passam a fazer parte da base de código confiável
  • O comportamento dinâmico pode variar conforme a ordem dos testes ou as entradas, tornando difícil verificar a confiabilidade total
  • Horspool e Marovac mostraram em 1980 que, para reverter um executável, é preciso distinguir com certeza código de dados e que, na maioria das arquiteturas, isso é equivalente ao problema da parada, portanto insolúvel no caso geral
  • Levantadores estáticos de binário existentes aproximam a distinção entre código e dados com heurísticas, e o problema fica especialmente sério ao prever destinos de transferências indiretas de controle
  • O LLBT eleva instruções ARM para LLVM IR e recompila para a arquitetura de destino, mas usa heurísticas para detectar alvos de desvios indiretos e faz várias suposições sobre o binário de entrada
  • Mesmo boas heurísticas falham em alguns casos, e como elevar corretamente o binário inteiro exige acertar todas as distinções entre código e dados, a chance de falha cresce à medida que o binário aumenta
  • Métodos dinâmicos seguem o fluxo de instruções realmente executado, então conseguem lidar com recuperação de instruções e fluxo de controle indireto, mas não elevam instruções que não sejam alcançadas na execução concreta
  • Em ISAs com instruções de tamanho variável, como x64, uma sequência de instruções pode conter outra sequência sobreposta, e um desvio para o meio de uma instrução multibyte pode fazer operandos previamente parte dela serem decodificados como instruções separadas
  • Ataques ROP e ofuscação de código podem explorar essa característica
  • O Rosetta II da Apple e o Prism da Microsoft combinam componentes de tradução antecipada e tradução dinâmica
  • WYTIWYG e Polynima elevam estaticamente ao longo de caminhos de fluxo de controle identificados por profiling dinâmico e usam fallback dinâmico para coletar informações de fluxo de controle quando atingem endereços-alvo não observados
  • O Elevator não decide quais bytes são código ou dados, nem se são palavra de instrução ou argumento; ele inclui cada byte do executável em todos os papéis possíveis como caminhos separados de fluxo de controle
  • Essa abordagem aplica disassembly superset à recompilação estática e à compilação cross-ISA, trocando precisão de decodificação por crescimento de código

Fluxo de controle e preservação de estado

  • O Elevator opera dentro do código AArch64 traduzido com o princípio de preservar todo o estado x64
  • Ele mapeia registradores x64 e AArch64 um a um, emulando o estado de cada registrador x64 no registrador AArch64 correspondente
  • A pilha x64 é emulada diretamente sobre a pilha AArch64, e a expansão normal da pilha durante a execução é tratada pelo sistema operacional
  • Sem analisar a ABI do binário x64 de entrada, ele faz tradução de ABI apenas nos pontos em que a execução sai para código externo ou retorna dele, seguindo a ABI System V x64 e o AArch64 Procedure Call Standard
  • Graças à preservação completa de estado e ao mapeamento um a um de registradores, cada instrução x64 pode ser traduzida de forma independente, sem conhecer as instruções anterior e posterior
  • Cada deslocamento de byte executável do binário original é interpretado ao mesmo tempo como dado e como ponto de início potencial de uma sequência de instruções
  • Todo alvo potencial que não possa ser analisado estaticamente, como saltos indiretos, callbacks e dispatch em runtime, ganha um ponto de aterrissagem correspondente dentro do binário reescrito
  • Em runtime, os alvos são resolvidos por uma tabela de consulta incluída no binário final, que mapeia endereços de instruções originais para endereços do código traduzido
  • Exemplo de instruções sobrepostas

    • Listing 1 mostra uma estrutura em que, ao iniciar a decodificação em .byte 0xB0, aparecem MOV AL, 0xC3 seguido de RET, enquanto ao iniciar um byte depois, em ReturnC2, aparece apenas RET
    • Ambas as decodificações podem ser alcançadas a partir do jz anterior, e se o tradutor escolhesse apenas uma interpretação para os dois bytes, perderia um dos caminhos
  • Exemplo de desvio indireto calculado

    • Listing 2 mostra call Label criando um endereço-base relativo à tabela, que é recuperado por pop rsi; em seguida soma-se um deslocamento dependente da entrada para formar o alvo de jmp rsi
    • O desvio pode aterrissar em uma das quatro instruções inc eax colocadas a intervalos de 2 bytes no fluxo codificado
    • Um tradutor que reescreve apenas alvos de salto interpretáveis estaticamente não teria onde fazer esse desvio aterrissar
  • Chamadas, retornos e desvios

    • Instruções call, return e branch não podem ser expressas como tiles C porque a posição do endereço de retorno, o contador de programa e o layout de flags condicionais diferem entre x64 e AArch64
    • Chamadas diretas empilham o endereço de retorno x64 original na pilha emulada e desviam para o tile traduzido do callee
    • Chamadas indiretas verificam se o alvo está dentro do binário traduzido ou em uma biblioteca externa; alvos internos são traduzidos pela tabela de offset x64 para tile e então desviam para esse tile
    • Para alvos externos, o endereço do gadget de tradução ABI reversa é colocado em X30, para onde a biblioteca AArch64 retornará; em seguida é feita a tradução ABI de saída antes do desvio ao alvo externo
    • Retornos retiram o endereço de retorno de 8 bytes da pilha emulada, comparam com o intervalo do binário x64 embutido e, se for um retorno interno, traduzem o endereço pela tabela de consulta e desviam para o tile correspondente
    • Desvios diretos têm o alvo conhecido no momento da tradução, e desvios condicionais são traduzidos em desvios condicionais AArch64 que testam os bits de flags x64 guardados em X14
    • Desvios indiretos emitem a mesma verificação de limites usada em chamadas indiretas e retornos; se o alvo for externo, fazem a tradução ABI de saída

Pipeline de tradução baseado em tiles

  • A tradução do Elevator é dividida em três etapas: geração offline do banco de tiles, reescrita por binário de entrada e empacotamento final
  • A etapa offline expressa a semântica das instruções x64 como funções C, especializa por combinação de operandos sob um mapeamento fixo de registradores x64 para AArch64 e compila com um LLVM 20 modificado para produzir sequências reutilizáveis de bytes AArch64
  • A etapa por binário de entrada realiza o disassembly superset e percorre o CFG resultante
  • Em cada nó, para cada instrução candidata encontrada, ela procura o tile pelo nome e concatena as sequências de bytes AArch64 correspondentes
  • Categorias de instruções difíceis de expressar como tiles C, como transferências de fluxo de controle e fronteiras de ABI, são tratadas com pequenos templates escritos manualmente
  • A etapa de empacotamento combina o código traduzido, o binário x64 original, a tabela de consulta de endereços e o driver de runtime para gerar um binário AArch64 executável de forma independente
  • Banco de tiles offline

    • Escrever manualmente uma sequência equivalente de instruções AArch64 para cada instrução x64 não é prático
    • Mesmo um único template como ADD Reg8, Reg8 se expande para 256 combinações concretas de registradores, e o conjunto completo de instruções x64 tem muitas variações de registradores, operandos de memória e endereçamento com imediatos
    • O Elevator escreve a semântica de cada instrução x64 como uma pequena função C, depois a especializa por combinação concreta de operandos e deixa o LLVM compilá-la para AArch64
    • No exemplo de ADD Reg8, Reg8, o template atualiza os 8 bits inferiores do registrador de destino com a soma de 8 bits e preserva os 56 bits superiores, reproduzindo a semântica de escrita parcial de registrador do x64
    • Como ADD Reg8, Reg8 em x64 também altera as flags Carry, Parity, Auxiliary Carry, Zero, Sign e Overflow de RFLAGS, e uma função C com valor de retorno único não comporta isso, a atualização das flags é capturada em um tile de flags separado
    • Uma instrução x64 pode corresponder a um ou vários tiles, e na emissão eles são concatenados novamente em sequência para reconstruir a semântica completa
    • O atributo aarch64_custom_reg declara em quais registradores AArch64 o LLVM deve colocar o valor de retorno e cada argumento
    • O mapeamento fixo é escolhido para alinhar o caráter callee-saved e caller-saved do x64 System V e do AAPCS64, reduzir a reorganização de registradores de argumentos inteiros e deixar registradores callee-saved livres do AArch64 reservados para futuro estado sombra
    • Os bits de RFLAGS e o arquivo de registradores XMM do x64 também são mantidos em registradores AArch64 dedicados sob o mesmo princípio de mapeamento um a um
    • O LLVM 20 modificado trata o atributo aarch64_custom_reg por função e reclassifica, dentro do alocador de registradores, os registradores AArch64 que carregam o estado x64 emulado como callee-saved
    • O TileGen percorre os templates C, cria cópias especializadas para cada combinação permitida de operandos e sintetiza mecanicamente os atributos a partir das posições dos parâmetros do template e do mapeamento de registradores
  • Reescrita por binário de entrada

    • Dado um binário x64 de entrada, a etapa por binário executa um disassembly superset e percorre o CFG resultante
    • Em cada nó, o formatador monta nomes de tiles a partir do opcode e dos operandos da instrução decodificada; quando uma instrução exige vários tiles, combina vários nomes
    • O x64 não tem restrição de alinhamento do ponteiro de pilha, mas o AArch64 exige alinhamento de 16 bytes ao usar o ponteiro de pilha em operandos de memória
    • Se RSP fosse mapeado diretamente para SP, padrões comuns de código x64, como PUSH consecutivos em prólogos de função, poderiam causar exceções de alinhamento no AArch64
    • O Elevator faz os tiles acessarem a pilha por um registrador separado, X25, e concretiza SP nele apenas quando o tile realmente precisa disso
    • Tiles compilados pelo LLVM esperam alinhamento de 16 bytes de SP na entrada; por isso, antes de executar tiles detectados como capazes de alocar espaço de spill, SP é alinhado para baixo e depois restaurado
    • Como tiles de cálculo de flags são relativamente caros, se as flags forem sobrescritas antes de serem lidas por uma instrução post-dominante, o cálculo de flags do nó atual é removido
    • As instruções ainda não suportadas são principalmente as extensões vetoriais largas AVX2 e posteriores do x64; nesses pontos, é inserida uma instrução de interrupção no lugar do tile
    • Na avaliação completa com SPECint 2006, a ISA inteira de inteiros x86-64 e o subconjunto SSE usado pelo SPECint bastaram para executar todos os benchmarks
    • O suporte a mais instruções pode ser ampliado adicionando novos tiles, mas os autores consideram improvável que engenharia adicional acrescente novos insights científicos

Tratamento das fronteiras de ABI

  • O Elevator suporta apenas binários com linkedição dinâmica
  • Binários com linkedição estática podem incluir diretamente instruções específicas de arquitetura, como CPUID, enquanto binários com linkedição dinâmica delegam isso à libc, reduzindo a necessidade de tradução
  • Ao interagir com bibliotecas dinamicamente vinculadas, ele suporta a transição entre a ABI Linux x64 emulada e a ABI Linux AArch64 nativa para entrar e sair do código de biblioteca AArch64 nativo
  • Os elementos centrais que exigem tradução de ABI são o posicionamento dos argumentos e a localização do endereço de retorno
  • A ABI System V x64 usa seis registradores de argumento: RDI, RSI, RDX, RCX, R8 e R9; argumentos adicionais são passados na pilha a partir de [RSP+8]
  • A instrução CALL do x64 armazena o endereço de retorno em [RSP]
  • O AArch64 Procedure Call Standard usa oito registradores de argumento, X0-X7, coloca os argumentos restantes na pilha em [SP] e guarda o endereço de retorno em X30
  • Chamada a bibliotecas externas

    • Quando uma chamada x64 traduzida tem como alvo uma biblioteca externa, o layout dos argumentos precisa ser reorganizado para seguir a convenção de chamada AArch64
    • Primeiro, subtrai-se 8 de SP para voltar ao alinhamento em 16 bytes, e o endereço de retorno x64 que já estava na pilha é colocado em [SP+0x8]
    • Os valores em [SP+0x10] e [SP+0x18] são carregados em X6 e X7, permitindo que a biblioteca AArch64 veja os potenciais 7º e 8º argumentos que o código x64 deixou na pilha
    • Os demais argumentos potenciais na pilha continuam a partir de [SP+0x20], o que não coincide com as posições esperadas pelo AArch64
    • Remover da pilha o endereço de retorno x64 e os valores movidos para X6 e X7 não é seguro, porque eles podem não ser argumentos reais, mas espaço de spill do caller ou parte de uma struct alocada na pilha do caller
    • O Elevator não mexe no layout de pilha do caller; em vez disso, aloca n×8 bytes extras de espaço de pilha e copia n argumentos potenciais de 8 bytes a partir da posição atual
    • O valor padrão de n é 10, e pode ser aumentado por configuração se o binário de entrada passar mais de 16 argumentos no total a funções de bibliotecas externas
    • Por fim, o endereço do gadget para onde a biblioteca externa deve retornar é armazenado em X30
  • Retorno de bibliotecas externas

    • Quando o controle volta ao gadget armazenado em X30 antes da chamada à biblioteca externa, soma-se n×8 ao ponteiro de pilha para limpar os argumentos copiados anteriormente
    • O valor de retorno da biblioteca externa é movido de X0 para X9, que é a posição de RAX esperada pelo código x64 emulado
    • O endereço de retorno x64 original e o padding relacionado são retirados da pilha, o endereço é traduzido e então é feito um desvio para lá, retomando a execução após o CALL original
  • Callbacks entrando no código traduzido

    • Quando código AArch64 nativo chama o binário traduzido, a convenção de chamada AArch64 precisa ser convertida para a convenção de chamada x64
    • O código x64 emulado espera o 7º e o 8º argumentos na pilha, não em X6 e X7; por isso, X7 é empilhado primeiro e depois X6, colocando-os nas posições de pilha esperadas pelo x64
    • Se o callee não espera de fato o 7º e o 8º argumentos, esses valores empilhados não têm efeito
    • O endereço de retorno colocado em X30 pela instrução branch-and-link AArch64 da biblioteca externa é empilhado na posição em que a instrução de retorno x64 espera encontrá-lo
  • Retorno do callback para a biblioteca externa

    • Quando o código traduzido retorna do callback para a biblioteca externa, o processo de entrada é executado ao contrário
    • O endereço de retorno é retirado da pilha, X6 e X7 são empilhados, e o espaço de pilha alocado é limpo somando 0x10 ao ponteiro de pilha

1 comentários

 
GN⁺ 1 시간 전
Comentários no Hacker News
  • Não sei exatamente o que o JIT em modo usuário do QEMU faz, mas parece haver bastante espaço para melhorar
    Em 2013, criei um motor JIT que convertia de x86-64 para aarch64 e, na época, consegui executar binários beta do Fedora aarch64 e reconstruir grande parte da porta aarch64 do Fedora em Linux x86_64
    Também fiz o JIT no sentido oposto, de aarch64 → x86-64, e por diversão mostrei os dois JITs executando um ao outro em loopback dentro do mesmo processo, algo como x86-64 → aarch64 → x86_64
    O JIT que eu fiz mapeava instruções e estado da CPU em uma relação de 1 para muitos, e ficava cerca de 2 a 5 vezes mais lento que código nativamente recompilado
    Depois, ao comparar com o JIT do QEMU, pareceu que o QEMU ficava na faixa de 10 a 50 vezes mais lento
    Infelizmente, como o licenciamento não era de código aberto, não posso divulgar o código para provar isso

    • Sim, o JIT do QEMU é quase um alvo fácil de vencer
      Principalmente se você puder especializar o projeto para “apenas x86 para aarch64” e “apenas modo usuário”, há muito ganho de desempenho possível
      O suporte a modo usuário do QEMU é quase um apêndice “que por acaso funciona” preso ao suporte de emulação de sistema, e toda a estrutura do JIT também segue o modelo “guest → representação intermediária → host”, o que é bom para suportar várias arquiteturas guest e várias arquiteturas host, mas dificulta aproveitar propriedades de combinações específicas guest/host, como “x86 tem poucos registradores inteiros, então dá para fazer alocação fixa” ou “se você colocar a CPU aarch64 no modo adequado, a semântica complicada de ponto flutuante sempre fica correta”
      Além disso, no desenvolvimento do QEMU gasta-se mais tempo em “emular o novo recurso de arquitetura X” do que em encontrar oportunidades de otimização de desempenho, porque quem paga o custo de desenvolvimento considera isso mais importante
    • O QEMU é menos um tradutor e mais um TCG, e como foi projetado para funcionar em n arquiteturas, ele tem limitações
  • O fato de a seção .text ficar 50 vezes maior é enorme, mas parece um preço aceitável para obter uma tradução totalmente determinística
    Em muitos casos, o ganho de desempenho em relação à emulação deve pesar mais do que o incômodo do aumento de tamanho
    Também é interessante que multithreading e tratamento de exceções não sejam impossíveis, apenas estejam fora do escopo deste projeto
    Fico curioso se o próximo passo seria usar heurísticas para podar o espaço de possibilidades e reduzir o tamanho do binário
    Isso quebraria a garantia da tradução, mas poderia tornar a portabilidade binária mais prática no mundo real

    • Não é garantido que o ganho de desempenho em relação à emulação seja maior
      Esse tradutor é muito mais lento que Box64 ou FEX, e, a menos que você não possa usar JIT por algum motivo, ele é simplesmente uma escolha pior
  • Sempre tive curiosidade sobre como o tradutor lida com saltos indiretos
    Ao analisar o binário, você só consegue descobrir trechos de código ligados por saltos diretos, em que o endereço de destino é conhecido
    Então isso significaria que, toda vez que ocorre um salto indireto, ele precisa localizar a função alvo, traduzi-la se necessário e então voltar para o código traduzido; isso não fica lento?
    Queria saber se existe um método mais rápido, se é possível alinhar o endereço da função traduzida com o da função original, ou se em vez disso se coloca no endereço original um salto para o código traduzido

    • O tradutor que eu fiz é hobby-level, mas mantém uma tabela grande do tipo “se houver um jmp indireto para o endereço X, o bloco correspondente está na posição Y”
      Isso é mais lento do que um jmp direto sem tabela, mas no programa original os saltos indiretos já eram mais lentos para começar e normalmente não aparecem com frequência dentro dos loops críticos para desempenho
  • Gosto muito da ideia de um grafo de fluxo de controle superset, mas quem for ler o artigo deveria saber o seguinte
    O tempo de execução melhora cerca de 4,75 vezes (mais rápido que o QEMU, mas ainda consideravelmente mais lento que o Box64), o número de instruções executadas aumenta 7 vezes e o tamanho do binário aumenta 50 vezes
    Ele emula a ABI x86 até antes das chamadas externas
    É preciso emular boa parte do estado da CPU x86, como EFLAGS, e até mov complexos precisam ser calculados individualmente
    Só suporta binários single-threaded
    Não há tratamento de exceções nem unwinding de pilha
    Não suporta o conjunto completo de instruções

  • Trabalho interessante
    Não examinei em detalhe, mas offsets relativos ainda parecem poder ser um problema
    De qualquer forma, como o resultado da geração de código terá tamanho diferente, parece que seria necessária algum tipo de camada de tradução ou MMU, afetando principalmente tabelas de salto e desvios internos
    Eu lido mais com coisas dos anos 90, e os disassemblers fazem muitas suposições sobre o início e o fim do código
    Mas às vezes há casos em que não dá para encontrar um bloco binário sem conhecimento prévio, como um ponteiro de entry point em posição fixa
    Com algumas passagens, talvez fosse possível refinar o binário em “áreas que certamente são código”

  • Se “o Elevator considera todas as interpretações possíveis de cada byte e gera previamente uma tradução separada para cada uma das possibilidades [...] podando apenas os casos que levam a falha anormal”, então todos os programas reais com possibilidade de colisão acabam sendo podados?

    • Acho que isso seria configurado no nível da tabela de busca endereço→código para um caminho de colisão padronizado
      Nesse caso, ainda haveria colisão, mas não seria igual a uma falha causada pela execução direta do código incorreto
  • Para mim, a parte mais interessante é a perspectiva de certificação
    Em setores regulados como aviação e dispositivos médicos, muitas vezes não se pode usar JIT exatamente porque o código executado precisa ser o código certificado
    Uma tradução estática que produza um binário assinável pode ser um avanço real, mesmo com o crescimento do código

    • Fico curioso sobre o tamanho dessa área na indústria de software
      Provavelmente nem LLMs teriam como ser aplicados em escala aí, mas esse aspecto quase não aparece na grande discussão sobre “IA no trabalho”
  • 50 vezes não é razoável, é um desastre de cache
    Isso pode consumir todo o ganho de desempenho obtido por evitar JIT

    • Só seria assim se todo esse código fosse realmente usado no tempo de execução; a maior parte dos pontos possíveis de início de decodificação provavelmente nunca será usada
    • Este é um caso que combina muito bem com reorganização de código em tempo de link
      Se você agrupar o código quente em um só lugar, pode fazer com que o código não usado nunca seja carregado
    • Eu não tiraria conclusões precipitadas
      As instruções em si não são tão grandes assim, e a CPU também faz otimizações enquanto executa
  • Ele consegue lidar com código auto-modificável?
    Também fico curioso sobre por que só x86_64
    Parece mais significativo converter programas de 32 bits, como jogos antigos

    • Se você ler o artigo linkado, ele trata disso explicitamente
      “Código auto-modificável e código compilado por JIT. O Elevator, como todos os reescritores de binário totalmente estáticos, não suporta código auto-modificável nem código compilado por JIT”
    • Acho que código auto-modificável fora de runtimes JIT hoje em dia é bem raro em comparação com os anos 80 e 90
      Hoje a seção .text é em geral somente leitura, e não há motivo para que as exigências de segurança diminuam
    • Se ele tratasse código auto-modificável, então deixaria de ser “totalmente estático”
      É uma contradição fundamental
    • Do ponto de vista de quem desenvolve para x86 hoje, código auto-modificável é até possível, mas normalmente horrível
      Porque destrói o desempenho de linhas de cache e da predição de desvios no pipeline
      Além disso, viola W^X, então em geral só deve ser usado em páginas de memória compatíveis com JIT
      Por isso quase sempre deve ser evitado
      Na época do 486 ou do P5 isso era usado em certa medida, como empregar imediatos como variáveis de loop interno, mas hoje isso quase não acontece
      Para alcançar uma emulação ou tradução quase perfeita, há muitos casos-limite sujos do x86 que precisariam ser tratados
  • Onde está o código-fonte?