Шокирующая правда про undefined behavior в C/C++ II

procenkotanya

Транк GCC научили выводить ограничения на диапазон переменных в зависимости от границ массивов, которые ими индексируются. Таким образом, если имеется следующий код:
int a[16], sum[16];
int i, cur;
for (i=0, cur=a[0]; i<16; i++, cur+=a[i])
sum[i] = cur;
GCC замечает, что после cur+=a[i] i гарантированно меньше 16, что делает цикл бесконечным. Инкремент i в цикле конечно никуда не девается, и, таким образом, циклы, ранее содержащие сравнительно безобидные off-by-one баги будут фигачить до сегфолта. Caveat programmator!
К сожалению, в данный момент соответствующего варнинга нет (его не так просто запрогать).

doublemother

Ничо не понял. Условие цикла же только i<16, а i изменяется только инкрементом i++, как цикл может быть бесконечным?

Serab

там баг: после инкремента берется a[i] еще до проверки i < 16. А gcc думает, что раз проверки нет, то уже «все проверено до нас» © и вообще не делает эту проверку потом (т.е. условие окончания цикла). Т.е. кроме одного вылезания за границу массива, у нас еще и бесконечный цикл.

elenangel

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

tinych1

Теперь станет ещё более очевидно, что undefined behaviour это не просто segfault, а _действительно_ абсолютно любое действие со стороны программы - она может хоть зависнуть, хоть диск отформатировать, хоть пользователя задушить шнуром от мышки.
Кстати, насчёт диск отформатировать:
int f(int i) {
int a[10];
if(i < 10) { exec("format c:"); }
return a[i];
}

int main {
f(15);
}

По идее оптимизация, подобная указанной топикстартером, может заставить эту программу соптимизироваться до безусловного форматирования диска. Компилятор может рассуждать: "возвращаем a[i], значит i < 10 иначе было бы UB. Значит можно не проверять условие в if.".

procenkotanya

