From bytes to instructions — and why it's harder than it looks
x86-64 instructions are variable-length, from 1 byte (ret = 0xC3) to 15. Code and data can sit adjacent in the same section. Embedded jump tables, alignment padding, and switch-case dispatchers regularly look like "code" but aren't reachable as such. Decoding 1MB of .text into instructions correctly is the first hard problem.
Two classical strategies:
- Linear sweep. Start at the section's first byte, decode forward, advance by the instruction length. Simple. Fails on inline data — once you decode a misaligned byte as the start of a multi-byte instruction, every following decode is wrong until alignment by luck.
- Recursive descent. Start at known entry points (export table,
_start, function symbols). Decode each instruction; forcallandjmptargets, recurse; for conditionaljcc, recurse both branches; stop atret/hlt/ud2. Misses code reached only via computed jumps (switch tables, function pointers, vtables) — which is why disassemblers also run heuristics over the gaps.
Modern tools (Ghidra, Binary Ninja, IDA) combine recursive descent with linear sweep over unknown ranges and call-graph reconstruction. ARM, RISC-V, and most other architectures have fixed-width instructions, which sidesteps most of this — variable-length encoding is an x86 inheritance from the 8086 era.
