Explain the Failure and Verify the Fix
The stopping place isn’t necessarily the cause
Section titled “The stopping place isn’t necessarily the cause”A program stops inside a function that calculates an average. It would be tempting to blame the division, because that’s where the arithmetic lives. But dividing a total by a positive count is perfectly reasonable. The more useful question is why this call has no records to average.
We’ll investigate a failed condition, then test a correction. You already know how to inspect frames and trace a value. This time the task isn’t finished when you find the bad state: the new behavior needs evidence too.
Reproduce a failed precondition
Section titled “Reproduce a failed precondition”Save this complete file as average.c in the debugging directory:
#include <assert.h>#include <stdio.h>#include <string.h>
int average(int total, int count) { assert(count > 0); return total / count;}
int main(int argc, char **argv) { int total = 12; int count = 3; if (argc == 2 && strcmp(argv[1], "empty") == 0) { total = 0; count = 0; } printf("average=%d\n", average(total, count)); return 0;}The function requires a positive count. This requirement is a precondition: something that must hold before the operation is valid. assert(count > 0) checks that expectation in this build and aborts execution if it is false. An assertion is a development check, not a complete policy for handling ordinary empty input.
Predict the normal result and what the empty selector will do. In the shell:
cd ~/underdunn-course/exercises/debugginggcc -std=c11 -Wall -Wextra -Wpedantic -O0 -g average.c -o averagesha256sum average.c average./averagegdb -q ./averageThe normal run should print average=4. In GDB, reproduce the other case:
run emptybacktraceExpect an assertion message naming count > 0 and a stop for SIGABRT, the abort signal. The innermost frames may be library functions involved in reporting and aborting. Their names and source paths vary with installed library information. We want the frame for our own average function.
Follow the failed condition to its caller
Section titled “Follow the failed condition to its caller”Find average in your backtrace. Select it with frame followed by its frame number from your run. If the trace labels it #4, for example, the command is frame 4; don’t assume four is universal.
Now in GDB:
print countprint totaluplistprint countInside average, both values should be zero. Moving to its caller shows that main deliberately selected an empty dataset and still called the function. The assertion caught the violated precondition. The earlier decision to call with zero records caused that violation.
Removing the assertion would let execution reach integer division by zero, not produce an average for a nonexistent dataset. The GNU C Library assertion reference explains both assertion failure and NDEBUG, which disables these checks. Don’t define NDEBUG here, and don’t treat disabling a check as a repair.
Check the failure message, your selected user-code frame, the values, and the call-site decision. Then quit GDB, answering y if asked to end the stopped example. We don’t need to continue the aborted calculation.
Preserve the baseline before changing behavior
Section titled “Preserve the baseline before changing behavior”The desired policy for this teaching program is explicit: no records should print no data and exit successfully; otherwise the existing integer average should be printed. That policy has to be handled before calling average, since no valid denominator exists in the empty case.
Shell input:
cp average.c average-fixed.cIn average-fixed.c only, insert this guard immediately before the final printf in main. It is an insertion, not a replacement for the complete file:
if (count == 0) { puts("no data"); return 0; }The early return prevents the invalid call. The original function and its assertion stay intact. This is a deliberately small correction: it handles the stated empty-input policy without changing the arithmetic for normal input.
Practice: test what the correction might break
Section titled “Practice: test what the correction might break”Predict both outputs before rebuilding. Shell input:
gcc -std=c11 -Wall -Wextra -Wpedantic -O0 -g average-fixed.c -o average-fixed./average-fixed emptyecho $?./average-fixedecho $?Expect no data followed by status 0, then average=4 followed by status 0. The previously valid case is a regression test: it checks that the correction didn’t break behavior that already worked. A patch that prints no data for everything would pass the empty case and fail this one.
Open the fixed executable in GDB and break at average. Run the normal case to confirm that it still reaches the function. Continue to exit, then run empty: it should finish without hitting that breakpoint. The changed control flow supports the explanation of why division is avoided.
Common failures and recovery
Section titled “Common failures and recovery”- The debugger stops inside the library: inspect the backtrace and select the named user function’s frame. Missing library source isn’t a reason to reinstall the guest.
- The assertion never appears: check your arguments, build, and whether
NDEBUGwas defined. Restore the baseline and exact build command. - The fix has no effect: check that you launched
average-fixed, notaverage, and rebuilt after saving the source. - Normal input now prints
no data: compare the guard with the intended condition. Restore from the original copy and change only the empty case.
Check your understanding
Section titled “Check your understanding”Knowledge check
Question 1 of 1
Practice complete
You've checked the key ideas. Revisit any explanation you need, then continue when it makes sense.