Debugging, Undefined Behavior and Safe C

Learn to find and prevent the bugs that make C programs crash, give wrong answers or become insecure. Turn on compiler warnings and read what they mean, track down crashes and wrong values step by step in gdb or lldb, and recognize undefined behavior in arithmetic, pointers and memory. Check your assumptions with assertions, catch memory errors and undefined behavior with AddressSanitizer and UndefinedBehaviorSanitizer, and write safer code by following secure coding rules for strings, input and errors. Finally, weigh what C's lack of memory safety costs against the control it gives you.

IntermediateLesson 8 of 8 in Programming in C Course

What you will learn

  1. Turn on the compiler's warnings, read its diagnostics and choose build settings that make bugs visible early, before the program ever runs.
    • Turn on warnings and read compiler messages
      Compile with a strict set of warning flags, read each diagnostic line by line and fix the cause instead of silencing the message.
    • Choose build settings for debugging, testing and release
      Use separate debug and release builds, know what -g and the optimization levels change, and add static analysis and hardening flags where they belong.
  2. Track down bugs methodically: reproduce and isolate the failure, then use gdb or lldb to pause the program, step through it, inspect values and find where a crash or a wrong value really comes from.
    • Debug methodically: reproduce, isolate and test a hypothesis
      Turn a vague failure into a precise one, read what the crash message tells you, and narrow the cause down with small, controlled experiments.
    • Run, pause and step through a program in gdb or lldb
      Start a program under gdb or lldb, set breakpoints, step line by line and inspect variables and arrays while the program is paused.
    • Diagnose crashes and unexpected values
      Use the call stack after a crash, move between frames, and use watchpoints, conditional breakpoints and core dumps to find where a bad value comes from.
  3. Explain what undefined behavior is and why it makes C programs unpredictable, and recognize its most common sources in arithmetic, expressions, pointers and memory.
    • Understand what undefined behavior means
      Distinguish undefined, implementation-defined and unspecified behavior, and explain why code with undefined behavior can seem to work and then fail.
    • Spot undefined behavior in arithmetic and expressions
      Recognize signed overflow, bad division and shifts, out-of-range conversions, unsequenced side effects and uninitialized reads, and write the checks that avoid them.
    • Spot undefined behavior with pointers, arrays and memory
      Recognize out-of-bounds access, bad strings, null and dangling pointers, use after free and wrong allocation sizes, and know the safe pattern for each.
  4. Make a program check its own assumptions while it runs: state them with assertions, and build test versions with sanitizers that stop at memory errors and undefined behavior and report exactly where they happened.
    • State assumptions with assertions
      Use assert for conditions that only a bug can break, understand what NDEBUG does, and keep assertions separate from handling expected errors.
    • Find memory errors with AddressSanitizer
      Build a test version with AddressSanitizer, read its error reports, and know what it finds, what it misses and what it costs.
    • Detect undefined behavior with UBSan and build a checking routine
      Use UndefinedBehaviorSanitizer and know the other sanitizers, then combine warnings, sanitizers and tests into a routine that catches errors early.
  5. Apply secure coding rules such as the SEI CERT C rules to strings, buffers, input and errors, and weigh what C's lack of memory safety costs against what it offers.
    • Use a secure coding standard
      Explain how the SEI CERT C Coding Standard is organized, read a rule with its examples, and know the core rules that prevent the most common C bugs.
    • Handle strings and buffers safely
      Always know how large a buffer is, read and copy strings with functions that respect that size, and detect truncation.
    • Validate input and handle errors
      Treat all outside data as untrusted, convert text to numbers with full error checks, and check the result of every library call that can fail.
    • Weigh memory-safety trade-offs
      Explain what memory safety is, why C lacks it, what that costs and what C offers in return, and how layers of defense and memory-safe languages reduce the risk.

Debugging and Undefined Behavior in C study guide

5 chapters

1

Let the compiler find problems for you

The compiler can point out many problems before you run a program. Learn to read its messages as evidence about your code, then choose flags that reveal more of what matters. Debugging information, optimization, static analysis, and release hardening serve different jobs. Keeping those jobs clear helps you build a program you can inspect during development and test again in the form your users will run.

1.1 Turn on warnings and read compiler messages

Warnings give you a chance to repair a defect before an input triggers it. An error prevents a successful build. A warning allows the build to continue, but reports something you need to understand: suspicious valid code, possible undefined behavior, or a language-rule violation the compiler accepts.

Choose useful checks

Start GCC or Clang with an explicit language version and a broad warning set:

clang -std=c17 -Wall -Wextra -Wpedantic -g program.c -o program

-Wall enables many common checks, not all warnings. -Wextra adds more, including signed/unsigned comparisons in C. -Wpedantic diagnoses many departures from the chosen ISO standard; it is not a complete conformance test. MSVC's /W4 serves a similar purpose on Windows.

Add checks that match the risks in your code:

FlagWhat it helps reveal
-WconversionImplicit conversions that may change a value
-WshadowA local declaration hiding another variable
-Wformat=2Additional checks of format strings
-WerrorMakes enabled warnings fail the build

For example, double price = 3.7; int whole = price; discards the fraction. -Wconversion makes that conversion visible. If truncation is intended and the value fits, (int)price states the choice. If the fraction matters, retain a floating-point type. For nearest-integer rounding, lround from <math.h> has a different job from a cast.

Read the first useful message

A diagnostic normally identifies a source location, a message, and the warning flag. A caret marks the relevant expression; following notes may identify a declaration or suggest a repair. Read the entire group. When one syntax error causes many later messages, fix the first error and compile again before chasing the rest.

Suppose a program contains these three lines, with average declared as double:

if (count = 0) { /* report no data */ }
printf("%d\n", average);
int unused;

The assignment in the condition sets count to zero, making the condition false. If the intention was a comparison, use count == 0. Adding parentheses around the assignment can silence a warning while leaving the bug intact.

The format %d expects an int, but the argument is a double. Use %f; a format mismatch is undefined behavior, not a request to convert the value. Delete the unused declaration if it serves no purpose. These are three different causes, so each needs its own repair.

Follow the evidence

Pay close attention to uninitialized values, missing returns, addresses of local variables being returned, and sizeof on array parameters. A missing header can leave a function undeclared; recent GCC and Clang releases reject this by default. Do not add a cast simply to make a diagnostic disappear. Explain the intended types and values first, then make the code express that intent.

A warning-free program can still have a faulty loop condition or a bad index supplied by input. Some warnings also depend on optimization and data-flow analysis, and different compilers catch different cases. Keep the warning set enabled in ordinary builds, then investigate run-time behavior with the debugger and testing tools.

Key Takeaways

  • Choose the C version explicitly.
  • -Wall does not enable every warning.
  • Repair the cause of a diagnostic before suppressing it.
  • A format conversion must match the argument type.
  • A clean build still needs run-time testing.

1.2 Choose build settings for debugging, testing and release

The same source can be built for easy inspection, intensive testing, or efficient release. Build settings decide what information you can see and which extra checks execute. Give each build a clear purpose.

Keep source information

The -g flag adds names, types, and mappings between machine instructions and source lines. It lets a debugger show your code and helps sanitizer reports identify locations. It does not turn on run-time checks or disable optimization. Without debug information, some function names may remain available, but source lines and local variables are much harder to recover.

Optimization changes how closely execution follows source order:

SettingUsual purposeWhat you may see
-O0Straightforward debuggingSource order is usually easiest to follow
-OgDebugging with light optimizationSome transformations remain
-O2Efficient release buildReordered work and removed variables

An optimized debugger session can skip lines or display a variable as "". The compiler has preserved the behavior of defined code, but it may no longer keep every source-level intermediate value. -O0 does not literally translate every C line into one machine instruction.

Build for the job

A simple debug command is:

gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 program.c -o program-debug

A testing build adds sanitizers. A release build commonly uses -O2, and may define NDEBUG to remove assertions. Ordinary input validation and error handling must remain active when assertions disappear. Test the release configuration too: a problem visible only with optimization is a strong reason to investigate undefined behavior, including uninitialized reads and signed overflow.

Keep shared flags in a Makefile or another build configuration. Separate output names prevent accidentally running an old release binary while inspecting newly edited source. After every source change, rebuild the executable you intend to run.

Add analysis and hardening

Static analysis follows possible paths through the source without executing the program. Try gcc -fanalyzer or clang --analyze; scan-build, clang-tidy, and cppcheck offer other analysis workflows. An analyzer may notice that an error branch frees a pointer twice even when your tests never enter that branch. Review its reasoning: possible-path analysis can produce false alarms.

Dynamic analysis checks actual execution. Sanitizers and Valgrind see only the paths you run, so their coverage depends on your inputs. Combine both kinds of analysis.

Release hardening limits damage from remaining defects. -fstack-protector-strong places checks in selected stack frames to detect many overwrites. On supporting systems, _FORTIFY_SOURCE adds checks to library calls when object sizes can be determined. It requires optimization; level 2 is common, while level 3 requires suitable compiler and C-library support. These are platform features, not ISO C guarantees.

A hardened program can stop when it detects corruption. The invalid operation still needs a source fix; changing release flags cannot correct an out-of-bounds loop.

Key Takeaways

  • Use -g for source-level debug information.
  • Debug with -O0 or -Og, and test the release build too.
  • Keep input checks active when NDEBUG removes assertions.
  • Static analysis and run-time analysis find different paths and defects.
  • Keep build flags in a repeatable configuration.
  • Hardening reduces damage from bugs that remain.
2

Find and fix bugs with a debugger

A crash tells you where execution stopped; the mistake may have happened much earlier. Start with a repeatable failure and a small input, then use a debugger to inspect the program at chosen moments. Breakpoints and stepping show how values develop. Stack frames, watchpoints, and conditional breakpoints help trace a bad value back to the code that created it, so each experiment answers a specific question.

2.1 Debug methodically: reproduce, isolate and test a hypothesis

A failure is the behavior you see: a crash, a wrong total, or output that never appears. A bug is the mistake that caused it. Keeping those separate prevents you from changing the crash line before finding out how the program reached it.

Make the failure repeatable

Record the command, input, build flags, expected result, and actual result. Keep the failing input unchanged while you experiment. If a program fails only on one file, save that file before editing it. Reproduction gives you a way to judge whether a proposed fix works.

Next, shrink the case while preserving the failure. Remove unrelated records or code and rerun. Suppose one record among 1,000 independently triggers a crash. Trying halves can locate it in about 10 rounds. If neither half fails alone, the failure may require a sequence of records or state built up earlier. Preserve that sequence and remove smaller pieces around it. Halving is useful only when the retained input still exercises the cause.

Ask one testable question

