---
title: 'Memory Debugging Tools'
source: 'https://academia.sh/en/courses/c-memory/memory-debugging-tools'
course: 'Memory and Pointers in C'
language: en
updated: '2026-09-26T03:02:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows how comparing the allocated and freed counts at a checkpoint models what a tool does'
    - 'Measures how a balanced allocation sequence and an unbalanced one get different results from the same check'
    - 'Speaks only in the caught/missed binary, without naming any tool, output, or flag'
---

# Memory Debugging Tools

Finding a leak by holding a counter by hand is not possible in a real program — a tool that does this is run instead. This lesson shows that the counting wrapper the course has used from the start already models the one thing such a tool does: is there a free for every allocation, or not.

The previous lesson showed that a leak produces no error message or crash, appearing only
as a counter difference. In a real program, no one compares `allocated_count()` with
`freed_count()` by hand — instead **a tool** that does this is run. This lesson's subject is
not such a tool itself; its subject is what that **one thing it does** actually is.
`counter.c`, which the course has used from the start, has in fact been modeling this since
the first lesson.

This is the natural continuation of the previous lesson. The `Memory Leaks` lesson showed
two kinds of the unowned class and measured both with a hand-held counter difference. But in
a real program, no one writes a line like `allocated_count() - freed_count()` inside `main`
and reads the result by hand; as a program grows, tracking by hand which of hundreds of
allocation calls went unmatched is not practical. So a layer that automates this job steps
in — and this lesson's only claim is that what that layer does rests on **no more
complicated an idea** than the counting wrapper the course has used from the start.

## What a Checkpoint Compares

What a tool does is, at its core, a single operation: at a given point in the program
(usually at exit), it counts whether there is a matching free call for every allocation call
made up to that point. If a match exists, the check comes out **clean**; if not, a finding
is reported for every block whose match could not be found. In this course, this comparison
is the difference between the two numbers `counter.c` keeps — `allocated_count()` minus
`freed_count()`. If the difference is zero, it is **missed**; if it is nonzero, it is
**caught**.

```c
// counter.h — a wrapper that counts every allocation and every free
#ifndef COUNTER_H
#define COUNTER_H

#include <stddef.h>

void *count_alloc(size_t size);
void *count_grow(void *old, size_t size);
void count_free(void *block);
int allocated_count(void);
int freed_count(void);

#endif
```

```c
// counter.c — the counters live only in this unit
#include <stdlib.h>
#include "counter.h"

static int allocated = 0;
static int freed = 0;

void *count_alloc(size_t size) {
    void *block = malloc(size);
    if (block != NULL) {
        allocated++;
    }
    return block;
}

void *count_grow(void *old, size_t size) {
    int new_block = (old == NULL);
    void *grown = realloc(old, size);
    if (grown != NULL && new_block) {
        allocated++;
    }
    return grown;
}

void count_free(void *block) {
    if (block != NULL) {
        freed++;
    }
    free(block);
}

int allocated_count(void) {
    return allocated;
}

int freed_count(void) {
    return freed;
}
```

**DM12.** `counter.c` has stood unchanged since this course's first lesson and carries the
word "tool" nowhere — only two counters and four functions. But what it does is nothing
more than comparing what was allocated with what was freed at a checkpoint; this is the
scaled-down, hand-built version of what a real tool does. The difference is that a real tool
does this comparison without ever touching the program's source code; here the same
comparison was written **by hand** into `counter.c`.

## A Balanced and an Unbalanced Sequence

```c
// check.c — a checkpoint distinguishing a balanced sequence from an unbalanced one
#include <stdio.h>
#include "counter.h"

static const char *check(int diff) {
    return diff != 0 ? "caught" : "missed";
}

static void observe(const char *name, const char *evidence) {
    printf("%-30s%s\n", name, evidence);
}

int main(void) {
    char buffer[64];
    int before, diff_after;

    printf("%-30s%s\n", "observation", "evidence");

    /* --- balanced sequence: every allocation has a match --- */
    before = allocated_count() - freed_count();
    int *first = count_alloc(2 * sizeof *first);
    int *second = count_alloc(3 * sizeof *second);
    count_free(first);
    count_free(second);
    diff_after = (allocated_count() - freed_count()) - before;
    snprintf(buffer, sizeof buffer, "checkpoint: %s", check(diff_after));
    observe("balanced sequence", buffer);

    /* --- unbalanced sequence: one allocation has no match --- */
    before = allocated_count() - freed_count();
    int *third = count_alloc(2 * sizeof *third);
    int *fourth = count_alloc(3 * sizeof *fourth);
    count_free(third);
    /* fourth is deliberately not freed: the unbalanced sequence is built here */
    diff_after = (allocated_count() - freed_count()) - before;
    snprintf(buffer, sizeof buffer, "checkpoint: %s", check(diff_after));
    observe("unbalanced sequence", buffer);

    count_free(fourth);
    return 0;
}
```

