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)

Возврат происходит под gnu/linux + gcc по умолчанию вроде через eax(fastcall). Какой мусор там умудрился оказаться тот и верётся если ты в return что-то не указал. Под виндовым компилём это ошибка по-умолчанию, да и там сложно было бы игнорить пропущенный возврат, т.к. по умолчанию значение возвращается через стэк (stdcall). Т.е. поведение по-сути в твоём случае зависело не от версии компилятора или наличия оптимизаций, а от погоды на марсе(на самом деле от состояния регистров). В gcc можно тоже сделать эту ситуацию ошибочной, на уровне флагов сборки(вроде -Wno-return -Werror, лень смотреть, знающие люди поправят).

pon4ik ★★★★★
()

Я где-то краем глаза слышал, что при отсутствии return statement в функциях, возвращающих не-void, gcc в некоторых условиях тупо выкидывает (не добавляет) в конце функции инструкцию ret. Имеет полное право, потому что UB. Ну и дальше очевидно как повезёт.

Попробуй дизассемблировать и посмотреть, наверняка вот это и будет.

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 2)
Ответ на: комментарий от pon4ik

fastcall, stdcall

Как там в 2007 x86?

по умолчанию значение возвращается через стэк (stdcall)

Нет, int будет в eax.

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

Я где-то краем глаза слышал, что при отсутствии return statement в функциях, возвращающих не-void, gcc в некоторых условиях тупо выкидывает (не добавляет) в конце функции инструкцию ret.

Да, видимо оно, спасибо.

На GCC 4.8-7.1 – добавляет ret и при обычной компиляции похоже не рисует warning вообще (проверял GodBolt’ом).

На GCC 8.1-10.2 – ret не добавляется, всё идёт по UB, зато warning рисуется.

Теперь стало немного ясно, почему эта ошибка была пропущена.

EXL ★★★★★
() автор топика

Когда-то давно забыл добавить return в функцию,…

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

https://timsong-cpp.github.io/cppwp/stmt.return

Flowing off the end of a constructor, a destructor, or a non-coroutine 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 or a coroutine ([dcl.fct.def.coroutine]) results in undefined behavior.

Почему это UB, а не ошибка компиляции?

Должен быть рабочим такой код.

template<typename T>
T maybe_call(std::function<T(void)> f) 
{
  if (f)
    return f();
  else
    abort_program();
}

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

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

Тепло, лампово, эммо тян нежно щекочат всякое. А что-то концептуально поменялось, или я не так регистр просто обозвал?

pon4ik ★★★★★
()

Не знал про такое поведение, буду держать на вооружении если начнётся какая-нибудь ерунда. Думал, что компилятор обязательно вставляет ret в конец функции, если он достижим.

В Обероне компилятор вставляет герерацию исключения если функция не вызвала RETURN. Сразу видно, что проектировали безопасный язык.

MODULE A;
	
	PROCEDURE RetInt (doRet: BOOLEAN): INTEGER;
	BEGIN
		IF doRet THEN
			RETURN 123;
		END;
	END RetInt;
	
	PROCEDURE Do*;
		VAR val: INTEGER;
	BEGIN
		val := RetInt(FALSE);
	END Do;
	
END A.

Дизассемблер:

PROCEDURE RetInt;
    PAR doRet: BOOLEAN; +8

F2000001:     PUSH    EBP		55
F2000002:     MOV     EBP,ESP		8BEC
F2000004:     CMPB    [EBP,8],0		807D0800	>>>
F2000008:     JE      $F2000018		0F840A000000
F200000E:     MOV     EAX,123		B87B000000	>>>
F2000013:     JMP     $F200001A		E902000000
F2000018:     TRAP    3		8DE3	>>>
F200001A:     MOV     ESP,EBP		8BE5
F200001C:     POP     EBP		5D
F200001D:     RET     4		C20400

PROCEDURE Do;

    VAR val: INTEGER; -12
F2000020:     PUSH    EBP		55
F2000021:     MOV     EBP,ESP		8BEC
F2000023:     PUSH    EDI		57
F2000024:     PUSH    ESI		56
F2000025:     ADD     ESP,-4		83C4FC
F2000028:     PUSH    0		6A00	>>>
F200002A:     CALL    $F2000001		E8D2FFFFFF
F200002F:     MOV     [EBP,-12],EAX		8945F4
F2000032:     LEA     ESP,[EBP,-8]		8D65F8	>>>
F2000035:     POP     ESI		5E
F2000036:     POP     EDI		5F
F2000037:     POP     EBP		5D
F2000038:     RET		C3

Ошибка при вызове:

function without RETURN

 A.RetInt   [00000018H] 
	.doRet	BOOLEAN	FALSE
 A.Do   [0000002EH] 
	.val	INTEGER	105751204
X512 ★★★★★
()
Ответ на: комментарий от pon4ik

Под виндовым компилём это ошибка по-умолчанию … В gcc можно тоже сделать эту ситуацию ошибочной, на уровне флагов сборки(вроде -Wno-return -Werror, лень смотреть, знающие люди поправят).

Странно, что это не ошибка компиляции по умолчанию в GCC, а в относительно старых версиях так вообще без -Wall даже warning’ом тебя не накажет, проверил на GCC 4.8 (CentOS 7).

С другой стороны, если бы GCC не изменил поведение и вставлял бы ret и дальше, то UB так и оставалось бы в коде ещё энное количество лет незамеченным. А тут выстрелило спустя столько лет :^)

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

