В мире программирования существует огромное количество багов, и если бы каждый баг стал бабочкой, то программеру в раю уже давно оставлена пара полян для развития навыков энтомолога. Несмотря на все совершенства этого мира: компиляторы, pvs-studio и другие статические анализаторы, юниттесты и отделы QA, мы всегда находим способы преодолеть преграды кода и выпустить на волю парочку новых красивых и удобных видов. Есть у меня txt файлик, которому очень много лет, и куда я складываю интересные экземпляры. Все примеры и действия описанные в статье вымышленные, ни один стажер, джун или студент уволены не были. Hello, World! Where are your bugs?
First...
Начну, пожалуй, с очепятки в коде. Этот код использовался в движке Unity 2014 для управления поворотом объектов с помощью гизмо (такой элемент управления, обычно в форме трех окружностей, который позволяет крутить объект на сцене). В данном случае, функция setDeltaPitch
применяется для изменения угла наклона объекта относительно его вертикальной оси (pitch). При углах близких к 0 (зависело от настроек редактора) просто переворачивала объект вверх ногами, что очень бесило дизайнеров уровней.
void UObject::setDeltaPitch(const UMatrix &gizmo) {
....
if (_fpzero(amount, eps))
return
rotateAccum.setAnglesXYZ(axis);
....
}
А где ошибка?
После return не стоит точки с запятой, что при определенных условиях приводило к вызову функции setAnglesXYZ в управляемом объекте и переворачивало его на произвольный угол.
Next...
А тут повеселился компилятор, эта функция использовалась для вычисления хеш-суммы данных в контенте при проверке целостности файлов. При создании контента вычислялся хеш файлов и сохранялся вместе с файлами. Позже, при загрузке того же файла, плеер юнити снова вычислял хеш файла и сравнивал его с сохраненным хешем, чтобы убедиться, что файл не был изменен или поврежден. Шифрование использовалось на финальном этапе упаковки. По замыслу автора этого кода, ключ не должен был утечь за пределы этой функции, но что-то пошло не так. Так как это асимметричная система шифрования, то любой, кто владеет закрытым ключом может зашифровать и подписать двоичные файлы. При загрузке эти файлы будут выглядеть "подлинными". Наличие части утекших исходников Unity и этого бага в движке помогло SKIDROW в 2017 поломать Сибирь 3 и еще несколько больших игр на этом движке, которые использовали нативные средства шифрования контента. Там конечно был еще Denuvo, но его кедры научились обходить еще до этого.
void UContentPackage::createPackage(dsaFile *m, const void *v, int size) {
unsigned char secretDsaKey[3096], L;
const unsigned char *p = v;
int i, j, t;
....
UContent::generateDsaKey(secretDsaKey, sizeof(salt));
....
// тут какойто код был, работали с переменными
// шифруем и подписываем файл
....
memset (secretDsaKey, 0, sizeof(secretDsaKey));
}
А где ошибка?
Функция memset() не вызывалась из-за агрессивной оптимизации компилятора, действия с secretDsaKey не проводились после memset() поэтому компилятор её просто выкинул. Все содержимое ключа оставалось на стеке.
Next...
А тут могут быть проблемы при работе из двух и более потоков с любой из переменных a/b. Эта ошибка присутствовала в движке CryTek при синхронизации стейта автомобилей по сети, из-за чего наблюдались рывки и телепорты при передвижение на авто в мультиплеере FarCry 1. Чем больше игроков было на карте, тем больше была вероятность телепорта у последнего игрока, при 16 игроках на карте, последнего стабильно телепортило если он использовал авто.
struct X {
int a : 2
int b : 2
} x;
Thread 1:
void foo() { x.a = 1 }
Thread 2:
void boo() { x.b = 1 }
А где ошибка?
Нарушение атомарности операции записи. Поля a
и b
не являются атомарными, что означает, что они могут выполняться частично и прерываться другими операциями. В данном коде присутствует общий доступ к комплексной переменной AB, которая состоит из двух двубитовых частей. Но компилятор не может выполнить такие присвоение атомарно, захватывая сразу байт ab и битовыми операциями выставляя нужное значение. Это может привести к гонкам данных и неопределенному поведению в многопоточной среде
foo(): # @foo()
mov AL, BYTE PTR [RIP + x] ; разорваное присвоение 1
and AL, -4 ; разорваное присвоение 2
or AL, 1 ; разорваное присвоение 3
mov BYTE PTR [RIP + x], AL ; закончили упражнение
ret
boo(): # @boo()
mov AL, BYTE PTR [RIP + x]
and AL, -13
or AL, 4
mov BYTE PTR [RIP + x], AL
ret
Next...
Этот код содержит гонку даже при наличии "вроде бы живого" мьютекса. Ошибка была замечена в прошивке Nintendo Switch 4.5.1 и выше. Наткнулись мы на неё совершенно случайно, когда пытались ускорить создание атласов ui текстур на старте игры. Если пробовали загрузить в атлас больше 100 текстур, то ломали её. А если меньше то атлас собирался нормально. Такие мьютексы зомби на свиче до сих пор не пофикшены. А еще на свиче, можно было сделать не больше 256 мьютексов на приложение, вот такая вот веселая система.
const size_t maxThreads = 10;
void fill_texture_mt(int thread_id, std::mutex *pm) {
std::lock_guard<std::mutex> lk(*pm);
// Access data protected by the lock.
}
void prepare_texture() {
std::thread threads[maxThreads];
std::mutex m;
for (size_t i = 0; i < maxThreads; ++i) {
threads[i] = std::thread(fill_texture_mt, i, &m);
}
}
А где ошибка?
Мы удаляем мютекс сразу по завершению функции prepare_texture(). Операционная система никак не реагирует на неактивный мютекс, потому что как объект ядра продолжает еще некоторое время жить и адрес является валидным и содержит корректные данные, но не предоставляет реальной блокировки тредов.
Next...
Функции могут быть определены так, чтобы принимать на месте вызова больше аргументов, чем указано в объявлении. Такие функции называются (variadic) вариативными. C++ предоставляет два механизма, с помощью которых можно определить вариативную функцию: шаблон с переменным числом параметров и использование многоточия в стиле C в качестве окончательного объявления параметра. Очень неприятное поведение было встречено в популярной библиотеке для работы со звуком FMOD Engine. Код привожу в том виде, какой он был в исходниках, похоже ребята хотели сэкономить на шаблонах. (https://onlinegdb.com/v4xxXf2zg)
int var_add(int first, int second, ...) {
int r = first + second;
va_list va;
va_start(va, second);
while (int v = va_arg(va, int)) {
r += v;
}
va_end(va);
return r;
}
А где ошибка?
В этой вариативная функции в стиле C для сложения ряда целых чисел. аргументы будут считываться до тех пор, пока не будет найдено значение 0. Вызов этой функции без передачи значения 0 в качестве аргумента (после первых двух аргументов) приводит к неопределенному поведению. Более того, передача любого типа, кроме int, также приводит к неопределенному поведению.
Next...
А вот тут мы опять ничего не залочили, хотя на первый взгляд залочили. Этот код был обнаружен в движке Metro: Exodus в некоторых местах при работе с ресурсами, что приводило к странным крашам. Нашли благодаря баг репортам и одному умному французу.
static std::mutex m;
static int shared_resource = 0;
void increment_by_42() {
std::unique_lock<std::mutex>(m);
shared_resource += 42;
}
А где ошибка?
Это неоднозначность кода - ожидается, что анонимная локальная переменная типа std::unique_lock будет блокировать и разблокировать мьютекс m посредством RAII. Однако объявление синтаксически неоднозначно, поскольку его можно интерпретировать как объявление анонимного объекта и вызов его конструктора преобразования с одним аргументом. Компиляторы почему-то предпочитают второе, поэтому объект мьютекса никогда не блокируется.
Next...
А вот этот код был притащен в репо одним умным студентом, и как-то просочился на ревью между двух мидлов, похоже пьяные были. На поиски странного поведения потратили час. Студента отправили готовить всем кофф, и запретили комитить в пятницу вечером.
// a.h
#ifndef A_HEADER_FILE
#define A_HEADER_FILE
namespace {
int v;
}
#endif // A_HEADER_FILE
А где ошибка?
Переменная v будет разной в каждом юните трансляции который его использует, потому что финальный код будет такой
namespace _abcd { int v; } в файле a.cpp
и
namespace _bcda { int v; } в файле b.cpp
в итоге используя такую переменную нет уверенности в её текущем состоянии
Next...
А вот такой код рандомно сбоил в релизной сборке на разных компиляторах, использовался для подсчета контрольных сумм областей в PathEngine (https://www.pathengine.com/). Солюшен для плойки содержал определенный флаг, который маскировал проблему, а на боксе его не было. Ошибку обнаружили, когда попробовали собрать либу клангом на пк.
struct AreaCrc32 {
unsigned char buffType = 0;
int crc = 0;
};
AreaCrc32 s1 {};
AreaCrc32 s2 {};
void similarArea(const AreaCrc32 &s1, const AreaCrc32 &s2) {
if (!std::memcmp(&s1, &s2, sizeof(AreaCrc32))) {
// ...
}
}
А где ошибка?
Структура будет выровнена до 4байт, и между buffType и сrc образуются мусорные байты, которые могут быть заполнены нулями, а могут мусором.
struct S {
unsigned char buffType = 0;
char[3] _garbage = {} // something, compiler don't care about content in release
int сrc = 0;
};
memcmp() сравнивает память побитово захватывая и мусорные биты тоже, поэтому получается неизвестный результат этой операции для s1, s2. Настройки компилятора для плойки, явно указывали компилятору забивать мусорные байты нулями. Правила C++ для структур говорят, что нам нужно использовать в этом месте оператор()==, и memcmp не предназначен для структур равенства.
Next...
Вдогонку к предыдущему, как-то раз на ревью было вот такое. Вовремя заметили.
class C {
int i;
public:
virtual void f();
// ...
};
void similar(C &c1, C &c2) {
if (!std::memcmp(&c1, &c2, sizeof(C))) {
// ...
}
}
А где ошибка?
Сравнивать плюсовые структуры и классы через memcmp плохая затея, ведь может прилететь в c2 отнаследованный класс, с другой vtbl. А vtbl в классе лежит первой, поэтому эти классы никогда не пройдут проверку, даже если все данные будут идентичны.
Next...
Знаете зачем программисту два монитора? Чтобы на одном он мог смотреть на код, который только что сломал, а на втором код, который еще компилится.
А где ошибка?
Она где-то точно есть, кода без ошибок не бывает.
Next...
Тут вроде совсем просто должно быть, но что-то часто на таких вещах претенденты спотыкаются на собесах.
struct S { S(const S *) noexcept; /* ... */ };
class T {
int n;
S *s1;
public:
T(const T &rhs) : n(rhs.n),
s1(rhs.s1 ? new S(rhs.s1) : nullptr) {}
~T() { delete s1; }
// ...
T& operator=(const T &rhs) {
n = rhs.n;
delete s1;
s1 = new S(rhs.s1);
return *this;
}
};
А где ошибка?
На строке 15 не проверили, что объект может быть присвоен сам себе. В итоге удалим себя, и загрузим какой-то мусор.
Next...
Даже в четырех строках кода можно словить неочевидный баг. Особенно, если это Unity Engine, который любит включать общие заголовки в разных DLL, а потом сравнивать их вот так super_secret() == super_secret(). Казалось бы, что может пойти не так, но секрет != секрету, и игра не грузится на Windows Phone.
// header a.h
constexpr inline const char* super_secret(void) {
constexpr const char *STRING = "string";
return STRING;
}
// a.dll
super_secret() == super_secret()
// b.dll
super_secret() != super_secret()
А где ошибка?
А ошибка в том, что общий хедер a.h использовался в разных либах, все работало пока движок собирался как единая длл, а собрав две разные DLL, мы получим два разных адреса для STRING. А вообще ошибка в том, что строки сравнивают поинтерами, а не через strcmp.
Next...
Можно вечно смотреть как течет вода, горит огонь, и как работает сортировка.
Next...
Я настолько не доверяю компилятору, что проверяю даже что this != null. Знаете где я нашел этот код? Вы наверное догадались :) Unity Engine 2014 года выпуска, а может он и до сих пор там.
class URendererOpengl : URendererBase {
...
void commitDraw() {
if (this == nullptr) {
// WTF?
}
}
...
}
struct A {
int x = 0;
void bar() { std::cout << "bar" << std::endl; }
};
int main() {
A *a = nullptr;
a->bar();
}
А где ошибка?
Формально ошибки нет, мы можем вызывать функции класса, не обращаясь к данным экземпляра этого класса. В этом конкретном случае у нас получается вырожденная функция класса.
Next...
Какой из циклов выполнится быстрее? Ключи компилятора для релиза в студии /GS /W4 /Gm- /Ox /Ob2 /Zc:inline /fp:precise. Думаю на кланге будет тоже самое.
int steps = 32 * 1024 * 1024;
int* a = new int[2];
1. for (int i = 0; i < steps; i++) { a[0]++; a[0]++; }
2. for (int i = 0; i < steps; i++) { a[0]++; a[1]++; }
Так кто же быстрее?
Второй будет быстрее, из-за параллелизма команд на уровне cpu. Обе команды ++ попадают на выполнение одновременно, но в первом если компилятор не на мухлюет будет задержка по данным, пока выполняется первая операция ++. Но это происходит на уровне процессора уже, компилятор действительно здесь может только помухлевать с разворачиванием цикла и тд
Next...
У этой функции может быть UB с переполнением буфера. Часто это происходит для clang с оптимизацией -O3, и не происходит с -O0. На кланге 12.10 и выше уже исправлено для всех режимов оптимизации. Код не мой, всплыл на каком-то из собесов, когда просто беседовали по душам.
char destBuffer[16];
void serialize(bool x) {
const char* whichString = x ? "true" : "false";
const size_t len = strlen(whichString);
memcpy(destBuffer, whichString, len);
}
int main() {
bool x;
serialize(x);
}
А где ошибка?
Компилятор перехитрил сам себя. Итак, что происходит? Агрессивная оптимизация преобразует это в len = 5 - x, bool это не 1/0, он зависит от компилятора. Clang определяет это 0 — ложь, другое — правда. X не инициализирован, поэтому в некоторых случаях у нас len = 5–7? И вылетает в memcpy по переполнению буфера.
Next...
// Порадуйте мир сами новыми видами баgoчек :)
int main() {
printf("Hello, World! Where are your bugs?");
}
Надеюсь вам понравилась эта небольшая коллекция. А чтобы знать и понимать разные виды этих насекомых, можно иногда поглядывать в правила хорошего кода (https://wiki.sei.cmu.edu/confluence/display/cplusplus)
Комментарии (43)
mayorovp
30.09.2023 02:20Сравнивать плюсовые структуры и классы через memcmp плохая затея, ведь может прилететь в c2 отнаследованный класс, с другой vtbl.
Не факт что это ошибка, обычно считается что несовпадение типов ведёт к неравенству объектов.
Тут скорее проблема снова в выравнивании и прочем.
Nick_Shl
30.09.2023 02:20Знаете зачем программисту два монитора?
Незачем. Окно IDE на два монитора ре развернешь и больше кода не увидишь...
Хочу 32 дюйма 4к монитор - вот там ух! Но не дают. Говорят "Мы тебе дадим, а завтра у нас на пороге IT отдела будет целая очередь стоять из желающих требующих себе такие же!".
dalerank Автор
30.09.2023 02:20+2А мне не понравилось, попробовал эту бандуру 34' Dell в повседневной работе. Начал быстро уставать, вечером побаливала голова и шея. Вернулся к двум монитора по 24дюйма после двух недель мучений
McKinseyBA
30.09.2023 02:2032" Dell (еще и кривой), 7 месяцев, полет нормальный. Вот эти 2 дюйма и играют разницу! :-)
А если серьезно, то боли у вас, возможно, от недостатка физической активности. Годовой абонемент в спортзал стоит дешевле топовых кресел, столов и затрат на лечение хроники опорно-двигательного аппарата после 35 и неврологии после 40
geher
30.09.2023 02:20+1На одном мониторе одна IDE, на другом - вторая (разные).
Еще можно окно отлаживаемого приложения перетащить, какое-нибудь дополнительное приложение разместить (какой-нибудь дополнительный млнитор ресурсов, справку, более толковую, чем предлагает IDE, тут у каждого свои потребности). Возможность открепить какое-нибудь окно IDE, которую уже упоминали, тоже никто не отменял.
Nurked
30.09.2023 02:20+3Для запуска того, что отлаживаешь. Особенно если пишешь фронт.
Nick_Shl
30.09.2023 02:20Я пока не научился запускать софт написанный под микроконтроллер на мониторе ????
А вообще вполне валидный комментарий. Но запуск отлаживаемой программы на отдельном мониторе это не совсем то, что я имел ввиду. Что бы было эквивалентно, в ваш пример нужно добавить третий монитор.
AlexanderS
30.09.2023 02:20На одном IDE, на втором - отладка, на третьем - доки и всё прочее. И да - я хардварщик при этом)
Sazonov
30.09.2023 02:20+5Получается, что почти все ошибки из-за программирования на си++ в стиле си.
eao197
30.09.2023 02:20+3Еще касательно вот этого кода:
T& operator=(const T &rhs) { n = rhs.n; delete s1; s1 = new S(rhs.s1); return *this; }
Если исключения в проекте не отключены, то потенциально new может бросить std::bad_alloc. В этом случае в this->s1 остается старое и, возможно, ненулевое значение. Которое затем будет передано в delete в деструкторе. И получим double free.
dalerank Автор
30.09.2023 02:20+2Специфика области такая, что про исключения я забыл лет 10 назад. 4% перфа слишком дорогая цена, когда мы тут за 15мс на фрейм бьемся насмерть ;)
old_bear
30.09.2023 02:20В данном случае, функция
setDeltapPitch
применяется дляА это бонусная очепятка?
bel1k0v
30.09.2023 02:20+8Вы случайно создаете дюжину копий объекта «вы» и всем им простреливаете
ногу. Срочная медицинская помощь оказывается невозможной, так как вы не
можете разобраться, где настоящие копии, а где — те, что только
указывают на них и говорят: «А вот он я!»
acordell
30.09.2023 02:20Шикарная статья, спасибо! Все-таки С++, это С++.
С выкидыванием memset оптимизатором только не очень понятно. По идее же это вызов функции, которой передается кусок данных... Мало ли что она там делает? Получается, что оптимизатор знает что такое memset?
mayorovp
30.09.2023 02:20+3Разумеется знает. Оптимизатор знает про все функции, которые есть в стандартной библиотеке, а также про те от которых ему доступны исходники.
dalerank Автор
30.09.2023 02:20+2там оптимизации идут уже на уровне IR компилятора, с выкидыванием мертвых веток кода, почитать можно например тут (https://en.wikipedia.org/wiki/Dead-code_elimination). С мемсетом была похожая ошибка в md5, с точки зрения компилятора, это не влияет на производительность программы. Факт того, что личные данные останутся в памяти, не повлияет на работу программы.
void MD5::finalize () { ... uint1 buffer[64]; ... // Zeroize sensitive information memset (buffer, 0, sizeof(*buffer)); }
dalerank Автор
30.09.2023 02:20но есть и обратная сторона, например на плойке все memcpy помечены как секьюрные и плоечный компилятор об этом знает, он их никогда не выкидывает и не оптимизирует, кроме того в самом memcpy сделаны проверки на пересечение с адресным пространством защищенных областей. В итоге сдкшный memcpy почти в два раза проигрывает наивной реализации копированию _m128 через регистры. Изза этой особенности все (с которыми я работал) игровые движки тащут свою имплементацию memcpy для ps5
voldemar_d
30.09.2023 02:20+1Про memset много материалов есть - здесь, например:
lrrr11
30.09.2023 02:20да. Чтобы такого не было, можно например пометить буфер как volatile (тогда компилятор не станет оптимизировать никакие обращения к нему. Этот режим был придуман специально для буферов памяти всяких устройств, когда запись данных в адрес означает их появление на экране, на диске и т.п.), либо воспользоваться специальной версией memset. https://en.cppreference.com/w/c/string/byte/memset
aarreezz
30.09.2023 02:20const char* whichString = x ? "true" : "false";
const size_t len = strlen(whichString);
Напоминает старинный индусский метод определения true/false:
if( strlen(whichString) == 4 ) ... true
"Знаете зачем программисту два монитора?" - а ведь хорошо сказано :)
boldape
30.09.2023 02:20+3Компиляторы почему-то предпочитают второе, поэтому объект мьютекса никогда не блокируется.
Ну это не так. Тот код эквивалентен
{ auto _ = std::unique_lock(m);}
Мьютекс блокируется и сразу разблокируется и это не то же самое, что он вообще не блокируется.
Формально ошибки нет, мы можем вызывать функции класса, не обращаясь к
данным экземпляра этого класса. В этом конкретном случае у нас
получается вырожденная функция класса.Это тоже не верно, формальная ошибка заключается в том, что нельзя разыменовывать нулевой указатель. Мало того, современный приличный компилятор вот такой код (вместе с телом ифа) просто удалит ни на секунду не задумываясь как заведомое УБ.
if (this == null) {...}
dalerank Автор
30.09.2023 02:20-1Нет, не удалит если внутри логика, этот код ничем не отличается от любого другого и корректен
Попробуйте скомпилить и запустить пример. (https://onlinegdb.com/YetgglcV-)
mayorovp
30.09.2023 02:20-1Нет, это вы попробуйте: https://godbolt.org/z/hr7j83a1r
dalerank Автор
30.09.2023 02:20-1Но все же нет
https://godbolt.org/z/7Yfsqd83b
https://godbolt.org/z/Y93YTsx9s
https://godbolt.org/z/s1zExb1Msmayorovp
30.09.2023 02:20Что "всё же нет"? У вас во всех трёх примерах в baz нет вызова bar, а main так и вовсе пуст: DCE удалил единственный путь исполнения.
boldape
30.09.2023 02:20+1Рекомендую ознакомиться с историей вопроса и пройти по всем ссылкам https://stackoverflow.com/questions/48067323/c-why-cant-this-be-a-nullptr в дополнение к стэку так же рекомендую заглянуть в современный стандарт https://en.cppreference.com/w/c/language/operator_member_access смотрите секцию dereferencing, а ещё вы можете найти здесь же на Хабре статьи про УБ + ещё здесь есть серия статей от компилятора строителя, можете написать в личку этим людям, а ещё можете написать вопрос в комитет для разъяснения, вот прям тут вызывайте российского представителя к сожалению не могу правильно написать имя юзера из мобильного Фокса, antoshka из Яндекса
Даже не собираюсь, УБ по определению может сгенерировать любой код, в том числе тот на чье поведение вы расчитываете, но это не значит что код с таким поведением будет сгенерирован в другом компиляторе включая тот же самый с другими флагами.
dalerank Автор
30.09.2023 02:20-1Я знаком с вопросом более чем, и с Антоном мы уже обсуждали этот вопрос на с++russia 2019. Программа может быть написана так, что желание или нежелание избежать UB на это не влияет
boldape
30.09.2023 02:20+1Программа может быть написана так, что желание или нежелание избежать UB на это не влияет
Извините, я не понимаю, что вы хотите сказать.
Я знаком с вопросом более чем
Ну, хорошо, тогда я сделал все что мог, что бы помочь вам избавиться от ложных убеждений, но если честно то как вы разъясняли суть некоторых багов у меня вызывает сомнения в том, что вы до конца разобрались с вопросом. Я не стал придираться к некоторым другим формулировкам, потому что там по сути нормально и можно игнорировать некоторые не точности, но вот эти 2 объяснения были не верны в корне.
Из опыта я знаю, что если люди допускают небольшие не точности ВМЕСТЕ с не корректными формулировками, то это верный индикатор не полного понимания проблемы.
Но опять, же вы художник вы так видите, я не против, считайте мои комментарии адресованными не вам, а тем кто будет читать эту статью, пусть они сами решают с какой версией соглашаться.
dalerank Автор
30.09.2023 02:20не игнорю, думаю просто неудачный пример получился. Стоило больше сместить акцент вот на эту часть.
A *a = nullptr; a->bar();
Студия ветку не выкинула даже с оптимизациями, кланг выкинул, но с O0 оставил
mayorovp
30.09.2023 02:20O0 — это костыль, а не аргумент. Во-первых, у вас нет гарантий какой именно уровень оптимизаций считается нулевым разработчиками конкретного компилятора. Во-вторых, в этом режиме производительность любых алгоритмов STL просто убога, работая в этом режиме вы обрекаете себя на постоянный выбор между простотой кода и производительностью.
av-86
30.09.2023 02:20+1Вот здесь, судя по всему, знающие люди собрались. Может кто-нибудь пояснить, а можно ли компилятор заставить ругаться каждый раз, когда он видит UB?
dalerank Автор
30.09.2023 02:20+3UB не всегда легко или полностью автоматически определяемое явление, и компайлер пропустит некоторые случаи. Иногда я включаю эти флаги в разных комбинациях и подчищаю код. Могу посоветовать для кланга включить:
-Wall/extra - больше предупреждений от встроеных анализаторов, включая некоторые для UB. Это не сделает UB ошибкой, но поможет выявить потенциально проблемные места.
-Werror - сделает все предупреждения ошибками, можено включить в паре с -Wall для более строгой проверки.
-fsanitize=undefined - включает AddressSanitizer и UndefinedBehaviorSanitizer в рантайме
-pedantic-errors - злой флаг, включает параноик режим у кланга и все несоответствия стандарту выводит как ошибку.
пройтись pvs-studio по проекту, отличный анализатор, ловит много чего, что пропускает компилятор
boldape
30.09.2023 02:20+2В дополнение к тому что выше, частично можно, пишите код в функциях/лямбдах с пометкой constexpr.
Не любой код можно запихнуть в constexpr, не все УБ будут отловлены, но если компилятор ругнеться на УБ то это точно УБ, а не может да может нет как с ворнингами.
Andrey2008
30.09.2023 02:20+1Спасибо за статью. По духу близко к моей подборке вредных советов :)
Пара мыслей про описанные баги.
Кажется, что ошибки с забытой точкой запятой весьма распространены. Они часто упоминаются в разных статьях. Но, на самом деле, они встречаются очень редко. Более чем за 10 лет проверки открытых проектов мы нашли всего 3 таких ошибки. Думаю, сказывается, что все знают про эти ошибки и внимательны к ним. Плюс анализаторы и компиляторы, видимо, тоже хорошо помогают обнаруживать их.
И наоборот, ошибка с исчезновением
memset
кажется чем-то экзотическим и редким. Но это очень распространённая потенциальная уязвимость. Мы их обнаружили 100500 штук и продолжает обнаруживать. Хотя, казалось бы на эту тему тоже уже много написано: CWE-14, Zero and forget -- caveats of zeroing memory in C, Безопасная очистка приватных данных.dalerank Автор
30.09.2023 02:20+1Видимо моя будет четвертой. У вас вышла мега отличная статья с антисоветами, благодарю. Тогда Всем отделом обсуждали несколько дней, но к счастью ничего словить не смогли. PVS стоит на страже. Слежу за Вашим блогом еще с По колено в Си++ г... коде.
allcreater
30.09.2023 02:20+1Компилятор перехитрил сам себя. Итак, что происходит?
Тут-то UB происходит: нельзя читать из неинициализированной переменной. Поскольку недопущение таких вещей в зоне ответственности программиста - компилятор справедливо считает что на входе будет bool с конвенционным значением, и оптимизирует как умеет.
Кстати, кто-нибудь знает, может ли сгенерироваться подобный код функции, если компилятор увидит вместо неинициализированной переменной какое-нибудьmemcpy(&x, src, sizeof(x))
?dalerank Автор
30.09.2023 02:20-1Конечно нельзя, я к тому что bool это не 0, 1 только, что и привело к такому.
Cheater
Зачётно, далеко не всё смог разгадать.
По стандарту так. https://en.wikipedia.org/wiki/Most_vexing_parse