Компилятор может рассуждать: "возвращаем a[i], значит i < 10 иначе было бы UB. Значит можно не проверять условие в if.".
Нет, дружище, из exec возврата не происходит, так что компилятор не может так рассуждать. Вот system, который ты имел в виду — другое дело (но только если компилятору откуда-то известно, что из system _всегда_ происходит возврат (EDIT: что, кстати, неверно, так как вызванная команда может убить породивший её процесс.

zorin29

Не понял аргументации.
Вот компилятор думает:
"если i < 10, то надо сделать exec. А если i>=10, то в следующей строке будет UB, так что всем пофиг, что я сделаю. Вот я и сделаю тоже exec так меньше будет операторов".

tinych1

Да, я имел в виду system.
Но даже с exec, разве он не может рассуждать так:
Рассмотрим случай, когда i >= 10. Условие if не выполняется, так что управление дойдёт до строки a[ i ], где мы получаем UB. Значит можно предполагать, что на вход функции никогда не будет подаваться число i >= 10. Значит можно не проверять условие if.

procenkotanya

Да, хм, наверно я поторопился и был неправ. Сорри

tinych1

Не, я говорил не про текущую оптимизацию, а про _подобную_, просто не очень удачно сформулировал. Под подобной подразумевал такой "пофигизм" компилятора - жесткая оптимизация путём введения неожиданного поведения в случае UB.

vall

классно чо.
в Ядре как-то был классный баг с зацикливанием перебора страниц в иноде если присутствует самая последняя страница с индексом ULONG_MAX, после неё он переходил на 0 и начинал всё сначала.
и я когда там переделывал итераторы пэйдж-кэша слегка заебался впихивать эту проверку переполнения, пару дней перебирал варианты :grin:

vall

непонятно зачем такую фичу вводить таким способом, который поломает то, что работало до, хотя и ошибочно работало.
бесконечный цикл или гарантированное падение просто БЕСКОНЕЧНО лучше незаметно подпорченного байта.

doublemother

Нет, дружище, из exec возврата не происходит
На самом деле иногда происходит, для этого собственно в fork+exec всегда после exec делают exit(-1);
RETURN VALUE
The exec functions only return if an error has have occurred. The return value is -1, and errno is set to indicate the error.

procenkotanya

Да, я вообще знатно облажался в том посте. По поводу exec, важно что иногда из него не происходит возврата, и компилятор не может предполагать, что следующий за ним код, содержащий UB, обязательно рано или поздно выполнится. exec тут не является какой-то особенной функцией: компилятор должен консервативно предполагать, что все неизвестные ему функции могут никогда не вернуть управление.
(компиляторам интересны noreturn-функции, такие как abort (гарантированно никогда не возвращающие управление потому что это позволяет выкидывать следующий за ними код как недостижимый)

apl13

Лучше бы NetHack запускал...

procenkotanya

Кстати, поскольку #pragma имеет implementation-defined behavior (не путать с UB! ранние версии GCC запускали игрушку типа nethack, встречая вообще любую прагму. А исполняемый файл компилятора, который это делал, назывался cccp. True story:
/* C Compatible Compiler Preprocessor (CCCP)
... [тут текст ранней версии GPL]
In other words, you are welcome to use, share and improve this program.
You are forbidden to forbid anyone else to use, share and improve
what you give them. Help stamp out software-hoarding! */

...

/*
* the behavior of the #pragma directive is implementation defined.
* this implementation defines it as follows.
*/
do_pragma
{
close (0);
if (open ("/dev/tty", O_RDONLY) != 0)
goto nope;
close (1);
if (open ("/dev/tty", O_WRONLY) != 1)
goto nope;
execl ("/usr/games/hack", "#pragma", 0);
execl ("/usr/games/rogue", "#pragma", 0);
execl ("/usr/new/emacs", "-f", "hanoi", "9", "-kill", 0);
execl ("/usr/local/emacs", "-f", "hanoi", "9", "-kill", 0);
nope:
fatal ("You are in a maze of twisty compiler features, all different");
}

zorin29

Теперь станет ещё более очевидно, что undefined behaviour это не просто segfault, а _действительно_ абсолютно любое действие со стороны программы - она может хоть зависнуть, хоть диск отформатировать, хоть пользователя задушить шнуром от мышки.
Кстати, насчёт диск отформатировать:
Напиши еще код, удушающий пользователя шнуром от мышки плз. Думаю, многим пригодится :)

Serab

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

Dasar

бесконечный цикл или гарантированное падение просто БЕСКОНЕЧНО лучше незаметно подпорченного байта.
Только не на стороне пользователя. Лучше плохо работающая программа, чем вообще не работающая. Проверку правильности результата можно и внешним способом организовать.

Serab

ну вот опять. Непадающая != работающая. Замаскированные баги пользователю не нужны. Чувствую завязку разговора про ассерты :crazy:

Serab

тем более, это похоже, что багофича компилятора с юмором небольшим: типа это как бы WAT поведение, которое получается как логичное (?) следствие корректности программы, но фишка в том, что оно еще может быть полезно при отладке, так-то это чисто оптимизация вроде бы, вряд ли это было by design именно для отладки.

Dasar

Непадающая != работающая.
Работу программы можно представить как функцию со входом и выходом.
Обозначим правильную программу как out P(in) = F(in тогда:
программа которая портит байт, оставаясь более-менее работоспособной, можно представить как out P(in)=G(F(in где G - это функция, которая искажает результат на меньше, чем 20%(условно). Где 20% искажения происходит, или по попыткам (каждая пятая возвращает неправильный результат или по объему результата (в каждом результате искажается меньше 20% бит в какой-то разумной топологии или какая-то смесь двух предыдущих сценариев,
программа которая циклится или падает на каждый чих представляется как out P(in) = for(;;); или out P(in)=exit(error_code которую очевидно можно выкинуть, т.к. она не отличима от уже много раз написанной такой же до этого.
Соответственно, первая лучше чем вторая, т.к. в первом случае, из функции G(F(in в значительном кол-ве случаев можно получить функцию F, используя композицию out P(in)=AntiG(G(F(in. И такая композиция часто доступна пользователю.

Serab

падающая на каждый чих — это преувеличение, такая не должна была пройти тестирование. в твоем рассмотрении просто завышен отрицательный эффект падения (т.е. чистосердечного признания, что результат не может быть получен для данных входных данных/в данный момент времени/при текущем положении звезд). Я бы вот сказал, что тихая выдача неправильного ответа — это -мыльён в карму. -мыльён * 20% ~ -200 000 в среднем. В общем, это в к вопросу о функции предпочтений клиента. Если речь о космосе там, то будет как у меня, если это контактик, где пускай лучше сиськи леночки будут видны в углу экрана, чем придется напрягать фантазию, то да, падения нехороши.

Dasar

Я бы вот сказал, что тихая выдача неправильного ответа — это -мыльён в карму. -мыльён * 20% ~ -200 000 в среднем.
Когда тоже самое делает человек, а не программа - тебя почему-то это не напрягает. Почему такие двойные стандарты?
Если брать форум, то тут почти все пишут с грамматическими ошибками, неправильно используют термины и т.д. - тебя это вроде не напрягает, по крайней мере, ты едва ли считаешь, что все сообщения где есть грамматические ошибки должны заменяться на сообщение: Грамматическая ошибка (текст сообщения не покажу).
В чем разница?

Dasar

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

Devid

Зависит от того, что программа делает. Например если это CAM система, которая считает программу для ЧПУ, то пусть она лучше упадет, чем накосячит, потому что косяк может привести к реальным потерям в будущем.

Serab

Когда тоже самое делает человек, а не программа - тебя почему-то это не напрягает. Почему такие двойные стандарты?
Двухсотлетний человек :grin: ладно, я спать, завтра подумаю :)

Dasar

Зависит от того, что программа делает. Например если это CAM система, которая считает программу для ЧПУ, то пусть она лучше упадет, чем накосячит, потому что косяк может привести реальным потерям в будущем.
Такие программы можно представить как:
out P(in) = отрежь(отмерь(in где отрежь - это действия, которые приводят к необратимым деструктивным последствиям, а отмерь - действия с обратимыми последствиями.
Для таких программ желательно, чтобы пользователь имел доступ к границе между отрежь и отмерь, или другими словами, чтобы программа представлялась как: out P(in) = отрежь(U(отмерь(in где U - это произвольная пользовательская функция (например, U=попробуй-тысячу-раз-и-проверь-правильность)
При таком представлении хочется, чтобы отмерь не падала при возникновении ошибки, а отрежь - по умолчанию падала, но чтобы при этом была возможность запустить отрежь, несмотря на ошибки.
Так же отмечу, что при таком представлении: код отрежь на несколько порядков меньше и реже меняется, чем код отмерь (и соответственно, его намного проще написать без ошибок).

Dasar

Чувствую завязку разговора про ассерты :crazy:
Assert хорош в виде монады, которая продолжает выполнение кода, но при этом помечает конечный результат функции, как полученный с нарушением такого-то assert-а.
т.е. лучше на экране увидеть:
результат: 42 (внимание! были нарушены следующие ассерты: bla-bla)
чем
результат: Value does not fall within the expected range exception

salamander

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

Papazyan

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

Papazyan

Если брать форум, то тут почти все пишут с грамматическими ошибками, неправильно используют термины и т.д. - тебя это вроде не напрягает, по крайней мере, ты едва ли считаешь, что все сообщения где есть грамматические ошибки должны заменяться на сообщение: Грамматическая ошибка (текст сообщения не покажу).
В чем разница?
Разница в том, что человек а) может заметить и исправить ошибку из общих соображений, в то время как компьютеры соображают на уровне амебы б) грамматические ошибки не влекут почти никаких последствий большую часть времени с) человека все равно не переделать, но не надо переносить его откровенные слабости на другие системы.

Papazyan

Соответственно, первая лучше чем вторая, т.к. в первом случае, из функции G(F(in в значительном кол-ве случаев можно получить функцию F, используя композицию out P(in)=AntiG(G(F(in. И такая композиция часто доступна пользователю.
Ты опять бредишь и это тем более удивительно, что у тебя вроде бы не филологическое образование и ты должен понимать, что ошибки не есть какие-то 20% искажения - любая ошибка - это 100% искажение и невозможно предсказать, как она повлияет на общий результат. Какие для данной программы установлены критерии оценки результата.

apl13

Я об этом, собственно.

Dasar

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

Dasar

который при возникновении сбоя переключается на запасной
а если нет запасного? или он падает на той же ошибке?

Papazyan

Не стыдно такое писать? Изменение одного байта может привести к каким угодно последствиям в неопределенном будущем.

Dasar

Не стыдно такое писать? Изменение одного байта может привести к каким угодно последствиям в неопределенном будущем.
И как из этого утверждения следует отсутствие предсказуемости?

xronik111

В контексте темы все просто — оптимизации делаются для того, чтобы быстрее работали правильные программы, а не для того, чтобы сохранять поведение неправильных. Специально для этого (в значительной степени) существуют языковые стандарты. Хотите, чтобы программа вела себя ровно как написано? Включите -O0 (без гарантий, просто в gcc очень мало при -O0 происходит, но с каждым годом это меняется или включите строго интерпретацию в яве, проиграйте разы в производительности. Хотите, чтобы быстро было? Пишите в соответствии со стандартом, скажите компилятору про оптимизации. Хотите и того, и другого, и чтобы компилятор еще угадывал, как вы имели в виду, но не написали? Напишите свой компилятор или пройдите на хуй из профессии.

Dasar

В контексте темы все просто — оптимизации делаются для того, чтобы быстрее работали правильные программы, а не для того, чтобы сохранять поведение неправильных.
Проблема только в том, что большие программы 100%-но правильными не бывают.

doublemother

Пишите в соответствии со стандартом, скажите компилятору про оптимизации.
Мне просто интересно, ты когда-нибудь компилировал хоть один крупный проект с максимальным уровнем предупреждений гцц?
Я вот знаю, что, например, boost, qt, qxt с высоким уровнем предупреждений не соберутся. А ты про оптимизации и стандарты.

vall

Мне просто интересно, ты когда-нибудь компилировал хоть один крупный проект с максимальным уровнем предупреждений гцц?Я вот знаю, что, например, boost, qt, qxt с высоким уровнем предупреждений не соберутся. А ты про оптимизации и стандарты.
вот только не надо -Werror лепить в общую кучу. включение его подефолту я его считаю большим злом — гавнокодеры вместо того чтоб подумать начинают судорожно менять код чтоб варнинг исчез.
линусово ядро с нормальными конфигами собирается почти всегда без варнингов, но они там конечно не все включены по дефолту.

Serab

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

doublemother

другие вещи типа сравнения знаковых-беззнаковых
А это типа не ошибка? В 95% случаев это повод задуматься над кодом.

Serab

ну я к тому, что это не к "пишите по стандарту", это больше. И к оптимизациям это не имеет отношения, потому что поведение известно.

Maurog

А это типа не ошибка? В 95% случаев это повод задуматься над кодом.
в 95% случаев это не ошибка

vall

в 95% случаев это не ошибка
в 95% случаев ошибка в другом месте

istran

Про boost точно вранье.
У нас (Яндекс.Карты) все проекты собираются с -Wall -Wextra -Werror. В исключительных случаях добавляем опции -Wno-*.

doublemother

Про boost точно вранье.
А с -Wunreachable-code? :)

istran

Ладно, такие опции не использую. Но в рамках Wall и Wextra код из boost компилируется без проблем.
По теме - лично меня не раз спасали от дурацких труднонаходимых ошибок предупреждения о сравнении беззнакового со знаковым.

vall

А с -Wunreachable-code?
это не ошибка

doublemother

На самом деле практически все предупреждения не нужно игнорировать. Тот же -Wunreachable-code вполне себе помогает найти косяки, ну и не таскать за собой мёртвый код, разумеется. Я раньше пытался писать софт с самым максимальным уровнем предупреждений, теперь часть закомментировал:
        add_definitions( 
-Wall
#-Weffc++ # breaks Qt
-Wstrict-null-sentinel
#-Wold-style-cast # breaks Qt
-Woverloaded-virtual
-Werror
-Wextra
#-Wshadow # breaks QxtCommandOptions
#-pedantic # breaks Qt and Qxt
-Wctor-dtor-privacy
-Wnon-virtual-dtor
-Winit-self
#-Wunreachable-code # breaks boost and Qt
#-Wconversion # breaks Qt
)

Такие дела.

doublemother

это не ошибка
Это может быть ошибкой и этого нужно избегать.

vall

Это может быть ошибкой и этого нужно избегать.
портабельный или статически настраиваемый код не может обойтись без недостижимого кода,
иначе он будет состоять на половину из макроопераций (в случае c++ из больных шаблонов из ада)

doublemother

иначе он будет состоять на половину из макроопераций (в случае c++ из больных шаблонов из ада)
Лично я знаю два способа написать портабельный код:
1. Использовать пресловутые макрооперации
2. В зависимости от архитектуры подключать разные файлы с имплементацией.
Оба эти способа подразумевают, что мёртвый код в любом случае в компиляцию не попадает.
Если ты знаешь другие способы, чтобы, скажем, в винде использовать HANDLE и _beginthreadex, а в юниксах — pthread_t и pthread_create, поделись.

xronik111

> Мне просто интересно, ты когда-нибудь компилировал хоть один крупный проект с максимальным уровнем предупреждений гцц?
Конечно, в основном тот же гцц. Да, я считаю, что в нормальном проекте предупреждения с высоким уровнем true positive и -Werror должны быть включены по умолчанию. Да, я считаю, что программист должен знать свои инструменты и отличать предупреждения, которые часто верны, от тех, которые нет. У больших, близких к системным проектов всегда будет свой набор включенных предупреждений. Ломается код, кривое левое предупреждение? Возьмите с vozbu пример, зашлите багрепорт.
Хотя, конечно, с точки зрения компиляторщика предупреждения — достаточно занудная вещь, требуется аккуратность, а качественно сделать отнюдь не всегда можно. Например, нормальный -Wuninitialized требует хорошего анализа, в результате выдаваемые предупреждения с -O0 и -O2 могут отличаться, пользователей это вымораживает. Вроде бы это пофиксили уже.
Оставить комментарий
Имя или ник:
Комментарий: