O que há dentro de um arquivo .wasm? Apresentando wasm-decompile

Temos muitos compiladores e outras ferramentas à nossa disposição para criar e trabalhar com arquivos .wasm. O número dessas ferramentas está em constante crescimento. Às vezes, você precisa procurar no arquivo .wasm e descobrir o que está dentro dele. Talvez você seja o desenvolvedor de uma das ferramentas Wasm, ou talvez seja um programador que escreve um código projetado para ser convertido em Wasm e esteja interessado em saber como ele será transformado em seu código. Esse interesse pode ser desencadeado, por exemplo, por considerações de desempenho.



O problema é que os arquivos .wasm contêm um código de baixo nível que se parece muito com o código real do montador. Em particular, ao contrário de, por exemplo, a JVM, todas as estruturas de dados são compiladas em conjuntos de operações de carregamento / armazenamento, e não em algo que tenha nomes claros de classe e campo. Compiladores, como o LLVM, podem alterar o código de entrada de maneira que o que eles obtêm não pareça próximo. 

E quem quer, pegando um arquivo .wasm, para descobrir o que está acontecendo nele?

Desmontando ou ... descompilando?


Você pode usar ferramentas como wasm2wat para converter arquivos .wasm em arquivos .wat contendo uma representação de texto padrão do código Wasm (isso faz parte do kit de ferramentas WABT ). Os resultados dessa conversão são muito precisos, mas a leitura do código resultante não é particularmente conveniente.

Aqui, por exemplo, está uma função simples escrita em C:

typedef struct { float x, y, z; } vec3;

float dot(const vec3 *a, const vec3 *b) {
    return a->x * b->x +
           a->y * b->y +
           a->z * b->z;
}

O código é armazenado em um arquivo dot.c.

Nós usamos o seguinte comando:

clang dot.c -c -target wasm32 -O2

Em seguida, para converter o que aconteceu em um arquivo .wat, aplicamos o seguinte comando:

wasm2wat -f dot.o

Aqui está o que isso nos dará:

(func $dot (type 0) (param i32 i32) (result f32)
  (f32.add
    (f32.add
      (f32.mul
        (f32.load
          (local.get 0))
        (f32.load
          (local.get 1)))
      (f32.mul
        (f32.load offset=4
          (local.get 0))
        (f32.load offset=4
          (local.get 1))))
    (f32.mul
      (f32.load offset=8
        (local.get 0))
      (f32.load offset=8
        (local.get 1))))))

O código é pequeno, mas por muitas razões, é extremamente difícil de ler. Além do fato de que as expressões não são usadas aqui e o fato de que, no geral, parece bastante detalhado, não é fácil entender as estruturas de dados representadas na forma de comandos para carregar dados da memória. Agora imagine que você precisa analisar esse código de tamanho muito maior. Essa análise será uma tarefa muito difícil.

Vamos tentar, em vez de usar wasm2wat, execute o seguinte comando:

wasm-decompile dot.o

Aqui está o que ela nos dará:

function dot(a:{ a:float, b:float, c:float },
             b:{ a:float, b:float, c:float }):float {
  return a.a * b.a + a.b * b.b + a.c * b.c
}

Já parece muito melhor. Além de usar expressões que lembram uma linguagem de programação que você já conhece, o decompilador analisa comandos destinados a trabalhar com memória e tenta recriar as estruturas de dados representadas por esses comandos. O sistema anota cada variável, que é usada como um ponteiro com uma declaração de estrutura “embutida”. O descompilador não cria uma declaração de estrutura nomeada, pois não sabe se há algo em comum entre estruturas que usam 3 valores flutuantes cada.

Como você pode ver, os resultados da descompilação se mostraram mais compreensíveis do que os resultados da desmontagem.

Qual idioma é o código escrito pelo decompilador?


A ferramenta wasm-decompile gera o código, tentando fazer com que esse código pareça algum tipo de linguagem de programação "média". Ao mesmo tempo, essa ferramenta tenta não ir muito longe de Wasm.

O primeiro objetivo do wasm-decompiler era criar código legível. Ou seja, um código que permitirá ao leitor entender facilmente o que está acontecendo no arquivo .wasm descompilado. O segundo objetivo desta ferramenta é fornecer a representação mais precisa do arquivo .wasm, gerando um código que represente totalmente o que está acontecendo no arquivo de origem. Obviamente, esses objetivos nem sempre estão em bom acordo.

O que o wasm-decompiler gera não foi originalmente concebido como código representando alguma linguagem de programação real. Atualmente, não há como compilar esse código no Wasm.

Comandos para carregar e salvar dados


Como mostrado acima, wasm-decompile procura comandos de carregamento e salvamento associados a um ponteiro específico. Se esses comandos formarem uma sequência contínua, o descompilador exibirá uma das declarações de estrutura de dados "internas".

