Dissecando um Módulo Webassembly Mínimo, mas Útil

Vamos escrever uma função em Webassembly usando a linguagem de baixo nível Assembly, usaremos Javascript para invocá-la, e detalhar todos os bytes do arquivo wasm gerado. Durante este processo nós vamos entender algumas instruções do Webassembly, a estrutura do seu módulo e algumas ferramentas. O arquivo wasm que geraremos é um módulo Webassembly mínimo, mas útil, e nós vamos testá-lo inserindo o módulo dentro de um arquivo HTML.

Qual Função?

Uma função que retorne o XOR de dois números. Você pode escolher outra função, como ADD, SUB, etc…, mas XOR talvez beneficie algum leitor que não é familiar com esta função, além domais, eu gosto do XOR, é como uma criança que gosta de ser diferente das outras. ADD, o que você faz? Eu adiciono. SUB, o que você faz? Eu subtraio. XOR, o que você faz? Bem, eu…

ADD, o que você faz? Eu adiciono. SUB, o que você faz? Eu subtraio. XOR, o que você faz? Bem, eu…

Formato Texto do Webassembly

Esta é a nossa função XOR em formato texto do Webassembly, como comentários:


( 

   module                                              ;; It is a module 
   ( 
   func $WasmXOR (param i32) (param i32) (result i32)  ;; WasmXOR function, 
                                                       ;; it takes two i32 parameters and returns an i32 
   get_local 0                                         ;; Gets first parameter and pushes it to the stack 
   get_local 1                                         ;; Gets second parameter and pushes it to the stack 
   i32.xor                                             ;; Pops two values from the stack, execute the XOR 
                                                       ;; and pushes the result to the stack 

                                                       ;; The return value is the value in the stack, 
                                                       ;; which is the result of the XOR operation 
                                                       ;; Note I did not add a return instruction! Comments later 
   ) 
   (export "XOR" (func $WasmXOR))                      ;; Exports the WasmXOR function with the name XOR
) 

Agora nos precisamos compilá-la, que converte este formato texto do Webassembly(WAT) para o formato binário(WASM). WABT: As ferramentas WebAssembly pode ser usada para a compiláção, mas há uma alternativa mais conveniente que é o wat2wasm demo page, copie o texto WAT acima e cole na caixa WAT, o resultante módulo WASM é mostrado na caixa BUILD LOG:

wat2wasm-xor-demo

Mas há algo errado. Os tamanhos das seções estao zerados e tem um comentário engraçado pedindo para ser adivinhado (guess)! Mas o arquivo que se baixa desta página está correto, clique em Download e salve com o nome xor.wasm.

Nossa função XOR gerou um arquivo wasm de 69 bytes. Adiante veremos que há uma seção não necessária, após sua remoção, o módulo wasm fica com 41 bytes.

xor-wasm-binary

Vamos olhar em cada seção em detalhe. O Binary Encoding Specification é o documento oficial que nos permitirá entender as seções deste módule.

Preâmbulo do Módulo

Um módulo Webassembly sempre se inicia assim:

Field Type Description
magic number uint32 Magic number 0x6d736100 (i.e., asm)
version uint32 Version number, 0x1

No xor.wasm:


0000000: 0061 736d    ; Magic number (here is little endian)
0000004: 0100 0000    ; version  (here is little endian)

Seções

Todas as seções têm o mesmo formato:

Field Type Description
id varuint7 section code
payload_len varuint32 size of this section in bytes
name_len varuint32 length of name in bytes, present if id == 0
name bytes ? section name: valid UTF-8 byte sequence, present if id == 0
payload_data bytes content of this section, of length payload_len – sizeof(name) – sizeof(name_len)

Seção Tipo(Type) (0x01)

Declara as assinaturas das funções que são usadas pelo módulo. A ideia é que uma mesma assinatura pode ser usada por várias funções.

Field Type Description
count varuint32 count of type entries to follow
entries func_type repeated type entries as described above

Func Type:

Field Type Description
form varint7 the value for the func type constructor
param_count varuint32 the number of parameters to the function
param_types value_type the parameter types of the function
return_count varuint1 the number of results from the function
return_type value_type? the result type of the function (if return_count is 1)

Value Type:

Value Type
0x7f i32
0x7e i64
0x7d f32
0x7c f64
0x70 anyfunc
0x60 func
0x40 pseudo type for representing an empty block_type

Em xor.wasm, veja os comentários e mapeie os bytes a seguir com a documentação acima:


