Chapter 12

Bytes on the Wire

This chapter covers the parts of the binary format that are not instructions: how integers and strings are encoded, how a module is cut into sections, and how types, imports and exports are written. By the end of it you will have written a module byte by byte, and it will run.

#The preamble12.1

Every module starts with the same eight bytes:

00 61 73 6d   ;; magic: "\0asm"
01 00 00 00   ;; version 1

That is a complete module. The version has been 1 since the MVP: new features add new sections, instructions and encodings, but never change what an existing byte sequence means, so there has never been a need for version 2. (The component model, a separate layer on top of core WebAssembly, uses a different version number to tell its binaries apart.)

Anything else at the start is rejected:

00 61 73 6d   ;; magic
02 00 00 00   ;; version 2
CompileError: WebAssembly.Module(): expected version 01 00 00 00, found 02 00 00 00 @+4

#LEB12812.2

Most integers in the format are written in LEB128, a variable-length encoding that uses as few bytes as the value needs. Each byte holds seven bits of the value, least significant first, and its top bit says whether more bytes follow:

The signed form, used for constants that can be negative, is the same, except that the last byte's seventh bit is a sign bit, sign-extended to the full width when decoding. So −1 is 7f, and 63 is 3f but 64 is c0 00, because a single byte 40 would mean −64.

Here are both, in Python:

def uleb(n):
    out = bytearray()
    while True:
        byte = n & 0x7f
        n >>= 7
        if n:
            out.append(byte | 0x80)
        else:
            out.append(byte)
            return bytes(out)

def sleb(n):
    out = bytearray()
    while True:
        byte = n & 0x7f
        n >>= 7                      # an arithmetic shift: n keeps its sign
        done = (n == 0 and not byte & 0x40) or (n == -1 and byte & 0x40)
        out.append(byte if done else byte | 0x80)
        if done:
            return bytes(out)
for n in (5, 127, 128, 624485):
    print(f"uleb({n}) = {uleb(n).hex(' ')}")
for n in (-1, 63, 64, -64, -65, -123456):
    print(f"sleb({n}) = {sleb(n).hex(' ')}")
uleb(5) = 05
uleb(127) = 7f
uleb(128) = 80 01
uleb(624485) = e5 8e 26
sleb(-1) = 7f
sleb(63) = 3f
sleb(64) = c0 00
sleb(-64) = 40
sleb(-65) = bf 7f
sleb(-123456) = c0 bb 78

A u32 in LEB128 takes at most five bytes, and an i64 at most ten. The specification also allows padded encodings, with redundant continuation bytes, up to that maximum: 5 can be written 85 80 80 80 00. Decoders must accept these. Producers use them when they need to write a number before they know it, reserving five bytes and filling them in later without moving anything. LLVM's object files rely on this for relocations.

The format uses LEB128 for counts, sizes, indices and integer constants. Floating-point constants are not LEB128; they are written as raw IEEE 754 bytes, little-endian, four or eight of them.

#Vectors and names12.3

A vector is a count followed by that many items. Almost every list in the format, from the functions in a section to the parameters of a type, is a vector.

A name is a vector of bytes holding UTF-8. So the export name main is 04 6d 61 69 6e: four bytes, then the letters.

#Sections12.4

After the preamble, a module is a sequence of sections. Each one is:

section id    1 byte
size          u32, LEB128: the number of bytes that follow
contents      that many bytes

The size lets a decoder skip any section it does not care about. A browser downloading a module can parse the sections it needs to start compiling and skip over the rest, and a tool can ignore sections it does not understand. Chapter 8 listed the sections and the fixed order they must appear in. Every known section may appear at most once, and an empty one can be left out.

#Types12.5

The type section is a vector of function types. A function type is the byte 60, then a vector of parameter types, then a vector of result types. Each value type is a single byte:

Byte Type
7f i32
7e i64
7d f32
7c f64
7b v128
70 funcref
6f externref
69 exnref

The garbage collection feature adds more reference types, some of which take more than one byte; Chapter 22 shows them. The bytes count down from 7f because they were chosen to be negative numbers in signed LEB128 (7f is −1, 7e is −2), leaving the non-negative numbers free to be type indices in places where either can appear.

So the type (func (param i32 i32) (result i32)) is:

60          func
02 7f 7f    two parameters, both i32
01 7f       one result, an i32

#Imports, functions and exports12.6

An import is a module name, a field name, and a description, which starts with a kind byte:

Kind Import description
00 a function: its type index
01 a table: its element type and limits
02 a memory: its limits
03 a global: its value type and a mutability byte, 00 or 01
04 a tag: 00, then a type index

Limits, which size memories and tables, are a flag byte followed by LEB128 numbers: 00 n for a minimum of n with no maximum, 01 n m for a minimum and a maximum. Other flag bits mark a memory as shared (Chapter 23) or 64-bit.

The function section is a vector of type indices, one per function the module defines. The bodies come later, in the code section, in the same order.

