Reading Compiled Code Patterns
The actual skill reverse engineering exercises day to day isn’t reciting instruction mnemonics, it’s pattern-matching disassembly back to the C/C++ source shape that produced it. This article catalogs the recognizable shapes for control flow, data structures, and calls, building on x86 Assembly Fundamentals and x86 Instructions and the Stack.
if/else
if (health <= 0) {
die();
} else {
regen();
}compiles to a compare, a conditional jump to the false branch (compilers invert the condition so the common/fast path falls through), and an unconditional jump at the end of the true branch to skip the false branch:
cmp dword [rbx+health_offset], 0
jg else_branch ; if health > 0, skip the "then"
call die
jmp end_if
else_branch:
call regen
end_if:The inversion (<= in source becomes jg, the opposite condition, to jump past the then block) is the single most important habit to build: when reading a jcc right after a cmp, mentally negate it to recover the source-level condition, don’t read it literally.
Loops
A while/for loop compiled with the test at the top looks like an if-guarded backward jump:
for (int i = 0; i < count; i++) { arr[i] = 0; }xor eax, eax ; i = 0
loop_check:
cmp eax, [rbx+count]
jge loop_end
mov dword [rdi+rax*4], 0 ; arr[i] = 0
inc eax ; i++
jmp loop_check
loop_end:Optimizing compilers more often emit the do-while transform: an unconditional jump straight to the check, so the backward branch (cheaper to predict, since backward branches are statically predicted taken) is the loop’s only per-iteration jump:
xor eax, eax
jmp loop_check
loop_body:
mov dword [rdi+rax*4], 0
inc eax
loop_check:
cmp eax, [rbx+count]
jl loop_bodyEither shape has the same tell: a compare against something that looks like a bound, immediately followed by a conditional branch that targets either just past a block (top-tested) or back into one (bottom-tested), with an increment somewhere in the loop body. At -O2+ expect loop unrolling (the body repeated 2–8× with the index pre-offset each copy) and vectorization (the same operation done on 4/8 elements at once via xmm/ymm SSE/AVX registers) on hot loops — both preserve this same check-and-branch skeleton around the transformed body, so the recognition strategy doesn’t change, there’s just more instructions per iteration to skim past.
switch statements → jump tables
A switch with enough densely-packed cases compiles to an indexed jump through a table of addresses rather than a chain of cmps — this is the pattern to recognize before spending time reading it as a chain of ifs:
switch (state) {
case 0: idle(); break;
case 1: walk(); break;
case 2: attack(); break;
case 3: jump_(); break;
}cmp eax, 3
ja default_case ; bounds check: out-of-range falls through to default
lea rcx, [jump_table]
movsxd rdx, dword [rcx+rax*4] ; table entries are often rip-relative 32-bit offsets, not raw addresses
add rdx, rcx
jmp rdx
jump_table:
dd offset case0 - jump_table
dd offset case1 - jump_table
dd offset case2 - jump_table
dd offset case3 - jump_tableThe bounds check (cmp/ja against the case count) right before an indirect jmp reg is the giveaway; a disassembler will usually recover and label the table automatically, but recognizing the shape matters when it doesn’t (obfuscated/packed binaries, or a table the tool mis-identifies as data). A switch with sparse or few cases instead just compiles to an ordinary cmp/je chain — no table needed.
Structs and field access
A struct field access is a constant-displacement memory operand — there’s no separate “field access” instruction, so this is purely inferred from a repeated [reg+const] pattern:
struct Entity { float x, y, z; int health; int team; };
entity->health -= 10;mov eax, [rbx+0xC] ; health is 12 bytes in: x,y,z are 3 floats = 12 bytes
sub eax, 10
mov [rbx+0xC], eaxOnce you see the same base register (rbx here) used across a function with several different constant offsets, that’s a struct pointer, and the set of offsets used is effectively the struct layout — reconstructing it (Ghidra’s structure editor is built exactly for this) is most of what “understanding a game’s entity/player structure” actually consists of. Floats are near-universally handled through the xmm registers and movss/movsd (scalar single/double) rather than the general-purpose registers — seeing movss xmm0, [rbx+0xC] instead of mov eax, [rbx+0xC] at the same-looking offset is how you tell a struct field is a float without any type information.
Arrays
Array indexing is the base+index*scale addressing mode from x86 Assembly Fundamentals used directly:
players[i].health; rbx = players base, eax = i, sizeof(Player) = 0x40
imul eax, eax, 0x40 ; i * sizeof(Player), if size isn't a power of 2 usable as a scale
lea rcx, [rbx+rax]
mov edx, [rcx+0xC] ; ->health field within that elementWhen the element size is 1/2/4/8 bytes, the multiply collapses directly into the addressing mode’s scale factor instead ([rbx+rax*4]) with no separate imul — seeing an explicit imul/shl by a struct-sized constant right before an address computation is the tell for “array of structs” rather than “array of primitives.”
Virtual function calls (vtables)
C++ virtual dispatch — extremely common in game engines (Unreal, Unity’s native side, most in-house engines) — has a distinctive two-level-indirection shape: a “this” pointer’s first field points at a table of function pointers, and the call reads through both:
entity->takeDamage(10); // takeDamage is virtualmov rax, [rcx] ; rax = vtable ptr = *this (first 8 bytes of any polymorphic object)
mov rdx, 10
call qword [rax+0x18] ; call the function pointer at vtable[3] (0x18 / 8 = index 3)versus a non-virtual (statically resolved) call to the same-looking method, which is just an ordinary direct call:
mov rdx, 10
call takeDamage ; direct call, address fixed at compile/link timecall qword [reg+const] — an indirect call through a register that was itself just loaded from [another_reg] — is the pattern to search for when you want to find every virtual method on a class: dump the table at that base pointer and each qword in it is a function address (Ghidra/IDA’s “create vtable” analysis does this automatically once it spots the pattern). This is also the mechanism function hooking exploits (see x86 Reverse Engineering for Games) — overwrite one function pointer in the table and every virtual call through it redirects, with no code patching required at all.
See also
- x86 Assembly Fundamentals — registers, flags, addressing modes used throughout this article.
- x86 Instructions and the Stack — the instructions and calling conventions these patterns are built from.
- x86 Reverse Engineering for Games — using a disassembler to find and patch these patterns in a real binary, plus a worked crackme walkthrough.
- Cheat Engine and Memory Scanning — finding the struct offsets this article describes (e.g.
health,x/y/z) by scanning live process memory instead of static analysis. - All Articles