; section "Type" (1) 
0000008: 01    ; section: 1 
0000009: 07    ; section size: 7 bytes 

; Number of types, we have only 1 function 
000000a: 01    ; num types: 1 
; type 0 
000000b: 60    ; func 
000000c: 02    ; num params: 2 
000000d: 7f    ; i32 
000000e: 7f    ; i32 
000000f: 01    ; num results: 1 
0000010: 7f    ; i32 

Seção de Funções (0x03)

Esta seção declara todas as funções do módulo. Não confunda esta seção com a seção onde as assinaturas são declaradas. As funções são referenciadas por número, não por nome.

Field Type Description
count varuint32 count of signature indices to follow
types varuint32 sequence of indices into the type section

Em xor.wasm:


; section "Function" (3) 
0000011: 03    ; section code: 3 
0000012: 02    ; section size: 2 
0000013: 01    ; num functions: 1 
0000014: 00    ; function 0 uses signature index 0 

Seção de Exportação (0x07)

Lista os objetos exportados. A nossa função XOR é listada aqui. O nome XOR aparece aqui, e somente aqui.

Field Type Description
count varuint32 count of export entries to follow
entries export_entry repeated export entries as described below

Export entry:

Field Type Description
field_len varuint32 length of field_str in bytes
field_str bytes field name: valid UTF-8 byte sequence
kind external_kind the kind of definition being exported
index varuint32 the index into the corresponding index space

External Kind:

value Description
0 Function
1 Table
2 Memory
3 Global

Em xor.wasm:


; section "Export" (7) 
0000015: 07    ; section code: 7 
0000016: 07    ; section size: 7 
0000017: 01    ; num exports: 1 
0000018: 03    ; string length: 3, which is the size of our function name 
0000019: 584f 52 ; export name: XOR 
000001c: 00    ; export kind: function 
000001d: 00    ; export func index: XOR() is function number 0 

Finalmente a Seção de Código, Onde as Instruções estão (0x0A)

Field Type Description
count varuint32 count of function bodies to follow
bodies function_body sequence of Function Bodies

Function Body:

Field Type Description
body_size varuint32 size of function body to follow, in bytes
local_count varuint32 number of local entries
locals local_entry local variables
code byte bytecode of the function
end byte 0x0b, indicating the end of the body

Local Entry:

Field Type Description
count varuint32 number of local variables of the following type
type value_type type of the variables

Em xor.wasm:


; section "Code" (10) 
000001e: 0a    ; section code: 0x0A 
000001f: 09    ; section size: 9 
0000020: 01    ; num functions: 1 
; function body 0 
0000021: 07    ; func body size: 7 
0000022: 00    ; local decl count:0 XOR() does not have any local variable 
0000023: 20    ; get_local 0 
0000024: 00    ; (0) from get_local 0 
0000025: 20    ; get_local 1 
0000026: 01    ; (1) from get_local 1 
0000027: 73    ; i32.xor 
0000028: 0b    ; end of function 

Onde esta a instrução de retorno? Eu não adicionei uma instrução de retorno no formato texto, pensei que não seria necessário já que no formato texto se define uma função, com inicio e fim, pensei que ou o compilador reclamaria ou adicionaria a instrução de retorno, nenhum aconteceu, eu esperava a instrução 0x0f:

Name Opcode Description
return 0x0f return zero or one value from this function

Não é claro para mim porque nao está no código, a função XOR() funcionou sem a adição da instrução de retorno, ela retornou corretamente, talvez isso esteja relacionado com otimização, o interpretador pode decidir que esta função seja usada inline, quando a instrução de retorno não é necessária ou talvez adicione a instrução de retorno.

A não é necessária Seção Customizada (Custom) (0x00)

De acordo com Binary Encoding Documentation:

Custom sections all have the same id (0), and can be named non-uniquely (all bytes composing their names may be identical). Custom sections are intended to be used for debugging information, future evolution, or third party extensions. For MVP, we use a specific custom section (the Name Section) for debugging information. If a WebAssembly implementation interprets the payload of any custom section during module validation or compilation, errors in that payload must not invalidate the module.

Basicamente fala que a seçao ID=0 não é necessária e correntemente está sendo usada para guardar informação para debug. Como queremos chegar a um módulo mínimo, ignoraremos e apagaremos esta seção.

Em xor.wasm:


