Отслеживаем переполнение 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 пройден успешно.
Самый главный плюс использования этих флагов – исходный код не требует никаких изменений. Если Вы уверены в корректности своей программы, то эти флаги можно опустить при компиляции релизных версии программ, если Вам так больше нравится.