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_body

Either 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_table

The 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], eax

Once 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 element

When 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 virtual
mov 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 time

call 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