Worked examples — Disassembly — objdump, reading assembly
Prerequisites we lean on: Calling Conventions (System V x86-64), Stack Frames & The Stack Pointer, Compiler Optimization Levels, and the parent Disassembly.
The scenario matrix
Every case this topic can throw at you, and the example that covers it:
| Cell | Scenario class | Covered by |
|---|---|---|
| C1 | Signed comparison, positive branch (>) |
Example 1 |
| C2 | Signed comparison, negative operand / opposite sign (<) |
Example 2 |
| C3 | Unsigned comparison — the sign trap | Example 3 |
| C4 | Zero / degenerate input — empty function | Example 4 |
| C5 | Loop (counting down to zero, back-edge jump) | Example 5 |
| C6 | Byte-level decode by hand (variable length) | Example 6 |
| C7 | Limiting case — optimizer deletes everything (-O2) |
Example 7 |
| C8 | Real-world word problem — crash address → source line | Example 8 |
| C9 | Exam twist — same mnemonic, two meanings (lea vs mov) |
Example 9 |
| C10 | Tail call — jmp where you expected call |
Example 10 |
Read top to bottom; each cell builds on the last.
C1 — Signed comparison, positive branch
Steps
test edi, edicomputesedi & edi. Why this step? ANDing a number with itself changes nothing but does set the flags — cheaper thancmp edi, 0. The sign flag (SF) ends up = the top bit ofa, the zero flag (ZF) = 1 ifa == 0.jle 0xbmeans "jump if less-than-or-equal (signed)". Why this step? Source says do thereturn 1if a > 0; assembly inverts it and jumps away to thereturn 0block whena <= 0. Inversion is the normal pattern (parent'sifexample).- For
a = 5:5 <= 0is false → no jump →mov eax, 1. Result 1. - For
a = 0:0 <= 0is true → jump to0xb→mov eax, 0. Result 0. - For
a = -3:-3 <= 0true → jump → 0.
Recall Why
jle and not jbe?
jle is the signed version. jbe ("below or equal") is the unsigned version.
The C type was int (signed), so the compiler must pick the signed jump. This choice
is the whole subject of C3 below.
Verify: the C function returns a > 0 ? 1 : 0. For {5, 0, -3} that is {1, 0, 0} —
matches steps 3-5. ✔
C2 — Negative operand, < branch
Steps
- Decode
0xfffffffc. Why this step? In 32-bit two's complement, the top bit is the sign.0xfffffffc = 4294967292if unsigned, but as a signedintit is . The compiler wrote-4this way. cmp [rbp-0x4], -4computesx - (-4) = x + 4and sets flags. Why this step?cmpnever stores a result; it only feeds the flags to the next jump (parent's rule).jge 0xf= "jump if greater-or-equal (signed)". Source wantsy=1whenx < -4; assembly jumps to the else (y=2) whenx >= -4.- For
x = -10: is-10 >= -4? No. So we do not jump → fall through toy = 1.
Verify: -10 < -4 is true, so y = 1 — matches step 4. And the immediate: -4 stored
as unsigned is . ✔
C3 — The unsigned trap
Steps
- Same bit pattern, two readings. Why this step?
0xffffffffis-1as a signedint, but4294967295asunsigned. The bits are identical; only the compiler's chosen instruction decides how they're compared. setguses signed semantics. Sofasks: is-1 > 3? No →al = 0.setauses unsigned semantics. Sogasks: is4294967295 > 3? Yes →al = 1.- Why does this matter for reading asm? The mnemonic tells you the C type.
g/a-family = unsigned,l/ge-family = signed. You can recover signedness that the raw bytes alone hide.
Verify: signed -1 > 3 = 0, unsigned 4294967295 > 3 = 1. ✔
C4 — Degenerate input: the empty function
Steps
- Even doing nothing,
-O0still emits the prologue (push rbp; mov rbp, rsp) and epilogue (pop rbp; ret). Why this step?-O0is mechanical: it sets up a frame for locals it might need, then tears it down. See Stack Frames & The Stack Pointer. - The lone
90is anop(no-operation). Why this step? It marks the (empty) function body and can aid alignment. It changes no state. - Net effect on registers:
rbppushed then popped → unchanged;rspback where it began.
Verify: count the bytes: 55 (1) + 48 89 e5 (3) + 90 (1) + 5d (1) + c3 (1) = 7
bytes, and the last address is 6 for a 1-byte ret, so the function spans 0x0..0x6
inclusive = 7 bytes. ✔ Register net change = zero. ✔
C5 — A loop that counts down (back-edge jump)