С другой стороны, если бы GCC не изменил поведение и вставлял бы ret и дальше, то UB так и оставалось бы в коде ещё энное количество лет незамеченным

Адекватный компилятор, к которым GCC не относится, выдает ошибку при UB и не генерирует кода, который передает выполнение на случайный адрес. Причем, делает это одновременно. Мне очень интересно было бы услышать оправдания от поехавшего, который реализовал в GCC выход за пределы функции. Наверняка, там будет что-то вроде «это оптимизация ускоряет работу программ в среднем на 0.2%».

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

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

Мне очень интересно было бы услышать оправдания от поехавшего, который реализовал в GCC выход за пределы функции.

Вот тут почитай их ответы:

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=43943

Update:

Status: 	RESOLVED WORKSFORME 

ахах, прикольный статус :)

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

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=43943

If the programmer guarantees the «missing return» will never happen then there's no error

Ага, то есть случай запущенный. Челы на полном серьезе обсуждают то, насколько поведение компилятора соответствует стандарту, а не то, как этим говном люди будут пользоваться. Именно поэтому в Microsoft сказали «да пошли вы с вашим стандартом» — и сделали удобное поведение. Радует разве что возможность таки активировать правильное поведение через Werror=return-type.

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

Вы вот смеетесь, а MSVC нынче — это единственный годный компилятор сей

А clang? На тяжёлый случай есть TCC, который компилирует в лоб практически без оптимизаций.

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

А clang?

Не знаю даже. Проблема в том, что clang копирует GCC, а GCC пишет большое число отбитых наглухо людей.

На тяжёлый случай есть TCC, который компилирует в лоб практически без оптимизаций

MSVC очень даже хорошо оптимизирует, сравнимо с GCC/Clang.

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

неужели в наше время он стал эмо-гёрл?

Владимир

anonymous
()

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

Кто тебя в этом уверил? Кто тебя так обманул…

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

Много лет назад, когда молодой я пришел на молодой ЛОР с каким то похожим вопросом, ЛОРовские аксакалы смешали меня с грязью за то что я не юзаю флаг -Wall. Я про него банально не знал, но мне стало очень стыдно и с тех пор я его юзаю всегда и вычищаю все ворнинги. И коллег заставляю. Вот!:-)

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

ЛОРовские аксакалы смешали меня с грязью за то что я не юзаю флаг -Wall.

URL?

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

И коллег заставляю. Вот!:-)

Тиран.

anonymous
()

Товарищи, а разве в стандарте прямым текстом не написано, что при отсутствии return в non-void функциях будет неопределенное поведение?

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

И коллег заставляю. Вот!:-)

Сто отжиманий?

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

Смотри черновик стандарта 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.

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

Плюс -Wextra -Werror, во всех проектах исключительно так. Да, бывают false positive типа sometimes uninitialized, когда логика компилятора ошибается, которые решаются непосредственно в коде путем явного присвоения начальных значений, /* NOTREACHED */, /* FALLTHROUGH */, и прочих радостей.

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

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

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

-Werror это уже чересчур, иногда ворнинги сыпятся на модулях сгенеренных каким нить swig-ом а иногда просто на ровном месте (от версии компайлера зависит).

А кое-где -Wpedantic что ли мя задолбал указывать на лишние точки с запятой!

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

В GCC давно не хватает флага -Weverything

Чтобы на все заголовочники стандартной библиотеки орало про использование имён, начинающихся с __?

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

выход за пределы функции.

В каком месте? Просто будет мусор в возврате, не? А там уж как повезёт что будет за мусор.

LINUX-ORG-RU ★★★★★
()

а сегодня вот пересобрал проект

Может ты return удалил уже после билда того что был 10 лет назад?

LINUX-ORG-RU ★★★★★
()

Недавно тоже огрёб проблем из-за забытого return. Сразу добавил себе -Werror=return-type в сборочный скрипт (в qmake). Нихера не понял почему по умолчанию забытый return считается предупреждением, а не ошибкой.

ox55ff ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

Неа, там его вообще не было.

Банально по невнимательности забыл его выставить. У проекта не было -Wall по какой-то причине, хотя -pedantic был. Старый компилятор, которым подобный код был собран ~10 лет назад, предупреждение не генерировал, но проставлял ret избавляя меня от сегфолта.

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

А плюсы-то здесь причём?

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94768#c5

Note C and C++ have slightly different notice about «no return statement in function returning non-void»; In C it is only undefined if the return value is used while in C++ it is undefined the moment that the end of the function happens.

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

Потому что от компилятора не требуется быть достаточно умным, чтобы обнаружить забытый return.

Следующий код корректен, но тупой компилятор не дал бы его собрать:

if (x > 0)
    return x;
else
    return -x;
wandrien ★★
()
Ответ на: комментарий от luke

Вроде никто не писал use xxx luke :D Ты уже за ранее? А ведь мог бы подождать и потом написать ‘А я то тут причём?’ ::)

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от ox55ff

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

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

Товарищи, а разве в стандарте прямым текстом не написано, что при отсутствии return в non-void функциях будет неопределенное поведение?

Нет, там такого не написано

Отвечено же: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=43943

В стандарте отсутствие return не является UB — UB становится только фактическое окончание выполнения функции без return. Очевидно, параграф в стандарте писал человек, который сам на языке не пишет.

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

В каком месте? Просто будет мусор в возврате, не? А там уж как повезёт что будет за мусор

Смотри сообщение, с которого начался тред. GCC именно передает выполнение в случайный код.

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

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

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

Хм, что-то новенькое.

Ты прав, такой фигни раньше не было.

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

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

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

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