---
title: 'Using a Debugger'
source: 'https://academia.sh/en/courses/c-systems-programming/using-a-debugger'
course: 'Systems Programming with C'
language: en
updated: '2026-09-26T03:02:44+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Models a breakpoint, a watchpoint, and a conditional breakpoint in C'
    - 'Measures which values each of the three stopping mechanisms actually observes'
    - 'Explains why a core dump can only read a stopped process, never a live one'
---

# Using a Debugger

Measures, by modeling breakpoints and watchpoints in C, why a breakpoint never sees an intermediate value between steps, and why a watchpoint sees that same value.

When a test fails, which input deviated from what was expected, and at which step, is
unknown — only the result is known. A debugger makes a running program's intermediate
values visible, either by stopping it or by watching it. But "making visible" doesn't
carry a single meaning: **what** a debugger sees depends on how it was told to stop.

The previous lesson showed that a test failed, but not why it failed. If an assertion
fails, the only thing known is that the condition was false; at which step the value
feeding that condition got that way never shows up in the test's own output. A debugger
fills that gap, either by stopping the program at a chosen point or by watching every
change to a variable. Even though both approaches observe the same running process,
which steps get seen depends on the strategy — and that difference directly determines
which value gets seen and which gets missed while hunting a bug.

This lesson compares three stopping strategies: one that stops by location (a
breakpoint), one that stops on every change (a watchpoint), and one that stops by a
state (a conditional breakpoint). All three observe the same running process, but each
was built to answer a different question — "what happens once this step is reached,"
"when does this value change," and "when does this state occur." A fourth tool, the
core dump, comes into play at a completely different time: while the process is no
longer running, it reads the state it left behind. What the four tools share is that
all of them answer the same underlying question — what was the program's internal
state at some moment — under different conditions.

## Breakpoint and Watchpoint

The model below defines a function that writes three values into a variable across
three steps, in order. This function can be run with two different observation styles:
one stops only at selected steps and calls the observer there, the other stops at every
step.

```c
// step.h — three-step write sequence and two observation styles
#ifndef STEP_H
#define STEP_H

typedef void (*ObserverFunction)(int step, int value);

/* Writes 1, 2, 3 into a variable in order across three steps. At a step not
   in the breakpoints array, the observer is never called at all -- the
   program moves on past that step without stopping. */
void run_with_breakpoint(ObserverFunction notify, const int *breakpoints, int count);

/* The same three-step write sequence, but the observer is called at EVERY
   step -- there's no breakpoint list, because every write is itself a reason to stop. */
void run_with_watchpoint(ObserverFunction notify);

/* Instead of a step list, a condition is defined over the value itself: the
   observer is only called once the value reaches the threshold. Which step
   it stops at isn't known in advance -- only which condition it stops under is known. */
void run_with_conditional_breakpoint(ObserverFunction notify, int threshold);

#endif
```

```c
// step.c — breakpoint stops selectively, watchpoint stops on every write
#include "step.h"

static int is_at_breakpoint(int step, const int *points, int count) {
    for (int i = 0; i < count; i++) {
        if (points[i] == step) return 1;
    }
    return 0;
}

void run_with_breakpoint(ObserverFunction notify, const int *breakpoints, int count) {
    for (int step = 0; step < 3; step++) {
        int value = step + 1;   /* step 0 -> 1, step 1 -> 2, step 2 -> 3 */
        if (is_at_breakpoint(step, breakpoints, count)) {
            notify(step, value);
        }
    }
}

void run_with_watchpoint(ObserverFunction notify) {
    for (int step = 0; step < 3; step++) {
        int value = step + 1;
        notify(step, value);   /* stops on every write, no selection */
    }
}

void run_with_conditional_breakpoint(ObserverFunction notify, int threshold) {
    for (int step = 0; step < 3; step++) {
        int value = step + 1;
        if (value >= threshold) {   /* looks at the value itself, not the step number */
            notify(step, value);
        }
    }
}
```

**PI8.** Stepping is modeled here as a single loop's progression: each loop iteration
represents a step, and whether the observer gets called represents whether the program
stops at that step. A real debugger triggers step-by-step progression through a user's
command; here it's run within a single execution flow to make the timing visible.

**PI9.** The condition here is reduced to a single integer comparison (`value >=
threshold`); in a real debugger the condition can be an arbitrary boolean expression,
but the mechanism is the same — the condition is evaluated at every step, and it only
stops when it holds.

