Отслеживаем переполнение int

Стандарт Си определяет только минимальные размерности целочисленных значений. Например, на системах, где байт представлен как 8 бит, int имеет минимальный размер в 2 байта. Верхняя граница размерности стандартном не ограничена.

Имплементации компиляторов Си вольны сами выбирать размерность под архитектуру. На моей системе sizeof(int) эквивалентен 4 байтам. Это в два раза больше минимального значения из спецификации, но, всё же, это конечное значение.

Что будет, если при работе приложения произойдёт выход за границы допустимых значений? Произойдёт переполнение. Как понять, где и когда это происходит?

Решить такую задачу поможет флаг компиляции, который можно передать для Clang и GCC – -fsanitize=undefined. Флаг влияет на исполнение приложения в момент, когда появляется undefined behavior. Переполнение целочисленного int – это undefined behavior. При переполнении тогда включаются механизмы санитизации.

Для тюнинга работы механизмов санитизации есть ещё один полезный флаг компиляции -fno-sanitize-recover=all. Он требует, чтобы программа в случае санитизации прекратила исполнение с ошибкой, а не продолжала работать как будто бы всё окей.

Иными словами, комбинация указанных флагов будет останавливать программу с выводом описания ошибки в момент появления undefined behavior. В частности, переполнения целочисленного int, но не только. Проверим, как это выглядит.

Опишем простую функцию main в файле test.c:

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
        int x = INT_MAX;
        printf("x + 1 = %d\n", x + 1);
        return EXIT_SUCCESS;
}

В момент вычисления x + 1 должен происходить тот самый undefined behavior. В зависимости от платформы можно наблюдать разный результат. Проверю у себя:

$ gcc -o test test.c
$ ./test

x + 1 = -2147483648

Вместо ожидаемого значения 2147483648 получаем -2147483648. Окей, всё понятно. Поскольку у меня платформа, на которой отрицательные значения представлены с помощью дополнительного кода, то INT_MAX + 1 даёт INT_MIN. Теперь скомпилируем и запустим тестовую программу с флагом -fsanitize=undefined:

$ gcc -fsanitize=undefined -o test test.c
$ ./test

test.c:7:3: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
x + 1 = -2147483648

$ echo $?

0

Как видим, сообщение об ошибке было выведено, но программа продолжила своё исполнение и завершилась с кодом успеха (0). Такое поведение мне очень не нравится, я предпочитаю выход из программы ровно в момент, когда происходит signed integer overflow. Добавляем флаг -fno-sanitize-recover=all:

$ gcc -fsanitize=undefined -fno-sanitize-recover=all -o test test.c
$ ./test

test.c:7:3: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'

$ echo $?

1

Вот! То, что нужно! Программа завершилась с ошибкой и вернула соответствующий статус-код (1). Тест на предотвращение signed integer overflow пройден успешно.

Самый главный плюс использования этих флагов – исходный код не требует никаких изменений. Если Вы уверены в корректности своей программы, то эти флаги можно опустить при компиляции релизных версии программ, если Вам так больше нравится.

Если Вы хотите обсудить содержание заметки, задать вопросы или предложить изменения, то со мной можно связаться в Telegram