Skip to content
VLSI Mentor

C Part I · Module 13 · Writing Correct and Reliable C

It Compiled Is Not the Same as It Is Correct

Twelve modules have each, somewhere, reported a measurement in the same shape: a program with a real defect in it, compiled at this curriculum's full warning level, and not one diagnostic. A leak. A use-after-free. A function declared one way in one file and defined another way in the next, which compiled twice and linked. A reordered structure that silently permuted every initialiser in the project. Those were scattered across chapters and easy to read as individual curiosities. This chapter collects fifteen of them into one directory, compiles them all, and counts — which turns a series of anecdotes into a number, and the number is the reason this module exists.

Intermediate18 min readCPart IFundamentalsCorrectness

Part I · Module 13 · Chapter 13.1 · It Compiled Is Not the Same as It Is Correct

1. What a Clean Build Actually Claims

When a C compiler produces an object file without complaint, it has established a specific and surprisingly short list of things:

  • the source is syntactically valid C;
  • every name used is declared, with a type;
  • every expression's types are compatible with the operators applied to them, after the conversions the language performs;
  • the constraints the standard requires it to diagnose are satisfied.

That is the claim. Read it against what people hear, which is "the program is probably fine".

A clean build says the program is well-formed. It says nothing at all about whether it does what you meant, and remarkably little about whether it has meaning at all.

The second half of that sentence is the part that distinguishes C from most languages a reader will have met. In a language with bounds-checked arrays and managed memory, "well-formed" rules out whole categories of disaster. In C it does not, and the rest of this chapter measures by how much.

2. The Census

Fifteen files. Each contains exactly one defect, and every one of them is a bug this curriculum has already measured somewhere in Modules 9 to 12:

DefectFirst measured in
d01a memory leak on an early-return path9.9
d02a use-after-free9.10
d03a double free9.10
d04summing an uninitialised array9.12
d05a struct whose members were reordered under a positional initialiser10.2
d06assigning an out-of-range value to an enum10.8
d07const defeated by a pointer typedef10.9
d08a multi-statement macro used after if11.6
d09a misspelled macro name in #if11.7
d10a non-literal printf format12.2
d11an overlapping memcpy12.9
d12a comparator that subtracts and overflows12.9
d13invalid code inside #if 011.7
d14shifting an int by 40—
d15dereferencing a null pointer7.4

Compiled at the level every example in this curriculum has used:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -Wall -Wextra -Wpedantic -c d01..d15 -o /dev/null

  defect classes:        15
  produced a warning:     1
  produced NOTHING:      14

One. The shift by 40 was diagnosed. The other fourteen — including the use-after-free, the double free, the null dereference and the leak — compiled in silence.

3. What Does Catch Them

That number improves, and the shape of the improvement is the useful part.

Fifteen real defect classes measured against escalating tooling. The default warning level caught one. Adding extra warning flags brought it to four. A static analyser alone caught six. The union of all static tooling caught nine. Adding run-time sanitizers brought it to eleven. Four defect classes were caught by nothing automated at all: a reordered structure initialiser, const defeated by a pointer typedef, a multi-statement macro, and code in a branch the preprocessor deleted.What you runCaughtof 15 real defectsStill silent-Wall -Wextra-Wpedanticthe curriculum's default114+ extra warning flags-Wundef ·-Wformat-nonliteral ·-Wassign-enum411a static analyseron its own69all static toolingflags and analyser together96+ run-time sanitizersand only on inputs thatreach the bug114design and review only12
Figure 1 — the same fifteen defects against escalating tooling. Three things are worth reading off it. The first row is where most people actually live, and it is nearly empty. The improvement from there is real but slow, and each step costs something — extra flags produce noise on legitimate code, an analyser takes a separate run, and sanitizers only report a defect on an input that actually reaches it. And the last row never reaches fifteen: four of these defects are invisible to every tool here, which is why the module does not end with the tooling chapter.

