Programming · free software · the open webSubscribe via RSS

C is an abstract machine, even on familiar hardware

C arithmetic follows language rules, not a promise about the processor’s usual instruction. That distinction matters when a compiler reasons about overflow and when code needs a checked result.

The machine C describes

It is tempting to think of an int as a processor register with a familiar width. But C specifies program behavior through its abstract machine. The implementation maps that model to hardware, and the compiler is entitled to use the language’s rules while transforming the program.

Signed overflow is undefined behavior in C. If a signed addition exceeds the representable range, the program has no required result. The language does not promise that the value wraps, traps, or matches a particular instruction. Unsigned arithmetic is different: its result is reduced modulo one more than the maximum representable value. For an unsigned type with maximum M, adding one to M produces zero.

Why the optimizer cares

Suppose a program evaluates x + 1 and then checks whether the result is less than x. For unsigned x, wraparound can make that comparison true at the maximum value. For signed x, the addition cannot overflow in a defined execution. A compiler may therefore reason that the result is greater than x and simplify the comparison away.

That reasoning is not a prediction of what a particular processor instruction will do. A machine instruction might produce a wrapped bit pattern, but C does not require the compiler to preserve that outcome for an operation whose signed overflow is undefined. Even at low optimization, observing one result does not turn it into a language guarantee. GCC documents signed-overflow assumptions as part of its optimization and warning behavior [1].

#include <limits.h>
#include <stdbool.h>

bool add_int(int a, int b, int *out) {
    if ((b > 0 && a > INT_MAX - b) ||
        (b < 0 && a < INT_MIN - b)) {
        return false;
    }
    *out = a + b;
    return true;
}
Locally checked C example · Download example

Make the check precede the addition

The checked-add function first tests whether a + b would exceed the int range. If b is positive, it compares a with INT_MAX - b; if b is negative, it compares a with INT_MIN - b. Those bounds are computed without performing the potentially overflowing addition. Only when both checks pass does the function evaluate the sum and store it through out.

For example, if b is positive, the largest safe a is INT_MAX - b. An input above that threshold would make the sum too large, so the function returns false without evaluating it. The negative case mirrors this reasoning at the lower bound. The successful call writes the result and returns true; a rejected call returns false and leaves out untouched. The caller must provide a valid pointer when a successful result is possible.

Choose arithmetic for its contract

If wraparound is the intended behavior, use an unsigned type and make the modulo semantics part of the design. If the value represents a quantity that must stay in range, perform a check before the operation or use an API that reports overflow. The choice affects more than edge cases: it determines which transformations preserve program meaning.

The key habit is to ask what the C operation means before asking what a disassembly appears to do. Assembly is evidence about one compiler build under particular options. The abstract machine defines the contract that source code and optimizers share.

References

  1. GCC warning options: strict overflow

Suggest a correction · All posts