LINUX.ORG.RU

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

 , , , ,


3

3

Случай из реального проекта, код ~10-летней выдержки. Когда-то давно забыл добавить return в функцию, где имеется хитрая условная компиляция. Долгое время всё нормально работало, а сегодня вот пересобрал проект и поимел весёлых проблем. При проигрывании звука сегфолтилось, хотя вроде как в коде всё было нормально. Старые версии (собранные старым GCC) не сегфолтились. При отключенной оптимизации сегфолта тоже не было. Что тут можно ещё сказать? Спасибо Сталлману за gdb, сильно удивился когда увидел вызов якобы вырезанной функции в нём. Ну и главное: читайте и анализируйте Warning’и, господа! Минимальный пример:

$ cat dead_code.cpp 
// dead_code.cpp

#include <cstdio>

int stub_0();
int pxt_PlayWithCallback(int chan, int slot, char loop, void (*FinishedCB)(int, int));

int pxt_Play(int chan, int slot, char loop) {
#ifdef _PLS_NO_DEAD_CODE
	if (stub_0()) {
		fprintf(stderr, "!!!!! GOOD CODE !!!!!\n");
	}
#else
    return pxt_PlayWithCallback(chan, slot, loop, NULL);
#endif
}

int pxt_PlayWithCallback(int chan, int slot, char loop, void (*FinishedCB)(int, int)) {
	fprintf(stderr, "????? DEAD CODE ?????\n");
	return stub_0();
}

int stub_0() { return 42; }

int main(int argc, char *argv[]) {
	return pxt_Play(-1, 20, 0);
}

// OK:
$ g++ dead_code.cpp
$ ./a.out 
????? DEAD CODE ?????

// OK:
$ g++ -D_PLS_NO_DEAD_CODE dead_code.cpp
$ ./a.out 
!!!!! GOOD CODE !!!!!

// WTF?:
$ g++ -O2 -D_PLS_NO_DEAD_CODE dead_code.cpp
$ ./a.out 
!!!!! GOOD CODE !!!!!
????? DEAD CODE ?????
Segmentation fault (core dumped)

// WTF???:
$ g++ -O3 -D_PLS_NO_DEAD_CODE dead_code.cpp
$ ./a.out 
!!!!! GOOD CODE !!!!!
!!!!! GOOD CODE !!!!!
...
!!!!! GOOD CODE !!!!!
!!!!! GOOD CODE !!!!!
Segmentation fault (core dumped)

А ведь довольно интересный простор за этим может скрываться. Ну право ведь, забыли return проставить, компилятор же по-дефолту return 0 впихнёт, верно? А я в этом был уверен.

P.S.

// Имеется предупреждение по-дефолту, отсутствует ret в конце функции, провал и сегфолт.
$ gcc --version
gcc (GCC) 10.2.1 20200723 (Red Hat 10.2.1-1)

// Предупреждение только с -Wall, ret в конце функции имеется, нет провала и сегфолта.
$ gcc --version
gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39)

P.P.S. поиграться с компиляторами:

C++: https://gcc.godbolt.org/z/7ne8PM
C: https://gcc.godbolt.org/z/b6vqbK

Может кто-нибудь из профи подробно объяснить механизм такого поведения? Спасибо.

См. комментарии и ссылки в теме.

★★★★★

Последнее исправление: EXL (всего исправлений: 4)

Ответ на: комментарий от rumgot

В стандарте отсутствие return не является UB — UB становится только фактическое окончание выполнения функции без return

Ну это вроде как очевидно

Что очевидно? Вываливание программы в неотлаживаемое поведение тебе очевидно? Может, ты еще и можешь предсказать, какие функции будут выполняться при забытом return?

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

Да, очевидно. Собрался писать на С++ – без -Wall никакого смысла начинать нет. Предупреждение проигнорировал – ССЗБ.

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

