5.3.15 · D4Build Systems & Toolchain

Exercises — GDB debugging — breakpoints, watchpoints, step, next, backtrace

3,516 words16 min readBack to topic

Level 1 — Recognition

(Can you name and identify the tools?)

Exercise 1.1

GDB inserts a single byte to implement a software breakpoint on x86. What is that byte (in hex), and what CPU event does it trigger?

Recall Solution 1.1

The byte is 0xCC — written in code as 0xCC — the encoding of the ==INT3== instruction. INT3 is the x86 single-byte software-interrupt (trap) instruction: a special one-byte opcode whose only job is to stop the CPU and hand control to a debugger. When the CPU executes it, it raises a software interrupt / trap, which the operating system delivers to GDB as the signal ==SIGTRAP==. What we did: recalled the breakpoint mechanism. Why it matters: a breakpoint is nothing magical — it is byte substitution + a trap. See Signals — SIGSEGV SIGTRAP for how SIGTRAP is routed.

Exercise 1.2

Match each command to its one-line job: break, watch, step, next, bt, finish.

Recall Solution 1.2
  • break — stop when execution reaches a line/function (a location trigger).
  • watch — stop when an expression's value changes (a data trigger).
  • step — run one source line and descend into any call.
  • next — run one source line but step over calls (run them fully).
  • bt — print the call stack (which function called which).
  • finish — run until the current function returns and print its return value.

Exercise 1.3

Look at the figure below. Which arrow (green or coral) is step, and which is next?

Figure — GDB debugging — breakpoints, watchpoints, step, next, backtrace

Figure description (in case the image does not load): two columns of boxes. The left column is the caller() function, with three stacked lines: "line X" (butter), "call helper()" (lavender), "line X+1" (butter). The right column is the body of helper() (two mint boxes). A green arrow leaves the "call helper()" box and curves rightward into the top of helper() — that is step (dive in). A coral arrow stays in the left column and drops straight from "call helper()" to "line X+1", never touching helper() — that is next (no-dive).

Recall Solution 1.3

The green arrow that dives inside helper() is step. The coral arrow that jumps over helper() and lands on the next line of caller() is next. Mnemonic: Next = No-dive.


Level 2 — Application

(Can you run the tool and compute with it?)

Exercise 2.1 — cost of a software watchpoint

A program runs source lines in . GDB falls back to a software watchpoint whose per-line trap+re-evaluate cost is . Compute the total watched runtime and the slowdown factor .

Recall Solution 2.1

Use the cost model from the parent note: What we did: the normal runtime is unavoidable; single-stepping adds one trap of cost to each of the lines, i.e. extra seconds. Lesson: a 0.4 s program becomes 100 s under a software watchpoint — this is why GDB prefers hardware watchpoints (see CPU debug registers DR0–DR7).

Exercise 2.2 — how many hardware watchpoints?

You want to watch four int variables (4 bytes each) and one double (8 bytes) simultaneously on x86. Debug registers DR0–DR3 give you 4 slots, each watching ≤8 bytes. Can GDB do all five in hardware at once? If not, what happens?

Recall Solution 2.2

No. There are only 4 hardware slots (DR0–DR3), but you asked for 5 watchpoints. Each variable (4 or 8 bytes) fits in one slot (both are ≤8 bytes), so slot size is not the limit — slot count is. GDB places the first 4 in hardware and the 5th falls back to a software watchpoint, which single-steps the whole program and re-reads the value every line. Because one trap costs far more than running one ordinary line (formally , where is the time for one line), the slowdown collapses to — the huge, single-step-dominated regime derived in 2.1. Fix: free a slot with delete, or narrow the scope so fewer are live at once. Check with info watchpoints — it prints hw (hardware) or sw (software) per entry, so you can see which kind you actually got.

Exercise 2.3 — reading a backtrace

Program received signal SIGSEGV, Segmentation fault.
#0  0x0040119a in strlen (s=0x0) at str.c:12
#1  0x004012f4 in greet (name=0x0) at greet.c:7
#2  0x00401340 in main () at main.c:3

Which frame contains the bug you should fix, and what is the bug?

Recall Solution 2.3

Frame #0 is merely where it diedstrlen dereferenced s = 0x0 (a NULL pointer). But strlen is a trusted library; it did nothing wrong. The bug lives one frame up, in #1 greet, which passed name = 0x0 onward. The real fix is wherever main (frame #2) obtained a NULL name and handed it to greet. What we did: we read the stack top-down and asked who supplied the bad value. See Stack frames & calling conventions for why each #n line is one caller up.


