LINUX.ORG.RU

Участвуйте в конкурсе глупых и страшных ошибок на языке C++

 , , ,


1

4

Команда PVS-Studio проводит конкурс прикольных/глупых/страшных ошибок на языке C++ (можно и C). Наверняка на практике у вас было что-то эпичное и интересное. Приглашаю поделиться. Самые «лучшие» ошибки мы соберем в статью, при условии, что их наберётся достаточное количество. А чтобы было интереснее, опишите историю бага.

Узнать детали и поучаствовать здесь: https://pvs-studio.ru/ru/blog/contest/

Дата окончания: 30 декабря 2023 года.

P.S. Отберём 10 участников с самыми интересными случаями и отправим им в подарок – мою бумажную книгу «Вредные советы для C++ программистов». Это переработанный и расширенный вариант «60 антипаттернов для С++ программиста». Если хотите, поставлю подпись. Я же знаю, что здесь есть мои почитатели ;)

  1. недавно на лорчике пробегало, мне кажется это грабли, на которые наступали все, некоторые даже по нескольку раз.
int* p1, p2;
  1. некоторым кажется, что они так а инкрементят
int a = 1;
int* p = &a;

...

*p++;
olelookoe ★★★
()
Ответ на: комментарий от seiken

Эм запустить-то запустил,но дальше пошли зависимости проекта, не ставить же их все в линуксовую эмуляцию ей-богу.

Смысл в том чтобы фрибздшную сборку подцепить и проанализировать.

alex0x08 ★★★
()

Даже не знаю, что можно считать «глупой и страшной» ошибкой.

Ошибки по большей части банальны - ошибки копипаста, когда в однотипном наборе присваиваний забыли поменять одну из переменных, ошибки в сравнениях, когда вместо =< стоит < или наоборот, ошибки смещения на 1, перепутанные единицы измерения, когда метры с футами в одном вычислении фигурируют и т.п. А вот последствия ошибок порой бывают действительно и глупые, и страшные.

У меня сейчас в рабочем списке фигурирует на исправление еще одна типичная ошибка многопоточного приложения:

  • Поток А запускает поток Б.
  • Поток Б ставит в очередь коллбек, который должен быть исполнен в потоке А.
  • Поток А до обработки коллбека завершает работу потока Б и освобождает общие ресурсы.
  • Срабатывает коллбек, и программа обращается к освобождённой памяти.
wandrien ★★
()
Последнее исправление: wandrien (всего исправлений: 1)
Ответ на: комментарий от alex1101

Мне кажется, такие ошибки в реале даже джуны не допускают)

в этом-то и проблема )

все такие профессионалы, ух, с кучей набитых шишек, аж вся голова буграми пошла, а потом по три часа вместе и по очереди по коду ползают, чтобы в итоге поймать какое-нибудь смешное if(a=b)

olelookoe ★★★
()

Вот это сильно меня удивило.

Это вообще законно? Провал выполнения в нижележащую «мёртвую» функцию

В случае с C там был бы просто UB при попытке прочитать возвратное значение, в случае с C++ там двойное UB и ноги превращённые в кашу, а выполнение кода ушло в мёртвую функцию, которая вообще нигде не вызывалась.

EXL ★★★★★
()

Я как-то долго пытался понять, что не так с кодом, который работает на более новых компиляторах, но на gcc 4.x.x (сколько-то там, не помню точную версию) валит программу абсолютно без видимой причины. Дело под 32-битной системой было.

Код был приблизительно такой, схематически изображаю:

static int foo_real_impl(int* a, int* b, int c, int d /* тут на самом деле несколько разных типов, включая указатели на разные структуры */)
{
  /* тут куча логики с вызовами разных функции,
     которые вызывают еще кучу функций.
     довольно глубокий уровень вложенности вызовов на стеке.

     И где-то в глубине этого кода программа падала из-за обращения по битому указателю.
  */
}

int foo(int* a, int* b, int d)
{
  return foo_real_impl(a, b, 0, d); 
}

Так вот когда я пытался анализировать стек после падения, там были видны функции которые были глубже, чем foo(), и также были видны те, который ближе к верхушке стека, чем foo(). А самой foo() не было.

Аргументы функций в глубине стека были частично бредовыми, было ясно, что там битые указатели и мусор.

В общем, я был вообще не уверен, что раскрутка стека отображается верно. Или может сам стек уже битый в момент падения.

В итоге я выписал хекс-дамп стека в текстовый файлик и пошел вручную из самой глубины расставлять комменты в нём, где какой фрейм, где какой аргумент и какая переменная.

В итоге пришел к тому, что что-то не так с кодом foo(), потому что именно она портит аргументы. Дизассемблировал функцию и обнаружил что да - что-то не так.

Компилятор, видя что, что foo_real_impl() - это static, решил присвоить ей соглашение о вызове fastcall и передать часть аргументов в регистрах. Далее при генерации кода для foo() он применяет оптимизацию, аналогичную хвостовой рекурсии, когда он не вызывает вложенную функцию, а замещает ею текущую функцию на стеке.

Поэтому foo() на стеке и не видна была.

Но генерируемый код, который аргументы должен в правильном порядке разложить, содержал ошибку, из-за которой в один из указателей попадало неверное значение.

В общем, на ошибку в компиляторе наткнулся.

wandrien ★★
()
Последнее исправление: wandrien (всего исправлений: 3)

Вчера наткнулся. Обработка ошибки. log_error — местная макромагия (к ней претензий нет), здесь выдавшая мне в лог вместо сообщения полный путь к какому-то хедеру.