The difference between a breakpoint and a conditional breakpoint is **what is known in
advance**. An ordinary breakpoint stops at specific step numbers — which steps are of
interest must be known beforehand. A conditional breakpoint never looks at the step
number at all; it looks only at the value itself and stops whenever the condition
holds, regardless of which step that turns out to be. This is the difference between
"stop at the third step" and "stop once the value reaches 2" — the first looks at
location, the second at state. When the location isn't known in advance but what the
state should be is known (the moment a counter crosses a given threshold, say), a
conditional breakpoint does something an ordinary breakpoint cannot: even if there are
hundreds or thousands of steps, it stops exactly when the sought state occurs.

```c
// measurement.c — breakpoint skips 2, watchpoint sees 2
#include <stdio.h>
#include "step.h"

static int seen_count = 0;
static int seen_values[3];

static void record(int step, int value) {
    (void)step;
    seen_values[seen_count] = value;
    seen_count++;
}

int main(void) {
    int points[2] = { 0, 2 };   /* breakpoints at step 0 and step 2 */

    run_with_breakpoint(record, points, 2);
    printf("breakpoint seen count: %d\n", seen_count);
    printf("breakpoint first seen: %d\n", seen_values[0]);
    printf("breakpoint second seen: %d\n", seen_values[1]);

    seen_count = 0;
    run_with_watchpoint(record);
    printf("watchpoint seen count: %d\n", seen_count);
    printf("watchpoint second seen: %d\n", seen_values[1]);

    seen_count = 0;
    run_with_conditional_breakpoint(record, 2);   /* stops once the value reaches 2 */
    printf("conditional breakpoint seen count: %d\n", seen_count);
    printf("conditional breakpoint first seen: %d\n", seen_values[0]);
    printf("conditional breakpoint second seen: %d\n", seen_values[1]);
    return 0;
}
```

```text
breakpoint seen count: 2
breakpoint first seen: 1
breakpoint second seen: 3
watchpoint seen count: 3
watchpoint second seen: 2
conditional breakpoint seen count: 2
conditional breakpoint first seen: 2
conditional breakpoint second seen: 3
```

The breakpoints were set at step 0 and step 2; the program stopped at those two steps
and saw the values 1 and 3, in that order. At the step in between, step 1, the variable
held the value 2, but since there was no breakpoint there, the observer was never
called — the value 2 was never seen. When the same three-step sequence was run with the
watchpoint, the observer was called at every step: all three values were recorded, and
the second observation was exactly the 2 that the breakpoint had missed.

The last three lines show the conditional breakpoint's behavior. With the threshold
given as 2, the value at step 0 (1) stayed below the threshold, so the observer was
never called there; the values at steps 1 and 2 (2 and 3) reached or crossed the
threshold, so it stopped at both. Seen count 2, first seen 2, second seen 3 — a
different set from the ordinary breakpoint, which stopped at steps 0 and 2 and saw 1
and 3. Both mechanisms scan the same three-step sequence, but one selects by
**location** and the other by **state**; that's why they produce different observation
sets from the same sequence. This difference determines which mechanism to choose based
on what kind of information is known in advance: if the step being sought is known, a
breakpoint is enough and cheaper; if only a state is being sought (a value crossing a
given threshold, say) and which step it occurs at isn't known, a conditional breakpoint
is required.

A watchpoint and a conditional breakpoint look similar but aren't the same thing. A
watchpoint stops on **every** write and makes no selection — that's why it sees all
three values. A conditional breakpoint stops only when the condition holds; if the
threshold had been given as 4 instead of 2, it would never have stopped at all, because
the largest value in the sequence is 3 and none of them reach 4. A watchpoint
guarantees "never miss anything," and it pays for that guarantee by stopping at every
step; a conditional breakpoint gives no such guarantee — it only never misses the state
its condition defines, and it skips everything outside that condition just like an
ordinary breakpoint. The choice among the three depends on whether what's being sought
is a **location**, any **change**, or a specific **state**.

## The Bounding Measurement

When a program terminates unexpectedly, a dump of its memory state at that moment can
be examined afterward — to look at the stack and variable values at the moment of the
crash, without running the program again. This dump isn't produced here: actually
crashing a process is outside this lesson's scope, only what it's used for is
explained. A breakpoint and a watchpoint stop and observe a **running** process; a dump
instead reads the state a **stopped** process left behind, after the process no longer
exists — the two answer the same visibility question at two different times.