An empty field in a minimized file suggests a hypothesis: "The parser uses the field before checking whether it exists." Test that with a small input containing an empty field. Inspect the returned pointer, or add one targeted check. Change one thing at a time; if you alter the parser, buffer size, and input together, success will not tell you which change mattered.

Fix the broken rule, rerun the minimized case, then rerun the original input and existing tests. Save the trigger as a regression test so a later edit cannot silently restore the same failure.

Interpret the stop

"Segmentation fault" usually reports SIGSEGV: the system stopped an invalid memory access. A null pointer, freed pointer, or out-of-range address is a common cause. An invalid access can also escape an immediate crash, so silence alone tells you little.

"Aborted" commonly reports SIGABRT, raised by abort(). A failed assertion or a library check detecting heap corruption can cause it. In bash, common signal exit statuses are 139 for SIGSEGV and 134 for SIGABRT; these numbers are shell conventions, not C return values your program must use.

Observe without losing output

A short diagnostic can expose the first wrong value:

fprintf(stderr, "index=%zu total=%d\n", index, total);

Labels make the output readable when the same line runs many times. stderr is useful for diagnostics because it is not fully buffered; stdout redirected to a file or pipe is normally fully buffered, so a crash can lose pending output. Remove temporary prints after the repair.

Explain a suspicious function line by line aloud. This "rubber duck" method forces each operation to match your intended behavior. If a bug changes when you add prints or change optimization, investigate undefined behavior with sanitizers. Making an array larger until a crash disappears can hide an invalid index without repairing it.

Key Takeaways

  • Save an exact reproduction before changing code.
  • Minimize input while preserving the failure.
  • Test one hypothesis with one controlled change or observation.
  • Trace a crash back to its cause, which may be earlier.
  • Rerun the original case and retain a regression test.

2.2 Run, pause and step through a program in gdb or lldb

A debugger lets you pause a running program and inspect the values that explain its behavior. Use GDB on many Linux systems or LLDB on macOS. IDE debugger buttons use the same ideas: break, run, inspect, and step.

Start from the right executable

Compile with -g -O0, then open the result with gdb ./program or lldb ./program. At the prompt, run records.txt 3 starts it with two arguments. In GDB, run < input.txt redirects standard input; in LLDB, use process launch -i input.txt. Use quit to leave.

Set a breakpoint before running. break main in GDB and b main in LLDB stop at the start of main. Both accept a file-and-line location such as b program.c:12. Full LLDB commands include breakpoint set --name main and breakpoint set --file program.c --line 12.

A breakpoint normally stops before the indicated source statement executes. If the selected line assigns a value, print after stepping over it to inspect the new value. Rebuild after source edits: the debugger executes machine code, not the text open in your editor.

Choose how to advance
CommandPurpose in GDB and LLDB
next or nExecute the current line, normally stepping over calls
step or sEnter a called function when source information allows
finishRun until the selected function returns
continue or cResume until another stop or program exit

Other breakpoints can interrupt next or finish. Use next for a library call you do not need to inspect. If you step into unwanted code, finish returns control to the caller.

Inspect a loop one pass at a time

Consider a total initialized to zero and this array:

int a[5] = {4, 8, 15, 16, 23};
for (int i = 0; i <= 5; ++i) {
    sum += a[i];
}

Stop at the body and print i and sum before the addition. After the first five additions, the running totals are 4, 12, 27, 43, and 66. The next stop has i == 5, but valid indices end at 4. You have found the extra iteration without needing to read the invalid element. Change the condition to i < 5, rebuild, and rerun.

print expr or p expr evaluates an expression in either debugger. Start with safe observations such as p i and p sum; a debugger expression that dereferences an invalid pointer can itself attempt an invalid read. GDB's display i repeats the print at every stop. info locals in GDB and frame variable in LLDB show local variables. For five valid elements through ptr, use GDB's p *ptr@5 or LLDB's parray 5 ptr.

You can test a hypothesis by changing a paused value: GDB accepts set var i = 4, while LLDB accepts expr i = 4. This changes one run, not the source. To manage stops, use GDB's info breakpoints and delete 1, or LLDB's breakpoint list and breakpoint delete 1.

Key Takeaways

  • Build and rebuild the executable with debug information.
  • A stopped source statement normally has not executed yet.
  • Use next, step, finish, and continue for different amounts of progress.
  • Inspect indices and bounds before evaluating memory expressions.

2.3 Diagnose crashes and unexpected values

A crash inside a library often begins with an invalid argument from your own code. The call stack shows the active function calls. Use it to move from the stop location to the caller whose values you can explain.

Read frames from the current call outward

Type bt in either debugger. Frame #0 is the innermost active call; higher frame numbers are its callers. A frame gives the function's execution context, including available arguments and local variables. Optimization may remove some of that information.

Suppose find_name(2) returns no match, and the caller does this:

const char *name = find_name(2);
printf("%zu\n", strlen(name));

The stop may be inside strlen. In bt, find the first frame belonging to your code. Select it with frame 1 in GDB or frame select 1 in LLDB when it is frame 1. Print name. A null value explains why the string operation failed. The caller must handle the missing name before calling strlen; editing the library cannot repair the argument.

Use up and down to move among callers and callees. Selecting a frame changes what the debugger displays and which local variables an expression refers to. It does not resume execution or undo a call.

Stop where a value changes

A breakpoint asks, "What happens at this location?" A watchpoint asks, "Which operation changes this value?" If a stored count becomes wrong long before a crash, pause while the count is in scope and use:

GDB:  watch count
LLDB: watchpoint set variable count

Continue until the debugger stops on a change. Inspect the old and new values and the writing instruction's source location. A change may be legitimate, so compare it with the data structure's rule. Hardware offers only a small number of watchpoints; four is common on x86-64. A local variable's storage is meaningful only while its function is active.

For a known late iteration, use a conditional breakpoint. GDB's break 20 if i == 999 pauses at line 20 only when the condition holds. LLDB's equivalent is breakpoint set --line 20 --condition 'i == 999'. This saves hundreds of manual steps.

Inspect a saved crash

A core dump records a crashed process's state. Where core dumps are enabled, ulimit -c unlimited allows them for programs started from that shell. Open a saved dump with gdb ./program core or lldb ./program -c core, using the executable that produced it. System policies control whether a dump is saved and where it goes; systemd-based Linux systems may manage it through coredumpctl.

Run bt, select your frame, and inspect values as in a live crash. The dump cannot continue the program. If malloc or free detects corruption, the original bad write may be absent from this stack. Reproduce with AddressSanitizer to stop closer to that write. If a local value looks arbitrary, check whether execution has reached its initializer before blaming the debugger.

Key Takeaways

  • Use bt to trace the active calls.
  • Select a frame in your code and inspect the arguments passed to the library.
  • Watchpoints stop on value changes; conditional breakpoints stop at chosen states.
  • A core dump supports inspection after a crash.
  • The corruption may predate the stack visible at the final crash.
3

Recognize and avoid undefined behavior

Some C operations have no result guaranteed by the language. They can appear to work in one build and fail in another, making ordinary trial and error misleading. Learn to separate undefined behavior from documented platform choices and permitted variation. Then inspect arithmetic, expression order, array bounds, and memory lifetimes, placing checks before an operation crosses one of those boundaries.

3.1 Understand what undefined behavior means

Undefined behavior, or UB, means the C standard imposes no requirements on what happens after an invalid operation. It does not promise an exception, a useful error message, or a predictable wrapped value. A program may crash, corrupt data, print a plausible answer, or appear to work.

Separate three kinds of behavior
KindWhat the language permitsExample in C17
UndefinedNo required behaviorSigned integer overflow
Implementation-definedA documented implementation choiceWhether plain char is signed
UnspecifiedA permitted choice that need not be documentedOrder of evaluating independent function arguments

Implementation-defined behavior can be used deliberately when the documented platform is part of a program's design. For portable code, avoid assuming that all implementations make the same choice. Unspecified behavior is also valid: f(a(), b()) can evaluate either independent call first. Write code whose correctness survives that allowed variation. The width of int and, in C17, right-shifting a negative signed integer are further platform-dependent choices.

Why optimization exposes bugs

C leaves array-bound and signed-addition checks to the programmer. The compiler may optimize on the assumption that the program obeys the language's rules. An attempted overflow detector can violate the very rule it tries to test:

int will_overflow(int x) {
    return x + 1 < x;
}

For every x where signed addition is defined, x + 1 is larger than x. The compiler can therefore replace the result with zero. At INT_MAX, performing the addition is UB, so the compiler owes the caller no wrapped result. Seeing a different answer in an unoptimized build does not make this a valid test.

The safe test runs before the operation:

if (x == INT_MAX) {
    /* handle overflow */
} else {
    ++x;
}

Use <limits.h> for the implementation's limits. A hard-coded maximum assumes a particular type width; the macro names the actual limit.

Put checks before first use

This ordering is also essential for pointers:

if (p == NULL) {
    return -1;
}
int v = *p;

Reversing those statements lets a null dereference occur before the protection. Once code dereferences p, an optimizer may reason that the defined path already requires p to be non-null and remove a later null test.

Changing flags, adding a print, or moving a variable can alter the visible symptom of UB. A successful run establishes only what that build did for that input. To repair the code, identify which operation lacks a defined meaning and which condition must hold before it. Warnings can identify some cases; static analysis can explore possible paths; ASan and UBSan check many executed operations. Coding rules provide the corresponding source-level patterns, such as checking an index or an arithmetic bound before using it.

Key Takeaways

  • UB guarantees neither a crash nor a stable result.
  • Implementation-defined choices must be documented.
  • Unspecified choices remain within the language rules.
  • Optimizers can rely on signed overflow never occurring in defined code.
  • Validate a pointer or arithmetic bound before the risky operation.

3.2 Spot undefined behavior in arithmetic and expressions

Arithmetic can fail before you have a chance to inspect its result. Check the operands while their values are still valid, and know which type performs each operation.

Signed and unsigned limits

Signed overflow is undefined. With a 32-bit int, INT_MAX is 2,147,483,647; adding one is not a portable way to obtain a negative number. INT_MIN / -1, -INT_MIN, and oversized multiplication also fail when the mathematical result does not fit.

Unsigned arithmetic wraps within its type's range. For a 32-bit unsigned int, UINT_MAX + 1u is zero, and 0u - 1u is 4,294,967,295. This is defined but can still break a size calculation. An unsigned reverse loop using i >= 0 never reaches a negative stopping value.

For general signed addition, test before adding:

if ((b > 0 && a > INT_MAX - b) ||
    (b < 0 && a < INT_MIN - b)) {
    /* report overflow */
} else {
    result = a + b;
}

