gdb Command: Tutorial & Examples

Freeze a running program, read its mind, and autopsy a crash that already happened.

What It Is

gdb — the GNU Debugger — is the tool that lets you stop a program mid-thought and ask it what it's doing. Set a breakpoint and execution halts on a chosen line, frozen in place, while you read every variable, walk the call stack, and step forward one statement at a time. It's been the standard Unix debugger since the 1980s, it ships in every distro's toolchain, and it speaks C, C++, Rust, Go, Fortran, and assembly with equal calm.

Here's the thing that makes it click, and it's the whole reason this page exists: a debugger does two jobs that sound like science fiction. First, it can pause a living process and read its mind — where it is, what it's holding, who called whom to get there. Second — and this is the one that feels like cheating — it can take a core dump, a snapshot of a program at the instant it died, and let you walk through the corpse as if it were still warm. Your service segfaulted at 3am, nobody was watching, and gdb will still tell you the exact line and the exact bad value. By the end of this page you'll do both without flinching, and you'll understand the machinery underneath — the stack, the frames, the symbols — far better than the average programmer who's only ever printed console.log.

You don't need to be a C wizard to follow along. If you can read a stack trace in your favorite language, you already have the instinct. gdb just gives you that instinct for the program while it's running, instead of after it's printed something.

Your First Look

The honest starting point: a program that crashes. Here's a tiny C program with a deliberate bug — it dereferences a null pointer, four function calls deep, the way real crashes always seem to happen somewhere annoyingly far from main:

int compute(int *p) {
    return *p + 1;   // dereference whatever we're handed
}
int level3(int *p) { return compute(p); }
int level2(int *p) { return level3(p); }
int level1(int *p) { return level2(p); }

int main(int argc, char **argv) {
    int x = 41;
    int *bad = NULL;          // the bug
    int r = level1(bad);      // boom
    return 0;
}

Compiled with debug symbols (gcc -g) and run under gdb, here's what it tells us — this is real output, not a sketch:

$ gdb -batch -ex run -ex bt ./crashme

Program received signal SIGSEGV, Segmentation fault.
0x0000555555555145 in compute (p=0x0) at crashme.c:5
5	    return *p + 1;   // dereference whatever we're handed
#0  0x0000555555555145 in compute (p=0x0) at crashme.c:5
#1  0x0000555555555164 in level3 (p=0x0) at crashme.c:8
#2  0x000055555555517e in level2 (p=0x0) at crashme.c:9
#3  0x0000555555555198 in level1 (p=0x0) at crashme.c:10
#4  0x00005555555551dd in main (argc=1, argv=0x7fffffffdaa8) at crashme.c:16

Read that bottom-up and it's a complete story: main called level1, which called level2, level3, and finally compute — which tried to read *p where p=0x0 (null), and the kernel killed it with SIGSEGV. The crash is on line 5, the bad value (p=0x0) is printed right there, and you didn't add a single print statement. That five-line block is what gdb is for. Everything else on this page is variations on getting that block, and getting more out of it.

How I Use It

When something crashes, here's the exact sequence in my head — and it's shorter than people expect.

First, I get a backtrace. Whether the program is crashing live, attached, or I'm holding a core dump, the first word I type is bt (backtrace). It answers the only question that matters at the start: where was the program when it died, and how did it get there? The bottom frame is main; the top frame (#0) is where it actually blew up. I read it like a chain of accusations.

Second, I move to the frame I care about and look around. gdb starts you at frame #0 (the crash site), but the bug is often in a caller — somebody passed a bad value down. frame 4 jumps me to main, and now print bad or info locals shows me the variables as they were in that function's world. This is the move that separates people who use gdb from people who run it once and give up: the stack is a stack of worlds, and you can step between them.

Third, if I can't reproduce it from a corpse, I set a breakpoint and watch it happen. break compute, then run, and the program races to that line and stops, alive, waiting. Now I print whatever I want, step forward one line, continue to the next hit. I'm no longer reading a photograph of the crime — I'm standing in the room before it happens.

That's the whole craft in three moves: bt for the map, frame + print to inspect any caller's world, break + run to catch it live. Let's make each one bulletproof.

The Core Dump: An Autopsy of a Crash That Already Happened

This is the genuinely magical part, and the reason a debugger earns its keep on a server and not just a laptop.

