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

Можно ли считать ветки с ведением undefined недостижимыми и оптимизированными как мертвый код?

Рассмотрим следующее утверждение:

*((char*)NULL) = 0; //undefined behavior

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

Будет ли следующая программа хорошо определена, если пользователь никогда не войдет в число 3?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

Или это полностью undefined поведение независимо от того, что пользователь вводит?

Кроме того, может ли компилятор предположить, что поведение undefined никогда не будет выполняться во время выполнения? Это позволило бы рассуждать назад во времени:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Здесь компилятор может сообразить, что в случае num == 3 мы всегда будем вызывать поведение undefined. Поэтому этот случай должен быть невозможным, и его не нужно печатать. Весь оператор if может быть оптимизирован. Является ли такое обратное рассуждение допустимым в соответствии со стандартом?

4b9b3361

Ответ 1

Существует ли существование такого утверждения в данной программе, что вся программа undefined или что поведение только становится undefinedкак только поток управления попадает в это утверждение?

Ни. Первое условие слишком сильное, а второе слишком слабое.

Доступ к объектам иногда упорядочивается, но стандарт описывает поведение программы за пределами времени. Данвилл уже цитировал:

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

Это можно интерпретировать следующим образом:

Если выполнение программы дает поведение undefined, то вся программа имеет undefined.

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

Теперь компилятор вообще не может сказать, что такое UB. Таким образом, чтобы оптимизатор мог переупорядочить операторы с потенциальным UB, которые были бы повторно упорядочены, если бы их поведение было определено, необходимо, чтобы UB "возвращался вовремя" и ошибался до предыдущей точки последовательности (или в С++ 11, поскольку UB влияет на вещи, которые были упорядочены до UB). Поэтому ваше второе условие слишком слабое.

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

Вы можете думать об этом так: "UB имеет машину времени".

В частности, чтобы ответить на ваши примеры:

  • Поведение - это только undefined, если прочитано 3.
  • Компиляторы могут и могут удалить код как мертвый, если базовый блок содержит определенную операцию undefined. Они разрешены (и я предполагаю, что делают) в случаях, которые не являются базовым блоком, но где все ветки ведут к UB. Этот пример не является кандидатом, если PrintToConsole(3) как-то точно не вернется. Это может вызвать исключение или что-то еще.

Аналогичным примером для второго является опция gcc -fdelete-null-pointer-checks, которая может принимать такой код (я не проверил этот конкретный пример, рассмотрим его как иллюстрацию общей идеи):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

и измените его на:

*p = 3;
std::cout << "3\n";

Почему? Потому что, если p имеет значение null, тогда код имеет UB в любом случае, поэтому компилятор может предположить, что он не является нулевым и оптимизирован соответствующим образом. Ядро linux сработало над этим (https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897) по существу потому, что он работает в режиме, когда разыменование нулевого указателя не должно быть UB, ожидается, что это приведет к определенному аппаратным исключением, которое может обрабатывать ядро. Когда оптимизация включена, gcc требует использования -fno-delete-null-pointer-checks, чтобы обеспечить эту гарантию нестандартного размера.

P.S. Практический ответ на вопрос "когда наступает поведение undefined?" "за 10 минут до того, как вы планируете выехать на день".

Ответ 2

Стандартные состояния при 1.9/4

[Примечание: Настоящий международный стандарт не устанавливает никаких требований к поведение программ, содержащих поведение undefined. - конечная нота]

Интересным моментом является, вероятно, то, что означает "содержать". Чуть позже в 1.9/5 говорится:

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

Здесь он конкретно упоминает "исполнение... с этим вводом". Я бы интерпретировал это как поведение undefined в одной возможной ветки, которая не выполняется прямо сейчас, не влияет на текущую ветвь выполнения.

Другая проблема - это предположения, основанные на поведении undefined во время генерации кода. См. Ответ Стива Джессопа для получения дополнительной информации об этом.

Ответ 3

Поучительный пример:

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

Оба текущих GCC и текущий Clang оптимизируют это (на x86) до

xorl %eax,%eax
ret

поскольку они выводят, что x всегда равен нулю от UB в пути управления if (x). GCC даже не даст вам предупреждение о неинициализированном значении! (потому что проход, который применяет вышеприведенную логику, выполняется перед проходом, который генерирует предупреждения неинициализированного значения)

Ответ 4

В текущем рабочем проекте С++ в 1.9.4 говорится, что

В этом Международном стандарте нет требований к поведению программ, которые содержат неопределенное поведение.

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

В undefined есть два действительно хороших статьи и что обычно делают компиляторы:

Ответ 5

Слово "поведение" означает, что что-то делается. Статут, который никогда не выполняется, не является "поведением".

Иллюстрация:

*ptr = 0;

Это поведение undefined? Предположим, что мы на 100% уверены ptr == nullptr хотя бы один раз во время выполнения программы. Ответ должен быть да.

