Подтвердить что ты не робот

Является ли поведение undefined проблемой только при развертывании на нескольких платформах?

В большинстве бесед вокруг undefined поведения (UB) рассказывается о том, как есть некоторые платформы, которые могут это сделать, или некоторые компиляторы делают это.

Что делать, если вас интересует только одна платформа и только один компилятор (такая же версия), и вы знаете, что будете использовать их в течение многих лет?

Ничего не меняется, но код, а UB не определяется реализацией.

Как только UB проявил себя для этой архитектуры, и что этот компилятор и вы протестировали, не можете ли вы предположить, что с тех пор, что компилятор сделал с UB в первый раз, он будет делать это каждый раз?

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

Кажется, существуют разные категории поведения:

  • Defined - Это поведение, задокументированное для работы по стандартам
  • Supported - это поведение, задокументированное для поддержки a.k.a реализация определена
  • Extensions - это документированное дополнение, поддержка низкого уровня битные операции, такие как popcount, подсказки ветвей, попадают в эту категорию
  • Constant - пока не задокументированы, это поведение, которое вероятно, будет последовательным на данной платформе, например, sizeof int пока не переносится, вероятно, не изменится
  • Reasonable - обычно безопасный и обычно устаревший, литье из unsigned to signed, используя низкий бит указателя как временное пространство
  • Dangerous - чтение неинициализированной или нераспределенной памяти, возврат временную переменную, используя memcopy для класса non pod

Казалось бы, Constant может быть инвариантным в версии патча на одной платформе. Линия между Reasonable и Dangerous, по-видимому, все больше и больше походит на Dangerous, поскольку компиляторы становятся более агрессивными в своих оптимизации

4b9b3361

Ответ 1

Изменения ОС, безобидные изменения системы (другая версия аппаратного обеспечения!) или изменения компилятора могут привести к тому, что ранее "работающий" UB не работает.

Но это хуже того.

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

Код, который работает в одном контексте, может внезапно прекратить работу в одном компиляторе, ОС и оборудовании, когда вы используете его в другом контексте. Примером этого является нарушение сильного сглаживания; скомпилированный код может работать при вызове в точке A, но когда он встроен (возможно, во время ссылки!), код может изменить значение.

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

Далее, многие виды поведения undefined по своей сути являются недетерминированными. Доступ к содержимому указателя после его освобождения (даже запись на него) может быть безопасным 99/100, но 1/100 страница была выгружена, или что-то еще было написано там, прежде чем вы добрались до нее. Теперь у вас повреждение памяти. Он проходит все ваши тесты, но вам не хватает полного знания о том, что может пойти не так.

Используя поведение undefined, вы обязуетесь полностью понять стандарт С++, все, что может сделать ваш компилятор в этой ситуации, и каждый способ, с которым среда выполнения может реагировать. Вы должны проверять произведенную сборку, а не источник С++, возможно, для всей программы каждый раз, когда вы ее создаете! Вы также обязываете всех, кто читает этот код, или кто изменяет этот код, на этот уровень знаний.

Иногда это стоит того.

Самые быстрые возможные делегаты использует UB, а знание о вызовах условных обозначений - действительно быстрый, не владеющий типом std::function.

Невозможно Быстрые делегаты. Это быстрее в некоторых ситуациях, медленнее в других и соответствует стандарту С++.

Использование UB может стоить того, для повышения производительности. Редко, что вы получаете что-то отличное от производительности (скорости или использования памяти) от такого взлома UB.

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

Альтернативной реализацией будет иметь некоторый набор функций фиксированного размера (10? 100? 1000? 1 млн.?), все из которых ищут std::function в глобальном массиве и вызывают его. Это ограничило бы количество таких обратных вызовов, которые мы установили в любой момент времени, но практически было достаточно.

Ответ 2

