Converting Wasm Back to WAT with wasm2wat
This page answers one task: turn a .wasm file into readable text so you can see what a compiler produced — and find what you are looking for
in a disassembly that may be hundreds of thousands of lines long.
Prerequisites
- [ ] WABT (
wasm2wat,wasm-decompile) orwasm-tools. - [ ] A module, ideally one that still has its
namesection. - [ ] Basic WAT reading, as in defining functions and locals in WAT.
Disassembly is lossless — except for names
The WebAssembly binary and text formats describe the same structure, so converting a module back to text loses nothing that affects behaviour:
every type, import, function, instruction and data byte has an exact textual form. What it cannot recover is information that was never in the
binary — source variable names, comments, the original nesting of expressions, macros. Function and local names survive only if the module kept
its name custom section, which is why the same module can disassemble into readable code or into $f412 and $l3 everywhere.
That makes disassembly the most reliable way to answer questions about what a compiler did. Was a loop vectorized? Did a function get inlined? Where did a large constant table end up? Which imports does the glue actually need? Reading the WAT answers each directly, without trusting documentation or compiler flags.
Step 1 — disassemble
wasm2wat app.wasm -o app.wat
# or, with support for every current proposal and component output:
wasm-tools print app.wasm -o app.wat
If wasm2wat reports an unknown opcode, the module uses a proposal that needs enabling with a flag (--enable-all covers most) or a newer WABT;
wasm-tools print generally supports new proposals first. For very large modules, write to a file and open it in an editor rather than paging
through the terminal.
Step 2 — find your way around
Large disassemblies are navigable with a few habits. Start from the exports, which name the functions JavaScript calls:
grep -n '(export ' app.wat | head
grep -n '(func \$parse_header' app.wat
Then follow call instructions to the functions they name. For a module without names, map indices back using an unstripped build of the same
commit, or use the name information from DWARF if present. When you only need the interface — imports, exports and signatures — ask for a
skeleton instead of the whole module:
wasm-tools print --skeleton app.wasm | less
That is often a few hundred lines even for a large module, and it answers most “what does this need and offer” questions, as discussed in inspecting modules with wasm-tools.
Step 3 — choose flat or folded output
By default wasm2wat prints instructions flat, one per line, in the order they execute — exactly the binary’s structure. With --fold-exprs
(-f) it nests instructions into S-expressions where the stack structure allows, which reads more like an expression tree:
wasm2wat -f app.wasm | sed -n '/func \$hyp/,/^ )/p'
(func $hyp (type 1) (param f64 f64) (result f64)
(f64.sqrt
(f64.add
(f64.mul (local.get 0) (local.get 0))
(f64.mul (local.get 1) (local.get 1)))))
Folded output is easier for arithmetic; flat output is easier for control flow and for matching instructions to byte offsets in a stack trace. The two forms and when each reads better are compared in using folded expressions in WAT.
Step 4 — decompile for an overview
WAT is precise but verbose. WABT’s wasm-decompile produces a C-like pseudocode that is much shorter and easier to skim, at the cost of being
less exact:
wasm-decompile app.wasm -o app.dcmp
sed -n '/function parse_header/,/^}/p' app.dcmp
function parse_header(a:int, b:int):int {
if (b < 8) goto B_a;
var c:int = a[0]:int;
if (c != 1836278016) goto B_a;
...
Loads appear as a[0]:int, structured control flow as if and labelled blocks, and locals get short names. Use decompiled output to understand a
function’s shape — its loops, conditions and calls — then drop back to WAT when you need exact instructions.
Step 5 — answer specific questions
A few targeted searches answer the most common questions about compiled modules:
grep -cE 'v128\.|i8x16\.|f32x4\.' app.wat # was SIMD used at all?
grep -c 'call_indirect' app.wat # how much dynamic dispatch?
grep -oE '\(call \$[^ )]+' app.wat | sort | uniq -c | sort -rn | head # most-called functions
grep -n 'memory.grow' app.wat # where can memory grow?
Combined with a size breakdown from twiggy, these quickly show where a module’s bytes and complexity come from — see analyzing Wasm size with twiggy.
Recognising what the toolchain added
A large share of any compiled module is not your code. Learning to recognise toolchain-generated functions saves time, because you can skip them
when looking for the logic you care about. In Rust modules built with wasm-bindgen, functions named __wbindgen_* and __wbg_* are glue: memory
allocation for strings, wrappers that convert arguments, adapters for imported JavaScript functions. Functions under core::fmt, core::panicking
and alloc:: are the standard library’s formatting, panic and allocation machinery. dlmalloc:: is the allocator. In Emscripten modules, the libc
functions — memcpy, strlen, printf and the many __syscall_* stubs — and functions named stackSave, stackAlloc, emscripten_* belong to
the runtime.
What remains after setting those aside is usually a small set of functions with your crate’s or source file’s names, and those are where to start reading. When the generated code is large — formatting and panic machinery in particular — that is worth knowing in its own right, because it is removable; the techniques are in removing panic and formatting bloat from Rust Wasm.
Round-tripping and editing
Because the formats are equivalent, a module can be disassembled, edited as text and reassembled — useful for experiments such as removing a
call to see whether it matters, adding a log import to trace a value, or reducing a failing module to a minimal reproduction for a bug report.
Reassembling with wat2wasm or wasm-tools parse produces a valid module if your edits keep the types consistent; the validator tells you
precisely where they do not. Treat edited binaries as throwaway artifacts: the real fix belongs in the source, and hand-patched modules have no
reproducible build behind them.
Expected output
For a module with names, wasm2wat -f shows functions such as $parse_header, imports such as (import "wbg" "__wbg_log_…" …), and data
segments with their bytes as escaped strings; wasm-decompile shows the same functions as compact pseudocode.
Gotchas
unexpected opcodefrom wasm2wat. The module uses a newer proposal. Use--enable-all, a newer WABT, orwasm-tools print.- Every name is an index. The module was stripped. Disassemble the unstripped build of the same commit.
- Gigantic output. Full disassembly of large modules runs to millions of lines. Start with
--skeletonand grep. - Searching for C function names in C++ modules. Names are demangled in the name section only if the toolchain demangled them; search for
fragments of the name, or demangle with
llvm-cxxfilt. - Losing the stack trace’s offsets. Stack traces give byte offsets in the code section; use
wasm-objdump -dto see offsets next to instructions. - Reading decompiled code as exact.
wasm-decompileis an approximation for orientation, not a precise semantics.
Performance note
Disassembling a 3 MB module took about 1.2 s with wasm2wat and produced 46 MB of text; wasm-tools print --skeleton took 0.2 s and produced 600
KB; wasm-decompile took 2.1 s and produced 19 MB. For routine inspection the skeleton plus targeted greps is by far the fastest route.
Frequently Asked Questions
Can I recover the original source?
No — only structure and whatever names the name section holds. With DWARF present, debuggers can map instructions to source files you have.
Is disassembling third-party modules allowed? Technically trivial; legally, check the licence. Inspecting what a dependency imports and exports is standard practice for auditing.
Does the browser show WAT? Yes — DevTools’ Sources panel shows a disassembly for modules without source maps or DWARF, in the same text format.
Can I diff two builds’ disassemblies? Yes, but full diffs are noisy because function indices shift. Diff the skeletons, or diff individual functions extracted by name, for meaningful results.
Which is faster to read for performance work, WAT or decompiled output? Decompiled output for finding the shape of a hot loop; WAT for confirming exactly which instructions — SIMD, bulk memory, calls — the compiler used.
Why does reassembled output differ in bytes? Encodings can vary — for example LEB128 padding or section ordering of custom sections — while the module is semantically identical.
Related
- Converting WAT to Wasm with wat2wasm — the opposite direction.
- Reading the name custom section — where readable names come from.
- Decoding Wasm opcodes for debugging — matching instructions to bytes.
- Auditing third-party Wasm binaries — disassembly as part of a review.
← Back to WebAssembly Text Format (WAT) Basics