Skip to content
academia.sh

Lesson 10 / 15

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.

Published Updated

In this lesson

  • 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
Contents

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.

// 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
// 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

// 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.

// 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.

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close