For an unfamiliar operation, use the instruction reference: it explains operand forms, changed values and flags, with an adjustable worked trace for each instruction.
Section: What a disassembler is for.
Lesson 5.8 followed changing gold through data addresses. The next question is which machine instruction reads or writes that field. We can copy a bounded code range, but raw bytes alone do not show where one instruction ends or what operation it performs.
The C P U, central processing unit, executes machine-code bytes. Humans prefer names such as mov, call, and jne. A disassembler decodes bytes into instruction records and then formats those records as assembly text.
In brief: Use assembly to identify operations, then observe the running target to connect those operations to game behavior.
It does not recover the original source. Compiler variable names, comments, generic types, and most high-level structure are normally gone. A disassembler can show that four bytes at [e b x+hexadecimal 3 0] are subtracted, but it cannot say that e b x points at a side’s data and hexadecimal 3 0 is the offset of its gold. That link comes from the running game: the instruction runs exactly when that side’s gold drops.
Machine code preserves operations more reliably than it preserves intent. Two different source programs can compile to similar instructions, and one source construct can compile differently across optimization levels. Assembly shows exactly what the machine does, not why the programmer wrote it.
An instruction can be described at three levels:
mechanics: which registers, memory locations, and flags can it change;
data flow: where its inputs came from and where its outputs are used;
program meaning: which game behavior it implements.
The decoder answers the first. Control-flow and data-flow analysis answer the second. Only the running target can establish the third.
A decoder answers structural questions: instruction length, operands, registers, immediate values, memory addressing, and flow control. A formatter chooses spelling and style, such as NASM or Intel syntax. Tooling should use decoded fields for logic rather than parsing the displayed string back into facts.
Section: Decoding is harder than formatting.
A disassembler does two jobs:
decode bytes into an instruction;
format that instruction as readable assembly.
x86 instructions have prefixes, several opcode maps, ModR/M and SIB bytes, displacements, immediates, and different modes. Hand-decoding add is a good exercise. A complete decoder is a major engineering project.
Very roughly:
an opcode selects an operation;
a prefix modifies size, repetition, locking, or another property;
a ModR/M byte helps describe register and memory operands;
a SIB byte can describe scaled index addressing;
a displacement is an encoded offset;
an immediate is a value stored directly in the instruction.
Not every instruction contains every piece. Their variable length is why a detour must decode whole instructions before deciding how many bytes to replace.

An instruction’s length is known only after its first bytes are decoded, and the next instruction starts right after it: B8 means four immediate bytes follow, so the instruction after mov e a x, 25 starts at 0 + 5 equals 5.
Predict first
In 32-bit mode the six bytes B8 19 00 00 00 C3 are mov e a x, 25 (offsets 0 to 4) and then ret (offset 5).
A disassembler is told to start at offset 2 instead of 0. What does it show?
Answer: Different instructions that look valid. The decoder treats the byte it starts on as an opcode. 00 00 decodes as add byte ptr [e a x], a l and 00 C3 as add b l, a l, so even the ret vanishes into a wrong instruction.

Section: A small opcode lookup table.
These are complete encodings in 32-bit mode, with hexadecimal bytes and decimal assembly operands. They are examples, not every form of each instruction:
Notice that 83 alone does not distinguish add from sub. A ModR/M byte has three fields: mod, reg, and r/m, containing 2, 3, and 3 bits. Here C0 is 11 000 000, while E8 is 11 101 000. For this opcode, 11 chooses a register operand, the middle field selects addition (000) or subtraction (101), and the final 000 names e a x. In other instructions, the middle field can name a second register instead.
The decoder below takes any ModR/M byte apart the same way. Try C0 and E8 from the table, then 45, which turns the r/m field into a memory address.