Extra warning flags: 1 → 4. -Wundef caught the misspelled #if (Chapter 11.7 §3), -Wformat-nonliteral caught the unchecked format (Chapter 12.2 §4), -Wassign-enum caught the out-of-range enum. None of these is in -Wall -Wextra, and each has to be asked for by name. Chapter 13.7 is about which to turn on.

A static analyser: 6 of 15. It traces execution paths rather than checking one statement at a time, which is why it finds the leak, the use-after-free, the double free, the uninitialised read and the null dereference — the ones that depend on what happens between statements.

Both together: 9 of 15. The two overlap only partly, which is the argument for running both rather than choosing.

Plus sanitizers at run time: 11 of 15. The overlapping memcpy and the overflowing comparator are reported by instrumented builds — with a condition that matters and §4 is about.

4. The Four Nothing Catches

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
d05  a struct whose members were reordered under a positional initialiser
d07  const defeated by a pointer typedef
d08  a multi-statement macro used after an if
d13  invalid code inside #if 0

None of these is exotic, and that is the point.

d05 is Chapter 10.2 §6: swapping two members of the same type silently permutes every positional initialiser in the project, and the compiler has nothing to object to because the count and the types still match. The cure is named initialisers — a habit, chosen before the bug exists.

d07 is Chapter 10.9 §5: const tp p qualifies the pointer rather than what it points at, so a function that promises not to modify can modify. The code is well-formed by construction. The cure is never hiding a pointer in a typedef — again a habit.

d08 is Chapter 11.6 §5: a macro expanding to two statements attaches only its first to an if. The preprocessor produced valid C, so there is nothing to diagnose. The cure is do { } while (0).

d13 is Chapter 11.7 §2: code inside a deleted branch is not compiled, not type-checked and not warned about, so it rots quietly. No tool examines text that no longer exists. The cure is building every configuration you ship.

Four of fifteen are not tooling failures. They are failures of design and review, and no amount of instrumentation reaches them — because in each case the compiler was handed a perfectly well-formed program that means something other than what the author intended.

That is why this module does not end at Chapter 13.10. Tooling has a ceiling, the ceiling is visible in the figure, and Chapter 13.11 is about the habits that work above it.

5. Why C Is Like This

Not an apology — an explanation, because it tells you where to expect gaps.

The compiler sees one translation unit. Chapter 11.2 §5 established the boundary and Chapter 11.1 §6 measured the consequence: a function declared with two doubles and defined with two ints compiled cleanly twice and linked. No compiler invocation ever saw both files.

Many properties are undecidable in general. Whether a pointer is still valid, whether two regions overlap, whether a free will eventually happen — these depend on values that only exist at run time. A compiler that tried to answer them would have to refuse correct programs.

C's design prefers a fast compile and no hidden cost. There is no bounds check because a bounds check costs an instruction you did not ask for. That trade is the reason the language is used where it is used, and the missing checks are the bill.

And the checks that do exist are mostly about types, which catches a real and useful class of mistake — d14's shift was caught, and Chapter 11.4 §7 measured a static mismatch being reported with both lines quoted. The type system is not nothing; it is simply answering a different question from "is this correct".

6. What This Module Is For

The gap in Figure 1 has a shape, and Module 13 is organised around it.

The gapThe chapter
what "no meaning at all" actually means13.2
which non-portable behaviours you may rely on13.3
the arithmetic that silently leaves range13.4, 13.5
the access that corrupts something else13.6
getting more out of the compiler you have13.7
stating an assumption so a violation stops the program13.8
finding a fault you cannot see by reading13.9
the tools that watch a running program13.10
the habits that work above the ceiling13.11

And one disposition to adopt before any of them.

Treat a clean build as the beginning of the question, not the end of it. The compiler has told you the program is well-formed. Whether it is correct is a separate matter, and in C it is almost entirely yours.

That is not pessimism, and it is not an argument for anxiety. It is the reason the habits in this module exist: in a language that checks this little, the checking has to come from somewhere, and the somewhere is how you write the code, what you turn on, what you run it under, and who reads it.

