Laying Out a Wasm Module’s Memory Map

This page answers one task: you need to know what occupies which addresses in a WebAssembly module’s linear memory — to debug corruption, size the stack, reserve space for something of your own, or interpret a memory dump — and you want to read the layout from the module rather than guess.

Prerequisites

  • [ ] A module compiled from C, C++ or Rust with LLVM and linked by wasm-ld (Emscripten, wasi-sdk, or Rust’s Wasm targets).
  • [ ] wasm-objdump or wasm-tools for inspecting globals and data segments.
  • [ ] Access to the module’s exports from JavaScript.

The regions of linear memory

Linear memory is one contiguous byte array starting at address 0. Toolchains based on LLVM and wasm-ld divide it into regions. Static data — initialised globals, string literals, constant tables, zero-initialised statics — is placed by data segments starting at a low address (typically 1024, leaving the first kilobyte unused so null-pointer dereferences hit unused memory). The shadow stack holds stack frames for values whose address is taken and for large locals; it is separate from WebAssembly’s own value stack, which the engine manages invisibly. The heap begins after these and extends to the end of memory, growing with memory.grow.

wasm-ld defines symbols that mark the boundaries: __data_end (end of static data), __heap_base (start of the heap), and a mutable global __stack_pointer (current top of the shadow stack). By default the layout is data, then stack, then heap, and the stack grows downwards from __heap_base towards the data. With --stack-first, the stack comes first, growing down towards address 0, followed by data and heap.

Default wasm-ld layout of linear memory From address zero, a reserved first kilobyte, then static data up to __data_end, then the shadow stack, which starts at __heap_base and grows downward toward the data, then the heap from __heap_base to the end of memory, growing upward as memory grows. linear memory (default layout, 1 MiB stack) res. static data stack ↓ heap ↑ 0 1024 __data_end __heap_base end

Step 1 — read the boundaries from the module

wasm-objdump -x lists globals and data segments. __stack_pointer appears as a mutable global with its initial value — the stack’s starting top, equal to __heap_base in the default layout. Data segment offsets and sizes show where static data lies:

wasm-objdump -x -j Global app.wasm
#  - global[0] i32 mutable=1 <__stack_pointer> - init i32=1114112
wasm-objdump -x -j Data app.wasm
#  - segment[0] memory=0 size=18432 - init i32=1048576

Here static data starts at 1048576 (1 MiB), which indicates --stack-first with a 1 MiB stack, as Rust’s wasm32-unknown-unknown uses by default. Export __heap_base and __data_end explicitly if you want them at runtime (-Wl,--export=__heap_base,--export=__data_end); Emscripten and wasi-libc use them internally.

Step 2 — choose the layout and stack size

Stack size is fixed at link time. Too small, and deep recursion or large stack arrays overflow; too large, and memory is wasted on every instance:

# C/C++ with wasm-ld via clang
clang --target=wasm32-wasi -O2 main.c -Wl,-z,stack-size=262144 -Wl,--stack-first -o app.wasm
# Emscripten
emcc main.c -sSTACK_SIZE=256kb -o app.js
# Rust
RUSTFLAGS="-C link-arg=-zstack-size=262144" cargo build --release --target wasm32-unknown-unknown

Prefer --stack-first. In the default layout, an overflowing stack grows down into static data and silently corrupts globals and string constants. With the stack first, overflow runs below address 0 — which in practice means the stack pointer wraps to a huge address and the next access traps — so overflows fail loudly instead of corrupting data.

Default layout versus --stack-first In the default layout the stack sits above static data and grows down into it on overflow, silently corrupting globals. With stack-first the stack sits at the bottom of memory, so overflow wraps below address zero and the next access traps immediately. default (data, stack, heap) stack grows into data overflow corrupts globals bugs appear far away silent corruption --stack-first stack at bottom of memory overflow traps fails at the cause recommended

Step 3 — inspect the map at runtime

From JavaScript, read the stack pointer and heap base to see how memory is used. Emscripten exposes stackSave() and the heap base; for other builds, export the globals:

const { memory, __heap_base, __data_end, __stack_pointer } = instance.exports;
console.table({
  memoryBytes: memory.buffer.byteLength,
  dataEnd: __data_end.value,
  heapBase: __heap_base.value,
  stackPointer: __stack_pointer?.value,        // only if exported (mutable globals can be exported)
});

Exported globals appear as WebAssembly.Global objects whose .value gives the address. The heap’s used portion depends on the allocator; many expose statistics (mallinfo in C, allocator-specific functions in Rust).

Step 4 — reserve a region of your own

Sometimes you need a fixed region — a shared scratch buffer with JavaScript, a ring buffer for audio, a region for memory-mapped data. Do not pick a hard-coded address; allocate it statically so the linker places it:

#[repr(C, align(16))]
struct Scratch([u8; 65536]);
static mut SCRATCH: Scratch = Scratch([0; 65536]);

#[no_mangle]
pub extern "C" fn scratch_ptr() -> *mut u8 { unsafe { core::ptr::addr_of_mut!(SCRATCH) as *mut u8 } }

The linker places it in static data (or .bss for zero-initialised), and JavaScript asks for its address. Static allocation keeps it out of the heap, so it never moves and is never freed.

Step 5 — catch stack overflow early

Beyond --stack-first, toolchains can check the stack explicitly. Emscripten’s -sSTACK_OVERFLOW_CHECK=1 writes a cookie at the stack’s end and checks it at exit points; =2 instruments every stack pointer update with a bounds check, slower but precise, for debug builds. Clang’s -fstack-protector guards individual frames against buffer overruns. Use the checks in debug and test builds; release builds rely on --stack-first making overflow trap.

Reading memory dumps

When debugging corruption, a dump of linear memory is easier to read with the map in hand. Addresses below __data_end are static data — compare them with the original data segments to find globals that changed unexpectedly. Addresses between the stack pointer and the stack base are live stack frames. Addresses above __heap_base belong to the allocator; allocator headers before each block show sizes and free flags. A small script that labels regions in a hex dump using these symbols turns a wall of bytes into something navigable.

Multiple memories and imported memory

With the multi-memory proposal, a module can have more than one memory, each with its own layout; LLVM-based toolchains still place everything in memory 0 by default. When memory is imported — for example when several modules share one memory, or JavaScript provides it — each module’s data and stack must not overlap; the linker cannot coordinate across modules, so the embedder must assign each module a separate base, as covered in sharing one memory between two modules.

Measuring how much stack you actually need

Choosing a stack size by guesswork leads to either waste or overflow. Measure instead: in a test build, fill the stack region with a known pattern at startup (Emscripten’s stack cookie mechanism does something similar), run the most demanding realistic workloads — deepest recursion, largest local buffers, longest call chains through callbacks — and afterwards scan from the stack’s far end to find how much of the pattern was overwritten. That high-water mark, plus a safety margin of 50–100%, is a defensible stack size. Repeat the measurement when recursion depth depends on input, such as a parser for nested documents, and either cap the nesting depth in the code or size the stack for the maximum you accept. Remember that worker threads in threaded builds each get their own stack of the configured size, so oversizing multiplies.

Static data that is larger than expected

The static data region sometimes grows surprisingly: large constant tables, embedded fonts, Unicode data from text libraries, or zero-initialised arrays declared as globals. Zero-initialised statics do not take space in the module file — they are not in data segments — but they still occupy linear memory from instantiation, raising the initial memory of every instance. wasm-ld --print-map or the map file lists each symbol with its address and size, so the largest static objects are easy to find; moving rarely used tables to lazily allocated heap memory or separate files reduces the baseline memory cost.

Expected output

wasm-objdump shows a 256 KiB stack placed first, static data at 262144–280576 and the heap from 280576; JavaScript logs the same boundaries from exported globals; a deliberately deep recursion traps instead of corrupting a global string; and a 64 KiB scratch buffer is placed by the linker and exposed via scratch_ptr().

Gotchas

  • Default layout with deep recursion. Overflow silently corrupts static data. Use --stack-first.
  • Hard-coded addresses. The linker may place data there. Allocate statically and ask for the address.
  • Assuming the Wasm value stack is the shadow stack. Only address-taken values and large locals live in linear memory.
  • Oversized stacks. Every instance reserves them. Size from measurement.
  • Separate modules sharing memory without separate bases. Data and stacks overlap.

Performance note

Reducing the stack from the 5 MiB default of an older Emscripten configuration to a measured 512 KiB cut initial memory per instance by 4.5 MiB, which mattered for an app that ran eight worker instances.

Initial memory per instance by stack size Mebibytes of initial linear memory per instance of a module with a 5 MiB stack and with a measured 512 KiB stack, for an application running eight instances. MiB initial memory per instance 5 MiB stack 6.2 MiB 512 KiB stack 1.7 MiB

Frequently Asked Questions

Why does data start at 1024 and not 0? wasm-ld leaves the first kilobyte unused by default so null dereferences hit unused memory; --global-base changes it.

Can the stack grow at runtime? No — its size is fixed at link time.

Does Rust use --stack-first? Rust’s wasm32-unknown-unknown target places the stack first with a 1 MiB default.

Where do thread stacks go in threaded builds? Each thread’s stack is allocated on the heap by the runtime when the thread starts.

How do I find the right stack size? Fill the stack with a pattern in a test build, run the most demanding workloads, and measure the high-water mark; add a margin.

← Back to Linear Memory Management & Allocators