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

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.

The same function disassembled with and without names With the name section, wasm2wat shows function, parameter and some local names from the source. Without it, every function and local is shown by index, which is valid but much harder to follow. name section kept (func $parser::next_token …) call $alloc::vec::Vec<T>::push local names where the compiler kept them readable name section stripped (func (;412;) …) call 87 local.get 3, local.get 7 … valid, hard to navigate

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.

A workflow for reading an unfamiliar module Print the skeleton to see the interface, pick an export, disassemble with names and folding, follow calls to the functions that matter, and when the WAT gets long, decompile the function to C-like code for an overview. skeleton imports, exports, types pick an export the entry point you care about wasm2wat -f folded, with names follow calls grep the callees wasm-decompile C-like overview

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 opcode from wasm2wat. The module uses a newer proposal. Use --enable-all, a newer WABT, or wasm-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 --skeleton and 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 -d to see offsets next to instructions.
  • Reading decompiled code as exact. wasm-decompile is 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.

Output size of three ways to view a 3 MB module Size of the text produced by a full flat disassembly, a decompiled C-like view, and a skeleton view without function bodies, for the same 3 MB module. MB of text produced wasm2wat (full) 46 MB wasm-decompile 19 MB wasm-tools print --skeleton 0.6 MB

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.

← Back to WebAssembly Text Format (WAT) Basics