Собрался писать на С++ – без -Wall никакого смысла начинать нет. Предупреждение проигнорировал – ССЗБ

Да, я совсем забыл, что хождение по минному полю является привычным для крестовика делом — даже эдаким развлечением. Удачи тебе искать нужное предупреждение в проекте на сотни тысяч строк, где нужно предупреждение похоронено под пластами бессмысленных сообщений о каких-нибудь неиспользованных переменных.

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

Так для твоего кода в тексте Си подходит куда как лучше. Ни классов, ни шаблонов, ни функциональщины.

Для C, насколько мне хватает опыта судить по тем же игрищам с godbolt, компилятор проставляет ret в любом случае и при любой оптимизации. В случае C будет другой UB с ароматом мусора. А в моём случае вышел провал (во всех смыслах этого слова).

Можешь сам проверить, всё аналогично, кроме хедера:

https://gcc.godbolt.org/z/b6vqbK

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

Для C, насколько мне хватает опыта судить по тем же игрищам с godbolt, компилятор проставляет ret в любом случае и при любой оптимизации. В случае C будет другой UB с ароматом мусора

Да, и потому я до сих пор так и не изучил кресты. Хотя с крестами меня рыночек оторвал бы с руками. Потому что каждый раз, когда я прикасаюсь к крестам, я испытываю лютую боль в анусе. Если реализации компиляторов Си писали хакеры, индусы-хакеры, но хакеры, то C++ писали полные кретины от начала до конца — примерно те же, которые написали стандарт ANSI C.

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

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

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

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

Там тот же алгоритм, но, как ты правильно указал, для крестов выход из функции — это не просто «ret», а еще и деструкторы с исключениями. Вот и экономят на спичках. Хотя это, по сути, преждевременная оптимизация, потому что провоцирует сложное неожиданное поведение по умолчанию, для оседлания которого нужно прикладывать усилия — а не наоборот, простое ясное поведение по умолчанию, с приложением усилий для оптимизации хотспотов.

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

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

Такова селяви программиста C/C++.

Проблема компилятора в том, что он ни чего не знает об архитектуре проекта и многое возлагает на программиста.

И правильно делает, так как C/C++ скорее язык для системных программистов, а не прикладных.

ИМХО ныне развитие C++ похоже на - кривая абстракция, над кривой абстракцией с 1001 ограничений.

Так и хочется сказать

Товарищи комитетчики, доценты с кандидатами!
Замучились вы с абстракциям, запутались в нулях,
Сидите там, разлагаете абстракции на атомы,
Забыв, что разлагается картофель на полях.

Из абстракций да из плесени бальзам извлечь пытаетесь
И все усложняете по десять раз на дню…
Ох, вы там добалуетесь, ох, вы доулучшаетесь,
Пока сгниёт-заплесневеет картофель на корню!

...
...

Так приезжайте, милые, — рядами и колоннами!
Хотя вы все там химики и нет на вас креста,
Вы же ведь там задохнетесь за github-ами,
А тут места отличные — воздушные места!

Товарищи комитетчики, не сумлевайтесь, милые:
Коль что у вас не ладится — ну, там, не тот UB, —
Мы мигом с Метапрогом к вам заявимся с лопатами и с вилами,
Денёчек пофлудим— и выправим дефект!

Владимир

anonymous
()
Ответ на: комментарий от byko3y

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

я до сих пор так и не изучил кресты

Короче, очередной диванный кукаретик, начитавшийся чего-нибудь вроде govnokod.ru, рассказывает нам каково это — писать на крестах.

anonymous
()
Ответ на: комментарий от anonymous

И правильно делает, так как C/C++ скорее язык для системных программистов, а не прикладных

Нет ни одной причины для того, чтобы устраивать такое порно и языка, которое устраивают C\C++. То есть, безопасная работа с типизированными указателями с нулевыми накладными расходами — это самая что ни на есть реальность. А сишные нуль-терминированные строки — это вообще издевательство, они даже работают медленнее нормальных, потому даже нет никаких оправданий типа «системному софту важна производительность».

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

