Reversing Loops with GDB
In the previous tutorial, we reversed a simple binary that had variables, arithmetic, and an if/else condition. We learned how cmp + jg translates to an if statement, and how to reconstruct C source from assembly.
Now let’s tackle something more interesting — loops. Loops are everywhere in real programs. Summing values, iterating over arrays, processing strings, searching data — it’s all loops. And in assembly, loops have a very recognizable pattern once you know what to look for.
By the end of this post, you’ll be able to spot a loop in disassembly in seconds, identify its counter, condition, and body, and reconstruct the original C code.
How Loops Look in Assembly
Before we jump into GDB, let’s understand the general pattern. A for loop in C:
for (int i = 0; i < 10; i++) {
// body
}
The compiler translates this into four parts:
- Initialization — Set the counter to its starting value
- Condition check — Compare the counter against the limit
- Body — The actual work
- Increment — Update the counter
- Backward jump — Go back to the condition check
In assembly, the telltale sign of a loop is a backward jump — a jump instruction that targets a lower address (earlier in the code). If/else statements jump forward (to higher addresses). Loops jump backward.
┌──────────────────────────────┐
│ Initialize counter │
▼ │
┌─── Condition check (cmp + jXX) ────┘
│ │
│ ▼ (condition true — enter loop)
│ Loop body
│ │
│ Increment counter
│ │
│ JMP back to condition ──────────► (backward jump!)
│
└──► (condition false — exit loop)
Continue after loop...
Example 1: A Simple For Loop
Let’s reverse a binary that prints nothing but does some computation internally.
user@protostar:~$ ./loop1
Sum: 55
It prints “Sum: 55”. That’s 1+2+3+…+10 = 55. Smells like a loop. Let’s confirm.
Disassembly
user@protostar:~$ gdb -q ./loop1
(gdb) set disassembly-flavor intel
(gdb) disass main
Dump of assembler code for function main:
0x080483c4 <+0>: push ebp
0x080483c5 <+1>: mov ebp,esp
0x080483c7 <+3>: sub esp,0x18
0x080483ca <+6>: mov DWORD PTR [ebp-0x8],0x0
0x080483d1 <+13>: mov DWORD PTR [ebp-0x4],0x1
0x080483d8 <+20>: jmp 0x80483e8 <main+36>
0x080483da <+22>: mov eax,DWORD PTR [ebp-0x4]
0x080483dd <+25>: add DWORD PTR [ebp-0x8],eax
0x080483e0 <+28>: add DWORD PTR [ebp-0x4],0x1
0x080483e4 <+32>: jmp 0x80483e8 <main+36>
0x080483e6 <+34>: jmp 0x80483f4 <main+48>
0x080483e8 <+36>: cmp DWORD PTR [ebp-0x4],0xa
0x080483ec <+40>: jle 0x80483da <main+22>
0x080483ee <+42>: jmp 0x80483f4 <main+48>
0x080483f0 <+44>: nop
0x080483f1 <+45>: nop
0x080483f2 <+46>: nop
0x080483f3 <+47>: nop
0x080483f4 <+48>: mov eax,DWORD PTR [ebp-0x8]
0x080483f7 <+51>: mov DWORD PTR [esp+0x4],eax
0x080483fb <+55>: mov DWORD PTR [esp],0x80484d0
0x08048402 <+62>: call 0x80482f8 <printf@plt>
0x08048407 <+67>: mov eax,0x0
0x0804840c <+72>: leave
0x0804840d <+73>: ret
End of assembler dump.
Let’s break this down piece by piece.
The Prologue and Initialization
0x080483c4 <+0>: push ebp
0x080483c5 <+1>: mov ebp,esp
0x080483c7 <+3>: sub esp,0x18
0x080483ca <+6>: mov DWORD PTR [ebp-0x8],0x0
0x080483d1 <+13>: mov DWORD PTR [ebp-0x4],0x1
Standard prologue, then two local variables are initialized:
[ebp-0x8]= 0 — This will be our accumulator (the sum)[ebp-0x4]= 1 — This is our loop counter, starting at 1
In C terms:
int sum = 0;
int i = 1;
The Forward Jump — Skipping to the Condition
0x080483d8 <+20>: jmp 0x80483e8 <main+36>
Right after initialization, there’s an unconditional jump to <main+36>. Let’s see what’s there:
0x080483e8 <+36>: cmp DWORD PTR [ebp-0x4],0xa
0x080483ec <+40>: jle 0x80483da <main+22>
It jumps to the condition check. This is a common compiler pattern for for loops — check the condition before the first iteration, not after. This ensures that if the loop condition is false from the start (e.g., for (i = 10; i < 5; i++)), the body never executes.
The Condition Check
0x080483e8 <+36>: cmp DWORD PTR [ebp-0x4],0xa
0x080483ec <+40>: jle 0x80483da <main+22>
cmp [ebp-0x4], 0xa — Compare the loop counter with 10 (0xa).
jle 0x80483da — Jump if Less or Equal. If i <= 10, jump to <main+22> (the loop body). If i > 10, fall through and exit the loop.
This is the condition i <= 10. Combined with the counter starting at 1, we have: for (i = 1; i <= 10; ...).
Notice the backward jump — from address 0x080483ec back to 0x080483da. That’s lower in memory. That’s the loop.
The Loop Body
0x080483da <+22>: mov eax,DWORD PTR [ebp-0x4]
0x080483dd <+25>: add DWORD PTR [ebp-0x8],eax
0x080483e0 <+28>: add DWORD PTR [ebp-0x4],0x1
Line by line:
- Load
i(from[ebp-0x4]) intoEAX - Add
EAXtosum(at[ebp-0x8]) — this issum += i - Increment
iby 1 — this isi++
After the increment, execution reaches the jmp back to the condition check at <main+36>, and the cycle repeats.
Watching It in GDB
Let’s set a breakpoint at the condition check and watch the loop iterate:
(gdb) b *0x080483e8
Breakpoint 1 at 0x80483e8
(gdb) run
Starting program: /home/user/loop1
Breakpoint 1, 0x080483e8 in main ()
(gdb) x/d $ebp-0x4
0xbffff7e4: 1
(gdb) x/d $ebp-0x8
0xbffff7e0: 0
(gdb) c
Breakpoint 1, 0x080483e8 in main ()
(gdb) x/d $ebp-0x4
0xbffff7e4: 2
(gdb) x/d $ebp-0x8
0xbffff7e0: 1
(gdb) c
Breakpoint 1, 0x080483e8 in main ()
(gdb) x/d $ebp-0x4
0xbffff7e4: 3
(gdb) x/d $ebp-0x8
0xbffff7e0: 3
(gdb) c
Breakpoint 1, 0x080483e8 in main ()
(gdb) x/d $ebp-0x4
0xbffff7e4: 4
(gdb) x/d $ebp-0x8
0xbffff7e0: 6
| Iteration | i | sum | sum after sum += i |
|---|---|---|---|
| 1 | 1 | 0 | 1 |
| 2 | 2 | 1 | 3 |
| 3 | 3 | 3 | 6 |
| 4 | 4 | 6 | 10 |
| … | … | … | … |
| 10 | 10 | 45 | 55 |
When i reaches 11, the cmp [ebp-0x4], 0xa + jle condition fails (11 is not <= 10), and execution falls through to the printf.
After the Loop
0x080483f4 <+48>: mov eax,DWORD PTR [ebp-0x8]
0x080483f7 <+51>: mov DWORD PTR [esp+0x4],eax
0x080483fb <+55>: mov DWORD PTR [esp],0x80484d0
0x08048402 <+62>: call 0x80482f8 <printf@plt>
This loads sum into EAX, pushes it as the second argument to printf, loads the format string address, and calls printf. Let’s check the format string:
(gdb) x/s 0x80484d0
0x80484d0: "Sum: %d\n"
The Reconstructed Code
#include <stdio.h>
int main() {
int sum = 0;
int i;
for (i = 1; i <= 10; i++) {
sum += i;
}
printf("Sum: %d\n", sum);
return 0;
}
We got the loop, the accumulator, the bounds, and the printf format — all from assembly.
Example 2: A While Loop
Let’s reverse another binary:
user@protostar:~$ ./loop2
Countdown: 10 9 8 7 6 5 4 3 2 1 Liftoff!
Disassembly
(gdb) disass main
Dump of assembler code for function main:
0x080483c4 <+0>: push ebp
0x080483c5 <+1>: mov ebp,esp
0x080483c7 <+3>: sub esp,0x18
0x080483ca <+6>: mov DWORD PTR [ebp-0x4],0xa
0x080483d1 <+13>: jmp 0x80483ea <main+38>
0x080483d3 <+15>: mov eax,DWORD PTR [ebp-0x4]
0x080483d6 <+18>: mov DWORD PTR [esp+0x4],eax
0x080483da <+22>: mov DWORD PTR [esp],0x80484d0
0x080483e1 <+29>: call 0x80482f8 <printf@plt>
0x080483e6 <+34>: sub DWORD PTR [ebp-0x4],0x1
0x080483ea <+38>: cmp DWORD PTR [ebp-0x4],0x0
0x080483ee <+42>: jg 0x80483d3 <main+15>
0x080483f0 <+44>: mov DWORD PTR [esp],0x80484d4
0x080483f7 <+51>: call 0x80482f8 <puts@plt>
0x080483fc <+56>: mov eax,0x0
0x08048401 <+61>: leave
0x08048402 <+62>: ret
End of assembler dump.
Breaking It Down
Initialization:
0x080483ca <+6>: mov DWORD PTR [ebp-0x4],0xa
One variable at [ebp-0x4] initialized to 10 (0xa). This is our countdown counter.
Jump to condition:
0x080483d1 <+13>: jmp 0x80483ea <main+38>
Same pattern as the for loop — jump to the condition check before the first iteration.
Condition check:
0x080483ea <+38>: cmp DWORD PTR [ebp-0x4],0x0
0x080483ee <+42>: jg 0x80483d3 <main+15>
Compare counter with 0. JG — Jump if Greater. While counter > 0, jump back to the loop body. This is while (n > 0).
Loop body:
0x080483d3 <+15>: mov eax,DWORD PTR [ebp-0x4]
0x080483d6 <+18>: mov DWORD PTR [esp+0x4],eax
0x080483da <+22>: mov DWORD PTR [esp],0x80484d0
0x080483e1 <+29>: call 0x80482f8 <printf@plt>
0x080483e6 <+34>: sub DWORD PTR [ebp-0x4],0x1
Print the counter value, then decrement by 1 (sub ... 0x1 is n--).
Let’s check the format string:
(gdb) x/s 0x80484d0
0x80484d0: "%d "
After the loop:
0x080483f0 <+44>: mov DWORD PTR [esp],0x80484d4
0x080483f7 <+51>: call 0x80482f8 <puts@plt>
(gdb) x/s 0x80484d4
0x80484d4: "Liftoff!"
How While Loops Differ from For Loops
In this disassembly, the structure is nearly identical to the for loop. That’s because, at the assembly level, for and while loops compile to the same pattern:
// These two are equivalent in assembly:
// For loop
for (int i = 10; i > 0; i--) { ... }
// While loop
int i = 10;
while (i > 0) { ... i--; }
The compiler generates the same initialization → condition check → body → update → backward jump pattern for both. You often can’t tell whether the original was a for or while just from the assembly. Context and coding conventions help, but it’s sometimes ambiguous.
The Reconstructed Code
#include <stdio.h>
int main() {
int n = 10;
while (n > 0) {
printf("%d ", n);
n--;
}
printf("Liftoff!\n");
return 0;
}
Example 3: Nested Loops
This one prints a triangle pattern:
user@protostar:~$ ./loop3
*
**
***
****
*****
Disassembly
(gdb) disass main
Dump of assembler code for function main:
0x080483c4 <+0>: push ebp
0x080483c5 <+1>: mov ebp,esp
0x080483c7 <+3>: sub esp,0x18
0x080483ca <+6>: mov DWORD PTR [ebp-0x8],0x1
0x080483d1 <+13>: jmp 0x8048401 <main+61>
0x080483d3 <+15>: mov DWORD PTR [ebp-0x4],0x0
0x080483da <+22>: jmp 0x80483ea <main+38>
0x080483dc <+24>: mov DWORD PTR [esp],0x2a
0x080483e3 <+31>: call 0x8048308 <putchar@plt>
0x080483e8 <+36>: add DWORD PTR [ebp-0x4],0x1
0x080483ec <+40>: mov eax,DWORD PTR [ebp-0x4]
0x080483ef <+43>: cmp eax,DWORD PTR [ebp-0x8]
0x080483f2 <+46>: jl 0x80483dc <main+24>
0x080483f4 <+48>: mov DWORD PTR [esp],0xa
0x080483fb <+55>: call 0x8048308 <putchar@plt>
0x08048400 <+60>: add DWORD PTR [ebp-0x8],0x1
0x08048404 <+64>: cmp DWORD PTR [ebp-0x8],0x5
0x08048408 <+68>: jle 0x80483d3 <main+15>
0x0804840a <+70>: mov eax,0x0
0x0804840f <+75>: leave
0x08048410 <+76>: ret
End of assembler dump.
This is longer, but now that we know the loop pattern, we can identify the two loops quickly.
Spotting the Two Loops
Look for backward jumps:
0x080483f2 <+46>: jl 0x80483dc <main+24>— jumps backward from +46 to +24. Inner loop.0x08048408 <+68>: jle 0x80483d3 <main+15>— jumps backward from +68 to +15. Outer loop.
The outer loop’s body (from +15 to +60) contains the inner loop (from +24 to +46). That’s how nested loops look — a backward jump inside another backward jump.
The Outer Loop
0x080483ca <+6>: mov DWORD PTR [ebp-0x8],0x1 ; i = 1
...
0x08048400 <+60>: add DWORD PTR [ebp-0x8],0x1 ; i++
0x08048404 <+64>: cmp DWORD PTR [ebp-0x8],0x5 ; i <= 5?
0x08048408 <+68>: jle 0x80483d3 <main+15> ; if yes, loop
Counter [ebp-0x8] starts at 1, increments by 1 each iteration, and loops while <= 5. That’s for (i = 1; i <= 5; i++).
The Inner Loop
0x080483d3 <+15>: mov DWORD PTR [ebp-0x4],0x0 ; j = 0
...
0x080483dc <+24>: mov DWORD PTR [esp],0x2a ; putchar('*')
0x080483e3 <+31>: call 0x8048308 <putchar@plt>
0x080483e8 <+36>: add DWORD PTR [ebp-0x4],0x1 ; j++
0x080483ec <+40>: mov eax,DWORD PTR [ebp-0x4]
0x080483ef <+43>: cmp eax,DWORD PTR [ebp-0x8] ; j < i?
0x080483f2 <+46>: jl 0x80483dc <main+24> ; if yes, loop
Counter [ebp-0x4] starts at 0 each time the outer loop iterates (it’s re-initialized inside the outer loop). It loops while j < i. The body calls putchar(0x2a) — and 0x2a in ASCII is *.
Between the inner loop and the outer loop’s increment:
0x080483f4 <+48>: mov DWORD PTR [esp],0xa
0x080483fb <+55>: call 0x8048308 <putchar@plt>
putchar(0xa) — 0xa is the newline character \n. This prints a newline after each row of asterisks.
The Reconstructed Code
#include <stdio.h>
int main() {
int i, j;
for (i = 1; i <= 5; i++) {
for (j = 0; j < i; j++) {
putchar('*');
}
putchar('\n');
}
return 0;
}
Iteration by iteration:
- i=1: prints 1 asterisk, then newline →
* - i=2: prints 2 asterisks, then newline →
** - i=3: prints 3 →
*** - i=4: prints 4 →
**** - i=5: prints 5 →
*****
Matches the output perfectly.
Understanding Jump Conditions
The conditional jump instruction after cmp tells you the loop’s condition. Here’s the complete mapping:
| Instruction | Meaning | Signed? | C Equivalent |
|---|---|---|---|
je / jz |
Jump if Equal / Zero | — | == |
jne / jnz |
Jump if Not Equal / Not Zero | — | != |
jl / jnge |
Jump if Less | Signed | < |
jle / jng |
Jump if Less or Equal | Signed | <= |
jg / jnle |
Jump if Greater | Signed | > |
jge / jnl |
Jump if Greater or Equal | Signed | >= |
jb / jnae |
Jump if Below | Unsigned | < |
jbe / jna |
Jump if Below or Equal | Unsigned | <= |
ja / jnbe |
Jump if Above | Unsigned | > |
jae / jnb |
Jump if Above or Equal | Unsigned | >= |
The signed versions (jl, jle, jg, jge) are used for regular int comparisons. The unsigned versions (jb, jbe, ja, jae) are used for unsigned int, pointer comparisons, and size comparisons.
When the jump is backward (to a lower address), the jump condition tells you the loop continuation condition — the loop keeps running while this condition is true.
Do-While Loops
There’s one more loop type worth mentioning. A do-while loop checks the condition after the body, not before:
do {
// body
} while (condition);
In assembly, the do-while is actually the simplest loop — no initial forward jump needed:
loop_body: ; ← body executes at least once
...
cmp ...
jle loop_body ; backward jump — check condition after body
Compare this to for/while, which always have a forward jmp to the condition check before the first iteration. If you see a loop with no initial forward jump — it’s a do-while.
Loop Patterns Cheat Sheet
Here’s how to quickly identify loops in disassembly:
Spotting a loop:
- Look for backward jumps (conditional jump to a lower address)
- The target of the backward jump is the start of the loop body (or the condition check)
Identifying the counter:
- Look for a
mov [ebp-X], immediatebefore the loop — that’s the initialization - Look for
add [ebp-X], 1orsub [ebp-X], 1inside the loop — that’s the increment/decrement - The same
[ebp-X]in thecmpinstruction is the counter
Identifying the condition:
cmpfollowed by a backward conditional jump — the jump condition is the continuation conditioncmp [ebp-X], 0xa+jlebackward =while (i <= 10)cmp [ebp-X], 0x0+jgbackward =while (n > 0)
For/while vs do-while:
for/while— Forwardjmpto condition check before the body (condition checked first)do-while— No forward jump; condition check is at the bottom (body executes at least once)
Nested loops:
- Multiple backward jumps, with one jump’s range contained within another’s
- The inner loop’s backward jump has a smaller range (fewer addresses between jump and target)
Infinite loops:
jmpbackward with no condition —while (true)orfor (;;)
Final Thoughts
Once you recognize the loop pattern — initialization, condition, body, increment, backward jump — you’ll start seeing it everywhere in disassembly. It becomes automatic. And that’s the whole point of these tutorials: building pattern recognition that lets you read assembly as fluently as C.
The key insight is that all loops (for, while, do-while) compile to essentially the same structure. The only differences are where the condition check happens (before or after the body) and how the counter is managed. The backward jump is universal.
Next up, we’ll look at how function calls work — how arguments are passed on the stack, how return values come back in EAX, and how multiple stack frames chain together. That’s where things start connecting to exploitation.
Happy reversing!