An export is a name, a kind byte from the same table, and an index.

#A module by hand12.7

That is enough to write a module that exports a function returning 42. Section by section:

Every section's size has to be counted by hand, which is the tedious part:

00 61 73 6d  01 00 00 00     ;; preamble

01                           ;; type section
05                           ;;   5 bytes follow
01                           ;;   one type
60 00 01 7f                  ;;   func, no params, one result: i32

03                           ;; function section
02                           ;;   2 bytes follow
01 00                        ;;   one function, of type 0

07                           ;; export section
08                           ;;   8 bytes follow
01                           ;;   one export
04 6d 61 69 6e               ;;   name: "main"
00 00                        ;;   a function, index 0

0a                           ;; code section
06                           ;;   6 bytes follow
01                           ;;   one body
04                           ;;   the body is 4 bytes
00                           ;;   no locals
41 2a                        ;;   i32.const 42
0b                           ;;   end
42

Thirty-seven bytes, the same as wat2wasm produced from the text in Chapter 2.

Getting a size wrong is the classic mistake of a hand-written or newly written encoder. Here is the same module with the code section's size one too small:

00 61 73 6d  01 00 00 00
01 05 01 60 00 01 7f
03 02 01 00
07 08 01 04 6d 61 69 6e 00 00
0a 05 01 04 00 41 2a 0b      ;; the size should be 06
CompileError: WebAssembly.Module(): expected 4 bytes, fell off end @+33

V8 read the body's own size, 4, and found the section, and so the module, ended after only three of those bytes. Wasmtime says unexpected end-of-file (at offset 0x20) about the same module. Neither message names the real mistake, which is typical of size errors: the decoder notices only when the bytes stop making sense, some way after the wrong number.

And here it is with the sections out of order:

00 61 73 6d  01 00 00 00
01 05 01 60 00 01 7f
03 02 01 00
0a 06 01 04 00 41 2a 0b
07 08 01 04 6d 61 69 6e 00 00  ;; the export section must come before code
CompileError: WebAssembly.Module(): unexpected section <Export> @+29

The @+29 is the offset of the offending section's id byte. Wasmtime reports the same module as section out of order (at offset 0x1d).

#Letting a tool count for you12.8

wat2wasm -v prints the binary it writes, annotated byte by byte, on stderr. When an encoder you are writing produces something you do not understand, writing the same module in the text format and comparing your bytes against this dump is the quickest way to find the difference:

(module
  (func (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))
wat2wasm -v add.wat -o add.wasm 2>&1
0000000: 0061 736d                                 ; WASM_BINARY_MAGIC
0000004: 0100 0000                                 ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                        ; section code
0000009: 00                                        ; section size (guess)
000000a: 01                                        ; num types
; func type 0
000000b: 60                                        ; func
000000c: 02                                        ; num params
000000d: 7f                                        ; i32
000000e: 7f                                        ; i32
000000f: 01                                        ; num results
0000010: 7f                                        ; i32
0000009: 07                                        ; FIXUP section size
; section "Function" (3)
0000011: 03                                        ; section code
0000012: 00                                        ; section size (guess)
0000013: 01                                        ; num functions
0000014: 00                                        ; function 0 signature index
0000012: 02                                        ; FIXUP section size
; section "Export" (7)
0000015: 07                                        ; section code
0000016: 00                                        ; section size (guess)
0000017: 01                                        ; num exports
0000018: 03                                        ; string length
0000019: 6164 64                                  add  ; export name
000001c: 00                                        ; export kind
000001d: 00                                        ; export func index
0000016: 07                                        ; FIXUP section size
; section "Code" (10)
000001e: 0a                                        ; section code
000001f: 00                                        ; section size (guess)
0000020: 01                                        ; num functions
; function body 0
0000021: 00                                        ; func body size (guess)
0000022: 00                                        ; local decl count
0000023: 20                                        ; local.get
0000024: 00                                        ; local index
0000025: 20                                        ; local.get
0000026: 01                                        ; local index
0000027: 6a                                        ; i32.add
0000028: 0b                                        ; end
0000021: 07                                        ; FIXUP func body size
000001f: 09                                        ; FIXUP section size

The FIXUP lines show how wat2wasm handles sizes: it writes a placeholder, writes the contents, then goes back and fills in the size. The placeholder is one byte here because every size turned out to fit in one; when one does not, wabt moves the contents along to make room. The next chapter shows a simpler approach.

wasm-objdump -s prints the raw contents of each section, and -x the decoded details. Between them and -v, you can see a module at every level.

#What you have seen12.9

A module is the preamble followed by sections, each an id, a size and contents. Integers are LEB128, signed where they can be negative, and floats are raw IEEE bytes. Lists are vectors, names are UTF-8 vectors, value types are single bytes counting down from 7f. Types, imports, functions and exports are each a few bytes, and a complete module that returns 42 is thirty-seven of them.

The next chapter encodes the code section's contents: instructions.