Комментарии 23
Это такое счастье забыть про core файлы и gdb и «все пропало… сделай что ни будь что бы обработчик не падал, но ничего кроме core файла нет».
Прочитал статью и порадовался, что не приходится это снова писать. Каждый кто писал на C++ эти и подобные «обертки» пытался делать.
Покритикую оформление, примеры в гифках это очень плохая идея. За них очень легко цепляться при беглом чтении, но результаты висят меньше секунды, прочитать ничего невозможно. Продублируйте статическим текстом, будет значительно лучше.
1. Проверять флаг переполнения после операции. Непереносимо, но быстро и дубово
2. Приводить перед сложением переменные на тип «выше», складывать, сравнивать с соответствующим INT_MAX, при успехе кастовать обратно
3. Проверять как Вы проверяете
4. Собираться с ключом -ftrapv и обрабатывать SIGABRT.
Но в большинстве случаев для передаваемых снаружи библиотеки/функции целых мы пишем "+", потому что Int большой, его скорее всего хватит, а нас устраивают риски
Про использование флагов непосредственно процессора и -ftrapv: есть обсуждение на стеке.
Насчет рисков — риски нас не устраивают. Задачей было написать программу, которая может давать определенные гарантии пользователю. Поэтому необходимо было обработать все случаи, в том числе и такие:
- тип long long (или, если угодно, __int128), который не получится полностью покрыть каким-либо другим типом;
- переполнения в беззнаковых типах, которые по факту UB не являются, но сообщить о них пользователю обязательно.
если проверять их постфактум, то уже случилось UB, и дальнейшие предположения о поведении программы невозможныя не могу вспомнить ни одного компилятора, который в этом случае «ломает» что-то сверх операции суммирования, если с помощью ключей мы убедились, что компилятор не считает переполнение невозможны (стандартное поведение gcc с 2008, проверял в последний раз в 2016м). И да, проверять флаг процессора проще всего на ассемблере. Альтернатива — builtins gcc или clang
если приводить к float или doubleДа не дай Шворц. К long или long long, если паранойя, всё-таки стандарт гарантирует int <= long <= long long. Сейчас чаще всего int 4байтовый, long long 8 байт.
Если проверить на входе уже длинный тип, то введение собственного «ещё более длинного» типа не вариант, тут действительно надо проверять заранее. Конечно, можно упороться (см.комменты), но принцип ровно тот же, что у вас в статье.
переполнения в беззнаковых типах, которые по факту UB не являются, но сообщить о них пользователю обязательноА разве ключами компилятора это сделать нельзя?
Встроенные чекеры в gcc и clang и вправду работают гарантированно, поэтому в случае именно с этим компиляторами, возможно, стоило использовать именно их. Спасибо!
Нужных ключей, по крайней мере у gcc, увы, я не нашел (и поискав слово overflow по странице, и перебрав все руками).
А в чем именно проблема с несколькими правилами применяемыми к одному и тому же месту? Вроде как если идти восходящим поиском и менять AST то проблем быть не должно, или нет?
И почему макросы а не функции? Как такой код будет обработан?
arr[foo(1,2)] = 42;
Маросы для того, чтобы получить номер строки и название файла из кода. Фукнции такого не умеет, только если очень сильно извратиться. То есть наши макросы устроены примерно так:
ASSERT_WHATEVER (PARAMS) checkWhatever(PARAMS, __FILE__, __LINE__)
Приведенный код будет обработан следующим образом:
ASSERT_IOB(arr, foo(1, 2)) = 42;
Внутри макроса завернута функция сохраняющая категорию значения, то есть присвоение произойдет корректно.
С макросом в принципе всё понятно (хотя если заниматься реконструкцией AST назад в код то строки и файлы сразу доступны), но есть традиционный comma operator который всегда макросы портит. К примеру вот вам валидный код который умрёт от прямой замены:
int a[2] = {};
a[0,1] = 4;
Классическое решение — заворачивать аргумент макроса в скобки.
Спасибо за проявленное внимание к проекту!
Вроде как он уже все вышеописанное (и много чего еще) делает.
Кстати, конкретно -fsanitize=undefined не найдет в следующем коде UB даже при вводе «5»:
int a, b; // b is uninit
std::cin >> a;
if (a != 5)
b = 5;
std::cout << b << '\n'; // b is still uninit if a == 5, UB
А наш анализатор найдет. Конечно, глупо сравнивать его и sanitizer, но тем не менее, у нас даже есть некоторое преимущество!
Вы упомянули о clang static analyzer а это не санитайзер. А в "динамические valgrind и sanitizer" не совсем очевидно, что речь именно о -fsanitize
в clang/gcc. Будем считать что упомянули. :-)
Для вашего примера санитайзер не нужен. Clang такое ловит сам с -Wall -Werror
во врямя компиляции:
https://godbolt.org/z/Edc4rq
source:5:7: error: variable 'b' is used uninitialized whenever 'if' condition is false [-Werror,-Wsometimes-uninitialized]
if (a != 5) b = 5;
^~~~~~
source:6:16: note: uninitialized use occurs here
std::cout << b << '\n'; // b is still uninit if a == 5, UB
^
source:5:3: note: remove the 'if' if its condition is always true
if (a != 5) b = 5;
^~~~~~~~~~~~
source:3:11: note: initialize the variable 'b' to silence this warning
int a, b; // b is uninit
^
= 0
1 error generated.
Конкретно для такой штуки, небось, memory sanitizer справится. С ним, правда, другие тонкости есть.
Как курсовая — отлично, как способ разобраться в clang — великолепно, как штука для практического применения — ненужно.
Попробую конструктивно покритиковать.
1) время компиляции увеличивается кратно.
2) код раздувается, его оптимизация ломается.
3) время выполнения кода увеличивается (об этом упомянуто).
4) ошибки будут найдены либо случайно (если дойдет ветвление, если подберутся условия ub), либо их нужно искать целенаправлено (читай — писать тесты). Т.е. добавление проверок никак не помогает обнаружить ошибку, но помогает ее быстрее локализовать в ходе случайного или целенаправленого возникновения.
5) согласно описанию, анализ и птсинг кода слишком примитивен. Для большинства мест в коде, можно доказать отсутствие ub, но, похоже, алгоритм все равно вставит патч. Например, for(auto i = 0; i < array.size(); i++) array[I];
struct Foo {
long long size() { return 1ll << 60; }
int operator[](signed int& i) {
if (i == 5)
i = __INT_MAX__; // на следующем шаге цикла знаковое переполнение - UB
return i;
}
};
int main() {
Foo array;
for (auto i = 0; i < array.size(); i++)
array[i]; // i обязано иметь тип signed int, чтобы можно было взять ссылку
}
Вероятно, если проверить, что array — это std::vector или другая структура из STL, то тогда этот код будет действительно безопасен, но данный пример хорошо иллюстрирует, что доказательство даже для такого простого случая не будет столь же простым. Возможно, это тема для отдельной курсовой работы…
Анализатор C++ на первом курсе: миф, иллюзия или выдумка?