Короче, очередной диванный кукаретик, начитавшийся чего-нибудь вроде govnokod.ru, рассказывает нам каково это — писать на крестах

Да, и первый govnodok.ru — это стандарт крестов.

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

Где ты в крестах видишь нуль-терминированные строки? Все реализации строк в С++, если и применяют нуль-терминатор, то для совместимости с сишным кодом.

Вот уж действительно,

Да, и потому я до сих пор так и не изучил кресты.

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

Где ты в крестах видишь нуль-терминированные строки? Все реализации строк в С++, если и применяют нуль-терминатор, то для совместимости с сишным кодом

Разуй глаза, я не писал, что в крестах нуль-терминированные строки.

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

Прикрутить подсветку ошибок к редактору, или использовать IDE, конечно, невыносимо сложно.

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

Я правильно понимаю, что в потоке сознания про отвратительность С++ ты сишные нультерминированные строки решил упомянуть спонтанно?

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

Где ты в крестах видишь нуль-терминированные строки? Все реализации строк в С++, если и применяют нуль-терминатор, то для совместимости с сишным кодом.

И мертвые с косами стоят …

anonymous
()
Ответ на: комментарий от byko3y

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

Т. е. предупреждения по твоему бесполезны?

anonymous
()
Ответ на: комментарий от byko3y

нужно предупреждение похоронено под пластами бессмысленных сообщений о каких-нибудь неиспользованных переменных

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

dsxl
()
Ответ на: комментарий от byko3y

То есть, безопасная работа с типизированными указателями с нулевыми накладными расходами — это самая что ни на есть реальность

Нет, очевидно. На хэлворде и коде для поклейки либ - там да, такое работает. Но там и указатели не нужны. Можешь попробовать реализовать std::deque, например(схема распределения памяти + итераторы, остальное не обязательно), и забэнчить своё против стандартного - сказки про «безопасная работа с типизированными указателями с нулевыми накладными расходами» перестанут мелькать в твоих постах.

anonymous
()
Ответ на: комментарий от byko3y

… где нужно предупреждение похоронено под пластами бессмысленных сообщений о каких-нибудь неиспользованных переменных.

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

Суждение, приведенное вами не очень «удачно».

anonymous
()
Ответ на: комментарий от fsb4000

И так как мы не знаем тип T, то не можем написать в конце return T(); или ещё что-нибудь, так что return не может быть обязательным в функции…

Пиши в «процедурном» стиле (процедуры из паскаля - подпрограммы, не возвращают значения). Возвращаемые значения только через параметры. «Функциональный» стиль - от лукавого. Стандартописатели явно намекают.

anonymous
()
Ответ на: комментарий от anonymous

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

Т. е. предупреждения по твоему бесполезны?

Бесполезно делать очевидное UB предупреждением. Я еще раз напоминаю, что MSVC трактует это ошибкой и не дает компилировать код — это единственное адекватное поведение. Всё остальное — жалкие оправдания мамкиных хлебушков, неспособных самостоятельно принимать решения и нести за них ответственность.

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

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

Тогда уже нужно использовать конкретные флаги компиляции вместо "-Wall".

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

std::deque, например(схема распределения памяти + итераторы, остальное не обязательно), и забэнчить своё против стандартного - сказки про «безопасная работа с типизированными указателями с нулевыми накладными расходами» перестанут мелькать в твоих постах

Я не знаю реализации std::deque, особенно учитывая тот факт, что она разная на разных компиляторах, так что оценивать не возьмусь. Я имел в виду скорее нулевые накладные расходы при написании примитивных вещей в стиле Си, а std::deque — это все-таки довольно сложный контейнер.

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

Ты на вопрос не ответил. Предупреждения бесполезны?

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

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