Нет, это небезопасно. Прежде всего, вам придется исправить все, а не только версию компилятора. У меня нет конкретных примеров, но я предполагаю, что другая (обновленная) ОС или даже обновленный процессор могут изменить результаты UB.

Кроме того, даже при использовании другого ввода данных в вашу программу можно изменить поведение UB. Например, доступ к внешнему массиву (по крайней мере без оптимизации) обычно зависит от того, что находится в памяти после массива. UPD: см. отличный ответ от Yakk для более подробного обсуждения этого вопроса.

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

UPD: просто заметили, что вы никогда не упоминали о том, как исправлять версию компилятора, вы указали только на исправление самого компилятора. Тогда все еще более опасно: новые версии компилятора могут определенно изменить поведение UB. Из этой серии сообщений в блоге:

Важное и страшное, что нужно понять, это то, что оптимизация на основе поведения undefined может запускаться buggy code в любое время в будущем. Встраивание, циклическая развертка, память продвижение и другие оптимизации будут улучшаться, и значительная часть их причины для существования заключается в том, чтобы оптимизации, такие как выше.

Ответ 3

Это в основном вопрос о конкретной реализации на С++. "Могу ли я предположить, что определенное поведение, undefined по стандарту, по-прежнему будет обрабатываться ($ CXX) на платформе XYZ таким же образом в условиях UVW?"

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

Весь смысл поведения undefined заключается в том, что в стандарте С++ не указано, что происходит, поэтому, если вы ищете какую-то гарантию от стандарта, что он "хорошо", вы не найдете его. Если вы спрашиваете, считает ли это "сообщество в целом" безопасным, это в первую очередь мнение.

Как только UB проявил себя для этой архитектуры, и что этот компилятор и вы протестировали, не можете ли вы предположить, что с тех пор, что компилятор сделал с UB в первый раз, он будет делать это каждый раз?

Только если производители компиляторов гарантируют, что вы можете это сделать, в противном случае нет, это принятие желаемого за действительное.


Позвольте мне попытаться ответить еще немного иначе.

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

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

В значительной степени то, что безопасно, а что нет, является социологическим вопросом. Но если вы хотите просто задать простой вопрос: "Когда я могу разумно предположить, что никто не пострадает от того, что я обвиняю вас в допуске, когда я не могу ничего принять о продукте", вот как я это сделаю. Предположим, я оцениваю, что, если я начну взимать плату с публики, я запустил ее в течение X лет, и за это время, возможно, на нее поедет 100 000 человек. Если это в основном смещенная монета, если она сломается или нет, то то, что я хотел бы увидеть, это что-то вроде: "это устройство было запущено миллион раз с помощью манекенов-краш-машин, и оно никогда не разбивалось и не показывало намеков на нарушение". Тогда я мог вполне разумно полагать, что если я начну взимать плату с публики, шансы на то, что кто-нибудь когда-нибудь пострадает, будут довольно низкими, хотя нет строгих технических стандартов. Это было бы основано на общем знании статистики и механики.

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

Ответ 4

То, о чем вы говорите, скорее реализовано, а не undefined поведение. Первый - это когда стандарт не говорит вам, что произойдет, но он должен работать одинаково, если вы используете один и тот же компилятор и ту же платформу. Примером этого является предположение, что int имеет длину 4 байта. UB - это нечто более серьезное. Там стандарт ничего не говорит. Возможно, что для данного компилятора и платформы он работает, но также возможно, что он работает только в некоторых случаях.

В примере используется неинициализированные значения. Если вы используете неинициализированный bool в if, вы можете получить true или false, и может случиться, что это всегда то, что вы хотите, но код будет разбиваться несколькими неожиданными способами.

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

Итак, если вы выполняете реализацию, тогда вы будете в безопасности, если только работаете только на одной платформе, и вы протестировали ее работу. Если вы выполняете undefined поведение, то вы, вероятно, небезопасны в любом случае. Возможно, это работает, но ничего не гарантирует.