```
observation                   evidence
balanced sequence             checkpoint: missed
unbalanced sequence           checkpoint: caught
```

**DM13.** In the `balanced sequence` observation, both allocations were freed inside their
own block; the difference at the checkpoint is **0** and the result is **missed**. In the
`unbalanced sequence` observation, `fourth` was not freed by the time it reached that
checkpoint — the code did this itself, marking it with a comment, because that is the only
defined way to show what a tool actually finds. The difference rose to **1** and the result
is **caught**. After the checkpoint, `fourth` is still freed regardless — the lesson does
not leave an undefined state (ending the program with a leak) behind it.

## Three Sequential Checkpoints

The two observations in the previous section built two **independent** sequences — each
started from its own `before` value. In a real program, checkpoints do not stand isolated
like this; they come one after another, and each sees only the span **since the point
before it**. This shows that the checkpoint where a leak is caught is the exact same span
as where the leak occurred — not the point before it, not the point after it.

```c
// sequence.c — a leak caught exactly at the checkpoint where it occurred
#include <stdio.h>
#include "counter.h"

static const char *check(int diff) {
    return diff != 0 ? "caught" : "missed";
}

int main(void) {
    char buffer[64];
    int point_before = allocated_count() - freed_count();

    /* --- first point: a clean allocate-free pair --- */
    int *a = count_alloc(sizeof *a);
    count_free(a);
    int diff1 = (allocated_count() - freed_count()) - point_before;
    snprintf(buffer, sizeof buffer, "point 1: %s", check(diff1));
    printf("%-30s%s\n", "sequential checkpoint", buffer);
    point_before = allocated_count() - freed_count();

    /* --- second point: a block deliberately not freed --- */
    int *b = count_alloc(sizeof *b);
    int diff2 = (allocated_count() - freed_count()) - point_before;
    snprintf(buffer, sizeof buffer, "point 2: %s", check(diff2));
    printf("%-30s%s\n", "sequential checkpoint", buffer);
    point_before = allocated_count() - freed_count();

    /* --- third point: independent of b, a new pair balanced on its own --- */
    int *c = count_alloc(sizeof *c);
    count_free(c);
    int diff3 = (allocated_count() - freed_count()) - point_before;
    snprintf(buffer, sizeof buffer, "point 3: %s", check(diff3));
    printf("%-30s%s\n", "sequential checkpoint", buffer);

    count_free(b);
    return 0;
}
```

```
sequential checkpoint         point 1: missed
sequential checkpoint         point 2: caught
sequential checkpoint         point 3: missed
```

**DM14.** `b` is allocated at the second point and never freed by the time that point is
reached, which is why **point 2** returns `caught`. The third point builds a new pair (`c`)
that is entirely independent of `b` and balanced on its own — within its own span there is a
free for an allocation, so **point 3: missed**. `b`'s leak is never re-reported anywhere
after the second point; the third point does not count it again, because each point only
sees the unmatched allocations **within its own span**, it does not rescan a span already
reported before. Together, these three results show something important: a leak is caught
at exactly the **same span** in which it occurred in real time — not at the point before it
(it had not been allocated yet) and not at the point after it (that is not that span's
subject). `b` is eventually freed with `count_free` too, but this free is not inside any
point's measured span — the code does this only so as not to end the program with a leak,
not as part of the checkpoint narrative.

## A Deferred Free Can Mask a New Leak

The mechanism the three sequential points showed rested on an assumption: the allocation and
free calls within each span belonged to that span's **own** events. But in a real program a
block can be allocated at one checkpoint and freed at another — and this can lead the
counter difference to numerically balance out and hide **another** leak occurring in the
same span. This is not a flaw of counter-based checking, it is its **limit**: the check only
sees the net difference, it does not see which block belongs to which event.

```c
// deferred.c — a free carried over from a previous span masking a new leak
#include <stdio.h>
#include "counter.h"

static const char *check(int diff) {
    return diff != 0 ? "caught" : "missed";
}

int main(void) {
    char buffer[64];
    int point_before = allocated_count() - freed_count();

    /* --- point A: d is allocated, not freed in this span at all --- */
    int *d = count_alloc(sizeof *d);
    int diffA = (allocated_count() - freed_count()) - point_before;
    snprintf(buffer, sizeof buffer, "point A: %s", check(diffA));
    printf("%-30s%s\n", "deferred free", buffer);
    point_before = allocated_count() - freed_count();

    /* --- point B: a new leak (e) forms, but d is also freed here --- */
    int *e = count_alloc(sizeof *e);
    count_free(d);
    int diffB = (allocated_count() - freed_count()) - point_before;
    snprintf(buffer, sizeof buffer, "point B: %s", check(diffB));
    printf("%-30s%s\n", "deferred free", buffer);

    count_free(e);
    return 0;
}
```

```
deferred free                 point A: caught
deferred free                 point B: missed
```

**DM15.** Point A correctly reports that `d` was not freed in that span, as **caught** —
just like point 2 in the previous section. At point B, two things happen at the same time:
`e` is newly allocated and **never freed** in this span, and also `d` (a debt carried over
from the previous span) is freed exactly in this span. The counter difference does not see
these two events separately, it only sees their sum: `+1` (newly allocated `e`) and `-1`
(the late-freed `d`) cancel each other out, and `diffB` comes out to **0** — the result is
**missed**, even though a new block (`e`) really has leaked in this span. `e` is only freed
at the end of `main`, at a point no checkpoint measures. This shows that a counter
difference being **zero** does not mean nothing went wrong in that span — it only means what
was allocated and what was freed became numerically equal. Most real tools get past this
limit not just by counting but by tracking each block's own identity (which line it was
allocated on); this course does not track such an identity, because its subject is not the
tool itself, but the underlying idea the tool rests on.

## The Bounding Measurement

A checkpoint only **counts** — it does not know which line a block was allocated on, which
part of the program that block belongs to, or what the block's content is. All it knows is
the **count** of allocation calls whose match could not be found. This is why, in this
course, a tool's answer is written as only one of two values: **caught** or **missed**. No
output format, no flag, and no tool name appears in this lesson outside this binary —
because which tool produces which output depends on the environment, not on the mechanism
being measured.

What a tool **cannot find** also matters: a checkpoint cannot know whether a pointer's old
value was saved somewhere before it was overwritten, or whether a block's ownership contract
matches the comment in `owner.h` — these require reading the source code. A checkpoint only
verifies the **number itself**; it does not explain why the number reached that value.

The reason the tool's name and output format never appear in this lesson is not just care —
it is a requirement. The same checking idea can be implemented with different tools in
different environments, with different output formats — some run the program inside a
separate process, some replace the allocation functions themselves, some only print a
summary at exit. Since this course cannot know which environment it will run in, it reduces
what it measures to a single environment-independent mechanism: a number, and whether that
number is zero. `caught` and `missed` are therefore not a simplification, they are the
lesson's **actual subject** itself.

## Summary

- The one thing a memory debugging tool does is compare what was allocated with what was
  freed at a checkpoint; the memory class this lesson measures is again **unowned**.
- `counter.c` has been doing this comparison by hand since the course's first lesson; this
  lesson names it for the first time.
- A checkpoint returned **missed** for a balanced sequence and **caught** for an
  unbalanced one; there is no third result besides these two.
- No tool name, output format, or flag appeared in this lesson — these depend on the
  environment, not on the mechanism being measured.

## Next Step

This lesson followed memory **moving between modules** — whose hand an allocated block sits
in, how that changes up to a checkpoint. The Dynamic Memory topic's last lesson goes back to
the very beginning: it puts the four `language` observations side by side with the
`allocate and free` observation, and shows how **which region** a block sits in — automatic,
static, or dynamic — lines up with the ownership question.