The subtractions used in these tests stay in range because each is guarded by the sign of b. Computing the sum first and then comparing it is too late. A wider type helps only when it can hold every intermediate result: convert an operand before the operation. C23 provides ckd_add in <stdckdint.h>; GCC and Clang offer __builtin_add_overflow.

Division, shifts, and conversions

Reject zero before integer division or remainder. Also reject the signed minimum divided by negative one when its result cannot fit. IEEE 754 floating-point systems commonly produce infinity or NaN for division by zero, allowing invalid numerical results to spread without an integer-style trap.

For bit operations, use unsigned operands. The shift count must be nonnegative and less than the width of the promoted left operand. With 32-bit operands, shifting by 32 is undefined. In C17, left-shifting a negative signed value is undefined; 1 << 31 is also undefined for a 32-bit int, while 1u << 31 is valid for a 32-bit unsigned int.

A floating-to-integer conversion requires the truncated value to fit, even when the destination is unsigned. (unsigned)-1.0 is undefined. Integer-to-unsigned conversion instead reduces the value into the destination's range: (unsigned)-1 becomes UINT_MAX. Out-of-range integer-to-signed conversion is implementation-defined in C17. Check ranges before narrowing; -Wconversion helps expose implicit changes.

Make evaluation order harmless

printf("%d %d\n", i, ++i); reads and changes i without the required ordering. i = i++ + 1; and a[i] = i++; also contain unsequenced operations. Split the work into separate statements with an explicit intended order.

Initialize automatic variables before reading them: int sum = 0; makes sum += x; meaningful. Globals and static variables start at zero. C23's two's complement representation requirement does not define signed overflow; portable code still needs the checks.

Key Takeaways

  • Check signed arithmetic before computing its result.
  • Unsigned wrap is defined but can make sizes and loops wrong.
  • Check division operands and shift counts.
  • Floating-to-unsigned conversion differs from integer-to-unsigned conversion.
  • Sequence changes explicitly and initialize values before use.
  • C23 still leaves signed overflow undefined.

3.3 Spot undefined behavior with pointers, arrays and memory

A pointer needs more than a plausible address. The object must exist, the access must stay in its bounds, and the operation must be allowed for that object. These checks explain both immediate crashes and corruption discovered much later.

Stay inside the object

An array of n elements has indices from zero through n - 1. A pointer one past its last element can mark a loop end, but you must not dereference it. i <= n visits one element too many. CERT ARR30-C covers these bounds errors.

An out-of-bounds write may land in accessible memory belonging to another object. The program can continue until a damaged count, pointer, or allocator record is used. A later crash in free does not identify the original write. ASan can stop at the write when its checks detect the boundary violation.

Make characters into a valid string

A C string ends with '\0'. Its storage needs one byte beyond its character count. C accepts char s[5] = "hello";, but all five bytes hold letters, leaving no terminator. Passing it to strlen or %s can read beyond the array. Use enough space, and ensure a terminator is actually present. STR31-C addresses capacity; STR32-C addresses passing a terminated string.

A string literal is not writable. After char *p = "hi";, p[0] = 'H'; is undefined. Use char p[] = "hi"; for a writable copy, or const char *p to make attempts to change the literal visible to the compiler. This is STR30-C.

Respect lifetime and ownership

Check NULL before dereferencing a result from an allocation, file open, or search. When a block is freed, every pointer into it loses access to a live object. Do not read it, write it, or free it again. Free only the appropriate pointer returned by a dynamic allocation, never a local array or an interior pointer. free(NULL) is harmless, but setting one pointer to null after freeing does not repair other aliases.

A function that returns a local array's address returns a dangling pointer:

char *greeting(void) {
    char text[16] = "Welcome";
    return text;
}

The array's lifetime ends when the function returns. Give the caller a buffer and capacity to fill, or allocate a result whose ownership passes to the caller. A successful print immediately after return cannot extend the old array's lifetime.

Allocate bytes for every element

malloc(n) requests bytes, not n objects of the pointed-to type. The usual element-based expression is n * sizeof *a. First prevent that product from wrapping:

if (n > SIZE_MAX / sizeof *a) {
    /* reject the requested size */
}

Include <stdint.h> for SIZE_MAX and <stdlib.h> for allocation functions. Once the size is valid, allocate, check the returned pointer, and write each value before reading it. malloc leaves memory uninitialized. calloc(n, sizeof *a) zeroes its storage and mainstream libraries reject overflowing products. A size check, an allocation-result check, and a valid index solve different problems; all three matter.

Key Takeaways

  • Only dereference pointers within a live object’s bounds.
  • Reserve and store the string terminator.
  • Do not modify string literals.
  • Freeing an allocation invalidates access through all its aliases.
  • Return storage with a lifetime the caller can use.
  • Check the element-count multiplication and allocation result.
4

Catch errors at run time with assertions and sanitizers

You can make a test build check more than its final output. Assertions check the assumptions your own code promises to maintain, while sanitizers check operations such as memory access and arithmetic as they happen. Learn to read each report, choose a tool for the error you suspect, and run inputs that reach the risky paths. A useful checking routine connects a detected error to a fix and a lasting test.

4.1 State assumptions with assertions

An assertion states something that should be true because your own code is correct. It turns a hidden broken assumption into a stop near the place where that assumption matters.

Check a programming contract

Include <assert.h> and write assert(condition);. A true condition lets execution continue. A false condition prints diagnostic information to stderr and calls abort(). The message identifies the expression, source location, and function; its exact wording depends on the platform.

For an internal averaging helper whose caller promises valid arguments:

double average(const int *a, size_t n) {
    assert(a != NULL);
    assert(n > 0);
    /* compute the average */
}

An accidental call with zero elements fails at the broken promise. Under a debugger, bt shows the assertion and abort machinery, followed by the calls in your code. Select the caller's frame to inspect how it supplied zero.

Contracts occur at several points:

KindWhat it statesExample
PreconditionRequired on entryA nonempty input array
PostconditionGuaranteed after workAn array is sorted
InvariantMaintained across stepsA stored count matches the structure

An assertion is executable documentation: it both names the property and checks it during a test run. Choose expressions that observe state without changing it.

Keep required work outside assertions

Defining NDEBUG before including <assert.h>, commonly with -DNDEBUG, disables assert. Its expression is not evaluated. This creates a serious bug if the expression performs needed work:

assert(++count <= MAX);

The increment disappears in a build with NDEBUG. Write the action separately:

++count;
assert(count <= MAX);

The same rule excludes opening files, allocating memory, or closing files inside an assertion. Those operations must occur in all configurations. Their failures need ordinary error handling.

Validate expected failures normally

A missing input file, invalid user text, and exhausted memory can occur during correct program use. Check them with if, report the error, and return or clean up. A private helper can assert a validated range, but the public input boundary must enforce that range with checks that remain in release builds.

For example, always test fclose(f) == EOF in ordinary control flow when closing an output file. Buffered output may fail during the close; removing the call or its check can hide lost data.

Check fixed facts at compile time

A static assertion tests a constant expression when compiling. In C17, <assert.h> provides static_assert as a macro for C11's _Static_assert:

static_assert(sizeof(int) == 4, "requires a 4-byte int");

A false condition fails the build. It cannot check a user-entered count known only at run time. C23 makes static_assert a keyword and permits omitting the message. Use this distinction to place each check where its required information is available.

Key Takeaways

  • Assertions check internal programming promises.
  • A failed run-time assertion reports its location and aborts.
  • Keep side effects out of assertion expressions.
  • Use ordinary error handling for invalid input and resource failures.
  • NDEBUG disables run-time assertions.
  • Static assertions check constant expressions during compilation.

4.2 Find memory errors with AddressSanitizer

AddressSanitizer, usually called ASan, adds checks to compiled memory operations. It can report a bad access at the moment it happens, before corrupted data causes a distant crash.

Build with the checks linked in

Use GCC or Clang to compile and link with ASan:

clang -std=c17 -g -fsanitize=address -fno-omit-frame-pointer program.c -o program-asan

-g supplies source locations. Keeping frame pointers helps stack traces. For a multi-file project, instrument the relevant source files and use the sanitizer flag at the final link step too. MSVC provides /fsanitize=address on supported Windows builds.

ASan detects many overflows of heap blocks, stack arrays, and global arrays. It also detects use after free, double free, invalid free, and supported use-after-scope errors. On Linux, the usual ASan runtime includes leak checking at exit. Apple's bundled Clang on macOS does not supply that leak detection; LLVM toolchains and platform support differ.

Follow a report to the cause

Consider a successfully allocated array, with 4-byte int:

int *a = malloc(10 * sizeof *a);
for (int i = 0; i <= 10; ++i) {
    a[i] = i;
}

The allocation contains 40 bytes. Indices zero through nine use those bytes. Index ten starts immediately beyond them. ASan can report "heap-buffer-overflow", "WRITE of size 4", and a location zero bytes after a 40-byte region. The write trace identifies the assignment; the allocation trace identifies the malloc that established the block's bounds.

Read reports in this order:

  1. Identify the error category and whether the access was a read or write.
  2. Find the first frame in your own code in the access trace.
  3. Compare the access with the allocated region and its element size.
  4. For a lifetime error, also find the allocation and free traces.

Here the repair is i < 10. Allocating another element while retaining the wrong stopping rule hides the mismatch between the intended count and loop. ASan stops at the first detected error by default. Fix it, rebuild, and run again.

Understand the checker's boundaries

ASan tracks permitted memory using shadow memory. Forbidden redzones surround supported allocations and arrays. Freed blocks spend time in a quarantine before reuse, helping expose stale accesses. A write past an array member into another member of the same struct may stay inside a larger valid region and escape detection.

ASan does not check ordinary uninitialized-value reads. Valgrind's Memcheck can find those uses and many heap defects without recompiling, but commonly runs about 20 to 30 times slower. ASan's typical slowdown is around two times, with additional memory use. Do not combine an ASan build with Valgrind, MemorySanitizer, or ThreadSanitizer. Run edge inputs deliberately: an error on an unexecuted path cannot appear in the report.

Key Takeaways

  • Compile and link with -fsanitize=address and debug information.
  • Read the access trace and the allocation or free traces together.
  • Repair the first reported cause, rebuild, and rerun.
  • ASan does not detect ordinary uninitialized-value reads.
  • Exercise edge inputs to reach risky memory accesses.

4.3 Detect undefined behavior with UBSan and build a checking routine

A memory checker does not cover every invalid C operation. UndefinedBehaviorSanitizer, or UBSan, instruments many arithmetic, pointer, and array operations. Pair it with ASan so a test run can detect more than one class of defect.

Make arithmetic failures visible

With 32-bit int, adding two values of 2,000,000,000 needs a total of 4,000,000,000. That result exceeds INT_MAX. A UBSan report identifies signed integer overflow at the addition and names the operands. Printing a wrapped-looking negative answer without the checker would not make the operation valid.