Ответ 5

Подумайте об этом по-другому.

Undefined поведение ВСЕГДА плохое и никогда не должно использоваться, потому что вы никогда не знаете, что получите.

Тем не менее, вы можете смягчить это с помощью

Поведение может быть определено сторонами, отличными от спецификации языка

Таким образом, вы никогда не должны полагаться на UB, но можете найти альтернативные источники, которые утверждают, что определенное поведение - это ОПРЕДЕЛЕННОЕ поведение вашего компилятора в ваших обстоятельствах.

Якк дал отличные примеры относительно быстрых классов делегатов. В этих случаях автор явно утверждает, что они участвуют в поведении undefined, согласно спецификации. Тем не менее, они затем идут, чтобы объяснить бизнес-причину, почему поведение лучше определено, чем это. Например, они заявляют, что макет памяти указателя функции-члена вряд ли изменится в Visual Studio, потому что из-за несовместимостей, которые отвратительны для Microsoft, будут иметь безудержные бизнес-издержки. Таким образом, они заявляют, что поведение является "де-факто определенным поведением".

Аналогичное поведение можно увидеть в типичной реализации linux pthreads (для компиляции gcc). Бывают случаи, когда они делают предположения о том, какие оптимизации компилятору разрешено вызывать в многопоточных сценариях. Эти предположения четко указаны в комментариях в исходном коде. Как это "де-факто определенное поведение"? Ну, pthreads и gcc идут рука об руку. Было бы неприемлемо добавить оптимизацию для gcc, которая сломала pthreads, поэтому никто никогда этого не сделает.

Однако вы не можете сделать то же самое предположение. Вы можете сказать, что "pthreads делает это, поэтому я должен быть в состоянии". Затем кто-то делает оптимизацию и обновляет gcc для работы с ним (возможно, используя __sync вызовы вместо того, чтобы полагаться на volatile). Теперь pthreads продолжает работать... но ваш код больше не работает.

Также рассмотрим случай MySQL (или был он Postgre?), где они обнаружили ошибку переполнения буфера. Переполнение фактически попало в код, но это было сделано с использованием поведения undefined, поэтому последний gcc начал оптимизацию всей проверки.

Итак, в общем, найдите альтернативный источник определения поведения, а не используйте его, пока он undefined. Полностью законно найти причину, по которой вы знаете, что 1.0/0.0 равно NaN, вместо того, чтобы создавать ловушку с плавающей запятой. Но никогда не используйте это предположение, не доказав сначала, что это действительное определение поведения для вас и вашего компилятора.

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

Ответ 6

Исторически, компиляторы C обычно имеют тенденцию действовать несколько предсказуемо, даже если это не требуется Стандартом. На большинстве платформ, например, сравнение нулевого указателя с указателем на мертвый объект будет просто сообщать, что они не равны (полезно, если код хочет с уверенностью утверждать, что указатель имеет значение null и trap, если это не так). Стандарт не требует от компиляторов делать эти вещи, но исторически компиляторы, которые могли бы сделать это легко, сделали это.

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

Надеемся, что такое поведение компилятора окажется недолговечным. Предположительно, это связано с желанием "оптимизировать" код, но для большинства приложений надежность более важна, чем скорость, и наличие компиляторов с кодом, который ограничивал бы ущерб, вызванный ошибочными вводами или поведение программы errand, является рецептом для катастрофы.

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

Ответ 7

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

Короче говоря, небезопасно, если компилятор или ОС не задают поведение (поскольку языковой стандарт этого не сделал).

Ответ 8

Существует фундаментальная проблема поведения undefined любого типа: она диагностируется дезинфицирующими средствами и оптимизаторами. Компилятор может спокойно изменять поведение, соответствующее тем из одной версии в другую (например, расширяя свой репертуар), и внезапно у вас будет некоторая непрослеживаемая ошибка в вашей программе. Этого следует избегать.

