Recognising if/else and switch in disassembly
Control flow is the skeleton of a function. Once you can see the ifs, loops and switches, the rest is filling in details. Here are the common shapes, from real GCC 13 output.
if / else if / else at -O0
int clamp(int x)
{
if (x < 0)
return 0;
else if (x > 255)
return 255;
return x;
}0000: push rbp
0001: mov rbp, rsp
0004: mov DWORD PTR [rbp-0x4], edi
0007: cmp DWORD PTR [rbp-0x4], 0x0
000b: jns 0014 ; x >= 0: skip the first branch
000d: mov eax, 0x0 ; return 0
0012: jmp 0027
0014: cmp DWORD PTR [rbp-0x4], 0xff
001b: jle 0024 ; x <= 255: skip the second branch
001d: mov eax, 0xff ; return 255
0022: jmp 0027
0024: mov eax, DWORD PTR [rbp-0x4] ; return x
0027: pop rbp
0028: retThe pattern: a compare, a conditional jump over the body using the inverted condition (x < 0 became jns, "not negative"), and an unconditional jmp at the end of each body to the common exit. Each if is a forward conditional jump; each else is the jmp that skips the rest.
The same function at -O1: no jumps at all
0000: mov eax, 0xff
0005: cmp edi, eax
0007: cmovle eax, edi ; eax = (x <= 255) ? x : 255
000a: test eax, eax
000c: mov edx, 0x0
0011: cmovs eax, edx ; if negative: 0
0014: retcmov (conditional move) lets the compiler compute both outcomes and pick one, avoiding branches that the CPU might mispredict. When you see cmovXX, read it as a small ? : expression.
switch: the jump table
int classify(int n)
{
switch (n) {
case 0: return 10;
case 1: return 23;
case 2: return 37;
case 3: return 41;
case 4: return 58;
case 5: return 66;
case 6: return 79;
default: return -1;
}
}004d: cmp edi, 0x6
0050: ja 007f ; n > 6 unsigned: default
0052: mov edi, edi ; zero-extend n to 64 bits
0054: jmp QWORD PTR [rdi*8+table] ; jump to table[n]
005b: mov eax, 0xa ; case 0
0060: ret
0061: mov eax, 0x25 ; case 2
0066: ret
...
007f: mov eax, 0xffffffff ; default: -1
0084: ret
0085: mov eax, 0x17 ; case 1
008a: retThree things identify a jump table:
- A single unsigned bounds check (
cmp+ja). Negative numbers become huge unsigned values, so one check covers bothn < 0andn > 6. - An indirect jump through memory, indexed by the value times 8 (the size of a pointer).
- A table of code addresses in
.rodata, one per case.
The case bodies don't have to be in order: here case 1 ended up after the default. Disassemblers usually recognise the table and label each target (case 1, case 2…); if yours doesn't, read the table from .rodata yourself: each entry is an 8-byte address.
When cases are sparse (say 1, 100 and 5000), compilers use a chain of compares or a binary search instead of a table. And when every case just returns a constant, as above, GCC at -O2 may turn the whole switch into a lookup table of values and a single load, with no jumps at all.
Loops, briefly
A backwards jump is a loop. The compare right before it is the loop condition. See "From C to x86-64" for a full example.
Habit
Draw the graph. IDA's graph view (space bar) and Ghidra's function graph do it for you: blocks are straight-line code, edges are jumps. ifs look like diamonds, loops like cycles, and a switch is one block fanning out to many.