Use a total type that can represent the accepted range, or reject an addition before it overflows. Converting the already-overflowed result to long long is too late; the operation itself must use a suitable type.

A common test command is:

clang -std=c17 -Wall -Wextra -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined program.c -o program-check

UBSan normally allows recovery after many reports, so later code may continue. -fno-sanitize-recover=undefined makes those failures stop the test with an error status. That helps automated checks detect a bug even when the final printed output looks right.

Match the tool to the defect
ToolMain purposeImportant limit
UBSanMany undefined arithmetic, shift, null, alignment, and array operationsDoes not detect every kind of UB
ASanMany bounds and lifetime errorsDoes not track ordinary uninitialized values
MemorySanitizerUses of uninitialized memoryNeeds instrumented dependencies for reliable results
ThreadSanitizerData racesRequires a separate build from ASan

Unsigned wrap is defined, so the undefined group does not report it. Clang has a separate -fsanitize=unsigned-integer-overflow check for code where wrap signals a bug. Do not infer that every unwanted integer result is UB.

MemorySanitizer is a Clang tool used mainly on Linux; uninstrumented libraries can confuse its tracking. ThreadSanitizer detects conflicting accesses by threads without synchronization, with at least one write. The accesses need not occur at the exact same instant. Neither tool combines with ASan in a single build.

Make checking routine

Keep warnings enabled and resolve them. Build with ASan and UBSan, then run empty inputs, one-element inputs, full buffers, invalid text, and values near the accepted limits. Run static analysis to inspect paths the tests may miss, and repeat the checks automatically when code changes. Also run the release configuration, where optimization and assertion settings differ.

Fuzzing supplies many generated or mutated inputs. libFuzzer and AFL++ can expose unusual parser paths; sanitizers turn a triggered invalid operation into a useful report. Save an input that finds a defect, minimize it without losing the failure, repair the cause, and keep it as a regression test. A clean run is evidence about the executed inputs, not a guarantee for all possible runs.

Key Takeaways

  • Use UBSan for many arithmetic and expression errors.
  • Make UBSan failures stop automated tests when recovery would hide them.
  • Unsigned wrap needs a separate check when it is unwanted.
  • Use separate builds for MemorySanitizer or ThreadSanitizer.
  • Combine edge tests, fuzzing, static analysis, and regression inputs.
  • Sanitizer coverage depends on execution.
5

Write safer C code

Safer C begins where data enters a program and continues through every size calculation, copy, allocation, and error path. A coding standard turns recurring defects into concrete checks you can apply while writing code. Learn to preserve buffer sizes, validate complete numeric inputs, and keep resources under clear ownership. Then weigh these responsibilities against C's control over memory and the protection offered by memory-safe languages.

5.1 Use a secure coding standard

A secure coding standard collects concrete rules that prevent recurring defects. It gives a reviewer more useful questions than "Does this look safe?" Each rule connects a risky operation with the condition or design needed to make it safe.

Read a rule for its reason

The SEI CERT C Coding Standard comes from Carnegie Mellon University's Software Engineering Institute. The 2016 book edition is accompanied by an evolving online version. It distinguishes rules, whose violation is likely to cause defects, from recommendations that improve reliability and security.

Identifiers combine a category, number, and -C. Rules use numbers 30–99; recommendations use 00–29. For example, INT32-C concerns signed overflow, while DCL00-C is a declaration recommendation. Categories include declarations (DCL), expressions (EXP), integers (INT), floating point (FLP), arrays (ARR), strings (STR), memory (MEM), I/O (FIO), environment (ENV), errors (ERR), and miscellaneous concerns (MSC).

A rule page normally explains the defect, shows noncompliant code, presents compliant solutions, and rates risk. Read the assumptions of each solution. A replacement function is useful only when its behavior actually meets the requirement.

Apply a rule to a data path

FIO30-C concerns format strings. This code treats outside data as a formatting program:

printf(user_name);

A user name containing format conversions makes printf look for arguments that were not supplied; %n can cause a memory write. Keep the format under program control:

printf("%s", user_name);

Alternatively, fputs(user_name, stdout) prints the text without format processing. -Wformat=2 includes checks that help find unsafe formats. The repair changes how data is interpreted, not simply how long the string can be.

Build a useful checklist

For memory, ask whether indices stay inside objects (ARR30-C), string storage includes the terminator (STR31-C), freed storage is never accessed (MEM30-C), owned allocations are released (MEM31-C), only valid allocations are freed (MEM34-C), and enough bytes are allocated (MEM35-C).

For expressions, check unintended unsigned wrap (INT30-C), signed overflow (INT32-C), invalid division (INT33-C), uninitialized reads (EXP33-C), and null dereferences (EXP34-C). At library boundaries, handle reported failures (ERR33-C) and conversion errors (ERR34-C). Avoid system() as directed by ENV33-C because a shell interprets its input.

Risk tables rate severity, likelihood, and remediation cost from 1 to 3; a higher cost score means cheaper remediation. Their product is the priority, from 1 to 27. Levels group priorities into L1 (12–27), L2 (6–9), and L3 (1–4). This helps order repairs without declaring lower-priority defects harmless.

Use rules during code review and use analyzer findings mapped to their identifiers. MISRA C is another standard used in safety-critical embedded work. Rules help prevent defects, while warnings, tests, and sanitizers check whether the implementation actually follows them. Memorizing identifiers alone cannot detect a missing terminator or a skipped error branch.

Key Takeaways

  • Read the failure mechanism and the compliant solution together.
  • CERT identifiers distinguish categories, rules, and recommendations.
  • Keep outside data out of format strings.
  • Apply concrete checks to bounds, lifetime, arithmetic, and errors.
  • Use risk ratings to order work, then verify the code with tools.

5.2 Handle strings and buffers safely

Safe string handling requires a valid source, a known destination capacity, and a decision about text that will not fit. A size-limited function prevents some overwrites, but you must still check whether the whole operation succeeded.

Carry capacity with the pointer

sizeof measures the type of its operand. For a local char buf[20], it gives 20 bytes. In void fill(char buf[]), the parameter is adjusted to a pointer; sizeof buf measures that pointer, not the caller's array. Pass a capacity parameter, such as fill(buf, sizeof buf), while the caller still has the array type.

A string containing five characters needs six bytes including '\0'. The capacity counts every writable byte; the string length excludes the terminator. This is why malloc(strlen(src)) allocates one byte too few for a copy.

Read a bounded line

Use fgets(buf, sizeof buf, stdin) and check its return value before using the contents. It stores at most capacity minus one characters, then a terminator. It retains a newline when that newline fits. After a successful read, buf[strcspn(buf, "\n")] = '\0'; removes a stored newline.

If a long line fills the buffer, unread characters remain for the next call. Decide whether to reject and drain the rest, read more, or accept truncation. Removing a newline does not establish that you read the complete line. gets cannot take a capacity and was removed in C11; never use it.

For a single word in char name[20], %19s in scanf limits stored characters and leaves room for the terminator. Check that the expected number of conversions succeeded. For untrusted numeric text, prefer a complete line followed by checked strtol; %d has undefined behavior when the converted value is unrepresentable, even if you inspect scanf's return count.

Detect a shortened copy

strcpy and strcat do not know the destination capacity. strncpy may leave the result unterminated. With a positive capacity, snprintf terminates the stored text and reports how many characters the complete output required:

char dst[8];
int r = snprintf(dst, sizeof dst, "%s", "debugging");
if (r < 0) {
    /* report encoding failure */
} else if ((size_t)r >= sizeof dst) {
    /* report truncation */
}

The stored string is "debuggi", and r is 9. The full output needs ten bytes including its terminator. Check negativity before converting the result to size_t. A fitting result requires r to be less than the capacity.

Match the operation to the storage

Use memmove when source and destination overlap; overlapping memcpy is undefined. strlcpy is available on BSD systems, macOS, and glibc 2.38 onward, but is not ISO C. Annex K functions are optional and unavailable in many libraries.

Keep user data out of the format itself. Match each conversion to its argument: %d for int, %ld for long, %zu for size_t, %f for double, and %s for a terminated string. %p requires a void *, so pass an appropriate pointer conversion. A width limit cannot repair a mismatched argument type.

Key Takeaways

  • Pass destination capacity alongside a pointer.
  • Reserve one byte for the string terminator.
  • Handle incomplete input lines explicitly.
  • Check snprintf for failure and truncation.
  • Use memmove for overlapping ranges.
  • Match every format conversion to its argument type.

5.3 Validate input and handle errors

Outside data can be syntactically valid and still unsafe for the operation you intend. Validate its length, complete format, and useful range before it becomes an index, allocation size, or loop count.

Parse the whole number

atoi gives no reliable way to separate a valid zero from invalid text, and an unrepresentable result has undefined behavior. strtol provides an end pointer and a range error. After reading a complete line and removing its newline, a strict decimal parser can use:

errno = 0;
char *end;
long v = strtol(text, &end, 10);
if (end == text || *end != '\0' || errno == ERANGE) {
    /* reject the numeric text */
} else if (v < 0 || v > 150) {
    /* reject this application's range */
} else {
    int age = (int)v;
    /* use age */
}

Include <errno.h> and <stdlib.h>. The first test separates three failures: no digits, trailing text, and a value outside long. The application-range test comes before narrowing to int. strtol accepts leading spaces and an optional sign; decide whether the application's grammar permits them.

For "42abc", conversion can return 42, but end points to the remaining letters. For "abc", no digits are consumed. For an oversized decimal integer, errno becomes ERANGE. A nonzero returned number alone establishes neither complete conversion nor a safe range.

Preserve ownership across failure

Every operation that can fail needs a response. Check allocation results, fopen, fgets, conversion counts, and output completion. fgets returning NULL can mean end of input or a read error. fclose can report a buffered write failure. Use error information appropriate to the function; many library calls set errno, which perror or strerror can turn into a readable diagnostic.

For positive resizing, protect the size calculation and the original allocation:

if (new_n == 0) {
    free(a);
    a = NULL;
} else if (new_n > SIZE_MAX / sizeof *a) {
    /* reject the size */
} else {
    int *tmp = realloc(a, new_n * sizeof *a);
    if (tmp == NULL) {
        /* handle failure; a still owns its old block */
    } else {
        a = tmp;
    }
}

Assigning realloc directly to the only owning pointer loses the old block's address on failure. Handle zero separately: C17 makes zero-size realloc implementation-defined, while C23 makes it undefined. After successful resizing, use the returned pointer and discard stale aliases.

Leave a clear error path

A single cleanup section can release acquired resources in reverse order. Initialize owned pointers to NULL, close only opened files, and return a failure status after reporting the cause. A goto cleanup can make these paths easier to audit.