Se nem todos os "campos" foram acessados, o decompilador não pode distinguir com segurança a estrutura de uma determinada sequência de operações com o trabalho com memória. Nesse caso, o wasm-decompile usa a opção fallback, usando tipos mais simples como float_ptr(se os tipos forem iguais) ou, no pior caso, gera código que ilustra como trabalhar com uma matriz, como o[2]:int. Esse código nos diz que oaponta para elementos do tipo int, e nos voltamos para o terceiro elemento desse tipo.

Essa última situação surge com muito mais frequência do que você imagina, pois as funções locais do Wasm estão mais focadas no uso de registradores do que em variáveis. Como resultado, no código otimizado, o mesmo ponteiro pode ser usado para trabalhar com objetos completamente não relacionados.

O descompilador busca uma abordagem inteligente para indexação e é capaz de identificar padrões semelhantes (base + (index << 2))[0]:int. A origem desses padrões são as operações de indexação usuais para C, base[index]como onde baseaponta para um tipo de 4 bytes. Isso é muito comum no código, já que o Wasm, nos comandos load e save data, suporta apenas compensações definidas como constantes. No código gerado por wasm-decompile, essas construções são convertidas em tipo base[index]:int.

Além disso, o descompilador sabe quando endereços absolutos apontam para uma seção de dados.

Controle de fluxo do programa


Se falamos sobre construções de controle, a mais famosa delas é a construção Was-if-ifm, que se transforma em if (cond) { A } else { B }, com a adição do fato de que essa construção em Wasm pode retornar um valor, de modo que também pode representar um operador ternário, como cond ? A : Bem alguns línguas.

Outras estruturas de controle Wasm são baseadas em blocos blocke loop, assim como transições br, br_ife br_table. O descompilador tenta ficar o mais próximo possível dessas estruturas. Ele não procura recriar enquanto / para / alternar construções que possam servir de base para elas. O fato é que essa abordagem se mostra melhor ao processar o código otimizado. Por exemplo, um design convencionalloop pode procurar no código retornado por wasm-decompile assim:

loop A {
  //    .
  if (cond) continue A;
}

Aqui Aestá um rótulo que permite criar estruturas aninhadas umas nas outras loop. O fato de existirem comandos ife continueusados ​​para controlar o ciclo pode parecer um tanto estranho para os loops while, mas eles correspondem à construção do Wasm br_if.

Os blocos são desenhados de maneira semelhante, mas aqui as condições estão no início e não no final:

block {
  if (cond) break;
  //    .
}

O resultado da descompilação da construção if-then é mostrado aqui. Nas versões futuras do descompilador, provavelmente, em vez desse código, sempre que possível, será formado um construto se-então mais familiar.

A ferramenta Wasm mais incomum usada para controlar o fluxo de um programa é br_table. Essa ferramenta é um tipo de instrução switch, exceto pelo uso de blocos embutidos. Tudo isso complica a leitura do código. O descompilador simplifica a estrutura de tais estruturas, esforçando-se para tornar sua percepção um pouco mais fácil:

br_table[A, B, C, ..D](a);
label A:
return 0;
label B:
return 1;
label C:
return 2;
label D:

Isso é remanescente do uso switchpara análise aquando a opção padrão é D.

Outras características interessantes


Aqui estão mais alguns recursos do wasm-decompile:

  • , . C++-.
  • , , , . . , .
  • .
  • Wasm-, . , wasm-decompile , , , .
  • , ( , C- ).

 


Descompilar código Wasm é uma tarefa muito mais complicada do que, por exemplo, descompilar código de byte da JVM.

O bytecode não é otimizado, ou seja, reproduz a estrutura do código fonte com bastante precisão. Ao mesmo tempo, apesar de esse código não conter os nomes originais, o bytecode usa referências para classes exclusivas e não para áreas de memória.

Diferentemente do bytecode da JVM, o código que entra nos arquivos .wasm é altamente otimizado pelo LLVM. Como resultado, esse código geralmente perde a maior parte de sua estrutura original. O código de saída é muito diferente do que o programador escreveria. Isso complica muito a tarefa de descompilar o código Wasm com a saída de resultados que podem trazer benefícios reais para os programadores. No entanto, isso não significa que não devemos nos esforçar para resolver esse problema!

Sumário


Se você está interessado no tópico de descompilação de código Wasm, talvez a melhor maneira de entender esse tópico seja pegar e descompilar seu próprio projeto .wasm! Além disso, aqui você pode encontrar orientações mais detalhadas sobre o wasm-decompile. O código do descompilador pode ser encontrado nos arquivos deste repositório, cujos nomes começam decompile(se você desejar, participe do trabalho no descompilador). Aqui você pode encontrar testes mostrando exemplos adicionais de diferenças entre arquivos .wat e resultados de descompilação.

E com quais ferramentas você pesquisa arquivos .wasm?

, , iPhone. , .


All Articles