Level 3 — Analysis

(Can you reason about why the tool behaves as it does?)

Exercise 3.1 — step-count on next vs step

int sq(int x)      { return x * x; }        // line A
int total(int a) {
    int t = 0;                              // line 1
    for (int i = 0; i < a; i++)             // line 2
        t += sq(i);                         // line 3
    return t;                               // line 4
}

You are stopped at line 3 with a = 3. You press next repeatedly to finish the loop. How many times does line 3 appear, and does GDB ever stop inside sq?

Recall Solution 3.1

The loop runs for i = 0, 1, 23 iterations — so line 3 is executed 3 times, and next stops on it 3 times (once per iteration, since the loop body is a single line that GDB re-visits). GDB never stops inside sq, because next runs each call to completion at the current level. If instead you had pressed step on line 3, GDB would have descended into sq (landing on line A) on each of the 3 iterations. What we did: counted iterations from the loop bounds, then applied the no-dive rule of next.

Exercise 3.2 — why the software watchpoint slowdown is dominated by

In the model , the parent note claims . Show that this inequality is exactly what makes (drop the 1), and state in words what and physically represent.

Recall Solution 3.2

is the normal time to run one source line (total time ÷ number of lines). is the cost of one trap plus one re-evaluation of the watched expression. A trap saves/restores CPU state and hands control to GDB — hundreds to thousands of times more work than executing one ordinary line, so . Now, . The 1 is the original runtime's contribution. The second term equals , i.e. (trap cost) ÷ (per-line cost). Since , this ratio is huge (hundreds+), dwarfing the 1. Therefore . Sanity check with 2.1: there , so keeping the 1 gives vs dropping it gives — a difference, confirming the 1 is negligible.

Exercise 3.3 — <optimized out> at -O2

You debug a -O2 build and print result prints <optimized out>, yet result is clearly in the source. Explain precisely why, and give two fixes — one that keeps -O2 and one that abandons it.

Recall Solution 3.3

At -O2 the optimizer may keep result only transiently in a register (never in a stable memory slot), or eliminate it entirely by folding its value into later computations (constant propagation, common-subexpression elimination). The debug info then has no location to point print at, so GDB reports <optimized out>. This is not a bug in GDB — the variable genuinely has no observable storage at that instant. See Optimization levels -O0 -O2 -O3. Fix A (keep -O2, recover more locals): rebuild with richer variable-location debug info — GCC's -fvar-tracking and -fvar-tracking-assignments tell the compiler to track where each variable lives (register or memory) instruction-by-instruction, so GDB can follow a value even as the optimizer shuffles it between registers; adding -fno-omit-frame-pointer keeps a stable frame-pointer register so GDB can reliably locate frame-relative locals and walk the stack instead of guessing. These make some <optimized out> variables inspectable again without dropping optimization. (Even so, truly eliminated variables cannot be resurrected.) Fix B (abandon -O2): rebuild the file with -O0 -g so every source variable gets a real, inspectable memory location. This is the standard advice from Compilation & -g debug symbols.


Level 4 — Synthesis

(Can you combine tools to solve a real problem?)

Exercise 4.1 — design a session for a corruption bug

A global counter count should never exceed 100, but it does — somewhere, some code writes a bad value. You do not know which line. Design a GDB session (command sequence) that catches the exact write that pushes count past 100, and explain each command's role.

Recall Solution 4.1
$ gcc -g -O0 prog.c -o prog     # symbols + no optimization: count stays inspectable
$ gdb ./prog
(gdb) break main                # stop early so 'count' is in scope for a hw watchpoint
(gdb) run
(gdb) watch count               # data trigger: fire on ANY write to count
(gdb) continue                  # fast run (hardware) until count changes
(gdb) print count               # is it > 100 yet? if not, continue again
(gdb) bt                        # WHO wrote it? the stack names the culprit function

Why a watchpoint, not a breakpoint? You do not know the location; you only know the value misbehaves — that is exactly the data-trigger case. Why -O0? so count keeps a stable location and the watchpoint stays hardware and fast (§2.1, §3.3). Why bt after it fires? the stack instantly identifies the offending caller — no need to guess. You can even add (gdb) condition logic (watch count if count > 100) to skip the legal writes and stop only on the illegal one.

Exercise 4.2 — a recursive breakpoint puzzle

You set break factorial on the recursive factorial(int n) and run with factorial(5). Assuming no inlining, how many times does GDB stop at that breakpoint before returning to main, and what does bt show at the deepest stop?