Avoid system() for tasks involving outside text: the shell can interpret that text as commands. Prefer a library operation, or a suitable program-launch interface with arguments passed separately. Treat command-line arguments, environment values, file contents, and network data as input requiring the same care as typed text.

Key Takeaways

  • Validate complete conversion and application range before using a number.
  • Reset errno before strtol and inspect its end pointer.
  • Check library failures and preserve resources on every path.
  • Use a temporary pointer for positive-size realloc.
  • Handle zero sizes and multiplication overflow before resizing.
  • Release only resources actually acquired.
  • Keep outside text out of shell command strings.

5.4 Weigh memory-safety trade-offs

Memory safety means accesses stay within storage a program is entitled to use. C gives direct memory control and leaves boundary and lifetime checks to the programmer.

Distinguish bounds from time

Spatial safety keeps an access inside its object's bounds. Writing element ten of a ten-element array violates it. Temporal safety keeps access inside the object's lifetime. Reading an allocation after free, freeing it twice, or using a returned local address violates lifetime rules.

The consequences can extend beyond a crash. A read can expose confidential data. A write can corrupt a file, change control data, or create a security vulnerability when outside input controls the operation. A program that continues after corruption may produce convincing but false results.

This burden affects experienced teams too. Microsoft reported in 2019 that about 70% of the vulnerabilities it had assigned CVE identifiers to, year after year over more than a decade, concerned memory safety. In December 2023, CISA and partner agencies urged software makers to plan moves toward memory-safe languages. These are specific historical observations, not a measured percentage for every C program.

Understand the reasons to choose C

C offers control over layout, allocation, and access to hardware, without requiring a garbage collector. Its small runtime suits constrained firmware. It has broad compiler support, large existing codebases, and interfaces that many other languages can call. These benefits explain its use in operating-system kernels, embedded software, runtimes, and libraries.

They do not make every C program predictably fast: algorithms, allocation, caches, and system calls still matter. The relevant benefit is control over costs and runtime requirements.

ApproachMemory protectionCost or responsibility
CProgrammer-enforced bounds and lifetimesManual reasoning, checks, and testing
Safe RustOwnership, borrowing, and bounds checksLearning its ownership rules
Go, Java, C#, PythonManaged lifetimes and bounds checksRuntime and memory overhead vary
SwiftReference counting and checked accessOwnership and runtime costs vary

Unsafe operations and interfaces to C need special care in otherwise memory-safe systems. Memory safety also leaves logic errors and input-driven injection to solve.

Choose layers for the remaining risk

Prevent defects with coding rules, warnings, and review. Detect defects with static analysis, edge tests, sanitizers, and fuzzing. Limit remaining damage with stack protectors, supported _FORTIFY_SOURCE checks, address space layout randomization (ASLR), and non-executable memory. A mitigation can stop an attack or turn corruption into a crash while leaving the source defect present.

Costs guide deployment. ASan typically doubles execution time and increases memory use; Valgrind commonly slows execution by 20 to 30 times. They fit testing better than ordinary release use. Lower-cost hardening commonly remains in release builds.

For a new internet-file parser with no constraint requiring C, a memory-safe language reduces a major risk. For existing C firmware on a microcontroller with 32 KB of memory, language support and resource limits may favor C with strong checks. Base the choice on constraints, input exposure, and maintainability; a gradual replacement of a risky parser can also be practical.

Key Takeaways

  • Spatial safety concerns bounds; temporal safety concerns lifetimes.
  • Memory defects can expose data and corrupt results.
  • C offers direct control and broad compatibility with a manual safety burden.
  • Memory-safe languages remove many memory defects but leave other bug classes.
  • Combine prevention, detection, and release mitigations.
  • Choose a language using actual resource and integration constraints.

Debugging and Undefined Behavior in C examples

5 Walkthroughs

1

Build a program you can inspect

9 steps
Read compiler feedback, repair the code it identifies, and choose separate build settings for debugging and release.
  1. 1
    IntroductionStart before the first run

    A program can compile and still contain a defect the compiler knows how to flag. Your first job is to ask for that feedback, as warnings, and interpret it.

    You will inspect an accidental assignment and a format mismatch, then choose settings for a debugger session (a debugger build) and a release build. These decisions prepare later tools to show useful evidence instead of unexplained addresses or missing local values.

  2. 2
    ExplanationChoose the warning set

    Start with -std=c17 -Wall -Wextra -Wpedantic. The first flag fixes the language version; the others enable common warnings, additional warnings, and diagnostics for many departures from that standard.

    -Wall does not mean every warning. Add -Wconversion for value-changing implicit conversions and -Wformat=2 for stricter format checks. -Werror changes enabled warnings into build errors.

  3. 3
    ExampleRead the condition

    The programmer wants to report an empty collection:

    if (items = 0) {
        puts("No items");
    }
    

    A compiler warning points to assignment in a condition. First ask what the expression does: it stores zero in items, then the condition tests that zero. The body never runs. Replace = with == to test the existing count; adding parentheses around the assignment would preserve the wrong operation.

  4. 4
    ChallengeRepair the output type

    double ratio = 2.5; is printed with printf("%d\n", ratio);. What should change, and why?

    ---reveal---

    Use printf("%f\n", ratio);. %d expects an int, while %f expects a double in printf. A mismatched format has undefined behavior. Casting to int would change the number being printed by dropping its fraction, so it would not meet the goal of printing the ratio.

  5. 5
    TipRead messages in order

    A diagnostic usually gives the source location, the problem, and the flag responsible. Read related notes too; they may identify the conflicting declaration.

    Fix the first compiler error, then rebuild. A missing semicolon can confuse later parsing and create a cascade of messages. After errors are gone, inspect each warning's cause. A warning-free build still needs tests for logic and input-dependent errors.

  6. 6
    ExplanationPreserve useful state

    For a simple debugger build, add -g -O0. Debug information maps execution to source names and lines; low optimization keeps more intermediate state available for inspection.

    -g does not disable optimization. With -O2, code can still be debugged, but lines may appear to jump and variables can be optimized away. -Og is another development choice that keeps light optimization.

  7. 7
    ConnectionChoose checks for different jobs

    Static analysis follows possible source paths without running the program. A sanitizer checks operations on paths the tests actually execute. Use both to extend coverage.

    Release hardening serves a further purpose: stack protectors and supported fortification checks can detect or limit some remaining corruption. Fortification requires optimization and suitable library support. A detected overwrite still needs a source repair.

  8. 8
    ChallengeKeep validation in release

    A debug build uses strict warnings and assertions. A release build adds -O2 -DNDEBUG. The programmer proposes using assert(valid_input) as the only input check. What must change before release?

    ---reveal---

    Validate input with ordinary control flow and handle failure. NDEBUG removes evaluation of assertions, so the proposed check vanishes. Keep warnings enabled and test the release configuration as well as the debug build; different settings can expose hidden defects.

  9. 9
    RecapConnect the build settings

    Use compiler messages to identify operations needing repair. Match formats to types, investigate conversions, and fix early errors before following their downstream messages.

    For the next debugging session, build the right executable with source information and a suitable optimization level. Keep configurations repeatable, add analysis and sanitizer testing, and retain input checks when release settings remove assertions.

2

Trace a wrong total and a crash

12 steps
Move from reproduction to stepping, backtraces, and targeted stops that identify a bad value’s origin.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionTurn a symptom into a question
  2. 2
    ExplanationPreserve the failing case
  3. 3
    ChallengeChoose a controlled experiment
  4. 4
    ExamplePause at the loop body
  5. 5
    ExampleFollow the valid passes
  6. 6
    ChallengeFix and verify the extra pass
  7. 7
    TipStep only into useful code
  8. 8
    ExampleMove to the caller
  9. 9
    ExplanationCatch a change when it happens
  10. 10
    ChallengeInvestigate an allocator crash
  11. 11
    ConnectionUse a saved crash state
  12. 12
    RecapChoose the view that answers the question
3

Check the operation before it becomes invalid

12 steps
Reason about optimization, arithmetic limits, and object lifetimes without relying on accidental output.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionAsk what C guarantees
  2. 2
    ExplanationDistinguish variation from a defect
  3. 3
    ExampleAn overflow test that arrives too late
  4. 4
    ChallengePlace the pointer check
  5. 5
    ExampleGuard a signed sum
  6. 6
    Key InsightDefined wrap can still break the program
  7. 7
    ChallengeWiden before multiplying
  8. 8
    TipCheck several expression boundaries
  9. 9
    ExampleSeparate an end marker from an element
  10. 10
    ExampleFollow every alias through release
  11. 11
    ChallengeRepair a total and its lifetime
  12. 12
    RecapCheck type, range, bounds, and lifetime
4

Read checks as evidence of a specific defect

11 steps
Choose assertion, ASan, and UBSan checks, then connect each report with a lasting repair.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionCheck more than final output
  2. 2
    ExplanationAssert an internal promise
  3. 3
    ChallengeKeep the operation in every build
  4. 4
    ExplanationBuild an instrumented executable
  5. 5
    ExampleRead the reported region
  6. 6
    ChallengeRepair the matching loop
  7. 7
    Key InsightA missing report has several explanations
  8. 8
    ExampleMake arithmetic failure stop the test
  9. 9
    ExplanationChoose other checkers deliberately
  10. 10
    ChallengeImprove an incomplete checking routine
  11. 11
    RecapTurn reports into repeatable checks
5

Keep input, sizes, and ownership explicit

15 steps
Apply coding rules to a small input-processing path, then weigh the safety costs of choosing C.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionFollow data through the program
  2. 2
    ExplanationUse the rule as a question
  3. 3
    ChallengeKeep input as data
  4. 4
    ExamplePass capacity from the caller
  5. 5
    ExampleDistinguish a safe prefix from success
  6. 6
    ChallengeHandle an incomplete line
  7. 7
    ExampleInspect the end of conversion
  8. 8
    ExampleApply the program’s range
  9. 9
    ExamplePreserve a resize owner
  10. 10
    ChallengePlan the failure path
  11. 11
    ExplanationIdentify the safety boundary
  12. 12
    ConnectionChoose layers with different jobs
  13. 13
    ChallengeMake a language decision
  14. 14
    ChallengeReview the complete input path
  15. 15
    RecapKeep each boundary visible

Debugging and Undefined Behavior in C practice questions

50 Practice Questions in 5 sets

Set 1: Compiler clues and debugger decisions10 Practice Questions

Question 1 of 10

A clean -Wall build silently converts a double count to int. What extra warning flag targets this implicit value change?
  1. -Wshadow
  2. -Wformat=2
  3. -Wconversion
  4. -Wpedantic