A core dump's real value is for the moments when the observer **isn't ready**. All
three of a breakpoint, a watchpoint, and a conditional breakpoint require the person
hunting the bug to have started the program under a debugger beforehand, or to attach
to it afterward — when and where the program will stop must be predicted, to some
degree, in advance. If a process running in production, with no one watching it,
crashes at an unexpected moment, recreating that moment is usually impossible; the
input, the timing, or the system state may never repeat in exactly the same way again.
At such a crash, the operating system can write the process's memory image at that
moment — the stack, the heap, and the register values — to a file. That file can later
be handed to a debugger, once the process is no longer running, and the variable
values, the call chain, and the state at the moment of the crash can be examined as
though the program were still standing at that moment — except it can no longer be
stepped forward, because the dump is nothing but a single frozen frame, not a running
process.

This exposes the fundamental difference separating a dump from breakpoints and
watchpoints: breakpoints and watchpoints require **an observation set up in advance**,
while a dump leaves behind a record **even if no observation was ever set up at all**.
If a bug can be reproduced and where to stop is known, a breakpoint or a watchpoint is
preferred, because it allows stepping forward over a live process. If the bug happens
only once, at an unpredictable moment, and while the observer isn't ready, the only
tool left is a dump — reading a frozen copy of that moment afterward, without needing
to recreate the crash itself.

This model has a limit of its own: the value observed here was set up to stay exactly
identical to the value written in the source code itself; in an optimized program, the
value a debugger sees may not be the same as the one in the source — a variable may be
held only in a register, or removed entirely.

Although the difference between a breakpoint and a watchpoint appears, in this lesson's
model, to be bounded by the length of a single sequence, in a real program it can stay
hidden across a much wider range. In a three-step sequence, noticing that a single
value in between got missed is relatively easy; in a loop running through hundreds of
steps, if a breakpoint is only set at the entry and the exit, a faulty intermediate
value in between can go completely unseen for the whole run. A watchpoint stopping on
every write removes this risk, but it has a cost: if a variable is written frequently,
the program runs far slower because it stops on every write. The choice between a
breakpoint and a watchpoint is therefore not just a question of "which one sees more,"
but "how well is it known in advance which step needs to be seen" — if it isn't known
at all, a watchpoint's slowness is preferred over a breakpoint's risk of missing
something.

The three stopping strategies can also be used one after another in a single bug hunt.
Suppose a total variable drops below zero at an unexpected point. The first step is
usually a watchpoint: it stops every time the variable is written, and after a few runs
it becomes clear in which range the sign flips. As the range narrows, the watchpoint's
cost — the slowdown from stopping on every write — is no longer needed; a fixed
breakpoint is placed at the start of that range, and the program is stopped and stepped
through only there. If the value turning negative repeats not under a single condition
but under several different conditions depending on the input, the breakpoint gives way
to a conditional breakpoint: the stop is now defined not as "once this line is reached"
but as "once the total drops below zero," independent of which input triggers it. Using
these three tools in sequence isn't arbitrary — each step narrows the information the
previous one found (an approximate location, then an exact location, then the state
itself) and hands it off to the next. If the bug occurred in production, only once, at
a moment no one was watching, this sequential hunt never even begins; the only thing
left is a core dump, and then the question isn't "where should it stop" but "what
record was left behind."

## Summary

- A breakpoint stops only at selected steps; an intermediate value passing between two
  breakpoints is never observed at all — the class this lesson measures is
  **unobserved**.
- A watchpoint stops on every write; it also sees the intermediate value a breakpoint
  missed — this is a contrast model.
- A conditional breakpoint looks at the value's own state, not at the step number; when
  the location isn't known but the state being sought can be defined, it does what a
  breakpoint cannot.
- A core dump doesn't stop a running process; it reads the state of a terminated
  process afterward, and it leaves behind a record even if no observation was ever set
  up in advance.
- In this model, the observed value was set up to match the source value; in an
  optimized program, this match isn't guaranteed.
- Which stopping strategy to choose depends on whether what's being sought is a
  location, any change, or a specific state; a real hunt uses all three in sequence —
  a range roughly narrowed with a watchpoint is pinned down precisely with a
  breakpoint, and a recurring, condition-dependent state is caught with a conditional
  breakpoint.

Both a breakpoint and a watchpoint work by **stopping** the program: it runs at its
normal speed until it stops, and at the moment it stops, its memory state becomes
readable as if frozen. This is a second axis separating the two methods from a core
dump — a dump is a single frame taken while the process itself is no longer running,
while a breakpoint or a watchpoint can show many frames, one after another, over a
running process. Which one is more suitable when hunting a bug depends on whether the
bug shows up **at a single moment** or **across a sequence of steps**: a single crash
is examined with a dump, while a drift accumulating across steps is examined with
stopping points.

## Next Step

The value a variable holds at runtime doesn't always stay identical to the value it's
assigned in the source — the next lesson takes up where that difference comes from.
