JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

gdb Basics: Breakpoints and Backtraces

Until now your only window into a running program was whatever it chose to print. A debugger pries that window wide open: you can freeze the program mid-flight, look at any variable, and ask the one question that ends most bug hunts — how did we even get here?

From printf to a real debugger

You climbed this whole ladder writing programs that talk to you only when you ask. You learned to scatter print statements, recompile, and read the trail of output — honest, useful, and the subject of a later guide on principled printf-debugging. But printf has a hard ceiling: every question you want to ask must be decided before you compile. If the program crashes somewhere you did not instrument, you are blind. A debugger lifts that ceiling. It is a separate program that runs your program for you and can stop it at any instant, inspect any byte of its memory, and resume it — all without changing a line of your source.

On Linux the classic tool is gdb, the GNU Debugger; on macOS the equivalent is lldb, with nearly the same ideas under slightly different command names. We will speak in gdb's vocabulary. The mental model is simple and worth holding firmly: gdb attaches to your process, becomes its supervisor, and from then on your program only moves when gdb lets it move. That single shift — from a program that races to the end to one you can pause and interrogate — is what this whole rung is built on, and it is the heart of any disciplined debugging process.

First: compile so the debugger can see

Here is the step almost everyone forgets the first time. The machine code in your executable is just numbered instructions; by default it carries no memory of your variable names, your line numbers, or which C statement produced which instruction. The compiler can attach that map, but only if you ask. You ask with the -g flag. Without it, gdb still runs your program, but it can only show you raw addresses and registers — it cannot say `the variable n is 7` or `you are on line 42`.

So the habit to build is: when you are about to debug, compile with debug information and turn the optimizer off. Use `gcc -g -O0 -Wall main.c -o prog`. The -g embeds the source map; -O0 disables optimization. Why kill the optimizer? Because at -O2 the compiler is allowed to reorder, merge, and delete code, so a single source line may not correspond to any single place in the binary — stepping jumps around bewilderingly and variables read as `<optimized out>`. Debug at -O0 first; only reach for higher optimization when you are specifically hunting a bug that only appears when optimized.

Breakpoints: freezing the program where it matters

A breakpoint is an instruction to gdb: *stop the program when it reaches this spot, and hand control to me.* You set one before running, then start the program; it sprints at full native speed until it touches the marked line, freezes there with everything intact, and waits for your questions. Under the hood gdb does something delightfully sneaky: it overwrites the first byte of the target instruction with a special trap, so when the CPU reaches it, a trap kicks back into gdb; then gdb quietly restores the real byte before letting you continue. You never see the swap — you just see the program stop exactly where you asked.

The everyday workflow fits on one hand. Launch with `gdb ./prog`. Set a breakpoint with `break main` (by function) or `break main.c:42` (by file and line). Run with `run` (abbreviated `r`). When it halts, look around: `print n` shows a variable, `info locals` dumps every local in view, `list` shows the surrounding source. Let it keep going to the next breakpoint with `continue` (`c`). That loop — set, run, look, continue — is 90 percent of all debugger use.

Two breakpoints earn their keep early. A conditional breakpoint fires only when a test is true: `break process.c:88 if i == 500` ignores the first 499 harmless iterations and stops exactly on the loop that misbehaves — priceless when a bug lives deep inside ten thousand passes. A watchpoint is its sibling, set with `watch total`: instead of a place in the code, it stops the program the instant a value changes, no matter which line did it. That is how you catch a variable being clobbered by code you never suspected.

Stepping: walking the program line by line

Once frozen at a breakpoint, you rarely want to let the whole program loose again. You want to advance one careful sip at a time and watch what changes. That is stepping, and the two commands you must not confuse are `next` and `step`. Both execute the current source line and stop on the following one — the difference is what they do when the current line calls a function. `next` (`n`) treats a function call as a single black box: it runs the whole call and stops after it returns. `step` (`s`) dives in, stopping on the first line inside the called function.

The rule of thumb is honest and simple: `next` over code you trust, `step` into code you suspect. If you `step` into a library call like printf() you will tumble into machine-level internals you did not want — so `next` past trusted calls and reserve `step` for your own suspicious functions. A third command, `finish`, says run until the current function returns and stop in the caller; it is the escape hatch when you stepped one level too deep. With just `next`, `step`, and `finish`, you can walk any execution at exactly the granularity your question needs.

Backtraces: how did we even get here?

This is the single most valuable thing gdb gives you, and it pays off the call stack you learned earlier. When a program is paused — at a breakpoint, or frozen at the moment it crashed — type `backtrace` (`bt`) and gdb prints the chain of function calls that led to this instant, innermost first. Each line is a stack frame: the function, its arguments, and the source line it is sitting on. Reading top to bottom, you see exactly how control flowed from main() all the way down to where you stand.

(gdb) bt
#0  parse_token (s=0x0) at parser.c:73
#1  next_field (line=0x55...e0) at parser.c:140
#2  read_record (f=...) at record.c:58
#3  main (argc=2, argv=...) at main.c:22

-> frame #0 is where it stopped; s=0x0 is a NULL pointer.
-> 'up' moves to #1, 'down' moves back toward #0.
A backtrace after a crash: read it top-down, and the argument s=0x0 in frame #0 names the culprit.

Look at the sketch. The program stopped in parse_token() because it was handed a null pointer (s=0x0) and tried to dereference it — a textbook segmentation fault. But the bug is almost never in the function that crashed; it is wherever that bad value was born. So you walk the stack: `up` to frame #1, inspect next_field's locals, `up` again, asking at each level `where did this value come from?` Frame by frame you climb from the symptom toward the line that actually created the wrong pointer. That climb is the whole art, and the backtrace is the map.

Notice how this closes the loop with everything you know about how a program runs. Each frame in that list is a literal region on the machine's stack, holding that call's return address and saved registers; gdb is just walking that chain of frames and translating raw addresses back into your names and line numbers using the -g map. The next guide turns this same skill on a program that has already died, reading its backtrace from a core dump long after the process is gone — but the move is identical: pause, `bt`, and climb from symptom to cause.

Putting it together on a real crash

Let the whole rhythm settle by walking one bug end to end. Suppose `./prog input.txt` dies with `Segmentation fault`. You have a symptom and nothing else. Here is the disciplined path from that one useless word to a fixed line of code — the same path you will reuse for almost every crash you ever meet.

  1. Rebuild with debug info and no optimization: `gcc -g -O0 -Wall prog.c -o prog`. Without -g the rest is half-blind.
  2. Run it under the debugger: `gdb ./prog`, then `run input.txt`. Let it crash inside gdb so the process is frozen at the fault instead of vanishing.
  3. When gdb stops at the fault, type `bt`. Read the backtrace top-down to find the exact frame and line where it died.
  4. In that frame, `print` the pointers and variables on the crashing line. Find the one that is 0x0 or garbage — that is the value that broke the rules.
  5. Walk `up` the stack frame by frame, printing as you go, until you reach the line that created the bad value. That line, not the crash site, is your bug.
  6. Fix the root cause, rebuild, and re-run under gdb to confirm the crash is gone and a clean run reaches the end.

Run that loop a few times and it stops feeling like a tool you operate and starts feeling like a sense you have. The crash word stops being a dead end and becomes a starting coordinate. And every later guide in this rung — core dumps, Valgrind, the sanitizers, strace — is the same instinct aimed at a different class of bug: stop the program where reality broke, ask what is actually true, and walk from symptom back to root cause. gdb is where that instinct is born.