Show answerHide answer
Answer: C
-Wconversion targets implicit conversions that may alter a value, including dropping a floating-point fraction. -Wshadow concerns a hidden declaration with the same name, not a change of type. A warning should lead to checking the intended value before adding a cast.

Question 2 of 10

A program must print a fractional reading. It declares double reading = 7.25; and calls printf("%d\n", reading);. What repair preserves that goal?
  1. Use %f with reading.
  2. Cast reading to int.
  3. Use %ld with reading.
  4. Cast reading to long.
Show answerHide answer
Answer: A
printf expects a double for %f, so changing the format preserves the fractional value and matches the argument. Casting to int could satisfy %d but would discard the fraction the program must print. The original mismatch has undefined behavior; the format is not an automatic numeric conversion.

Question 3 of 10

A debugger shows function addresses but cannot display source lines or local variable names. What compiler flag supplies the missing source information?
  1. -Wextra
  2. -DNDEBUG
  3. -Werror
  4. -g
Show answerHide answer
Answer: D
-g adds the debug information that maps execution to source locations, types, and names. -Wextra enables additional diagnostics during compilation but does not provide those mappings. Optimization remains a separate choice and may still remove values even when debug information is present.

Question 4 of 10

A 240-record input crashes, but each half succeeds when run alone. What is the most useful next reduction strategy?
  1. Keep only the last record and test it as an independent trigger.
  2. Remove smaller pieces while preserving the failing sequence.
  3. Double each buffer and discard the original input.
  4. Run either successful half until the crash returns.
Show answerHide answer
Answer: B
The failure may depend on several records together or on accumulated state, so retain a failing sequence while removing smaller pieces. Selecting a single record just because it is last assumes independence that the two successful halves do not establish. Every reduction must be checked against the actual failure.

Question 5 of 10

Execution is paused on a call you want to treat as one source step. Which debugger command normally runs over that call?
  1. step
  2. next
  3. print
  4. backtrace
Show answerHide answer
Answer: B
next normally executes the current source line while stepping over its function calls. step is intended to enter a called function when useful source information exists. A breakpoint inside the call can still interrupt next, so the command describes stepping behavior rather than disabling all stops.

Question 6 of 10

A breakpoint is on result = compute();. Printing result shows its earlier value, and no call has yet been executed at this stop. How should you inspect the computed result?
  1. Enter print result again without resuming.
  2. Delete the breakpoint and edit the initializer.
  3. Switch to the caller without running the call.
  4. Use next, then inspect the new result.
Show answerHide answer
Answer: D
The stop precedes the assignment, so result still contains its earlier value. Running the line with next allows the call and assignment to complete before inspection. Repeating a print without resuming observes the same state and supplies no evidence that the assignment failed.

Question 7 of 10

A backtrace shows frame zero in strlen and frame two in your render function. You want to inspect render’s caption pointer in GDB. What should you do?
  1. Run frame 2, then p caption.
  2. Run frame 0, then p caption.
  3. Run next, then p caption.
  4. Run continue, then p caption.
Show answerHide answer
Answer: A
Frame selection makes the chosen function’s local context available without executing more code. frame 2 selects render, where caption can be inspected. Staying in frame zero examines the library context, and resuming execution is unnecessary when the evidence you need is already in a caller’s frame.

Question 8 of 10

A count becomes wrong somewhere between two known good and bad observations, but the writing location is unknown. What debugger feature best locates that write?
  1. A breakpoint at the final printf call.
  2. A backtrace after normal termination.
  3. A watchpoint on the live count variable.
  4. A source listing around its declaration.
Show answerHide answer
Answer: C
A watchpoint stops when the monitored storage changes, revealing the responsible operation and its old and new values. A breakpoint at the final print only observes the already-wrong state. Check each stopped write against the intended count rule, because some changes to that variable may be legitimate.

Question 9 of 10

A warning-free program produces different totals at -O0 and -O2. At -O2, an intermediate variable is also unavailable in the debugger. What investigation addresses both observations?
  1. Remove optimization permanently and accept the original debug total.
  2. Keep -O2 and treat the missing variable as corruption.
  3. Disable debug symbols and compare the printed addresses.
  4. Inspect UB with sanitizers and use -g -O0 for stepping.
Show answerHide answer
Answer: D
Optimization can legitimately remove intermediate values, while changing totals is a reason to investigate undefined operations in the code. A sanitizer build helps find those operations, and a low-optimization debug build aids inspection. Accepting the debug result alone leaves the cause of the release difference unresolved.

Question 10 of 10

A parser crashes in free after reading many records. Enlarging a name buffer stops the visible crash, but the saved input still causes ASan to report a write past that buffer. What action addresses the cause?
  1. Keep the larger buffer and disable ASan for that test.
  2. Trace and repair the write’s bound, then retain the input.
  3. Replace the allocator call with an assertion and keep the larger buffer.
  4. Catch the allocator crash and continue with the records.
Show answerHide answer
Answer: B
The ASan report identifies an invalid write that remains despite the changed symptom. Repair the index or copy bound, rerun the original input, and retain it as a regression test. Making nearby storage larger can hide corruption from a later allocator check without making the original access valid.
Set 2: Types, ranges, and live objects10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

Two independent calls are passed as arguments in merge(left(), right()). Either evaluation order is permitted and no shared state creates a conflict. How is that order classified in C?
  1. Undefined behavior.
  2. Unspecified behavior.
  3. Implementation-defined behavior.
  4. A required left-to-right order.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

Assume 32-bit unsigned int. What value does unsigned int slots = 0u - 1u; store?
  1. Zero, because unsigned values cannot be negative.
  2. Negative one, preserving the mathematical result.
  3. No defined value, because subtraction underflows.
  4. 4,294,967,295, by defined unsigned wrapping.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

Assume 32-bit unsigned int. An input supplies shift = 32. What is the status of 1u << shift?
  1. Undefined: the shift count reaches the type width.
  2. Defined zero: all set bits are discarded beyond the result width.
  3. Defined one: the count wraps modulo the width.
  4. Implementation-defined: unsigned shifts vary.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

An array has seven elements. What is the largest valid element index?
  1. 7
  2. 8
  3. 6
  4. 5
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A function returns local after declaring char local[24] = "ready";. Why is reading the result in the caller invalid?
  1. The array has too much unused space for a string.
  2. The return converts the array into a string literal.
  3. The local array’s lifetime ended at function return.
  4. The caller must apply sizeof before any string read.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

For signed int a and positive int b, what condition detects an overflowing a + b without evaluating that sum?
  1. a > INT_MAX - b
  2. a + b > INT_MAX
  3. a > INT_MAX + b
  4. a - b > INT_MAX
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A helper executes int old = *p; before if (p == NULL) return -1;. A null-input test seems to reach the error return at low optimization but crashes in release. What repair establishes a defined null-input path?
  1. Keep the original order and make the loaded local variable volatile.
  2. Keep the order and compile without -Werror.
  3. Print p between the read and the check.
  4. Move the null test before the first dereference.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

On a platform with 32-bit int and 64-bit long long, both factors are int: units = 70000, rate = 40000. What expression correctly computes their product in long long?
  1. (long long)(units * rate)
  2. (long long)units * rate
  3. (int)((long long)units * rate)
  4. (long long)(int)(units * rate)
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A C function owns a heap buffer through p; alias points into it. A positive-size realloc succeeds and the block moves. The function assigns the result to p, writes through alias, then frees both p and alias. What ownership repair is required?
  1. Discard the stale alias, use the returned allocation, and free it once.
  2. Keep the stale alias, make the new block larger, and free both pointers.
  3. Check the alias for null, write through it, and free only that pointer.
  4. Use the returned allocation, then free the alias before freeing the owner.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A checked build uses only -fsanitize=undefined. A size_t multiplication wraps but produces no report. Why can this happen?
  1. The check runs only when NDEBUG is defined.
  2. The wrap is hidden because size_t is a pointer.
  3. Unsigned wrap is defined and outside that group.
  4. UBSan checks only stack and heap array writes.
Show answerHide answer

Save this lesson to see this answer.

Set 3: Assertions and sanitizer reports10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A private list helper promises its stored count always equals the number of nodes after each update. What kind of programming condition is this?
  1. An invariant.
  2. An input format.
  3. An allocation result.
  4. A compiler option.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

An application uses assert(user_count > 0); as its only rejection of invalid user counts. Release builds define NDEBUG. What should replace that protection?
  1. A static assertion on the same run-time variable.
  2. An extra assertion after the allocation succeeds.
  3. A cast from user_count to an unsigned type.
  4. An ordinary range check with an error path.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

ASan reports a four-byte write "0 bytes after 28-byte region". Assume each array element is four bytes. Which attempted index starts at that address?
  1. Index 6.
  2. Index 7.
  3. Index 8.
  4. Index 28.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A test needs to identify ordinary uses of uninitialized values. Why is an ASan-only build insufficient?
  1. ASan tracks those uses only on the heap.
  2. ASan tracks those uses only in globals.
  3. ASan does not track those value uses.
  4. ASan needs recover mode to track them.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

An automated UBSan test logs signed overflow but continues and returns success. Which flag makes a detected UB error stop the run?
  1. -fno-omit-frame-pointer: preserve stack frames.
  2. -Wconversion: diagnose implicit value changes.
  3. -fno-sanitize-recover=undefined
  4. -D_FORTIFY_SOURCE=2: check supported copies.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A test writes past a struct’s array member into its next member and also wraps an unsigned byte-count product. ASan and -fsanitize=undefined both stay silent, but the neighboring value and allocation size are wrong. What investigation addresses both defects?
  1. Inspect member bounds and guard the unsigned size product before use.
  2. Accept member bounds and inspect only signed arithmetic in the total.
  3. Enlarge the struct and accept the wrapped allocation size as checked.
  4. Keep the member index and turn every stored count into an assertion.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A team wants to investigate both heap overruns and uninitialized-value uses. Its current ASan build links third-party libraries compiled normally. What checking arrangement addresses the two tool requirements?
  1. Combine ASan and MSan instrumentation but keep all third-party libraries unchanged.
  2. Use UBSan instead of both and keep the libraries unchanged.
  3. Run the ASan executable under Valgrind for every test.
  4. Keep ASan and build MSan separately with compatible dependencies.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

You have a core dump from a crash that no longer reproduces. What can you do using the matching executable?
  1. Continue from the crash and change the input.
  2. Inspect frames and available saved variables.
  3. Rerun earlier writes from the saved backtrace.
  4. Recompile the dump into a repaired executable.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A helper does assert(++used < capacity); before storing at index used. Tests pass, but with NDEBUG it repeatedly overwrites one slot. What source change preserves the intended state update in both builds?
  1. Increment separately, then assert the internal bound.
  2. Retain the increment in assert and use -O0.
  3. Replace the assertion with static_assert unchanged.
  4. Make used unsigned and retain the assertion.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