она разная на разных компиляторах

Одинаковая с точностью до констант/if vs ?:/разбиения по функциям. Суть не отличается.

Я имел в виду скорее нулевые накладные расходы при написании примитивных вещей в стиле Си, а std::deque — это все-таки довольно сложный контейнер

Предложенный контейнер и есть довольно примитивная вещь. В стиле си - самое основное да, в стиле си. От крестов там только шаблоны + объекты(первое здесь не важно, второе делается на си вручную, хоть и менее элегантно/понятно/просто).

anonymous
()
Ответ на: комментарий от anonymous

Выше правильно сказали, что неиспользованные переменные нужно закомментировать и памятку наваять для чего она планировалась использоваться /а то и вовсе удалить/

Неиспользованная переменная — это далеко не единственный тип предупреждений компилятора. Strict aliasing, switch-case, неинициализированные переменные, которые почему-то компилятор продолжает упорно считать неинициализированными, хотя они инициализируются во всех путях выполнения — и так далее.

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

Из твоего аргумента про «кучу предупреждений» складывается противоположное ощущение(и как можно заметить, этим аргументом можно пытаться доказать бесполезность предупреждений в целом). А по теме - да, я тоже не слишком понимаю, почему нет ошибки в отсутствие return-а.

anonymous
()
Ответ на: комментарий от anonymous

Я имел в виду скорее нулевые накладные расходы при написании примитивных вещей в стиле Си, а std::deque — это все-таки довольно сложный контейнер

Предложенный контейнер и есть довольно примитивная вещь. В стиле си - самое основное да, в стиле си. От крестов там только шаблоны + объекты(первое здесь не важно, второе делается на си вручную, хоть и менее элегантно/понятно/просто)

Сложное — потому что комбинирует хранение блоками и связанными списками. Отсюда начинаются компромисы и общие знаменатели.

Я бы написал спеку на язык, которым должен был быть Си, но я не вижу в этом никакого смысла, потому что все будут продолжать писать классы на C++. Даже несмотря на то, что Страуструп признавался, что не планировал вводить классы в кресты, а хотел сделать язык на базе шаблонов и перегрузок операторов.

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

Из твоего аргумента про «кучу предупреждений» складывается противоположное ощущение(и как можно заметить, этим аргументом можно пытаться доказать бесполезность предупреждений в целом). А по теме - да, я тоже не слишком понимаю, почему нет ошибки в отсутствие return-а

Это такой риторический вопрос с покачиванием головы, или действительно не понимаешь? Давали же ссылку на додиков из GCC, которые оправдывались «я помолился на стандарт и спросил подсказки у боженьки — и боженька мне ответил». UB — это ошибка, и должна быть ошибкой, что в среде сишников и крестовиков считается нормой плодить UB. Только в последних крестовых стандартах вахтеры взялись за ум и начали чистить стандарт от мусора.

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

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

Про что ты такое говоришь?

Любой проект собирается минимум с /W4 /WX или -Wall -Wextra -Wpedantic -Werror и коммиты которые вызывают предупреждения просто не проходят CI и не могут быть внесены в проект.

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

Сложное

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

anonymous
()
Ответ на: комментарий от fsb4000

-Wall -Wextra -Wpedantic -Werror

Поднимите руку, кто так делает. Ты не сможешь скомпилировать с этими флагами ядро линя, например.

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

UB — это ошибка, и должна быть ошибкой [компиляции]

Уже даже здесь обсосали эту тему, не помню где именно, но начиналось всё вот с этого треда: Вызов никогда не вызываемой функции. Спойлер: невозможно.

anonymous
()
Ответ на: комментарий от anonymous

Поднимаю. С той оговоркой, что -Werror нет. Есть частные случаи(-Werror-something)

Вот именно, об этом я и пишу — смысл на практике есть только для отдельных флагов, но не повального «Werror».

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

