Ir para o conteúdo
AZ Tools

Inspetor de módulos WebAssembly

Um módulo WebAssembly são oito bytes de preâmbulo — a assinatura \0asm e uma versão — seguidos de uma sequência de seções, cada uma com um byte de identificador, um comprimento em LEB128 e exatamente esses bytes. Esta ferramenta percorre essa sequência dentro do navegador e mostra tudo o que o módulo declara sobre si mesmo: a versão, cada seção na ordem do arquivo com o tamanho em bytes, o deslocamento e a contagem de itens, as importações e exportações com o tipo completo, os limites de memória em páginas e em bytes, a função start quando existe e as seções personalizadas do fim. O detalhamento de tamanho costuma ser o motivo real para abrir um binário. Na maioria das compilações a seção de código é quase todo o arquivo, mas isso não é uma regra: um módulo em Go carrega uma seção de dados que passa de um megabyte, uma compilação do emscripten com arquivos embutidos é sobretudo dados, e uma compilação de depuração esconde o peso em seções personalizadas que não mudam nada do que o módulo faz. As importações são o contrato com o hospedeiro: o emscripten pede env e wasi_snapshot_preview1, o wasm-bindgen pede por caminho o seu arquivo de cola em JavaScript, o Go pede gojs, e uma única entrada não atendida é o LinkError que aparece na instanciação. As exportações são a superfície que você pode chamar, e cada assinatura é lida da seção de tipos em vez de deduzida do nome. Num binário sem símbolos, duas seções personalizadas valem mais do que todo o resto: name, que devolve os nomes do módulo e das funções, e producers, que registra o compilador e as ferramentas que passaram pelo arquivo. As duas são decodificadas aqui quando estão presentes. O que isto não diz é o que o módulo faz. A seção de código é medida, nunca desmontada, e nada é executado, então um recurso usado apenas dentro do corpo de uma função não deixa rastro a menos que também apareça numa assinatura ou num sinalizador de limites.

Como usar

  1. Arraste um arquivo .wasm para a caixa ou clique para escolher. Ele é lido na própria página e nunca sai do seu navegador.
  2. Comece pela tabela de seções: ela dá o tamanho em bytes e a fatia do arquivo de cada seção, que é a resposta para «por que este módulo tem três megabytes».
  3. Leia as importações como o contrato que o hospedeiro precisa cumprir: módulo, nome, espécie e a assinatura exata que cada uma espera.
  4. Leia as exportações como a API que você pode chamar do JavaScript, com os tipos de parâmetros e resultados tirados da seção de tipos do próprio módulo.
  5. Confira o painel de memória para as páginas inicial e máxima (uma página tem 65.536 bytes) e as seções personalizadas, caso ainda tragam name ou producers.

Perguntas frequentes

Por que meu arquivo .wasm é tão grande?
Olhe primeiro a tabela de seções. Numa compilação comum de Rust ou C a seção de código fica com 70-90 % do arquivo, e aí a solução honesta é ter menos código: menos instanciações genéricas, sem formatação de mensagens de pânico, rodar wasm-opt -Oz. Mas a divisão surpreende com frequência. O Go embute os dados estáticos do runtime, então a seção de dados é enorme; uma compilação do emscripten com arquivos embutidos é sobretudo dados; e uma compilação que manteve informação de depuração DWARF a carrega em seções personalizadas como .debug_info, que não são usadas em tempo de execução e são exatamente o que o wasm-strip remove.
O que significa uma memória de «17..∞ páginas»?
A memória do WebAssembly é contada em páginas de 65.536 bytes, então um valor inicial de 17 páginas reserva cerca de 1,1 MB no instante da instanciação. O segundo número é o máximo declarado; quando ele falta, o módulo pode crescer até onde o hospedeiro permitir, que no WebAssembly de 32 bits são 65.536 páginas, ou 4 GiB. Declarar um máximo não é promessa de economia: é um teto para o qual o hospedeiro pode reservar espaço de endereçamento de antemão. A memória compartilhada usada por threads é o único caso em que o máximo é obrigatório.
Por que todas as importações e exportações se chamam a, b, c?
É o emscripten com otimização e Closure ligados. Nomes de importação e exportação são cadeias dentro do binário, então encurtá-los diminui o arquivo, e a cola em JavaScript distribuída junto é minificada do mesmo jeito. Em execução nada se perde, mas o binário deixa de ser legível sozinho. A seção personalizada name, que devolve os índices aos nomes originais, é removida no mesmo passo. Quando sobrevive, esta ferramenta a decodifica; quando não, a seção producers costuma ser a única pista de quem construiu o arquivo.
O módulo importa env e wasi_snapshot_preview1: o que preciso fornecer?
Cada importação é um espaço que o seu objeto hospedeiro tem de preencher, casado por nome de módulo e nome de campo, com um tipo que o motor confere na instanciação. env é o espaço de nomes onde o emscripten junta os retornos de chamada do runtime de C que espera do JavaScript; wasi_snapshot_preview1 quer dizer que o módulo foi compilado para WASI e precisa de uma camada de compatibilidade no navegador; gojs é a ponte do runtime do Go; e um caminho como ./algo_bg.js é o wasm-bindgen esperando o módulo de cola que ele gerou. Se faltar uma entrada ou a aridade não bater, WebAssembly.instantiate lança um LinkError citando o nome.
Isto executa o módulo ou o desmonta?
Nenhum dos dois. Ele interpreta as declarações do módulo: seções, tipos, importações, exportações, limites, o índice da função start e as seções personalizadas. Os corpos das funções são contados e o tamanho total é medido, mas as instruções nunca são decodificadas e nenhum motor é instanciado, de modo que um módulo malicioso não faz nada aqui. Em troca, o comportamento fica invisível: não dá para saber o que uma função calcula, quais importações são de fato chamadas, quanta memória o módulo usa na prática, nem se há instruções SIMD ou atômicas dentro de um corpo.
Se não lê o código, como pode afirmar que há tipos de referência ou multivalor?
Porque algumas propostas posteriores ao MVP mudam o formato da codificação, e não apenas o conjunto de instruções. Um tipo de função com dois ou mais resultados é multivalor; um externref numa assinatura ou uma segunda tabela são tipos de referência; o terceiro bit dos sinalizadores de limites de uma memória é o bit de memória compartilhada que as threads exigem; uma seção data count, ou um segmento passivo em vez de ativo, só existe com bulk memory; e um v128 numa assinatura é SIMD. Só se afirma o que os bytes provam, e por isso a lista pode ser menor do que o que o módulo realmente usa.

Ferramentas relacionadas