How GDB Inspects Memory, Stack Frames, and Registers

The GNU Debugger (GDB) is an indispensable tool for debugging C programs, providing deep visibility into a program's runtime environment at the lowest levels. This article explains how GDB accesses and examines raw memory addresses, analyzes the call stack and its associated frames, and monitors hardware CPU registers. By leveraging operating system debugging interfaces and debug symbols, GDB exposes execution states that are critical for diagnosing segmentation faults, memory corruption, and logic errors.

Inspecting Memory Addresses

GDB inspects the memory space of a running C process by interfacing directly with the operating system’s kernel, typically via system calls like ptrace on Linux or by reading from /proc/<pid>/mem. Because a C program operates in virtual memory, GDB reads the bytes at a specified virtual address and formats them according to user-defined specifications.

The primary command for viewing raw memory is examine, abbreviated as x. The syntax follows the structure x/[Count][Format][UnitSize] [Address]:

  • Format specifiers: Format output as hexadecimal (x), decimal (d), unsigned decimal (u), instruction/disassembly (i), character (c), or string (s).
  • Unit sizes: Specify bytes (b), halfwords/2 bytes (h), words/4 bytes (w), or giant words/8 bytes (g).

For example, to view 4 consecutive 32-bit words in hexadecimal starting at a pointer address ptr:

(gdb) x/4xw ptr

GDB can also dereference pointers and evaluate expressions directly using the print command (e.g., print *ptr or print buffer[0]). If debugging symbols compiled via the -g flag are present, GDB interprets data types according to the source definitions; without them, it relies purely on specified raw addresses and formats.

Inspecting Stack Frames

Each time a C function call occurs, an activation record known as a stack frame is pushed onto the call stack. A stack frame stores the return address, incoming function arguments, local variables, and the saved frame pointer (such as EBP on x86 or RBP on x86_64). GDB reconstructs this call sequence by interpreting the target architecture's Application Binary Interface (ABI) calling conventions and reading DWARF unwind tables embedded during compilation.

Key commands used to navigate and inspect stack frames include:

  • backtrace (or bt): Prints the entire execution chain from the currently active function back to main(). Each frame is assigned a numerical index, starting with frame 0 for the current execution point.
  • frame <number> (or f <number>): Shifts GDB's operational context to a specific frame. This changes the scope so that variable lookups apply to that specific function call.
  • info frame: Displays metadata regarding the selected frame, including its memory address, the addresses of the caller and callee frames, the current instruction pointer, and the saved registers.
  • info locals and info args: Dumps all local variables and function parameters associated with the current frame, resolving variable names and types via the symbol table.

Inspecting Register States

Registers represent the highest-speed storage locations residing directly inside the CPU. They hold critical execution state, such as the current instruction pointer (RIP/EIP), the stack pointer (RSP/ESP), base pointers, function arguments, and CPU condition flags.

When a program stops under GDB, the operating system saves the target process's CPU context. GDB accesses these snapshots using system requests (such as PTRACE_GETREGSET) and maps the raw data to the processor's register architecture.

Common commands for checking register states include:

  • info registers (or info reg): Outputs the values of all general-purpose registers in both hexadecimal and decimal representations.
  • info all-registers: Extends the output to include floating-point, SIMD (AVX/SSE), and system control registers.
  • print $<register_name>: Prints the value of a specific register using GDB's pseudo-variable syntax (e.g., print $rax or print/x $rsp).

By modifying register queries, developers can check flags (such as the Zero or Carry flags) to evaluate conditional branch behavior, examine the program counter to see the next instruction scheduled for execution, or manually track assembly-level argument passing as dictated by the system's ABI.