Как насчет этого?

 if (ptr) *ptr = 0;

Это undefined? (Помните ptr == nullptr хотя бы один раз?) Надеюсь, что нет, иначе вы вообще не сможете написать какую-либо полезную программу.

Никакой срандарзе не был нанесен вред при принятии этого ответа.

Ответ 6

Если программа достигает оператора, вызывающего поведение undefined, никаких требований не предъявляется ни к какой выходу/поведению программы; неважно, будут ли они выполняться "до" или "после" undefined поведение.

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

Ответ 7

Поведение undefined срабатывает, когда программа вызывает поведение undefined, независимо от того, что произойдет дальше. Однако вы привели следующий пример.

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Если компилятор не знает определения PrintToConsole, он не может удалить if (num == 3) условный. Предположим, что у вас есть системный заголовок LongAndCamelCaseStdio.h со следующим объявлением PrintToConsole.

void PrintToConsole(int);

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

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

Компилятор фактически должен предположить, что любая произвольная функция, которую компилятор не знает, что она делает, может выйти или выбросить исключение (в случае С++). Вы можете заметить, что *((char*)NULL) = 0; не будет выполняться, так как выполнение не будет продолжаться после вызова PrintToConsole.

Поведение undefined срабатывает, когда PrintToConsole действительно возвращается. Компилятор ожидает, что этого не произойдет (так как это приведет к тому, что программа выполнит поведение undefined независимо от того, что), поэтому все может случиться.

Однако рассмотрим еще кое-что. Пусть говорят, что мы выполняем нулевую проверку и используем переменную после нулевой проверки.

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

В этом случае легко заметить, что для lol_null_check требуется указатель не-NULL. Присвоение глобальной энергонезависимой переменной warning не является чем-то, что может выйти из программы или выбросить любое исключение. pointer также является энергонезависимым, поэтому он не может волшебным образом изменить свое значение в середине функции (если это так, это поведение undefined). Вызов lol_null_check(NULL) приведет к поведению undefined, которое может привести к тому, что переменная не будет назначена (поскольку на данный момент известно, что программа выполняет поведение undefined).

Однако поведение undefined означает, что программа может что-то сделать. Поэтому ничто не останавливает поведение undefined от времени назад, а сбой вашей программы перед первой строкой int main() выполняется. Это поведение undefined, это не имеет смысла. Он также может потерпеть крах после ввода 3, но поведение undefined вернется вовремя и сработает до того, как вы даже наберете 3. И кто знает, возможно, поведение undefined перезапишет вашу оперативную память и приведет к сбою вашей системы 2 недели спустя, пока ваша программа undefined не работает.

Ответ 8

Многие стандарты для многих видов вещей прикладывают большие усилия для описания вещей, реализация которых СЛЕДУЕТ или НЕ ДОЛЖНА делать, используя номенклатуру, подобную той, которая определена в IETF RFC 2119 (хотя и не обязательно ссылаясь на определения в этом документе). Во многих случаях описания вещей, которые должны выполняться, за исключением случаев, когда они были бы бесполезны или непрактичны, важнее требований, которым должны соответствовать все соответствующие реализации.

К сожалению, стандарты C и С++, как правило, избегают описания вещей, которые, хотя и не требуются на 100%, должны, тем не менее, ожидаться от реализаций качества, которые не документируют противоположное поведение. Предложение о том, что реализации должно делать что-то, можно рассматривать как подразумевающее, что те, которые не являются низшими, и в тех случаях, когда в целом было бы очевидно, какое поведение было бы полезным или практичным, по сравнению с непрактичным и бесполезным, при данной реализации малозначительная потребность в Стандарте вмешиваться в такие суждения.

Умный компилятор может соответствовать стандарту, исключая любой код, который не будет иметь никакого эффекта, кроме случаев, когда код получает входные данные, которые неизбежно вызывают поведение Undefined, но "умные" и "немые" не являются антонимами. Тот факт, что авторы Стандарта приняли решение о том, что могут быть некоторые виды реализаций, где добродетельное поведение в данной ситуации было бы бесполезным и нецелесообразным, не подразумевает какого-либо суждения относительно того, следует ли рассматривать такое поведение как практическое и полезное для других. Если бы реализация могла отстаивать поведенческую гарантию без каких-либо затрат, помимо потери возможности отсечения "мертвой ветки", почти любой ценный код пользователя мог бы получить от этой гарантии превышение стоимости ее предоставления. Уничтожение мертвых ветвей может быть прекрасным в тех случаях, когда ему не нужно отказываться от чего-либо, но если в данной ситуации пользовательский код мог бы обрабатывать практически любое возможное поведение, отличное от исключения веткистой ветки, любой пользовательский код с усилием должен был бы расходовать избежать UB, вероятно, превысит значение, достигнутое от DBE.