Pull to refresh

Comments 13

Видал случай, когда компилятор, обнаружив неопределённое поведение, считал код недостижимым. Это могло привести к выкидыванию целых функций, в том числе main.

Например, если взять отравленное значение и проделать с ним and 0, можно предположить, что результат всегда будет 0

Нет, нельзя. UB — это UB всегда. «Значение», полученное в результате UB, имеет право быть сильно отравленным, например, как описано тут, и убить ваш процесс.

Почему нельзя? UB на то и UB, что компилятор может делать всё, что угодно, в том числе считать результат всегда 0. То есть, можно предположить, что результат всегда 0, а можно не предполагать :) как компилятор захочет.

Но нельзя исходить из того, что это сойдёт с рук.

Вы придираетесь к словам. «Можно предположить...» в данном контексте означает «Мы имеем право считать что...». А Стандарт такого права не даёт.

Довольно неудивительно, что gcc упорствует с подходом, при котором обрушивается программа ... clang компилирует оба вывода, как до, так и после деления, просто удаляя саму операцию деления

Отчасти по этой причине именно gcc до сих пор является основным компилятором для ядра и остальных низкоуровневых интерфейсов. GCC верит программисту, даже если он откровенно врет. CLANG же пытается доказать себе и окружающим, что он умнее.

Вообще все указанные примеры UB - это по сути и UB как таковое. Это просто откровенные ошибки и недостаточные проверки входных данных. Да, "самые умные" типа Rust'а их выловят еще на этапе компиляции. И в этом есть своя правда. Но править код, написанный программистом - это за гранью. А если падение программы это штатное поведение? Если я делю ноль целенаправленно? Допустим чтоб по завершении послать соответствующий сигнал родителю? Ну да - немного извращенный вариант, но тем не менее.

Настоящее неопределенное поведение - это что-то в стиле того, что должен возвращать malloc(0) - NULL или уникальный валидный указатель.

А если это код теста, который проверяет перехват ошибок/исключений. А если это платформа на которой операция деления на ноль не ошибка и возвращает бесконечность. А если этот код будет потом изменяться на лету. Да миллион причин и полезных использований бывает у любого машинного кода.
С каких пор компилятор по умолчанию ограничивает возможности по генерированию нужного мне результата/машинного кода. Контрибьюторы в gcc/clang любящие интерпретировать UB в стандарте во вред программисту, похоже заигрались. Если кто-то когда то пошутил, что UB означает, что программа может отформатировать жесткий диск, то не значит что это надо воплощать на практике специально.

кто-то когда то пошутил, что UB означает, что программа может отформатировать жесткий диск

Шутка неудачна по причине повального заблуждения, что “неопределённое поведение” — это о “моей” программе. А оно, вообще-то, о компиляторе. Поведение “моей” программы всегда абсолютно однозначно определено архитектурой целевой системы и средой исполнения.

То, что стандарт позволяет разработчикам компиляторов делать их настолько “заумными”, не освобождает программиста от необходимости изучать опции управления оптимизатором :)

Что до malloc(0), то это не “неопределённое поведение” (undefined behavior), а “поведение, определяемое реализацией” (implementation-defined behavior). Таким образом, обязано (по стандарту) быть документировано реализацией и прочитано программистом.

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

Токарь, помимо непосредственно токарки, должен уметь как затачивать резцы и много чего еще. Только вот всегда обидно оказаться в ситуации когда надо точить детали, а приходится точить те самые резцы. Крупные опции компилятора, типо оптимизаций под размер или под скорость уже стали почти бесполезными - их в обязательном порядке приходится уточнять. Либо pragma'ми либо ключами компиляции. Да, это прогресс и отменять его несколько глупо, но создается ощущение что именно сейчас маятник где-то в районе другого максимума и ситуация близка к абсурду. И ладно "монстры индустрии" типа GCC или CLANG, но ребята попроще, типа того же IAR C Compiler не всегда позволяют адекватно все отстроить (особенно из своего IDE).

В целом очень хочется начать применять ключевое слово volatile к функциям. Но по большому счету ситуация совсем не так страшна, как ее принято описывать на Хабре. Как всегда - аккуратно, нож острый - можно порезаться. И да, пока этим инструментом овладеешь - руки порезаны будут обязательно. За то (если не бросишь) получишь чувство материала и хорошо развитую мелкую моторику.

P.S.

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

В целом очень хочется начать применять ключевое слово volatile к функциям

А зачем?

Но вообще у меня в шланге бывает [[nodiscard]] и [[no_inline]], но кроме эзотерических техник по редактированию кода программы на рантайме представить зачем оно надо особо не могу.

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

Зачем? На самом деле в основном с целью укрощения излишней прыти оптимизатора в части упаковки повторяющегося кода в функции (он специально развернут ради скорости). Особенно классно когда оптимизатор повторяющиеся сдвиги и битовые операции выносит в функции и вместо одной ожидаемой ассемблерной инструкции мы имеем много, включая возню со стеком (привет IAR). Или не менее классный вариант, когда компилятор по одной ему ведомой причине меняет порядок двух последовательных операций присвоения. Привет KEIL. Да, с точки зрения компилятора нет никакой разницы, но с точки зрения последовательности доступа к регистрам периферии еще как есть. При чем выловить это можно только в дизассемблере.

Как-то так...

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

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

Известен случай, когда утонул какой то аппарат ВМС США по причине того, что оператор оставил пустым одно поле ввода на панели управления, по дефолту там оказался ноль и программа аппарата на него поделила и упала.

Понятно, что разработчикам хотелось бы подобные ситуации списыывать на UB языка, компиляторы, операторов и пятницу 13-е, однако это ошибка алгоритма, как ни крути. Выход за пределы массива - туда же. Ну а уж переполнение при целочисленных вычислениях это то, что разработчик обязан учитывать изначально - либо усекать промежуточные результаты , либо расширять разрядность, либо ещё что.

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

Sign up to leave a comment.

Articles