When a program crashes, the kernel can write out a core dump — a complete snapshot of the process at the instant of death: every byte of its memory, every register, the full call stack. (The name is wonderfully old: 1950s memory was woven from tiny magnetic cores, so a "core dump" literally meant dumping the contents of core memory. Seventy years later the hardware is gone and the word survives — it is, like much of Unix, a fossil we kept because it still fits.) The point: nobody has to be watching. The crash happens, the core is written, and you debug it whenever you get around to it — an hour later, on a different machine, over coffee.

On a modern systemd box, crashes are captured automatically by systemd-coredump. You list them and pull one out:

$ coredumpctl list crashme
TIME                         PID  SIG     COREFILE EXE          SIZE
Wed 2026-06-03 23:49:45     2011319  SIGSEGV present  /tmp/crashme  19K

$ coredumpctl dump crashme -o /tmp/crashme.core

Then you hand the binary and the core to gdb — and it reconstructs the moment of death exactly:

$ gdb /tmp/crashme /tmp/crashme.core

Core was generated by `./crashme'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00005626d8ff7145 in compute (p=0x0) at crashme.c:5
5	    return *p + 1;   // dereference whatever we're handed

(gdb) bt
#0  compute (p=0x0) at crashme.c:5
#1  level3 (p=0x0) at crashme.c:8
#2  level2 (p=0x0) at crashme.c:9
#3  level1 (p=0x0) at crashme.c:10
#4  main (argc=1, argv=...) at crashme.c:16

Same backtrace, same bad value (p=0x0), same line 5 — except the program isn't running. It hasn't run for an hour. You're performing an autopsy and the corpse is telling you everything. The first time you debug a 3am crash from a core file the next morning, calmly, with the full stack in front of you, the whole job changes character. You stop fearing crashes you didn't witness.

Pro Tip

No core file appearing? The shell default is usually ulimit -c 0 — cores are disabled. Run ulimit -c unlimited (per-shell) before launching, or check where they're routed with cat /proc/sys/kernel/core_pattern — a value starting with |/usr/lib/systemd/systemd-coredump means systemd is catching them, and coredumpctl is how you get them back.

Breakpoints, Stepping, and Watching It Live

A core dump shows you the end. Breakpoints let you watch the middle. Here's the same program, but instead of letting it crash we stop it the moment it enters compute:

$ gdb -batch -ex "break compute" -ex run -ex "print p" -ex bt ./crashme

Breakpoint 1, compute (p=0x0) at crashme.c:5
5	    return *p + 1;
$1 = (int *) 0x0
#0  compute (p=0x0) at crashme.c:5
#1  level3 (p=0x0) at crashme.c:8
...

The program is paused and alive. print p shows 0x0 — we've caught the bad value before it gets dereferenced, one instruction before the crash. From here the controls are the whole interactive vocabulary:

  • continue (c) — let it run until the next breakpoint or the end.
  • next (n) — run the next line, stepping over any function calls (don't dive in).
  • step (s) — run the next line, stepping into function calls (dive in).
  • finish — run until the current function returns, and print what it returned.
  • until — like next, but skips past the end of a loop instead of crawling it iteration by iteration.

And print does far more than dump a variable. It's a tiny calculator and inspector wired into your program's live memory: print x+1, print argv[0], print *somestruct, even print myfunc(42) to call a function in the paused process and see what it returns. The $1, $2 it prints are value history — print $1 later recalls it.

Two breakpoint upgrades are worth learning early. A conditional breakpoint, break compute if p == 0, stops only when the condition is true — priceless inside a loop that runs a million times and you only care about iteration 999,999. And a watchpoint, watch myvar, stops the instant a variable's value changes, no matter which line did it. A watchpoint is how you catch "who on earth is corrupting this pointer?" — the single hardest bug class there is, and gdb will name the exact line that touched it.

Attaching to a Running Process

You don't always have the luxury of starting the program under gdb. Sometimes it's already running — hung, spinning at 100% CPU, or just misbehaving — and you need to look inside without restarting it. That's attach:

$ gdb -p 4242          # attach to PID 4242, freezing it
(gdb) bt               # where is it stuck right now?
(gdb) thread apply all bt   # backtrace for EVERY thread
(gdb) detach           # let it go, unharmed

The instant you attach, the process freezes — every thread stops dead. That's your chance to get a bt (or thread apply all bt for a multi-threaded server, which is how you catch a deadlock: two threads each waiting on a lock the other holds, plainly visible in two stacks). When you're done, detach and it carries on as if nothing happened. This is the single best way to answer "why is this server pegged at 100% and not responding?" when restarting it would destroy the evidence.

Note

If gdb -p says Operation not permitted even as the right user, you've met Yama ptrace_scope, a kernel hardening knob. cat /proc/sys/kernel/yama/ptrace_scope returning 1 means a process may only be traced by its own parent — a deliberate defense so malware can't read another process's memory. 0 is wide open. Both gdb and strace ride the same underlying ptrace syscall, so this gate applies to both. On a server you'll typically debug as root, which sails past it.

Symbols: Why Sometimes You Get Names and Sometimes ??

Here's the thing nobody explains until you've already been burned by it. Run the exact same crash on a binary that's been stripped, and gdb gives you this instead:

Program received signal SIGSEGV, Segmentation fault.
0x0000555555555145 in ?? ()
#0  0x0000555555555145 in ?? ()
#1  0x0000555555555164 in ?? ()
#2  0x000055555555517e in ?? ()
#3  0x0000555555555198 in ?? ()

Same crash, same addresses — but the function names are gone, replaced by ??. The stack structure is still there (the addresses are real), but the human-readable layer is missing. That layer is the symbol table: a map from raw memory addresses to function names, variable names, and source line numbers. It's what gcc -g bakes into the binary, and what strip (or a release build) throws away to make the file smaller.

So the rule that saves you hours: to debug well, you need the symbols. Three ways they show up:

  • Compiled with -g — names, source lines, locals, everything. The dream.
  • Compiled without -g but not stripped — function names, no source lines. Workable.
  • Stripped — ?? all the way down. You can still get raw addresses, but you'll be reading assembly.

On production binaries the symbols often live in a separate .debug file (a "debuginfo" package your distro ships separately) precisely so the shipped binary stays small. gdb will find and load them automatically if they're installed — which is why "install the -dbg/-dbgsym package" is step zero of debugging a packaged service. Without symbols, even the perfect core dump just says ??.

The Flags, Explained

gdb is mostly driven interactively, but the command-line flags set up the session, and a few are essential:

  • gdb ./prog — load a program, ready to run.
  • gdb ./prog core — load a program and a core file: the autopsy mode above. (Or gdb ./prog --core=core.)
  • gdb -p PID / gdb --pid PID — attach to a running process.
  • gdb --args ./prog arg1 arg2 — load a program with its command-line arguments, so run passes them. The clean way to debug something that needs args.
  • gdb -ex "CMD" — run a gdb command on startup; repeatable. -ex run -ex bt is the classic "crash and show me the stack" one-liner.
  • gdb -x script.gdb — run a file full of gdb commands. This is how you script debugging.
  • gdb -batch ... — run the -ex/-x commands, print output, and exit — no interactive prompt. The mode for capturing a backtrace inside a shell script or CI job. (-batch-silent suppresses even gdb's own chatter.)
  • gdb -q — quiet: skip the licensing banner on startup.
  • gdb -tui — the Terminal User Interface: a split-screen with your source code highlighted above and the command line below, the current line marked as you step. Toggle it live with Ctrl-x a. Most people never discover it; it makes stepping through code feel almost like an IDE.
  • gdb --nx — don't read any .gdbinit startup file (useful when a project's .gdbinit is doing something surprising).

Cheat Sheet

The interactive commands worth committing to muscle memory:

  • run (r) start · continue (c) resume · kill stop the inferior · quit (q) exit
  • break FILE:LINE / break func / break func if cond — set a breakpoint; tbreak for a one-shot
  • watch VAR — stop when a value changes · info breakpoints list · delete N remove
  • next (n) step over · step (s) step into · finish run to return · until past a loop
  • bt backtrace · bt full with all locals · thread apply all bt every thread · frame N jump to a frame · up / down walk frames
  • print EXPR (p) · p/x in hex · info locals · info args · info registers · x/16xw $sp examine raw memory
  • list (l) show source · ptype EXPR the full type · whatis EXPR the short type
  • set var x = 5 change a variable mid-run · return EXPR force a function to return early
  • Ctrl-x a toggle the -tui split-screen view

Inspecting Types and Memory

Two underrated commands turn gdb from "print a number" into "understand a data structure." whatis gives the short type, ptype expands it fully — here on the program's own argv:

(gdb) whatis x
type = int
(gdb) ptype argv
type = char **

For a struct, ptype mything prints every field and its type — invaluable when you're staring at someone else's code and print mything dumped a wall of nested braces. And x ("examine") reads raw memory at any address in any format: x/16xb &buf shows 16 bytes in hex, x/s ptr reads a C string, x/4i $pc disassembles the next 4 instructions. When you're past ?? territory and into bare assembly, x and info registers are how you navigate.

How You'll Actually Use It

Three real shapes, in rough order of how often they come up on a server:

  1. A service crashed; I have a core. coredumpctl dump myservice -o /tmp/c && gdb /usr/bin/myservice /tmp/c -ex bt. Read the stack, find the frame in our code (skip the libc frames), frame N, print the suspicious variable. Ninety percent of crash investigations end here.
  2. A process is hung or pegged. gdb -p $(pgrep myservice) -ex 'thread apply all bt' -ex detach -batch. One non-destructive snapshot of every thread's stack — usually the deadlock or the hot loop is obvious — and the process keeps running.
  3. I can reproduce the bug and need to watch it. Interactive: gdb --args ./prog --flag, break the_suspect_function, run, then print and step until I see the value go wrong.

If you find yourself reaching for gdb to trace which system calls a program makes (file opens, network connects), that's not a gdb job — that's strace. And if the bug is memory corruption you can't pin down — a use-after-free, a buffer overrun — valgrind will catch the exact bad access, and it even hands the frozen program to gdb for you. Different tools, one investigation.

Gotchas

  • No symbols, no joy. ?? in the backtrace means the binary was stripped or you're missing the debuginfo package. Install -dbgsym/-debuginfo before you need it.
  • Optimized code lies a little. A release build (-O2) inlines functions and reorders lines, so the debugger may show <optimized out> for a variable or jump between lines oddly. For real debugging, rebuild with -O0 -g if you can.
  • attach freezes the process. The moment you attach, the program stops — every thread. On a live production service that's a real pause. Get your bt, then detach promptly.
  • The crash site isn't always the bug. Frame #0 is where it died; the bad value was often set by a caller. Walk up the stack with frame / up — the bug usually hides a frame or two above the corpse.
  • ptrace_scope blocks attach. Operation not permitted despite correct user → check /proc/sys/kernel/yama/ptrace_scope (see the Note above).

History & Philosophy

gdb was written by Richard Stallman in 1986 as part of the GNU project — the same effort that gave us gcc, bash, and the userland that wraps the Linux kernel. It's one of the oldest pieces of software you still use daily without noticing, and it has accreted four decades of features: reverse debugging (yes — record then reverse-step to run your program backwards through time), Python scripting of the debugger itself, remote debugging of a target board over a serial cable, and support for dozens of languages and architectures.

The deep idea worth carrying away is how any of this is possible at all. A debugger seems like it must be doing something supernatural — reaching inside a sealed, running program. It isn't. Underneath, gdb uses a single kernel facility called ptrace ("process trace"), which shipped in Version 6 Unix in 1975. ptrace lets one process become the parent-inspector of another: peek and poke its memory, read its registers, stop it, single-step it, intercept its signals. That's the whole trick. A breakpoint is gdb quietly overwriting one instruction in the target with a special trap instruction (int3 on x86); when execution hits it, the kernel stops the process and notifies gdb, which restores the real instruction, lets you poke around, then puts the trap back. The "magic" of pausing a program mid-flight is one byte, swapped out and swapped back, thousands of times a second, faster than you can perceive. The same ptrace powers strace, ltrace, and every debugger on the system — they're all different faces over the one 1975 syscall that lets a process hold another process still and look. Once you've seen that, the box stops being sealed.

See Also

  • strace — trace system calls instead of stepping code
  • valgrind — catch memory corruption, then hand the program to gdb
  • ptrace — the 1975 kernel syscall every debugger is built on
  • kill — send signals to processes; SIGQUIT leaves a core for gdb
  • ulimit — enable core dumps with ulimit -c unlimited
  • gcc — compile with -g so gdb has symbols
  • segfault — the crash class gdb exists to explain
  • software crash — diagnosing a service that won't stay up
  • signal — SIGSEGV, SIGABRT, SIGQUIT and what triggers a core

A service crashed at 3am and nobody saw it — now what?

CleverUptime watches your processes restart, flags the service that's flapping, and points you at the core dump so you can autopsy the crash with gdb the next morning instead of guessing.

Want to see your own server's health right now? One command, no signup, no install.

Check your server →