In brief: The opcode is only part of an instruction. Read its operand form and C P U mode before interpreting the following bytes.
For complete forms and flag effects, consult Intel’s instruction manuals. Bytecode is a separate instruction format designed for a software interpreter; its numeric opcodes need not match x86. The toy C P U’s encoding table shows that distinction with seven deliberately simple operations.
For our tool, use iced-x86.
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Section: Decode a byte slice.
The decoder needs the byte slice, the target’s C P U bitness, and the address where those bytes execute. That address lets it print meaningful destinations for relative branches; it does not change the copied bytes. The function below returns one formatted line per decoded instruction:
Alternative: Copied bytes are a photograph of a street; the start address supplies its location so relative directions lead to the right destination.
Code example. The code’s comments explain: 🛡️ The same bytes decode differently in different C P U modes. Refuse a guessed value before the decoder can produce convincing nonsense. 🧭 `start_ip` gives relative branches their real live destination; it does not say where this local byte slice is stored in the analysis tool. ♻️ Reuse these allocations inside the loop; a large code section may contain thousands of instructions. 📏 Convert the decoder's virtual i p back into an index only after the decoder has advanced, then validate the complete instruction range.
Decoder::with_ip makes relative branches and instruction addresses meaningful.
The instruction pointer passed as start_ip does not change the input bytes. It gives them a location so a relative branch can be reported as its real destination. Decoding the same bytes at a different address can therefore produce a different displayed branch target.
Section: Use the target’s bitness.
Decode 32-bit process code as 32-bit and 64-bit process code as 64-bit. The same bytes can mean different things in different modes.
Bitness changes default operand/address sizes and which encodings are valid. It is a property of the code being decoded, not necessarily of the computer or the analysis tool. A 64-bit Windows system can run the 32-bit Wesnoth course build.
Section: Read an executable section.
Use your process wrapper and P E, portable executable, parser to:
find the target module;
locate an executable section such as .text;
copy a bounded byte range;
pass its real virtual start address as start_ip.

Do not decode unreadable gaps as though they were code.
Section: Instructions are data.
iced-x86 exposes more than formatted text:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
This is more reliable than searching formatted strings for jmp.
Section: Linear sweep versus control flow.
The loop above is a linear sweep: decode one instruction after another. It may decode data embedded in a code section as nonsense instructions.
Narration: A readable instruction listing can still be wrong: bytes belonging to a nearby constant may accidentally resemble valid machine code.
A control-flow disassembler begins at a known entry point and follows reachable branches and calls. That needs:
a work queue of addresses;
a set of visited addresses;
range checks;
branch-target extraction;
an explicit choice to enter callees or continue after calls.
Start linear. Add control flow only after the basic decoder and bounds are solid.
A basic block is a straight-line run of instructions with one entry and a control transfer at the end. Connecting blocks by possible jumps creates a control-flow graph. Loops appear as edges that return to an earlier block; if statements often appear as a branch whose paths later meet.
The graph still contains uncertainty. An indirect jump may obtain its destination from a table or register, exception handling can create non-obvious edges, and code may be reached only through callbacks. Mark unknown successors instead of pretending the graph is complete.
Section: Invalid bytes.
Decoders can return an invalid instruction. Show it clearly and decide whether to stop or advance by the decoder’s reported length.
Never enter an infinite loop at one bad byte.
Section: Test known examples.
These four bytes have a known 64-bit interpretation: mov r a x, r b x followed by ret. Passing them with start address hexadecimal 1 0 0 0 should produce two formatted instructions in that order, so the test checks both instruction boundaries and basic decoding:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Add tests for truncated instructions, invalid bitness, a relative branch, 32-bit bytes, and a start address near overflow.
Section: What a disassembler can and cannot prove.
Use a mature decoder for instruction truth. Spend your effort on the tool around it: safe process reads, section selection, navigation, symbols, and clear error messages.
Section: Start from a string and follow what refers to it.
An unfamiliar executable has thousands of functions, and nothing labels the one behind a message such as Not enough gold. The message is easier to find: its bytes sit in read-only data, where a text search finds them. The code that shows it must use the string’s address, so the next search is for code that refers to that address. Each hit is a cross-reference, and decoding the instructions around it leads to the function you wanted.
How the address appears in code depends on bitness. 32-bit code, such as this course’s Wesnoth build, usually stores the whole address inside the instruction: push this value is the bytes 68 F0 A2 C4 00, so a byte search of .text for F0 A2 C4 00 finds it. Decode around each hit, because data can contain the same four bytes by chance. If the loader moved the module, it has already rewritten such addresses, so search the live code for the live address. 64-bit code usually stores a distance instead, measured from the end of the instruction. The C P U adds it to r i p, the 64-bit instruction pointer, which by then holds the address of the next instruction.
Predict first
A 64-bit game keeps the string at this value. A byte search of its code for the eight address bytes F0 A4 02 40 01 00 00 00 finds nothing.
What does the empty search show?
Answer: Nothing yet: decode each instruction and work out where its distance points. Only decoding gives each instruction its length, and so the address of the next instruction that the distance is measured from. Then every target can be compared with this value.

Section: Disassemble the real Wesnoth function.
The completed binary opens the live 32-bit Wesnoth process, reads hexadecimal 5 0 bytes beginning at the verified lesson address shown here, decodes with a 32-bit iced-x86 decoder, and prints the real address, raw bytes, and NASM-style instruction text:
Terminal window
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
It stops on invalid bytes or an out-of-range decode instead of silently inventing output. See the entire tool in disassembler.rs and the checked cross-process read in process.rs.