Debugging & Disassembly
The Debugging Toolkit
Four tools, all shipped with binutils, cover almost everything. You do not need an IDE to understand a crash.
| Tool | Question it answers | Typical use |
|---|---|---|
objdump | What instructions did I actually produce? | objdump -d program |
readelf | How is the file structured, and where does it load? | readelf -S program |
nm | What symbols exist, and at which addresses? | nm program |
gdb | What are the registers and memory now? | gdb ./program |
The workflow is divide and conquer. objdump tells you whether the code is what you meant to write; gdb tells you whether the values at run time are what you expected. Almost every assembly bug answers one of those two questions.
Assemble with debug information whenever you plan to use GDB:
nasm -f elf64 -g -F dwarf program.asm -o program.o # -g embeds DWARF debug info
ld program.o -o program
gdb ./program
Using GDB
Starting a Session
Reassemble with debug information first; otherwise GDB only knows addresses.
nasm -f elf64 -g -F dwarf hello.asm -o hello.o
ld hello.o -o hello
gdb ./hello
# Inside GDB — make it speak the syntax of this tutorial:
(gdb) set disassembly-flavor intel
(gdb) layout asm # optional: split-screen disassembly TUI
(gdb) break _start
(gdb) run
The Commands You Actually Need
| Command | Effect |
|---|---|
break _start | Set a breakpoint at a symbol (also accepts *0x401000) |
run | Start the program (restarts it if already running) |
si | Step one instruction, following call into the callee |
ni | Step one instruction, treating call as a single step |
info registers | Show every register, including rflags |
info registers rax rdi | Show only the named registers |
x/8xw &buffer | Examine 8 words of memory in hex at buffer |
x/16cb msg | Examine 16 bytes in decimal and as characters |
x/s msg | Examine memory as a C string |
disassemble /r _start | Show code with the raw bytes alongside |
p/x $rax | Print a register in hex |
continue / c | Resume until the next breakpoint |
quit / q | Leave GDB |
A useful session looks like this: break at _start, ni through the setup, then info registers rax rdi rsi rdx right before the syscall. You are checking, by hand, that every argument holds what you intended — and nine times out of ten that is enough to find the bug.
Watching the Flags
Bugs in conditional jumps are almost always flag problems, and GDB shows the flags as a decoded string:
(gdb) info registers eflags
eflags 0x202 [ IF ]
# Decoding the interesting bits:
# ZF (bit 6) zero -> set when the result was 0
# SF (bit 7) sign -> copy of the result's top bit
# OF (bit 11) overflow -> signed result out of range
# CF (bit 0) carry -> unsigned result carried out
If a jle does not behave as expected, print eflags there and check SF and OF. If a jbe misbehaves, check CF and ZF instead — the signed and unsigned families read different bits.
Reading Disassembly
Before GDB, there is objdump: a static view of everything the assembler produced. It answers "did I really write that?" without running anything.
# Disassemble the .text section (your code)
objdump -d hello
# Section headers and where each one will be loaded
readelf -S hello
# Symbols (labels and functions) with their addresses
nm hello
# Program headers: entry point, segment permissions, layout
readelf -l hello | head -30
; Sample objdump -d output for the hello program
0000000000401000 <_start>:
401000: b8 01 00 00 00 mov eax,0x1 ; mov rax, 1
401005: bf 01 00 00 00 mov edi,0x1 ; mov rdi, 1
40100a: 48 be 00 20 40 00 movabs rsi,0x402000 ; mov rsi, msg (absolute!)
401011: 00 00 00
401014: ba 06 00 00 00 mov edx,0x6 ; msg_len = 6
401019: 0f 05 syscall
Read it carefully: the mnemonic is spelled in Intel order (mov dst, src), and the byte column on the left is the machine code the CPU actually executes. Here mov rsi, msg became a full 64-bit absolute address (movabs) — exactly the position-dependent code that default rel is meant to avoid.
Two habits make disassembly readable. Always add set disassembly-flavor intel in GDB so the output matches the syntax you write; and remember that one source line can become several instructions (a 64-bit immediate load) or none at all (an equ directive).
Common Bugs and How to Find Them
| Symptom | Likely cause | How to confirm |
|---|---|---|
| Segmentation fault immediately | Wrong pointer, or a bad syscall argument | Break before the crash, print rdi/rsi/rax |
| Crashes in the caller after a return | Unbalanced stack: ret popped the wrong value | Break at ret; compare rsp on entry and exit |
| Floating point exception (integer code) | Division by zero, or a missing cdq/cqo | Print rdx and rax before the div |
| Loop runs once too many or too few | Wrong conditional mnemonic, or signed vs unsigned | Print the counter and the flags at the branch |
| Output is garbage text | Wrong byte count in write, or missing terminator | Check rdx against the string length |
| Works in GDB, fails outside it | Relying on zeroed registers or memory | Initialize every register you read |
The last row deserves attention. A debugger often leaves registers in a convenient state, hiding a bug that appears the moment the program runs normally. Treat every register as containing random garbage unless you wrote to it first.
A Debugging Recipe
- Shrink the input. Find the smallest case that still fails.
- Assemble with
-g -F dwarf. Now GDB can show your source lines. - Break at the suspect instruction and step over it, not into it.
- Compare expectation to reality. Print the registers you are sure about; usually one is not what you think.
- Bisect. If ten instructions are suspect, break in the middle — the bug is above or below.
Summary
objdump -dshows machine code as assembly;readelfandnmshow structure and symbols.- GDB essentials:
b,run,si,ni,info registers,x, andset disassembly-flavor intel. - Assemble with
-g -F dwarfso debug symbols refer to your source lines. - Disassembly spells operands in Intel order:
mov dst, src. - Most assembly bugs are one of four things: a bad pointer, an unbalanced stack, a wrong conditional, or uninitialized data.
Next: Assembly Flavors — the same ideas written for ARM64, RISC-V, and WebAssembly.