Fuzzing finds a crash when a parser converts "-3", casts it to size_t, allocates a count-dependent buffer, and fills it. It checks neither conversion completion nor application range. What repair order addresses the input-to-memory path?
  1. Increase the allocation, cast first, and accept any nonzero conversion.
  2. Check the fill after execution, then decide whether conversion succeeded.
  3. Validate conversion and count, guard the size product, then check allocation.
  4. Make the fill count unsigned and keep the existing conversion and allocation.
Show answerHide answer

Save this lesson to see this answer.

Set 4: Strings, numeric input, and resource ownership10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A program must print a terminated string supplied in a file without interpreting embedded format conversions. What call does that?
  1. printf(label);
  2. printf("%s", label);
  3. printf("%d", label);
  4. printf("%p", label);
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A caller passes char line[48] to read_line(char line[]) on a system with eight-byte pointers. What does sizeof line measure inside the function?
  1. The 48-byte caller array.
  2. The current string length.
  3. The array size plus a terminator.
  4. The eight-byte pointer parameter.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

snprintf is called with a destination capacity of 11 and returns 11. What does this establish?
  1. The complete output did not fit.
  2. Exactly eleven characters were stored.
  3. The output fit with its terminator.
  4. An encoding error prevented output.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A strict decimal parser resets errno and converts "64kg" with strtol. Conversion returns 64 and errno stays zero. What must cause rejection?
  1. The returned numeric value is different from zero.
  2. The base argument is decimal.
  3. The end pointer identifies trailing text.
  4. The result fits within the long range.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A program accepts decimal counts from 1 through 900. It parses " -4rest" with strtol, checks only errno, casts to size_t, and multiplies by the element size for allocation. What checks must precede allocation?
  1. Cast the prefix, multiply, then reject a zero result and a failed allocation.
  2. Require full conversion and accepted range, then guard the unsigned product.
  3. Accept the prefix when errno is zero, then enforce only the buffer capacity.
  4. Remove the minus sign, accept the prefix, then use an assertion for the size.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

An array is owned only through a. After a positive input count is read into new_n, the code does a = realloc(a, new_n * sizeof *a); a[new_n - 1] = 0;. Every valid count must be handled, and a failed resize must preserve the old array. What sequence is required?
  1. Guard the product, resize through a temporary, and write only after success.
  2. Compute the product first, resize directly into a, and check the old size.
  3. Resize through a temporary, ignore null, and write through the original a.
  4. Check only for null, replace a, and validate the product after the write.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A 12-byte line buffer receives 11 characters from fgets, with no newline stored. The application requires one complete record per line. What handling avoids treating a possible remainder as a new record?
  1. Remove the final stored character and then accept the prefix as a full line.
  2. Parse the prefix and let the next call create a record.
  3. Append a terminator and declare the line complete.
  4. Determine the remaining input and enforce the line policy.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A function receives a destination pointer and capacity 7. It must preserve an entire nine-character input or report failure. It uses strncpy(dst, src, sizeof dst) and later prints dst with %s. Assume eight-byte pointers. What redesign meets the requirement?
  1. Keep sizeof dst, append a terminator after eight bytes, and print the prefix.
  2. Copy six characters, append a terminator, and report success for the prefix.
  3. Use the supplied capacity, check a bounded copy result, and reject truncation.
  4. Use the input as the printf format so no destination-size check is required.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A program now builds as C23. It reads a size_t count from a file and immediately does a = realloc(a, count * sizeof *a);. Empty arrays are permitted, and a failed request must preserve the old array. What redesign handles these requirements for every count?
  1. Call realloc for every size, assign to the owner, then inspect product overflow.
  2. Handle zero separately, guard the product, then use a temporary.
  3. Convert the product to signed int, then call realloc.
  4. Use assert(new_n > 0), then directly replace the owner.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A new file parser handles internet data and has no integration, hardware, or resource constraint requiring C. What design choice most directly reduces manual bounds and lifetime risk?
  1. Use a memory-safe language for the parser.
  2. Use C with only a larger default buffer.
  3. Use C and disable all dynamic allocations.
  4. Use C with only address randomization.
Show answerHide answer

Save this lesson to see this answer.

Set 5: Repair interacting defects10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

Text is shifted left within the same array, and the source and destination byte ranges overlap. What function provides defined copying for this situation?
  1. memcpy
  2. memmove
  3. strncpy
  4. snprintf
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A CERT risk assessment gives severity 3, likelihood 2, and remediation score 2. What priority and level result?
  1. Priority 7, level L2.
  2. Priority 9, level L2.
  3. Priority 12, level L1.
  4. Priority 18, level L1.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A pointer is used after its object has been freed. Which part of memory safety is violated?
  1. Format-string safety.
  2. Spatial safety.
  3. Numeric safety.
  4. Temporal safety.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

For C17 int i = 4;, a programmer writes printf("%d %d\n", i, ++i);. What repair makes an intended "old value, then new value" output well-defined?
  1. Save i in old, increment i, then print old and i.
  2. Swap the order of the two arguments and retain the increment inside the call.
  3. Add parentheses around ++i and keep both arguments.
  4. Cast both arguments to unsigned within the same call.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A function owns word, computes int bytes = strlen(word);, allocates that many bytes, copies the complete string, frees the new block, and returns its address. Inputs may be too long for int. What design addresses the size and lifetime path?
  1. Retain the int count, add one byte, and keep the free before returning.
  2. Use size_t but allocate length bytes and transfer the resulting block.
  3. Keep a checked size_t length plus terminator and transfer a live checked allocation.
  4. Keep the int count and allocation, but remove only the early free.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A parser strips a newline from a complete line, calls strtol, and checks only errno != ERANGE. It then casts the result to int for an array index. What missing checks can still admit invalid input?
  1. Whether the input has a decimal point and whether any digit precedes it.
  2. Only whether the result equals LONG_MAX.
  3. Only whether the conversion used decimal base.
  4. No digits, trailing text, and the valid index range.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A size-dependent loop is tested only with one element under ASan and UBSan. It passes. Production fails with a full buffer and assertions disabled. What verification plan addresses these differences?
  1. Exercise boundary inputs in checked and release builds.
  2. Repeat the existing one-element checked test with additional diagnostic prints.
  3. Keep assertions enabled and skip the full-buffer case.
  4. Run static analysis and drop all dynamic tests.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A legacy C parser enables stack protection and ASLR. A fuzzed input still triggers an ASan write beyond a heap block. What response correctly combines prevention and mitigation?
  1. Remove the sanitizer because hardening defines the write.
  2. Repair the heap bound and keep the release protections.
  3. Disable ASLR to make the invalid write valid.
  4. Increase the heap block until the crash is delayed.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

After a crash in a string function, frame zero has no useful locals. Frame three in your code passes a pointer returned by a lookup. That pointer is null, and missing entries are valid input. What repair and verification follow?
  1. Add a run-time assertion at the library boundary, then keep the existing caller.
  2. Replace the lookup with a larger allocated array.
  3. Use continue until the pointer becomes non-null.
  4. Handle the missing lookup before calling, then test that input.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A function opens an output file and allocates two buffers. The second allocation fails. Its cleanup closes the file and returns but never frees the first buffer. Which correction preserves both ownership and error reporting?
  1. Report failure and close the file, but retain the first buffer for no caller.
  2. Release the first buffer and file, but return success for the incomplete work.
  3. Free the acquired buffer, check close, and report the operation as failed.
  4. Free the null second result and close, leaving the first allocation unreleased.
Show answerHide answer

Save this lesson to see this answer.

Stuck on your own question? Homework Help

Key Terms

78 Key Terms, each with its definition

A

AddressSanitizer
AddressSanitizer is compiler-based instrumentation that detects many invalid bounds, lifetime, and deallocation operations at run time.
A report can identify the bad write and the allocation whose bounds that write exceeded.
ASLR
Address space layout randomization (ASLR) varies memory locations between runs to make some attacks on remaining memory defects harder.
ASLR can complicate abuse of a bad access while leaving the original out-of-bounds write unfixed.
Assertion
An assertion is an executable check of a programming assumption that reports failure and aborts when enabled and false.
An internal averaging helper can assert that its validated caller supplies a nonzero count.

B

Backtrace
A backtrace is a debugger's report of active call frames, usually numbered from the innermost frame as zero.
After a crash, bt helps you locate the first source frame belonging to your own code.
Breakpoint
A breakpoint is a chosen execution location where a debugger stops the program, normally before the indicated statement runs.
A breakpoint before sum += a[i] lets you inspect i without evaluating the array access.
Buffer capacity
A buffer capacity is the total amount of writable storage available, including any space needed for a string's terminator.
A ten-byte array can store at most nine single-byte characters as a terminated C string.
Bug
A bug is a defect in code that can cause incorrect behavior when a particular path or input reaches it.
A loop condition may be wrong even on runs where the invalid access it causes produces no visible crash.

C

Call stack
The call stack is the chain of active function calls, from the current function outward through the functions that called it.
A string-library call at frame zero may have received its invalid pointer from main.
Checked arithmetic
Checked arithmetic detects an unrepresentable mathematical result without requiring the program to perform undefined overflow.
C23's ckd_add reports whether a sum fits so the program can handle failure explicitly.
Cleanup path
A cleanup path releases resources actually acquired before a function returns, including after a partially completed operation.
A shared cleanup section can free a buffer and close a file after a later allocation fails.
Compiler diagnostic
A compiler diagnostic is a message identifying a source location and a suspected or definite problem found during compilation.
A format diagnostic can point to %d and identify the double argument that does not match it.
Compiler error
A compiler error is a diagnostic that prevents a successful build until the reported problem is resolved.
Fixing a missing semicolon can remove several later messages caused by the same syntax error.
Compiler warning
A compiler warning reports suspicious code or a detected problem while normally allowing compilation to continue.
An assignment used as a condition may compile successfully and still receive a warning.
Compliant solution
A compliant solution is a design or code example that meets a rule's requirement under the solution's stated conditions.
Using a fixed %s format keeps supplied text in the data argument of a printf call.
Conditional breakpoint
A conditional breakpoint stops at a chosen location only when its condition evaluates true in the current program state.
A condition such as i == 650 avoids manually stepping through earlier loop iterations.
Core dump
A core dump is a saved snapshot of a process at a crash that a debugger can inspect using the matching executable.
A saved dump supports a backtrace and local-value inspection after the failing process has ended.

D