Мне очевидно, что текст стандарта (черновик для C++17)

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf

9.6.3 The return statement [stmt.return]

2.

…

Flowing off the end of a constructor, a destructor, or a function with a cv void return type is equivalent to a return with no operand. Otherwise, flowing off the end of a function other than main (6.6.1) results in undefined behavior.

говорит, что отсутствие return в non-void функции И дохождение потока выполнения до конца этой функции И последующий выход за пределы этой функции дают в сумме неопределенное поведение. Не очень понимаю твою претензию.

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

Поднимите руку, кто так делает.

Microsoft STL и Microsoft SDK например.

Даже более жесткие требования:

https://github.com/microsoft/STL/blob/master/tests/universal_prefix.lst

https://clang.llvm.org/docs/UsersManual.html

/WX = -Werror

/W4 = -Wall -Wextra

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

Отвечаю по пунктам:

int c = a + *b;
// Ворнинг: сложение может переполниться. Рекомендуем выполнить сложение в типе long long и проверить результат.

В языках с поддержкой сильных типов есть проверка границ в рантайме. Также, здесь может быть опущена операция обрезания числа, если a или b имеют тип «long».

// Ворнинг: b может быть равен nullptr. Рекомендуем добавить проверку

Для этого в крестах ввели поддержку ссылок. Практически всегда сишный указатель — это на самом деле ссылка. Отсюда некорректные оптимизации, которые ломали работающий код, если в коде указатель был указателем, а не ссылкой. Этот костыль подперли костылем volatile, а потом какой-то поехавший из GCC обкурился стандартов и решил ввести правило strict aliasing, которое невозможно проверить во время компиляции и нарушение невозможно обнаружить в отладочной сборке.

// Ворнинг: a может быть не инициализирован. Пожалуйста, проверьте все динамические пути работы программы, ведущие к данной строке, и убедитесь, что а всегда инициализирован.

Правильное предупреждение.

// Ворнинг: c используется только в cout на следующей строке и поэтому не будет сохранён на стек.

Неправильное предупреждение. Хз, откуда его взяли.

// Ворнинг: убедитесь, что указатель b не нарушает правила strict aliasing.

Уже ответил: strict aliasing — абсолютное зло, нет никакого оправдания его существованию.

// Ворнинг: убедитесь, что указатель b не был получен выделением динамической памяти, но уже освобождён, либо получен из фрейма уже завершившейся функции.

Опять ссылкопроблемы.

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

говорит, что отсутствие return в non-void функции И дохождение потока выполнения до конца этой функции И последующий выход за пределы этой функции дают в сумме неопределенное поведение. Не очень понимаю твою претензию

Моя претензия уже была написана: «как этим говном потом пользоваться?». То, что стандарт описывает неопределенное поведение, не значит, что если ты сделаешь в своем компиляторе его определенным, то комитет придет и вырежет всю твою семью — ты просто не имеешь гарантию, что на другом компиляторе поведение не будет неопределенным, только и всего. Более того, различным поведение оказывается даже при сборке с разными опциями на одном и том же компиляторе. Лучшая реакция на такой стандарт — это всегда трактовать код с данным UB как ошибку.

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

Microsoft STL и Microsoft SDK например
/WX = -Werror
/W4 = -Wall -Wextra

Компиляция кода из исходного сообщения MSVC с «/W0 /WX-»:

https://gcc.godbolt.org/z/zfMj3z

MSVC где-то строже, а где-то добрее (strict aliasing), потому аналогия ключей все-таки не такая прямая.

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

потому аналогия ключей все-таки не такая прямая.

а я не про аналогию ключей. Microsoft STL собирается двумя компиляторами в CI, если любой из компиляторов выдаст предупреждение/ошибку то тесты не пройдут…

cl и clang-cl

а тут написано как именно clang-cl трактует /W4 и /WX

https://clang.llvm.org/docs/UsersManual.html

fsb4000 ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.