; section "name"
0000029: 00    ; section code: 0
000002a: 1a    ; section size: 26
000002b: 04    ; string length: 4
000002c: 6e61 6d65 name  ; custom section name: name
... etc
... etc

Como Testaremos Nossa Função? Produzindo 0xdead 0xbeef (Carne Morta)

Vamos usar as constantes conhecidas como 0xdead 0xbeef (Carne Morta em Inglês) para verificar se a função XOR() esta funcionando.

O que segue é o pequeno e inteiro arquivo xor.html a ser usado para testar a função XOR(), e abaixo vamos olhar em todas as suas partes.

Note que o módulo Webassembly esta embutido dentro do arquivo HTML, desta forma não precisamos usar a função Javascript fetch() para baixar o módulo Webassembly, desta forma teremos o prazer de evitar promises do Javascript, desculpa, mas este é um nome terrível para algo em linguagem de Software, nós ja temos bastante problemas tentando manter nossas promessas… A outra vantagem desta abordagem é a de que não precisamos de um servidor HTTP para executar o arquivo xor.html, simplesmente abra este arquivo no browser.


<html>
<head>
</head>
<body>
  <script>
    var xorBinWasm = new Uint8Array([ 0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00,
                                      0x01, 0x07, 0x01, 0x60, 0x02, 0x7F, 0x7F, 0x01,
                                      0x7F, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01,
                                      0x03, 0x58, 0x4F, 0x52, 0x00, 0x00, 0x0A, 0x09,
                                      0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x73,
                                      0x0B]);

    var WebAssemblyInstance = new WebAssembly.Instance(new WebAssembly.Module(xorBinWasm));

    var xorFunc=WebAssemblyInstance.exports.XOR;

    if(
        (xorFunc(0xFF00, 0x21AD) !== 0xdead ) ||
        (xorFunc(0xAA55, 0x14BA) !== 0xbeef )
        )
    {
        alert('Webassembly cant calculate XOR!');
    }
    else
    {
        alert('Webassembly XOR function is correct!');
    }

  </script>
</body>
</html>

Vamos detalhar as partes do arquivo xor.html:


    var xorBinWasm = new Uint8Array([ 0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00,
                                      0x01, 0x07, 0x01, 0x60, 0x02, 0x7F, 0x7F, 0x01,
                                      0x7F, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01,
                                      0x03, 0x58, 0x4F, 0x52, 0x00, 0x00, 0x0A, 0x09,
                                      0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x73,
                                      0x0B]);

Este é o conteúdo do arquivo xor.wasm sem a seção customizada, que não é necessária. Estes são os 41 bytes do nosso módulo.


    var WebAssemblyInstance = new WebAssembly.Instance(new WebAssembly.Module(xorBinWasm));

Webassembly módulo e instance são criados, eles sao objetos necessários para que possamos usar o módulo Webassembly. Note que a instance está sendo criada com o construtor Instance() e não o Instantiate(), este último retorna uma promise.

Qual a diferença?

Usando a promise, a criação da instância(compilação de segundo nível) é feita em segundo plano e permite que o seu código do Javascript continue rodando sem esperar que o trabalho da criação da instância termine, isso é particularmente interessante para módulos Webassembly grandes onde a segunda fase da compilação demore muito para acabar, mas promise traz complexidades e não faz sentido usar uma promise para o nosso módulo minimalista.


    var xorFunc=WebAssemblyInstance.exports.XOR;

Não necessário, esta aqui apenas para clarificar o que está acontecendo, a variável xorFunc é criada que na verdade é a função que vamos usar, você também poderia usar a função chamando WebAssemblyInstance.exports.XOR(,), mas eu acredito que xorFunc(,) seja mais claro.


    if(
        (xorFunc(0xFF00, 0x21AD) !== 0xdead ) ||
        (xorFunc(0xAA55, 0x14BA) !== 0xbeef )
        )
    {
        alert('Webassembly cant calculate XOR!');
    }
    else
    {
        alert('Webassembly XOR function is correct!');
    }

Executa a função XOR() e compara com Carne Morta(0xDEAD 0xBEEF)!

O que é o XOR?

Se este é o seu primeiro encontro com a função XOR, tente descobrir como XOR(0xFF00,0x21AD) resulta em 0xDEAD.Veja a resposta aqui.

Post Mindmap

mindmap-webassembly-minimal-useful-module-webassemblycode.com

Leave a message below. Webassembly is evolving rapidly, please let me know if this post got outdated.

Enjoyed this post?

Don't miss new posts: Share it with your friends:

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *