Законно ли исходный код, содержащий неопределенное поведение, вызывать сбой компилятора?

85

Скажем, я собираюсь скомпилировать какой-то плохо написанный исходный код C ++, который вызывает неопределенное поведение, и поэтому (как говорится) «все может случиться».

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

Джереми Фриснер
источник
22
«UB есть UB. Живи с этим» ... Нет, погоди. «Пожалуйста, опубликуйте MCVE». ... Нет, подождите. Мне нравится этот вопрос из-за всех рефлексов, которые он неуместно вызывает. :-)
Yunnosch
14
На самом деле ограничений нет, поэтому говорят, что UB может вызывать носовых демонов .
Какой-то чувак-программист
15
UB может заставить автора задать вопрос на SO. : P
Танвир Бадар
45
Независимо от того, что говорит стандарт C ++, если бы я был автором компилятора, я бы определенно расценил это как ошибку в своем компиляторе. Поэтому, если вы видите это, отправьте отчет о дефекте.
Джон
9
@LeifWillerts Это было еще в 80-х. Я не помню точную конструкцию, но думаю, что она зависела от использования запутанного типа переменной. После того, как я поставил замену, у меня возник момент «о чем я думал - все не так». Я не обвинял компилятор в том, что он отклонил конструкцию, а только за перезагрузку машины. Сомневаюсь, что сегодня кто-то встретил бы этот компилятор. Это был кросс-компилятор HP C для HP 64000, предназначенный для микропроцессора 68000.
Ави Бергер

Ответы:

71

Нормативное определение неопределенного поведения выглядит следующим образом:

[defns.undefined]

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

[Примечание. Неопределенное поведение может ожидаться, если в этом международном стандарте отсутствует какое-либо явное определение поведения или когда программа использует ошибочную конструкцию или ошибочные данные. Допустимое неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами до поведения во время трансляции или выполнения программы задокументированным образом, характерным для среды (с выдачей диагностического сообщения или без него), до прекращения трансляции или выполнения (с выдачей диагностического сообщения). Многие ошибочные программные конструкции не приводят к неопределенному поведению; они должны быть диагностированы. Оценка постоянного выражения никогда не проявляет поведения, явно указанного как неопределенное. - конец примечания]

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

Рассказчик - Незеленка Моника
источник
43
Тем не менее, если вы действительно можете заставить компилятор выполнять произвольный код во время компиляции, без какой-либо песочницы, то различным специалистам по безопасности было бы очень интересно узнать об этом. То же самое и с ошибкой компилятора.
Кевин
67
То же, что сказал Кевин. Как разработчик компиляторов C / C ++ / etc в предыдущей карьере, наша позиция заключалась в том, что неопределенное поведение может привести к сбою вашей программы , испортить ваши выходные данные, поджечь ваш дом и т.д. Но компилятор никогда не должен давать сбой, независимо от того, какой ввод. (Это может не выдавать полезных сообщений об ошибках, но должно производить какую-то диагностику и выход, а не просто кричать: «КТУЛХУ ВЗЯТ КОЛЕСО и segfaulting».)
Ti Strga
8
@TiStrga Готов поспорить, из Ктулху получится отличный гонщик Формулы 1.
zeta-band
35
«Если реализация крадет ваши пароли, это не является нарушением какого-либо контракта, изложенного в стандарте». Это правда независимо от того, есть ли в коде UB, не так ли? Стандарт только указывает, что должна делать скомпилированная программа - компилятор, который правильно компилирует код, но крадет ваши пароли в процессе, не будет нарушать стандарт.
Carmeister
8
@Carmeister, ооо, это хороший момент, я обязательно напомню людям об этом всякий раз, когда всплывают аргументы «UB дает компилятору разрешение начать ядерную войну». Еще раз.
ilkkachu
8

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

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

Behaving во время перевода части в ISO C ++ цитаты в @ ответ рассказчика похож на язык , используемый в стандарте ISO C. C не включает шаблоны или constexprобязательный eval во время компиляции.