Dangling pointer
A dangling pointer refers to storage whose object's lifetime has ended, so it cannot provide valid access to that old object.
Returning a local array's address leaves the caller with a pointer to expired storage.
Data race
A data race occurs when threads make conflicting accesses without required synchronization, with at least one access writing.
Two unsynchronized updates to shared state need not happen at the same instant to form a race.
Debug build
A debug build is an executable configured for inspection, commonly with source information and little optimization.
A -g -O0 build makes a faulty loop easier to follow one source statement at a time.
Debug information
Debug information describes source names, types, and locations in an executable so inspection tools can relate instructions to code.
The -g flag helps a debugger identify the source line and locals at an ordinary breakpoint.
Debugger
A debugger controls a program's execution so you can pause it, inspect state, and advance through selected operations.
GDB and LLDB can stop before a loop body so you can compare the current index with the bound.
Defense in depth
Defense in depth combines prevention, detection, and damage reduction so several protections address remaining defects.
Warnings and review prevent bugs, sanitizers detect them in tests, and hardening limits some damage.
Double free
A double free attempts to release the same allocation more than once without a new valid allocation establishing ownership.
A cleanup branch that frees an already-released block can trigger an allocator or ASan report.

E

End pointer
An end pointer returned by strtol identifies the first unconverted character, or the input start if no digits were consumed.
For "73x", the end pointer identifies the trailing "x" that a strict numeric parser rejects.
Expression evaluation
Debugger expression evaluation computes an expression using the selected frame's current state while execution is paused.
Printing count observes a scalar; evaluating *p attempts an access through that pointer.

F

Failure
A failure is an observed departure from intended behavior, such as a crash, wrong result, or missing output.
A crash in free can be the visible failure caused by a write much earlier in execution.
Format string
A format string tells a formatted I/O function how to interpret arguments, so its conversions must match their actual types.
A printf format uses %zu for size_t; user text belongs in a separate data argument.
Fuzzing
Fuzzing tests a program with many automatically generated or mutated inputs to discover failing cases missed by planned tests.
A mutated record can reach a parser branch where a sanitizer detects an invalid access.

H

Hardening
Hardening adds protections intended to detect some corruption or limit damage from defects that remain in a released program.
A stack-protector check can stop a function after detecting an overwrite of its canary.
Hypothesis
A debugging hypothesis is a proposed explanation of a failure that makes a prediction you can test with an observation or change.
Suspecting a missing field suggests inspecting its pointer immediately before string processing.

I

Implementation-defined behavior
Implementation-defined behavior permits a choice that each C implementation must document so programs can account for it.
A C17 implementation documents whether its plain char type is signed or unsigned.
Implicit conversion
An implicit conversion changes a value's type without a written cast, and can lose information or alter the value.
Assigning a double to an int drops the fractional part when the truncated value fits.
Indeterminate value
An indeterminate value is the state of an object whose usable value has not been established, as with many uninitialized automatic variables.
Declaring int sum; and immediately using sum += x; reads a value that was never initialized.
Instrumentation
Instrumentation adds checking or observation code to a program so tools can inspect operations during execution.
An ASan build needs instrumented object files and the sanitizer runtime at final linking.
Integer promotion
An integer promotion converts a smaller integer type to int or unsigned int before many arithmetic and shift operations.
Shift-count validity depends on the promoted left operand's width, not only its original spelling.
Invariant
An invariant is a property maintained across repeated steps or valid states of a loop or data structure.
A list's stored count should match its number of nodes after every completed insertion.

M

Memory-safe language
A memory-safe language enforces bounds and lifetime rules for its safe operations, reducing the need for manual memory-safety checks.
Safe Rust combines ownership rules with bounds checks; calling unsafe C code still needs care.
MemorySanitizer
MemorySanitizer is Clang's run-time tracker for uses of uninitialized values, requiring compatible instrumentation of dependencies.
It targets a class of uninitialized-value errors that ordinary ASan checking does not track.
Minimal reproducer
A minimal reproducer is a small input and program case that still triggers the failure being investigated.
Removing unrelated records from a crashing parser's input leaves an empty field that still consistently triggers the crash.

N

Noncompliant code
Noncompliant code is an example that violates a particular coding rule under the conditions described by that rule.
Using printf(input) lets outside data supply format conversions and violates FIO30-C.
Null terminator
A null terminator is the zero character that marks the end of a C string and requires storage beyond its visible characters.
The string "ready" needs six bytes so string functions can find a terminator after its five letters.

O

Object lifetime
An object lifetime is the period during execution when an object exists and its storage may be accessed under C's rules.
A local automatic array exists during its function call, while an allocation lasts until release.
Optimization
Optimization transforms a program to improve execution or storage while preserving the required behavior of defined code.
At -O2, a debugger may show no separate value for a local the compiler removed.
Out-of-bounds access
An out-of-bounds access reads or writes outside the valid elements of an object, even if the address happens to be mapped.
Index six is outside a six-element array because its last valid element is index five.
Overlapping ranges
Overlapping ranges share at least one byte of storage, so copying between them requires semantics that preserve the original source data.
Shifting text within the same array can require memmove because memcpy forbids overlap.
Ownership
Ownership is a program's responsibility for managing a resource's lifetime, including releasing it and keeping its valid handle.
A temporary result preserves the original allocation owner when a positive-size realloc fails.

P

Portability
Portability is the ability of code to behave as intended across implementations without relying on unsupported platform assumptions.
Using INT_MAX avoids assuming every implementation gives int the same maximum value.
Postcondition
A postcondition is a property an operation promises to establish when it completes successfully.
After sorting an array, a postcondition can state that each element is no larger than the next.
Precondition
A precondition is a requirement that must hold when a function or operation begins, established by its caller or earlier validation.
A private helper may require a non-null pointer before examining the first array element.

Q

Quarantine
An ASan quarantine delays reuse of freed blocks, increasing the chance that stale accesses hit memory still marked invalid.
A dangling pointer may be detected before the released block is recycled for another allocation.

R

Range error
A range error occurs when a conversion's mathematical value is outside the result type's representable range.
After resetting errno, a strtol range error sets ERANGE even though a limit value is returned.
Redzone
A redzone is a forbidden memory area placed around a supported object so a checker can detect accesses across its boundary.
An extra write immediately after a heap block can enter its redzone and trigger ASan.
Regression test
A regression test checks behavior that previously failed, so later changes can be checked for a return of that defect.
A saved empty-record input that once crashed a parser is rerun after each parser change to catch a return of that crash.
Release build
A release build is an executable configured for ordinary use, commonly with optimization and supported hardening enabled.
A release build must retain input checks even when NDEBUG removes its assertions.
Risk priority
A CERT risk priority is the product of severity, likelihood, and remediation scores, used to help order enforcement or repair work.
A higher remediation score raises priority because the corresponding fix is cheaper to carry out.

S

Secure coding standard
A secure coding standard is a set of actionable requirements and recommendations for avoiding known defects and vulnerabilities.
A review checklist can connect each allocation-size check to the defect it is meant to prevent.
SEI CERT C Coding Standard
The SEI CERT C Coding Standard is Carnegie Mellon SEI's collection of C rules, recommendations, examples, and risk assessments.
Its MEM35-C rule helps reviewers ask whether an allocation contains enough bytes for its objects.
Shadow memory
Shadow memory is auxiliary storage a checking tool uses to record information about the application's memory bytes.
ASan consults shadow information to check whether an instrumented access touches permitted storage.
Shell injection
Shell injection occurs when outside text changes a shell command's structure and causes unintended commands or operations.
Building a system() string from an unchecked argument lets a shell interpret supplied syntax.
Signed integer overflow
Signed integer overflow occurs when an arithmetic result cannot fit its signed destination type and has undefined behavior.
With 32-bit int, adding one to INT_MAX cannot produce a portable, defined next value.
Spatial memory safety
Spatial memory safety requires every memory access to remain inside the bounds of the object it is entitled to access.
A bounds check before an array write prevents an index from selecting an element outside that array.
Stack frame
A stack frame is the execution context of an active function call, with its available arguments, locals, and return information.
When a caller passes a name pointer, selecting the caller's frame after a crash makes that pointer available for inspection.
Static analysis
Static analysis examines source code and possible execution paths without running the program on test input.
An analyzer may find a double-free path that no current regression test reaches.
Static assertion
A static assertion checks a constant expression during compilation and makes the build fail when the condition is false.
A C17 static_assert can reject a platform where a required int size assumption is false.
Stepping
Stepping advances execution by a chosen source-level amount, allowing inspection between statements or function calls.
Use next to run over a call and step when you need to enter the function's own code.
String length
A string length is the number of characters before the first null terminator, excluding that terminating character itself.
strlen("safe") is four, while storing the complete string requires five bytes.

T

Temporal memory safety
Temporal memory safety requires access only while an object is alive and valid for that access.
A stale alias after free violates lifetime safety even when its address still looks familiar.
ThreadSanitizer
ThreadSanitizer is a run-time checker that identifies conflicting unsynchronized accesses to shared memory by threads.
It needs a separate build from ASan when investigating potential data races.
Truncation
Truncation discards the part of text that does not fit in a destination, even when the stored prefix is correctly terminated.
A nonnegative snprintf return at least as large as capacity signals that the full text did not fit.

U

Undefined behavior
Undefined behavior is behavior for which the C standard imposes no requirements after an invalid operation occurs.
A signed overflow may appear to wrap in one build and behave differently after optimization.
UndefinedBehaviorSanitizer
UndefinedBehaviorSanitizer inserts checks for many undefined operations, including signed overflow and invalid shifts, during execution.
A UBSan report can identify an overflowing addition even when the program prints a plausible total.
Unsequenced side effects
Unsequenced side effects are changes and conflicting uses of the same scalar without the ordering C requires for defined evaluation.
Passing both i and ++i to one call mixes a read and a change without a safe ordering.
Unsigned wrap
Unsigned wrap reduces an arithmetic result into the unsigned type's range; it is defined but can violate a program's size assumptions.
An overflowing byte-count product can request a much smaller allocation than the intended array needs.
Unspecified behavior
Unspecified behavior permits a choice among valid outcomes without requiring the implementation to document which it chooses.
Independent calls used as arguments may be evaluated in either order without invalidating the call.
Untrusted input
Untrusted input is data outside the program's control whose format, length, and range must be checked before sensitive use.
A command-line count needs validation before it controls an array allocation or loop bound.
Use after free
A use after free accesses an allocation after it has been released, violating the lifetime of the original object.
Freeing an owner pointer also ends valid access through an alias that still holds its old address.

V

Variable shadowing
Variable shadowing occurs when a declaration in an inner scope hides another declaration with the same name.
An inner loop's count may receive updates while the outer count remains unchanged.

W

Watchpoint
A watchpoint stops execution when a monitored memory location changes, helping identify the instruction that wrote its value.
Watching a structure's count can reveal an unexpected assignment long before a later crash.

About this lesson

By StudeliReport a mistake

Make your own Study Materials

Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.

Get Started