How if/else, for, while, and switch statements compile to x86 — recognising these patterns in IDA Pro and Ghidra to reconstruct program logic from raw disassembly.
Sign in to mark this article as read and track your progress.
High-level constructs like if/else, for, while, and switch do not exist in machine code. The compiler translates them into sequences of comparison instructions, conditional jumps, and unconditional jumps. A reverse engineer must recognise these patterns in disassembly to reconstruct the original program logic.
An if/else block compiles into a comparison followed by a conditional jump that skips the true body and enters the false body.
/* C source */
if (eax == 0) {
// true body
} else {
// false body
}
; x86 assembly
cmp eax, 0
jnz else_block ; jump when NOT equal (opposite condition)
; --- true body ---
jmp end_if
else_block:
; --- false body ---
end_if:
Key insight: The compiler generates a jump for the opposite condition so the true body falls through naturally.
jnz(jump if not zero) is used even though the C condition is== 0.
else_blockend_ifRefer to REMA eBook 2026, Figure 3.1 for the CFG diagram of an if/else block.
Both for and while loops compile to the same pattern: a comparison at the top and a conditional jump back to the beginning.
/* C source */
for (int i = 0; i < 10; i++) {
buffer[i] ^= key;
}
xor ecx, ecx ; i = 0
loop_start:
cmp ecx, 0Ah ; i < 10?
jge loop_end ; exit if i >= 10
xor [esi+ecx], bl ; buffer[i] ^= key
inc ecx ; i++
jmp loop_start
loop_end:
The back-edge — the unconditional jmp from the end of the loop body back to loop_start — is the defining feature of any loop in a CFG. In IDA Pro graph view, a back-edge points upward. Spotting upward arrows immediately identifies loops.
When you see a tight loop containing XOR operating byte-by-byte over a buffer (ECX as counter, ESI or EDI as buffer pointer), you are almost certainly looking at a decryption routine. Extract the key byte and buffer address to decrypt the payload statically without executing the malware.
A switch statement with a small number of cases compiles into a chain of CMP/JE pairs. When the number of cases is large and values are contiguous, the compiler generates a jump table.
cmp eax, 4 ; value > max case?
ja default_case ; if above max, go to default
jmp [jump_table+eax*4] ; index into table
jump_table:
dd case_0
dd case_1
dd case_2
dd case_3
dd case_4
The jump table is an array of code addresses indexed by the switch variable. In IDA Pro, it appears as a block of dd offset entries in .rdata. Ghidra renders it as a switch statement in the decompiler output automatically.
Malware relevance: Malware that dispatches C2 commands frequently uses a switch on the command byte. The jump table is a direct map of the malware's command vocabulary — each entry is a handler for a specific command.
| Pattern | Assembly Signature | What It Means |
|---|---|---|
| If/else | CMP + Jcc + body + JMP + else body | Conditional logic |
| Loop | CMP + Jcc (exit) + body + JMP (back) | Iteration |
| XOR decrypt loop | Loop with XOR [ESI+ECX], BL | Decryption routine |
| Jump table | CMP + JA default + JMP [table+reg*4] | Command dispatcher |
| Back-edge (upward arrow in CFG) | Unconditional JMP to lower address | Loop of any type |
Malware authors attempt to break the CFG using:
| Technique | Effect on Disassembler |
|---|---|
| Opaque predicates | Creates false edges to unreachable garbage code |
| Overlapping instructions | Misaligns subsequent disassembly |
Indirect jumps (JMP EAX) | Destination unknown without execution |
| Exception-based flow | CFG misses exception-driven paths |
Opaque predicate example:
xor eax, eax
cmp eax, eax ; always equal
jnz garbage_code ; NEVER taken — but disassembler does not know
; real code here
garbage_code:
db 0xFF, 0xFF ; intentionally invalid bytes
Bypass: confirm with dynamic analysis. The CPU always takes the correct path — let the debugger show you the real flow, then patch the garbage jump to a NOP in the disassembler.