Steps
- Initialise
eax=0(sum),edx=3(i). Why this step? The compiler keeps both in registers at this level — no stack spill needed for a tight loop. add eax, edxthensub edx, 0x1. Why the reordered order? The compiler ran the body first, decrement after, so the loop can end on a singlejnetesting thesub's flags — one branch per iteration.jne 0xa= "jump if not equal" — it reads the zero flag left bysub. As long asididn't hit 0, we loop. Why this step? Counting down to zero is cheaper: the CPU sets ZF for free onsub, so no separatecmpis needed.- What the loop looks like (described inline, in case the figure isn't loaded): the four
instructions
add → sub → jnesit in a straight vertical column, butjneat address0xftargets address0xa— an address earlier than itself. That upward jump is the back-edge (drawn as the thick red curved arrow on the right of the figure): it loops control from the bottom of the body back toadd eax, edxat the top. Whenifinally reaches 0 the zero flag is set,jneis not taken, and control falls straight through toret— leaving the loop. - Trace:
sum=0,i=3→ add→sum=3, sub→i=2, back-edge taken. → add→sum=5,i=1, taken. → add→sum=6,i=0, not taken → fall through →ret.
Verify: . Final eax = 6. ✔
C6 — Decode the raw bytes by hand (variable length)
Steps
- First byte
48is a REX prefix. Why this step? In the range0x40–0x4f,48means "this operation uses 64-bit operands" (the parent noted48= 64-bit). It is a prefix, not a standalone op — the real opcode follows. - Next byte
83= opcode for "arithmetic with an 8-bit immediate" on a 32/64-bit register. Why this step?0x83is the compact form used when the constant fits in one signed byte. c0is the ModR/M byte selecting the operation (the/0field = ADD) and the register (rax).05is the immediate =5.- Assemble the meaning:
add rax, 0x5, total length 4 bytes. Why this matters: you cannot chop x86 into fixed 4-byte words like RISC — see CISC vs RISC. You must walk prefix → opcode → ModR/M → immediate.
Verify: The instruction length here is prefix(1) + opcode(1) + modrm(1) + imm8(1) =
4 bytes, and 0x05 decodes to decimal 5. ✔
C7 — Limiting case: the optimizer deletes it all
Steps
- Everything is constant. Why this step? The optimizer does constant folding: it
evaluates
2*3+4 = 10, then10*10 = 100at compile time. See Compiler Optimization Levels. 0x64is that folded constant. Why this step? . The entire function collapses to "load 100, return".- This is the limiting behaviour of optimization: when a result depends on nothing at runtime, the runtime work disappears entirely.
Verify: , , and . ✔
C8 — Real-world: crash address → source line
Steps
- The bug is division by zero. Why this step?
idiv esiwithesi = 0triggers a#DE(divide error) CPU exception — a hardware trap, not a memory segfault, but the OS surfaces it to your process. cdqbeforeidiv. Why this step?idivdivides the 64-bit value inedx:eax.cdqcopies the sign bit ofeaxacross all ofedxso a signed divide is correct.- The faulting instruction is at
0x401133(idiv). Why is the crash line printed as0x40113a? Precisely: on a#DEfault the CPU pushes the faultingripitself (0x401133) onto the kernel stack — the saved instruction pointer points at theidiv, not past it. But your program's own signal/crash reporter didn't print that rawrip; it printed the symbol boundary it resolved the address into.0x40113ahere is the start of the next symbol — functions are commonly padded to a 16-byte boundary, so the linker left gap bytes betweendivide(ending at0x401135) and the next function at0x40113a. A naive reporter that rounds "which function am I nearest to" upward, or that reads a stale return address off the stack, lands on that padded boundary. - The reliable method: never trust a rounded address. Find the largest instruction
address ≤ the reported address that lies inside a real function.
0x401133(idiv) is the last real instruction ≤0x40113ainsidedivide, so that is the culprit. Confirm the liveripin Debugging with GDB rather than trusting the printed line.
Verify: idiv is at 0x401133; the padding gap up to the next symbol is
bytes (after ret), and is undefined → divide error. ✔
C9 — Exam twist: same brackets, two meanings
Steps
[rdi+rsi]is an address expression:100 + 4 = 104. Why this step? Square brackets in Intel syntax mean "compute this address".mov eax, [rdi+rsi](A) dereferences: go to address104, load the value stored there. Why this step?movwith a bracket source reads memory. Soeax = 77.lea eax, [rdi+rsi](B) does not touch memory — it stores the address itself. Why this step?lea= "load effective address"; it hands you104as a number. The optimizer abuses this as a free adder (parent's-O2 addexample). Soeax = 104.- The trap: identical brackets, opposite behaviour — one reads RAM, one computes a number.
Verify: (A) loads memory[104] = 77. (B) computes 100 + 4 = 104. ✔
C10 — Tail call: a jmp where you expected call
Steps
add edi, 0x1prepares the argument. Why this step?ediholds arg 1 (System V convention).helper(a+1)needsa+1inedi, so the compiler increments it in place.- The last instruction is
jmp helper, notcall helper. Why this step? This is a tail call:wrapper's only remaining act is to return whateverhelperreturns. Instead ofcall helper(which pushes a return address) thenret, the compiler reuseswrapper's own return address — itjmps straight intohelper. Whenhelperruns itsret, it returns directly to wrapper's caller, skippingwrapperentirely. - Why no
rethere? There's nothing left to do afterhelperreturns, sowrapperhas no epilogue at all. The stack frame count stays flat — this is the essence of tail-call optimization. - Reading rule: a
jmpto another function's label at the end of a function is not a loop and not a bug — it is a tail call. The return value flows through untouched.
Verify: wrapper(a) returns helper(a+1). For a = 7, edi becomes 8, and the
result is whatever helper(8) yields — wrapper adds nothing on top. So the observable
result equals helper(8), i.e. the +1 is the only transformation. ✔
Question — setg on 0xffffffff (as signed int > 3)?
-1 > 3 is false.Question — seta on 0xffffffff (as unsigned > 3)?
4294967295 > 3 is true.Question — final eax for the down-counting loop 3+2+1?
6.Question — decode 0x64 in -O2 constant-folded compute?
Question — which of mov/lea on [rdi+rsi] reads RAM?
mov reads RAM; lea only computes the address number.Question — jmp helper at the end of a function with no ret?
helper, which returns straight to the original caller.Connections
- 5.3.17 Disassembly — objdump, reading assembly (Hinglish) — the parent topic, read alongside.
- Calling Conventions (System V x86-64) — why args land in
edi/esiand results ineax. - Stack Frames & The Stack Pointer — the prologue/epilogue in C4 and the tail call in C10.
- Compiler Optimization Levels — constant folding in C7,
leareuse in C9, tail calls in C10. - Debugging with GDB — confirm the live
ripin the C8 crash. - CISC vs RISC — why C6's bytes are variable length.