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:
- 5 is
05, one byte; - 127 is
7f, one byte; - 128 is
80 01: the low seven bits (0) with the continuation bit set, then the next seven bits (1); - 624,485 is
e5 8e 26.
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:
- Type section (id 1): one type,
() -> i32. - Function section (id 3): one function, of type 0.
- Export section (id 7): one export,
"main", function 0. - Code section (id 10): one body. A body is its size, then a vector of
local declarations (here empty), then the instructions, ending with
0b, theendopcode.i32.const 42is opcode41followed by 42 in signed LEB128,2a.
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.