100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Debugging Assembly with GDB

Practical GDB workflows for stepping through raw instructions, inspecting registers and memory, and setting breakpoints in assembly-level code.

System InteractionBeginner8 min readJul 10, 2026
Analogies

Why Source-Level Debugging Isn't Enough

GDB's default behavior assumes source-level debugging: it steps by source line, shows local variables by name, and hides the underlying instructions. When you're debugging hand-written assembly, or chasing a bug that only manifests in a compiler's generated machine code, you need to switch GDB's mental model down a level — stepping by individual instructions rather than source lines, and reading raw register and memory state rather than named variables. The key command for this is layout asm (or layout regs for a combined register-and-code view), which switches GDB's text-mode UI into a disassembly view showing the actual instruction stream with the current instruction pointer highlighted.

🏏

Cricket analogy: It is like watching a match on a highlights package that shows only the boundaries and wickets (source-level debugging) versus watching the full ball-by-ball footage to see exactly how the bowler's wrist position changed before a crucial delivery (instruction-level debugging).

Stepping and Inspecting State

At the instruction level, stepi (si) and nexti (ni) replace step and next — si executes exactly one machine instruction, stepping into calls, while ni executes one instruction but steps over calls, running the entire called function before stopping again. info registers dumps the full general-purpose register set (RAX, RBX, RDI, RIP, RFLAGS, and so on) in one shot, while print $rax or p/x $rax prints a single register, and x/4xg $rsp examines memory at RSP as four hexadecimal giant (64-bit) words — the x command's format letters (x for hex, d for decimal, i for disassembled instruction) combined with size letters (b byte, h halfword, w word, g giant) give precise control over how raw memory is displayed.

🏏

Cricket analogy: It is like a slow-motion replay system where 'stepi' rewatches a single delivery in full, including any DRS review that follows it, while 'nexti' skips straight past the DRS review process to the next ball, only showing the final outcome.

Breakpoints, Watchpoints, and ASLR

break *0x401234 sets a breakpoint at a raw address rather than a source line, which is essential in stripped or hand-written binaries with no symbol table; watch *(int*)0x404050 sets a hardware watchpoint that halts execution the instant that memory location's value changes, invaluable for catching exactly which instruction corrupts a variable when the culprit isn't obvious from reading the code. Because modern Linux binaries are typically compiled as position-independent executables and loaded at a randomized base address (ASLR), raw addresses from a previous run, a disassembly listing, or objdump output will not match the addresses GDB sees at runtime unless you either disable ASLR for the debug session (set disable-randomization on, GDB's default) or compute offsets relative to the load base using info proc mappings.

🏏

Cricket analogy: It is like setting a fixed camera to trigger recording the instant the ball crosses a specific stump-height threshold (a watchpoint), rather than the vaguer instruction 'watch for something unusual happening near the stumps.'

shell
$ gdb ./a.out
(gdb) layout asm
(gdb) break *0x401136
(gdb) run
Breakpoint 1, 0x0000000000401136 in _start ()
(gdb) info registers rax rdi rsi rdx
rax            0x1      1
rdi            0x1      1
rsi            0x404020  4212768
rdx            0x16     22
(gdb) x/4xg $rsp
0x7fffffffe420: 0x0000000000000000  0x00007fffffffe530
0x7fffffffe430: 0x0000000000000001  0x0000000000401136
(gdb) watch *(long*)0x404020
Hardware watchpoint 2: *(long*)0x404020
(gdb) stepi
0x0000000000401140 in _start ()
(gdb) x/5i $rip
=> 0x401140 <_start+10>:  mov    edx,0x16
   0x401145 <_start+15>:  syscall
   0x401147 <_start+17>:  mov    eax,0x3c
   0x40114c <_start+22>:  xor    edi,edi
   0x40114e <_start+24>:  syscall

GDB's TUI layout regs (rather than plain layout asm) shows both the register bank and the disassembly simultaneously, with registers that changed since the last step highlighted, which is often faster for spotting exactly what a single instruction did than switching back and forth between 'info registers' and the code view.

By default, GDB disables ASLR for the process it launches (set disable-randomization on), but if you instead attach to an already-running process with gdb -p <pid>, that process was almost certainly loaded with ASLR active. Addresses you see with info proc mappings in that session are only valid for that specific run and will differ the next time the program is launched.

  • layout asm and layout regs switch GDB's TUI to instruction-level views necessary for debugging hand-written or stripped assembly.
  • stepi (si) steps a single instruction into calls; nexti (ni) steps over calls, running the whole callee before stopping.
  • info registers shows the full register set; print/p and x with format/size letters (x, d, i / b, h, w, g) inspect specific values.
  • break *ADDRESS sets a raw-address breakpoint, essential when no symbol table or source line is available.
  • watch sets a hardware watchpoint that halts the instant a specific memory location's value changes.
  • ASLR randomizes load addresses in position-independent executables; GDB disables it by default for launched processes but not for attached ones.
  • info proc mappings shows the actual runtime load base, needed to translate static disassembly addresses to live addresses under ASLR.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#AssemblyLanguageStudyNotes#DebuggingAssemblyWithGDB#Debugging#Assembly#GDB#Source#StudyNotes#SkillVeris#ExamPrep