std::ostringstream ss;
ss << "bla-bla-bla in [" << x << ", " << y + "]";
log_error(ss.str());
JaneDoe
()
Ответ на: комментарий от EXL

В случае с C там был бы просто UB при попытке прочитать возвратное значение, в случае с C++ там двойное UB и ноги превращённые в кашу, а выполнение кода ушло в мёртвую функцию, которая вообще нигде не вызывалась.

А что не так? При UB компилятор не даёт никаких гарантий на поведение бинарника. Вот тебе полная лажа и выдалась.

hateyoufeel ★★★★★
()
Ответ на: комментарий от EXL

А что не так?

То, что в GCC это предупреждение (в старых GCC предупреждения в дефолте не было вообще), а не явная ошибка как, к примеру, в MSVC.

Разыменование NULL – это тоже в лучшем случае предупреждение. А зачастую даже близко нет. Ичо?

hateyoufeel ★★★★★
()
Ответ на: комментарий от thesis

Фугкции Win32 api обычно возвращают не код ошибки, а -1 замаскированный под дефайн, например INVALID_HANDLE. Чтобы узнать код ошибки, надо позвать GetLastError(), что-то вроде errno (которая тоже функция на самом деле). Чтобы вывести диалог с сообщением об ошибке надо ещё парочку Win32 фунций позвать, которые, закончившись успешно переписывают код ошибки, и GetLastError() уже возвращает success. Если первым делом не сохранить результат GetLastError(), он будет переписан, что и приводит к таким комическим messageboxам.

rupert ★★★★★
()
Ответ на: комментарий от Andrey_Karpov_2020

Вообщем заработало, внезапно оказалось что «все не так как на самом деле»:

To generate 'compile_commands.json, add one flag to the CMake call:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On .

Вам бы надо как-то ярче описать что генерировать список команд сборки надо из самой системы сборки. А то мальчик Саша узнал что cmake такое умеет делать только в 40 лет например. Поэтому полез сразу отладчиком изучать почему ‘trace’ не работает, последовательно пройдя все приключения с /compat/linux, историю strace/truss и rfork.

Попробую еще из остальных BSD запустить теперь.

alex0x08 ★★★
()

Опитмизровал вычислительно тяжёлый кусок, решил поиспользовать std::minmax.

single_calc - лёгкая однострочная функция, она везде инлайнилас

Старый код (работал)

void heavy_calc(int a, int b){
  int min_sum = 0;
  int max_sum = 0;
  for (int i =0; i < 100000; ++i)
  {
    int sa = single_calc(a, i);
    int sb = single_calc(b, i);
    int s_min = std::min(sa, sb); 
    int s_max = std::min(sa, sb); 
    min_sum += s_min;
    max_sum += s_max;
  }
  //что-то делаем с min_sum и max_sum
}

Новый код (на части платформ работал, но через месяц при релизном тестировании на малоподдерживаемой платформе откуда-то взялись некорректные результаты)

void heavy_calc(int a, int b){
  int min_sum = 0;
  int max_sum = 0;
  for (int i =0; i < 100000; ++i)
  {
    int sa = single_calc(a, i);
    int sb = single_calc(b, i);
    auto min_max = std::minmax(sa, sb);
    min_sum += min_max.first;
    max_sum += min_max.second;
  }
  //что-то делаем с min_sum и max_sum
}

Бисекция была невозможна, поскольку из-за слабого CI код промежуточный между релизами не компилирвался под минорную платофрму. Был проведён анализ метоодом добавления логов и сравнения их на идентисчночть между платформами и спустя неделю анализа стало ясно что входные данные этой функции - нормальные, а вычисляемые min_sum и max_sum - уже ненормаьлные.

Любая попытка вставить отладочное логгирование внутрь цикла загадочно исправляло проблему.

В результате я 2дня искал багу в 5 строчках внутри цикла (время теста, проявляющего проблему - было порядка 1 часа, попытка тестировать функцию отдельно от комплекса ПО - также исправляла проблему)

Причина под псевдоспойлером

V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
V
auto min_max - это пара ссылок, а не пара int-ов. И она теряла валидность к моменту её использования. 
GPFault ★★
()
Ответ на: комментарий от Virtuos86

unwrap unwrap пейшу чрез строчку я

ведь если я свинья

то педантичная

UBи нигде я встретить не хочу

я лучше за процессорную мощность приплачу

Syncro ★★★★★
()
Последнее исправление: Syncro (всего исправлений: 1)
Ответ на: комментарий от NickNotNick

да, что-то подобное (не уверен что скопилировалиось именно так, давно это было).

Скорее всего в обоих компиляторах auto выводилось как std::pair<int&, int&> но некоторые варианты кодогенерации затирали временные int, а другие варивнты - нет

GPFault ★★
()

нет предела человечской, железячник программирует

void get_pcie_addr(char *device_name, char *pcie)
{
  int i=0;
  FILE *cmd_fp;
  char *ptr = NULL;
  char cmd[256] = {0};
  char cmd_ret[64] = {0};
  
  if(!device_name)
  {
    return ;
  }
  ptr = device_name + 5;
  snprintf(cmd, sizeof(cmd) - 1, "udevadm info -q path -n %s | perl -nle'print $& "
           "while m{(?<=/)[0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\\.[0-9a-f]}g' | tail -n 1",ptr);
  cmd_fp = popen(cmd, "r");
...
hizel ★★★★★
()