Skip to content

Series Overview

A decompiler is a pipeline. Bytes go in one end, each stage recovers a little more structure than the last had, and C-like source comes out the other. The chapters follow that order, because every stage depends on the guarantees the previous one established.

bytes → decode → lift to p-code → disassemble → IR containers
      → simplify → dataflow → SSA → stack & memory → types → variables
      → structure → emit C

Read them in order the first time. Each chapter assumes the representation the one before it produced.

Published

Post 0 — What is decompilation? The pipeline and the contract

The whole pipeline end to end, at low resolution: what each stage promises the next one, and what is irrecoverably gone once a compiler has run. Sets up the running example and gets tiny-dec installed so you can decompile a binary before you understand how.

Start here even if you have used a decompiler for years — it is the chapter that defines the terms the rest of the series leans on.

Post 1 — Decoding RV32I instructions from binary

Turning a 32-bit word into a structured instruction: opcode, source and destination registers, immediates scattered across non-contiguous bit fields. Why RV32I's fixed-width encoding makes this a table lookup, and what makes the same problem miserable on x86.

You build the decoder that every later stage reads from.

Post 2 — Lifting RV32I to p-code

Decoded instructions still carry architecture in them. Lifting normalises them into p-code — Ghidra's IR — where an add is an add whether it came from RISC-V, x86, or ARM. Address spaces, varnodes, and the small set of operations everything else is expressed in.

This is the last chapter where the RISC-V-ness matters; from here the analyses are architecture-independent.

In progress

These are drafted and being revised. They will appear here as they are finished.

Chapter What it covers
Post 3 — Recursive disassembly and basic blocks Finding the code in the first place. Why a linear sweep produces garbage, how following control flow instead recovers real instruction boundaries, and how basic blocks fall out of it.
Post 4 — Durable IR containers The data structures the analyses need: a representation that survives being rewritten repeatedly without losing the mapping back to the original addresses.

Planned

The remaining stages, grouped by what they recover. Order and chapter boundaries may shift as they are written.

  • Simplification and dataflow — constant folding, dead code, and the arithmetic the compiler emitted that no human wrote.
  • SSA form — giving every value a single definition, which is what makes the analyses that follow tractable rather than heuristic.
  • Stack and memory — deciding which stack offsets are variables, which are spill slots, and which are one array being indexed.
  • Type recovery — inferring int32_t versus a pointer versus a struct field from nothing but how a value gets used.
  • Variable recovery — turning surviving registers and stack slots into named locals with a coherent lifetime.
  • Structuring — recovering if, while, and for from a control-flow graph that has no such concepts, without lying about what the code does.
  • C emission — printing something a person can read, and deciding what to do when the recovered program has no legal C spelling.

Following along in code

Every chapter has an Explore the Code section pointing at the matching module in tiny-dec, the Python decompiler this series is built around. It is MIT-licensed and runs on any machine with Python 3.12 and clang:

git clone https://github.com/ZhangZhuoSJTU/tiny-dec
cd tiny-dec
pip install poetry && poetry install
./scripts/build_fixtures.sh
poetry run tiny-dec decompile ./tests/fixtures/bin/fixture_basic_O0_nopie.elf

Post 0 walks through that setup in full. You can read the series without running anything, but the stages are much easier to believe once you have watched one produce wrong output on a binary you chose.