Recall Solution 4.2

A breakpoint fires every time control reaches the function's entry. factorial(5) calls factorial(4) calls … calls factorial(1) (base case) — that is n = 5, 4, 3, 2, 1, i.e. 5 entries, so GDB stops 5 times. At the deepest stop (entering factorial(1)), bt shows the full chain, deepest frame first:

#0 factorial (n=1)
#1 factorial (n=2)
#2 factorial (n=3)
#3 factorial (n=4)
#4 factorial (n=5)
#5 main ()

That is 6 frames total (5 factorial + 1 main). What we did: counted recursive entries = number of distinct n values from 5 down to the base case, and used the fact that each un-returned call adds one stack frame (Stack frames & calling conventions).


Level 5 — Mastery

(Can you reason at the boundary — degenerate cases, overhead trade-offs, tool choice?)

Exercise 5.1 — overhead break-even: watchpoint vs. instrumented print

You must observe count changing. Option A: software watchpoint, slowdown over the whole -line run. Option B: manually add a printf inside the one function that writes count, which executes times, each print costing seconds. Give the runtimes and the condition (inequality) under which the manual print is faster.

Recall Solution 5.1
  • Option A (software watchpoint): .
  • Option B (manual print at the single write site): . The base run is untouched (no single-stepping); you only add prints of cost each. The manual print is faster when : Interpretation: because a write site is usually hit far fewer times than the program has lines () and a buffered printf is far cheaper than a trap (), the left side is tiny — the manual print almost always wins on speed. The watchpoint wins on convenience and coverage (it catches writes from any site, even ones you forgot). Numeric check: with , (from §2.1, s) versus prints at s: s s — the print is cheaper here.

Exercise 5.2 — degenerate & boundary cases

For each scenario, state what GDB does and why: (a) break file.c:999 where the file has only 40 lines. (b) watch x where x just went out of scope (function returned). (c) next pressed on the program's very last line of main. (d) A hardware watchpoint on a char (1 byte) — does it use a full 8-byte slot?

Recall Solution 5.2

(a) No line 999 exists. GDB reports it cannot place the breakpoint there (or places it at the nearest pending/valid line and warns). Nothing runs into a phantom line — the breakpoint simply never fires. Boundary lesson: a location trigger with no valid location is inert. (b) When x leaves scope, the watched storage is gone. GDB automatically deletes the watchpoint and prints a message like Watchpoint N deleted because the program has left the block…. This is why the parent note warns to watch a live, in-scope variable. (c) next on main's last line runs it, then main returns → the program exits. GDB reports [Inferior … exited normally]; there is no "next line" to stop on. Degenerate end-of-program case. (d) No. Debug registers watch a range; a 1-byte char occupies a 1-byte watch, not a full slot's 8 bytes — the ≤8-byte limit is a maximum per slot, not a minimum. You still consume one of the four DR0–DR3 slots, though (count is the scarce resource, per §2.2). See CPU debug registers DR0–DR7.

Exercise 5.3 — choose the right tool

You have a program that (i) crashes with SIGSEGV only after ~2 hours, (ii) the crash address changes run-to-run, and (iii) bt shows the death inside free(). Which tool do you reach for, and why is a plain GDB watchpoint the wrong first move?

Recall Solution 5.3

The symptoms — non-deterministic crash address and death inside free() — scream heap corruption (a write past a buffer, a double-free, or a use-after-free somewhere earlier that only detonates at free()). A GDB watchpoint is the wrong first move because:

  1. You do not know which address is corrupted (it changes each run), so you have nothing concrete to watch.
  2. A software watchpoint over a 2-hour run would inflate to an impossible duration (, §3.2).

Right tool: run under AddressSanitizer (-fsanitize=address) or Valgrind, which instrument every heap access and report the corruption at the moment it happens, with the allocating and freeing stacks — turning a 2-hour needle-hunt into an immediate, precise report. See Memory debugging — Valgrind & AddressSanitizer and Signals — SIGSEGV SIGTRAP. Only after they localise the bad write do you return to GDB with a concrete address to watch.


Recall Self-test checklist

Software-watchpoint slowdown formula ::: Why the 1 is negligible ::: because , so dominates. Number of x86 hardware watchpoint slots ::: 4 (DR0–DR3), each ≤8 bytes. Which frame usually holds the real bug on a crash ::: one (or more) frames up from #0, i.e. the caller that supplied the bad value. Right tool for non-deterministic heap corruption ::: AddressSanitizer or Valgrind, not a plain GDB watchpoint.