Но забавный факт : ISO C говорит в примечании, что если перевод прерывается, это должно быть с диагностическим сообщением. Или «вести себя во время перевода ... задокументированным образом». Я не думаю, что «полное игнорирование ситуации» можно толковать как включая прекращение перевода.


Старый ответ, написанный до того, как я узнал о времени перевода UB. Однако это верно для runtime-UB и, следовательно, потенциально все еще полезно.


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

Дефекты в программе, которые делают невозможным даже компиляцию, не являются UB, это синтаксические ошибки. Такая программа является «некорректной» в терминологии C ++ (если я правильно придерживаюсь своего стандарта). Программа может быть правильно сформированной, но содержать UB. Разница между неопределенным поведением и неправильно сформированным, диагностическое сообщение не требуется

Если я чего-то не понимаю, ISO C ++ требует, чтобы эта программа компилировалась и выполнялась правильно, потому что выполнение никогда не достигает деления на ноль. (На практике ( Godbolt ) хорошие компиляторы просто создают рабочие исполняемые файлы. Gcc / clang предупреждает, x / 0но не об этом, даже при оптимизации. Но в любом случае мы пытаемся сказать, насколько низким ISO C ++ допускает качество реализации. Итак, проверяем gcc / clang вряд ли является полезным тестом, кроме как подтвердить, что я правильно написал программу.)

int cause_UB() {
    int x=0;
    return 1 / x;      // UB if ever reached.
 // Note I'm avoiding  x/0  in case that counts as translation time UB.
 // UB still obvious when optimizing across statements, though.
}

int main(){
    if (0)
        cause_UB();
}

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

Можно предположить, что пути выполнения, которые вызывают видимый во время компиляции UB, никогда не выполняются, например, компилятор для x86 может выдать ud2(вызвать исключение недопустимой инструкции) в качестве определения для cause_UB(). Или внутри функции, если одна сторона if()ведет к доказуемому UB, ветвь может быть удалена.

Но компилятор по-прежнему должен правильно и разумно компилировать все остальное . Все пути, которые не встречаются (или не может быть доказано, что встречаются) UB, все равно должны быть скомпилированы в asm, который выполняется, как если бы абстрактная машина C ++ выполняла его.


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

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


Функции, содержащие возможные или доказуемые UB внутри ветвей

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

int minefield(int x) {
    if (x == 3) {
        *(char*)nullptr = x/0;
    }

    return x * 5;
}

Компилятор должен создать asm, который работает для всех, xкроме трех, до тех x * 5пор, пока не вызывает подписанное переполнение UB в INT_MIN и INT_MAX. Если эта функция никогда не вызывается x==3, программа, конечно, не содержит UB и должна работать так, как написано.

С таким же успехом мы могли бы написать if(x == 3) __builtin_unreachable();на GNU C, чтобы сообщить компилятору, что xэто точно не 3.

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

Питер Кордес
источник
3

Что здесь означает «законный»? Все, что не противоречит стандарту C или стандарту C ++, является законным в соответствии с этими стандартами. Если вы выполняете заявление, i = i++;и в результате динозавры захватывают мир, это не противоречит стандартам. Однако это противоречит законам физики, поэтому этого не произойдет :-)

Если поведение undefined приводит к сбою вашего компилятора, это не нарушает стандарты C или C ++. Однако это означает, что качество компилятора может (и, вероятно, должно) быть улучшено.

В предыдущих версиях стандарта C были утверждения, которые были ошибками или не зависели от неопределенного поведения:

char* p = 1 / 0;

Допускается присвоение константе 0 символу *. Допуск ненулевой константы - нет. Поскольку значение 1/0 является неопределенным поведением, это неопределенное поведение, должен или не должен компилятор принимать этот оператор. (В настоящее время 1/0 больше не соответствует определению «целочисленное постоянное выражение»).

скряга729
источник
3
Если быть точным: динозавры, захватывающие мир, не противоречат никаким законам физики (например, вариация Парка Юрского периода). Это очень маловероятно. :)
причудливая