Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In C , why does this `main` function with an incrementing integer never crash from overflow?

In C, the most basic main function for embedded system like an stm32 is:

int main(void) {
    int i = 0;

    while(1) {
        i++;
    }
}

But, since i is an integer, won't it overflow and, therefore, create problem at some point?

Also, is it safe to use the same tactic (using while(1) i++) to loop around while waiting for a specific interrupt?

Edit : thanks for all the answer. My bad for not knowing that overflow handler were so much different from C and C++. Now I know !

like image 880
B1ackAnge1 Avatar asked Dec 12 '25 16:12

B1ackAnge1


1 Answers

The following applies to both C and C++. All of the following code is compilable as both C and C++.

why does this main function with an incrementing integer never overflow?

It does overflow. Since the integer is signed, however, it is undefined behavior and a bug. Depending on the compiler and hardware architecture the code is being compiled for, the compiler may handle signed integer overflow and underflow in expected ways, ex: wrapping around to the minimum value on overflow, but it's not a guarantee, not allowed per the language standards, and not a good idea.

See the quotes in this question: Why is unsigned integer overflow defined behavior but signed integer overflow isn't? (emphasis added):

Unsigned integer overflow is well defined by both the C and C++ standards. For example, the [C99 standard][http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf] (§6.2.5/9) states

A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.

However, both standards state that signed integer overflow is undefined behavior. Again, from the C99 standard (§3.4.3/1)

An example of undefined behavior is the behavior on integer overflow

Next question:

Also, is it safe to use the same tactic (using while(1) i++) to loop around while waiting for a specific interrupt?

Using the following is just fine in a microcontroller, yes:

int main(void) 
{
    while (1) 
    {
        // loop continually as fast as the processor can, while waiting for 
        // interrupts; ISR (interrupt service routine) function definitions not
        // shown 
    }

    // On a microcontroller, this code must never be reached!
    return 0;
}

On any system with an operating system, however, you should sleep in the while loop, even if only for a millisecond, to give the scheduler a chance to run other processes. On bare-metal (meaning: having no operating system) microcontrollers, however, the infinite while loop running as fast as it can is normal.


You must not rely on undefined behavior signed integer overflow and underflow. Instead, rely on well-defined behavior unsigned integer overflow and underflow.

This is perfectly fine:

#include <stdint.h>  // for uint16_t, etc.

int main(void) 
{
    uint16_t counter = 0;
    while (1) 
    {
        counter++; // increment unsigned counter; overflow is fine

        // use the `counter` variable somewhere or else it may be optimized out
        // completely by the compiler

        // loop continually as fast as the processor can, while waiting for 
        // interrupts; ISR (interrupt service routine) function definitions not
        // shown 
    }

    // On a microcontroller, this code must never be reached!
    return 0;
}

With unsigned integers, things like this are also perfectly fine. Note that uint8_t is an unsigned 8-bit integer, with values ranging from 0 to 255:

uint8_t u1 = 0 - 1;  // underflows to 255 at compile-time

uint8_t u2 = 0; 
u2 - 1;  // underflows to 255; can be at run-time

uint8_t u2 = 255;
u2 + 1; // overflows to 0; can be at run-time

uint8_t u3 = 255;
u3 + 2; // overflows to 1; can be at run-time

uint8_t timestamp_end = 128;
uint8_t timestamp_beginning = 129;
// The result is 255
uint8_t time_difference = timestamp_end - timestamp_beginning; 

timestamp_end = 100;
timestamp_beginning = 200;
// The result is 156
time_difference = timestamp_end - timestamp_beginning;

More on the infinite while loop in microcontrollers

As an example, below is the main() C++ function for the AVR microcontroller cores in Arduino (think: ATmega328: Arduino Uno, Nano, etc).

Notice the infinite for (;;) loop. Using for (;;) is exactly identical to using while (1) or while (true). Inside there, it calls the user-defined loop() function, calls serialEventRun() so long as that function is defined (see Note 1 below), then repeats, as fast as the processor can run! Everything else you write must be handled through interrupts processed in ISRs outside of the main loop, or through cooperative multi-tasking inside the main loop (see some of my example code doing this in my answer here), which is really quite effective.

Note 1: in the gcc compiler, __attribute__((weak)) functions which are never defined are equal to zero, which would make if (serialEventRun) false. See my test code in my eRCaGuy_hello_world repo here: cpp/check_addr_of_weak_undefined_funcs.cpp. Example output is at the very bottom of that file.

The serialEventRun() function calls an optional, custom, user-defined serialEvent() function if it is defined. If the user doesn't define it, it's a weak empty function declared as void serialEvent() __attribute__((weak));. See my answer on this here: Arduino "SerialEvent" example code doesn't work on my Arduino Nano. I can't receive serial data. Why?.

Also notice the USBDevice.attach(); call. I haven't looked into it, but I suspect that attaches a callback to an ISR (interrupt service routine function), so that the callback function gets called whenever (apparently) a USB-related interrupt occurs.

Also notice that the user's setup() function gets called only once, prior to entering the infinite for (;;) loop, whereas the user's loop() function gets called repeatedly inside the infinite for (;;) loop:

https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/main.cpp:

#include <Arduino.h>

// Declared weak in Arduino.h to allow user redefinitions.
int atexit(void (* /*func*/ )()) { return 0; }

// Weak empty variant initialization function.
// May be redefined by variant files.
void initVariant() __attribute__((weak));
void initVariant() { }

void setupUSB() __attribute__((weak));
void setupUSB() { }

int main(void)
{
    init();

    initVariant();

#if defined(USBCON)
    USBDevice.attach();
#endif
    
    setup();
    
    for (;;) {
        loop();
        if (serialEventRun) serialEventRun();
    }
        
    return 0;
}

Pedandic, language-lawyer-like note

From @Eric Postpischil's comment here:

C++ 2020 draft n4849 6.8.1 says, in a note,

Unsigned arithmetic does not overflow. Overflow for signed arithmetic yields undefined behavior (7.1).

As used by the standards, signed integer arithmetic overflows, because a specified result (such as the sum for an addition) cannot be represented in the result type, and unsigned arithmetic never overflows, because operations are specified to wrap, so the specified result is always representable.

In other words, technically the word "overflow" means "cannot hold the specified result", and so only signed integers do this, but unsigned integers do not do this, because they are specified to wrap around, so their results are always representable.

That's backwards from how (virtually all) of us use those words. We use "overflow" to mean "wrap around", and so unsigned integers properly overflow, but signed integers do not. Again, technically we have these definitions backwards in our common vernacular, myself included, even in my answer above.

But, we aren't language lawyers here, so I will continue to say that unsigned integers have "well-defined" underflow and overflow, and signed integers have "undefined behavior" underflow and overflow.

See also

  1. It turns out I've answered a question like this before. See my answer here: Adding int type to uint64_t c++: Undefined behavior signed-integer overflow/underflow, and well-defined behavior unsigned-integer overflow/underflow, in C and C++
like image 157
Gabriel Staples Avatar answered Dec 14 '25 09:12

Gabriel Staples



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!