Существует поведение undefined, которое сделано "определенным" вашей конкретной реализацией. Левая смена отрицательным количеством бит может быть определена вашей машиной, и было бы безопасно использовать ее там, поскольку нарушение изменений документированных функций происходит довольно редко. Еще один распространенный пример: строгое сглаживание: GCC может отключить это ограничение с помощью -fno-strict-aliasing.

Ответ 9

Хотя я согласен с ответами, которые говорят, что это небезопасно, даже если вы не нацеливаете несколько платформ, каждое правило может иметь исключения.

Я хотел бы привести два примера, где я уверен, что правильный выбор undefined/реализацией был правильным выбором.

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

  • Очень маленькие встроенные устройства. Самые дешевые микроконтроллеры имеют память, измеренную в несколько сотен байт. Если вы разрабатываете маленькую игрушку с мигающими светодиодами или музыкальную открытку и т.д., Каждый пенни рассчитывает, потому что он будет производиться миллионами с очень низкой прибылью на единицу. Ни процессор, ни код никогда не изменяются, и если вам нужно использовать другой процессор для следующего поколения вашего продукта, вам, вероятно, придется переписывать свой код в любом случае. Хорошим примером поведения undefined в этом случае является наличие микроконтроллеров, которые гарантируют нулевое значение (или 255) для каждой ячейки памяти при включении питания. В этом случае вы можете пропустить инициализацию переменных. Если ваш микроконтроллер имеет только 256 байт памяти, это может сделать разницу между программой, которая помещается в память, и кодом, который этого не делает.

Любой, кто не согласен с точкой 2, представьте, что произойдет, если вы скажете что-то вроде этого своему боссу:

"Я знаю, что стоимость оборудования составляет всего 0,40 доллара США, и мы планируем продать его за 0,50 доллара США. Однако программа с 40 строками кода, написанная мной для нее, работает только для этого конкретного типа процессора, поэтому, если в отдаленное будущее, которое мы когда-либо переходим на другой процессор, код не будет использоваться, и мне придется его выбросить и написать новый. Стандартно-совместимая программа, которая работает для каждого типа процессоров, не будет вписываться в наши 0,40 доллара США поэтому я прошу использовать процессор стоимостью 0,60 долл., потому что я отказываюсь писать программу, которая не переносима".

Ответ 10

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

Если вы делаете что-то необычное с указателями, возможно, есть способ использовать броски, чтобы определить, что вы хотите. Из-за своей природы они будут не быть "независимо от того, что компилятор сделал с UB в первый раз". Например, когда вы ссылаетесь на память, на которую указывает указатель uninitialize, вы получаете случайный адрес, который отличается при каждом запуске программы.

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

printf("%d %d", ++i, ++i);

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

Ответ 11

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

Хотя переносимость программы может не быть проблемой, переносимость программистов может быть. Если вам нужно нанять кого-то для поддержки программы, вам нужно будет просто посмотреть на разработчика '< language x > с опытом работы в < application domain > , который хорошо подходит в вашу команду, а не для того, чтобы найти разработчика '< language x > с опытом работы в < домене приложения > зная (или желая изучить) все свойства поведения undefined версии xyz на платформе foo, когда используется в комбинации с баром при использовании baz на мешковине.

Ответ 12

Ничего не меняется, но код, а UB не определяется реализацией.

Изменение кода достаточно, чтобы инициировать различное поведение оптимизатора по отношению к поведению undefined, и поэтому код, который, возможно, работал, может легко сломаться из-за кажущихся незначительными изменений, которые предоставляют больше возможностей оптимизации. Например, изменение, которое позволяет встроить функцию, это хорошо описано в Что должен знать каждый программист C undefined Поведение № 2/3 который гласит:

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

Поставщики компиляторов стали очень агрессивными с оптимизацией вокруг поведения undefined, и обновления могут выявить ранее неисследованный код:

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