Debugging & Disassembly

Assembly has no type checker, no bounds checking, and no stack traces by default. When something is wrong, you read the machine directly — one instruction at a time, with the registers in front of you.

The Debugging Toolkit

Four tools, all shipped with binutils, cover almost everything. You do not need an IDE to understand a crash.

ToolQuestion it answersTypical use
objdumpWhat instructions did I actually produce?objdump -d program
readelfHow is the file structured, and where does it load?readelf -S program
nmWhat symbols exist, and at which addresses?nm program
gdbWhat 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

CommandEffect
break _startSet a breakpoint at a symbol (also accepts *0x401000)
runStart the program (restarts it if already running)
siStep one instruction, following call into the callee
niStep one instruction, treating call as a single step
info registersShow every register, including rflags
info registers rax rdiShow only the named registers
x/8xw &bufferExamine 8 words of memory in hex at buffer
x/16cb msgExamine 16 bytes in decimal and as characters
x/s msgExamine memory as a C string
disassemble /r _startShow code with the raw bytes alongside
p/x $raxPrint a register in hex
continue / cResume until the next breakpoint
quit / qLeave 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

SymptomLikely causeHow to confirm
Segmentation fault immediatelyWrong pointer, or a bad syscall argumentBreak before the crash, print rdi/rsi/rax
Crashes in the caller after a returnUnbalanced stack: ret popped the wrong valueBreak at ret; compare rsp on entry and exit
Floating point exception (integer code)Division by zero, or a missing cdq/cqoPrint rdx and rax before the div
Loop runs once too many or too fewWrong conditional mnemonic, or signed vs unsignedPrint the counter and the flags at the branch
Output is garbage textWrong byte count in write, or missing terminatorCheck rdx against the string length
Works in GDB, fails outside itRelying on zeroed registers or memoryInitialize 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

  1. Shrink the input. Find the smallest case that still fails.
  2. Assemble with -g -F dwarf. Now GDB can show your source lines.
  3. Break at the suspect instruction and step over it, not into it.
  4. Compare expectation to reality. Print the registers you are sure about; usually one is not what you think.
  5. Bisect. If ten instructions are suspect, break in the middle — the bug is above or below.

Summary

  • objdump -d shows machine code as assembly; readelf and nm show structure and symbols.
  • GDB essentials: b, run, si, ni, info registers, x, and set disassembly-flavor intel.
  • Assemble with -g -F dwarf so 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.