A global is a named value that lives for the module's whole life. It is the closest thing WebAssembly has to a static variable, and the reason the format has one at all is a specific and rather elegant constraint: a module may not modify anything it did not create. No global, no write to a read-only import; a global it declared, and then only if it declared that global mutable.
That constraint is what makes a WebAssembly module safe to run next to code it does not trust. There is no writable memory that two modules share unless both agreed to share it, and a module cannot scribble on an imported memory or a stack pointer belonging to its host. The global section is where that rule is expressed, and the mutability bit is the whole of it.
The second thing this section teaches is a shape you have not met anywhere else in a binary format: an initialiser that is an expression, not a value. Every other section stores data. This one stores a tiny program — an opcode stream, terminated by its own end marker — because a global's initial value may depend on another global. And that is the reason a value-looking field has to be walked rather than read, which is a trap worth studying.
Two bytes of type information, then an expression that produces the value:
The mut byte is a boolean written as a whole byte, not a bit inside a flags word. It has exactly two legal values, and they are 0 and 1 — this is the one place in the core format where a flag really is an enumeration, which is why the previous concept's warning about limits flags does not apply here. The distinction is worth holding precisely: limits flags have independent bits that happen to combine into small numbers, while this is a single yes/no and a reader that accepted 0x02 would be accepting an illegal module.
An expression in the WebAssembly sense is a sequence of instructions operating on a stack. A constant expression is the tiny subset that can be evaluated at load time rather than at run time, and it is what a global's initialiser must be:
41 xx 0b i32.const xx ; end 42 xx 0b i64.const xx ; end 43 xxxx 0b f32.const (4 bytes, little-endian) 44 xxxxxxxx 0b f64.const (8 bytes, little-endian) 23 xx 0b global.get xx ; end d0 xx 0b ref.null <heaptype> ; end d2 xx 0b ref.func <funcidx> ; end
Every one ends with 0x0b, the same byte that ends a function body. That is not a coincidence and not a shared implementation convenience: the specification defines one end opcode used by both, and the body framing in the code section is what distinguishes the two uses. A global's initialiser is a function body with no locals, no parameters, and a restricted opcode set.
And that restricted set is why the initialiser can reference another global: global.get is in the constant subset, so
works. What is not in the subset is arithmetic. There is no i32.add in a constant expression, so a global cannot be defined as "twice another global" — the compiler has to emit a global.get 0 plus a global.get 0 and add them in the code that uses it. The subset is chosen so that a reader can evaluate it with a stack of a known small size and no possibility of looping, branching, or trapping.
A module with eight globals covering the full signed range of both integer types. Eleven bytes of section, and every byte is visible:
0045: 08 EIGHT globals
| |
| +-- the sixth: 7e 01 42 80 80 80 80 7f 0b
| | | |
| | | +-- i64.const, the value, then end
| | +---- 0x01: MUTABLE
| +---------- 0x7e: i64
+--------------------- 0x45: payload starts here
Read the last entry in full, because it is the one that shows why the initialiser has to be walked:
7e valtype: i64 01 mutable 42 opcode: i64.const 80 80 80 80 7f the operand: a 5-byte SIGNED LEB128 0b end
That operand is -1. And a decoder that assumes a constant expression is opcode, one byte, end reads 0x80 as the value, treats 0x80 as an opcode, and is already lost. The rule is simple and the discipline is what matters: an opcode stream is read until its terminator, never to a guessed length. This is the same trap as the data section's offset expressions, the same trap as a DIE chain that must be walked rather than sized, and the reason this course keeps finding it in a new place is that a length-prefixed container and a self-delimiting one look identical until one of them is variable-width.
Here is the same eight globals read by wabt's detail view, with the values my decoder recovers from the same bytes:
wabt -x my decoder global[0] init i32=0 i32.const 0 end global[1] init i32=42 i32.const 42 end global[2] init i32=-1 i32.const -1 end global[3] init i32=-1000000 i32.const -1000000 end global[4] init i32=2147483647 i32.const 2147483647 end global[5] init i32=-2147483648 i32.const -2147483648 end global[6] init i64=-1 i64.const -1 end global[7] init i64=9223372036854775807 i64.const 9223372036854775807 end
All eight agree. And the interesting part is that wabt gets this right in one subcommand and wrong-looking in another. The same three bytes 41 7f 0b mean i32.const -1. Ask for details:
$ wasm-objdump -x globs.wasm | grep 'global\[2\]' - global[2] i32 mutable=0 - init i32=-1 <- signed, correct $ wasm-objdump -d neg.wasm | grep '41 7f' 000049: 41 7f | i32.const 4294967295 <- UNSIGNED
One tool, two subcommands, two presentations of the same two bytes. The detail view is interpreting the operand as the value of an initialiser and printing it signed; the disassembler is printing an instruction operand and treating it as the 32-bit bit pattern. Both are internally consistent, and one of them will mislead anyone who copies a number out of it.
i32 initialiser is a signed integer. The wrong one to trust is the disassembly, because it is the one people read when they want to know what a number is. If you copy 4294967295 out of a disassembly into a program and compare it to a source constant -1, the comparison fails and the number of plausible explanations is large. When a tool prints a signed value in two different ways, the difference is not a bug in either tool; it is two different questions being answered, and the reader has to know which question was asked.The mutability bit is the module's own declaration about whether it will write to a global, and it is enforced by the validator rather than by convention. Three consequences follow, and the third is the one that makes the rule worth having:
global.get 0 on an immutable global can be replaced by a literal. A mutable one cannot, because a global.set might have changed it. This is why the mutability bit affects optimisation, not just permission.That third point is the security argument for the whole feature, and it is worth seeing it against a native platform. In a PE image a global variable is an address, and the linker will happily point one at a function, another at a data object, and a third at an import thunk — three "globals" with three unrelated meanings distinguished only by convention. WebAssembly makes all three of those explicit and checked, and pays for it with a global that can only be a number.
Evaluating an initialiser, which is the operation a reader does once per global at load time, and the reason it cannot simply be "read the bytes and interpret them as a value":
Two details in that are the whole lesson. read_sleb32, not read_uleb32 — the operand is a signed LEB128, so 0x7f is -1, and a reader that reads it unsigned gets 127 for every -1 in the file. And global.get reads the value of an earlier global, which is legal only because the constant subset contains no arithmetic and no control flow, so the dependency graph over globals is acyclic by construction: a global can only read globals numbered below it, and reading a later one is a validation error.
That acyclicity is a design choice, and it is load-bearing. A constant expression with i32.add in it could still be acyclic by the same argument, so the restriction is not strictly necessary — but restricting to leaf values makes the evaluation obviously terminating and the dependency obviously a DAG, and it is why a reader can evaluate globals in declaration order without a fixpoint loop. Choose the subset that makes the naive algorithm correct.
Now the failure that the naive algorithm invites, which is the most valuable thing in this concept because it is entirely silent. Suppose the reader treats an initialiser as a fixed four bytes — a plausible guess for "a value" before you think about the encoding:
correct: 7f 01 42 80 80 80 80 7f 0b 9 bytes, 5-byte operand
naive: 7f 01 42 80 80 80 80 6 bytes read
-> reports i64 = 0x80808080 = 2155905152
-> the next global starts at 0x0b, which is 0x7f
-> reads 0x7f as a valtype (i32), 0x0b as mutability (11!),
then runs off into the instruction bytes
and the worst part: the wrong value 2155905152 is a perfectly
plausible i64. Nothing complains. The global is just wrong.
So the desynchronisation is caught eventually, but the first thing the reader gets wrong is a number that looks entirely reasonable, and if the module had no second global the desync would never happen at all — the reader would report one wrong value and stop. A decoder that cannot detect its own desync will report a confident wrong answer rather than an error, and the only defence is to decode each structure by the rules that structure has.
$ wasm-objdump -x globs.wasm | sed -n '/^Global/,$p'
$ python3 courses/wasm/assets/samples/wasm_decode.py globs.wasmglobs.wasm. It will report a plausible wrong value for the 5-byte-LEB entries and a correct one for the 1-byte entries, and the pattern of which ones are wrong is itself the clue: the failures are exactly the entries whose operand is not one byte long. That is what a variable-width encoding looks like from the outside.i32.const with values 63, 64, 8191, 8192, and then the extremes, and record the operand length at each. 63 and 8191 are one byte; 64 and 8192 are two. The thresholds are the proof that the width is a function of the value and not a fixed field width, and finding them yourself is much stronger than being told.$a = 10, $b = global.get $a, $c = global.get $b and confirm all three initialise. Then try $a = global.get $b where $b is defined later, and see what the validator says. The error message names the cycle, which is more informative than it needs to be and worth reading.global.set 0 to a function that targets an immutable global, and read the validation error. Then make it mutable and confirm it validates. The mutability bit is checked, not advisory, and seeing the rejection is how you know the rule is real rather than documentation.wasm-objdump -x and wasm-objdump -d over neg.wasm and write down, side by side, how each prints 41 7f. Then state in one sentence which one you would trust for a value and which for a bit pattern. Getting that sentence right is the practical outcome of this concept.static variable and a constant, and read the resulting global section. An int that is never written becomes an immutable global; one that is written becomes mutable; and one the compiler can fold into its uses often does not appear at all. That last case is the constant-folding consequence made visible in a real toolchain output.Without looking back: a global's bytes are 7e 01 42 80 80 80 80 7f 0b. What is its type, its mutability, and its value? And what would a reader that read the initialiser as 0x42 plus a single unsigned byte report?
You are writing a loader. A module's globals come out as: the first correct, the second correct, the third reporting i64 = 2155905152 when the source says -1, and the fourth reporting a mutability of 11. Everything after the third is garbage, and the module validated before you ran it. The third global's bytes are 7f 01 42 80 80 80 80 7f 0b. What is the defect, why did validation not catch it, and what is the general rule for reading any field in this format that could be variable-width?
The general habit: after decoding each structure, assert that your position matches what the enclosing section's framing says it should. That turns a silent desync into a clean error at the section boundary, and a desync at the section boundary is a bug you can find in one print statement.
The mutability bit is the WebAssembly answer to a question every executable format has to answer differently, and the three answers are worth comparing. An ELF section's SHF_WRITE flag is a property of a whole region, fixed at link time. A COFF section's characteristics work the same way. A PE section's VirtualProtect is mutable by default and only becomes read-only if something asks. WebAssembly's answer is the most restrictive of the four: the unit of mutability is a single value, it is declared, and the default is read-only.
That default matters more than it looks. A format where immutable is the default and mutable is something you have to ask for means a mistake is always in the safe direction, which is a property worth designing for. It is the same instinct behind the ELF header's read-only program headers preceding the writable ones, and behind a linker putting .text before .data.
The constant-expression subset connects to PE base relocations, which is the nearest thing in this collection to a limited computation at load time. A base relocation record says "add the image base to this address", and the set of them is fixed by the linker. WebAssembly's initialisers are the same idea generalised: a tiny declarative language whose only job is to produce a value before execution starts. The difference is that WebAssembly's is genuinely an expression with a stack, so it can reference other globals, where a relocation can only add a constant.
And the walked-to-terminator discipline has now appeared in the element section, the data section, the global section, and every function body. That is four places in one small format where the same rule applies, which is the strongest argument in this course for treating "how do I know where this ends" as the first question to ask about any field. DWARF's DIE chains and the PE resource directory tree are the same shape in completely different formats, and a reader who has internalised it there will not have to relearn it here.
Next: the element and data sections, where the eight element encodings come from, and then the object format the final module covers.