7. What You Should Be Able to Say Now

  1. A clean build claims: valid syntax, declared names, compatible types, and the constraints the standard requires diagnosed. Nothing more.
  2. Measured: 15 real defects, 1 warning at -Wall -Wextra -Wpedantic — the level this curriculum has used throughout.
  3. The fourteen silent ones include a leak, a use-after-free, a double free, a null dereference and an uninitialised read.
  4. Extra warning flags: 4 of 15 — -Wundef, -Wformat-nonliteral, -Wassign-enum, each asked for by name.
  5. A static analyser: 6 of 15, because it traces paths rather than statements.
  6. All static tooling together: 9 of 15. They overlap only partly — run both.
  7. Plus run-time sanitizers: 11 of 15, and only on inputs that reach the defect.
  8. Four are caught by nothing: a reordered initialiser, const through a typedef, a statement macro, and code in a deleted branch.
  9. Those four are design and review failures, which is why the module continues past the tooling chapter.
  10. The reasons C is like this: one translation unit at a time, undecidable properties, and no hidden cost.
  11. A measurement that flatters the tooling is the one to re-check — mine did, and was wrong by a factor of nearly four.
  12. A clean build is the beginning of the question.

8. Exercises

Exercise 1 — predict the diagnostics. For each, say whether -Wall -Wextra -Wpedantic warns, and name the chapter that measured it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int *p = malloc(4); free(p); return *p;
struct s { int b; int a; }; struct s x = { 1, 2 };
const tp q = r; q->n = 1;                  /* typedef struct t *tp; */
#if ENABLE_FEATRUE
printf(fmt, value);                        /* fmt is a const char * */
memcpy(b + 2, b, 6);

Exercise 2 — build the census. Reproduce §2 with five defects of your own choosing. Report the count, and say which of the five you expected to be caught and were not.

Exercise 3 — raise the floor. Given the figure, write the compile command you would use on a new project, and justify each flag beyond -Wall -Wextra by naming a defect it catches.

Exercise 4 — classify the four. For each of d05, d07, d08 and d13, say why no tool can catch it, and give the habit that prevents it.

Exercise 5 — the attractive number. A colleague reports that a new linter finds 100% of the bugs in their test corpus. Give three questions you would ask before believing it, using the §4 callout.

Exercise 6 — the boundary. Explain why a compiler cannot catch the Chapter 11.1 §6 type mismatch, and what single change to the source would move it inside the boundary where it can.

Exercise 7 — cost the checks. For each of bounds checking, null checking and lifetime tracking, say what C would have to give up to provide it, and name one language that makes the opposite trade.

Exercise 8 — write the disposition. In three sentences, state what you now believe a clean build tells you, as advice to someone starting their first C project.

9. Summary

  • A clean build means well-formed: syntax, declarations, types, required diagnostics. Not correct.
  • Measured: 15 defects, 1 warning — including a leak, a use-after-free, a double free and a null dereference going silent.
  • Extra flags → 4, analyser → 6, both → 9, plus sanitizers → 11.
  • Four are caught by nothing automated — reordered initialiser, const through a typedef, statement macro, dead branch — and all four are design and review failures.
  • C is like this because of one translation unit at a time, undecidable properties, and no hidden cost.
  • A flattering measurement is the one to re-check. Mine said 15 of 15; the truth was 4.
  • Treat a clean build as the beginning of the question.

The census used a word repeatedly that this curriculum has so far relied on you half-knowing. Eight of the fifteen defects were not merely wrong — they were undefined behaviour, and that phrase has been appearing since Module 2 with a working definition of roughly "anything could happen". That definition is good enough to make you cautious and not good enough to reason with, because it suggests a program with undefined behaviour is a program whose output is unpredictable. It is worse than that, in a way Chapter 9.12 §3 already measured once without naming: a compiler is entitled to assume undefined behaviour never occurs, and to work backwards from that assumption through code that runs before the offending line. The next chapter defines the term properly, and explains why a program containing it has no predictable meaning at all — not even the behaviour you observed on the last run.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.