Рассмотрим следующее утверждение:
*((char*)NULL) = 0; //undefined behavior
Он явно вызывает неопределенное поведение. Означает ли существование такого оператора в данной программе, что вся программа не определена или что поведение становится неопределенным только после того, как поток управления попадает в этот оператор?
Будет ли следующая программа четко определена на тот случай, если пользователь никогда не введет номер 3
?
while (true) {
int num = ReadNumberFromConsole();
if (num == 3)
*((char*)NULL) = 0; //undefined behavior
}
Или это полностью неопределенное поведение, независимо от того, что вводит пользователь?
Кроме того, может ли компилятор предположить, что неопределенное поведение никогда не будет выполняться во время выполнения? Это позволило бы рассуждать назад во времени:
int num = ReadNumberFromConsole();
if (num == 3) {
PrintToConsole(num);
*((char*)NULL) = 0; //undefined behavior
}
Здесь компилятор может решить, что в случае, если num == 3
мы всегда будем вызывать неопределенное поведение. Следовательно, этот случай должен быть невозможным, и номер не нужно печатать. Весь if
оператор может быть оптимизирован. Допускается ли этот вид обратных рассуждений согласно стандарту?
const int i = 0; if (i) 5/i;
.PrintToConsole
он не вызывает,std::exit
поэтому он должен выполнить вызов.Ответы:
Ни то, ни другое. Первое условие слишком сильное, а второе слишком слабое.
Доступ к объектам иногда является последовательным, но стандарт описывает поведение программы вне времени. Данвил уже цитировал:
Это можно интерпретировать:
Итак, недостижимый оператор с UB не дает программе UB. Оператор достижимости, который (из-за значений входных данных) никогда не достигается, не дает программе UB. Вот почему ваше первое условие слишком сильное.
Теперь компилятор не может вообще сказать, что есть UB. Таким образом, чтобы позволить оптимизатору переупорядочивать операторы с потенциальным UB, который будет переупорядочен, если их поведение будет определено, необходимо разрешить UB «вернуться назад во времени» и пойти не так до предыдущей точки последовательности (или в C Терминология ++ 11, чтобы UB влиял на вещи, которые упорядочены до вещи UB). Следовательно, ваше второе условие слишком слабое.
Главный пример этого - когда оптимизатор полагается на строгое алиасинг. Весь смысл строгих правил псевдонима состоит в том, чтобы позволить компилятору переупорядочивать операции, которые нельзя было бы правильно переупорядочить, если бы было возможно, что рассматриваемые указатели являются псевдонимами одной и той же памяти. Таким образом, если вы используете незаконные указатели псевдонимов, а UB действительно встречается, то это может легко повлиять на оператор «перед» оператором UB. Что касается абстрактной машины, то оператор UB еще не был выполнен. Что касается фактического объектного кода, он был частично или полностью выполнен. Но стандарт не пытается подробно описать, что значит для оптимизатора переупорядочить операторы, или каковы последствия этого для UB. Он просто дает лицензию на реализацию пойти не так, как только захочет.
Вы можете думать об этом так: «У 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";
Зачем? Поскольку if
p
имеет значение NULL, код все равно имеет UB, поэтому компилятор может предположить, что он не равен NULL, и соответственно оптимизировать. Ядро Linux споткнулось об этом ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) по сути потому, что оно работает в режиме, в котором разыменование нулевого указателя не должно быть UB, ожидается, что это приведет к определенному аппаратному исключению, которое ядро может обработать. Когда оптимизация включена, gcc требует использования-fno-delete-null-pointer-checks
, чтобы предоставить эту нестандартную гарантию.PS Практический ответ на вопрос "когда поражает неопределенное поведение?" это «за 10 минут до того, как вы собирались уехать на день».
источник
void can_add(int x) { if (x + 100 < x) complain(); }
можно оптимизировать прочь полностью, потому что еслиx+100
Безразлично» переполнения ничего не происходит, а еслиx+100
же переполнение, что это UB в соответствии со стандартом, так что ничего не может случиться.3
если она захочет, и упаковать домой на день, как только она увидит один входящий.Стандартные состояния на 1.9 / 4
Интересно, наверное, что означает «содержать». Чуть позже в 1.9 / 5 говорится:
Здесь конкретно упоминается «выполнение ... с этим вводом». Я бы интерпретировал это как неопределенное поведение в одной возможной ветке, которая не выполняется прямо сейчас, не влияет на текущую ветвь выполнения.
Однако другой проблемой являются предположения, основанные на неопределенном поведении во время генерации кода. См. Ответ Стива Джессопа для получения более подробной информации об этом.
источник
Поучительный пример:
int foo(int x) { int a; if (x) return a; return 0; }
И текущий GCC, и текущий Clang оптимизируют это (на x86) для
потому что они выводят, что
x
это всегда ноль из UB вif (x)
пути управления. GCC даже не выдаст предупреждение об использовании неинициализированного значения! (поскольку проход, в котором применяется указанная выше логика, выполняется перед проходом, который генерирует предупреждения о неинициализированном значении)источник
a
даже если во всех обстоятельствах, когда неинициализированныйa
будет передан функции, которая никогда не будет с ней ничего делать)?В текущем рабочем проекте C ++ в версии 1.9.4 говорится, что
Исходя из этого, я бы сказал, что программа, содержащая неопределенное поведение на любом пути выполнения, может делать что угодно в каждый момент своего выполнения.
Есть две действительно хорошие статьи о поведении undefined и о том, что обычно делают компиляторы:
источник
int f(int x) { if (x > 0) return 100/x; else return 100; }
определенно никогда не вызывает неопределенное поведение, даже если100/0
оно, конечно, не определено.printf("Hello, World"); *((char*)NULL) = 0
не гарантируется. Это способствует оптимизации, поскольку компилятор может свободно изменять порядок операций (конечно, с учетом ограничений зависимости), которые, как он знает, в конечном итоге произойдут, без необходимости принимать во внимание неопределенное поведение.int x,y; std::cin >> x >> y; std::cout << (x+y);
можно сказать, что «1 + 1 = 17», только потому, что есть некоторые входные данные, которыеx+y
переполняются (что является UB, посколькуint
это тип со знаком).Слово «поведение» означает, что что-то делается . Государственный деятель, который никогда не исполняется, не является «поведением».
Иллюстрация:
*ptr = 0;
Это неопределенное поведение? Предположим, мы
ptr == nullptr
хотя бы раз за время выполнения программы уверены на 100% . Ответ должен быть положительным.Как насчет этого?
if (ptr) *ptr = 0;
Это не определено? (Помните
ptr == nullptr
хотя бы раз?) Надеюсь, что нет, иначе вы вообще не сможете написать какую-либо полезную программу.Этот ответ не пострадал ни от одного шрандардца.
источник
Неопределенное поведение проявляется, когда программа вызывает неопределенное поведение независимо от того, что происходит дальше. Однако вы привели следующий пример.
int num = ReadNumberFromConsole(); if (num == 3) { PrintToConsole(num); *((char*)NULL) = 0; //undefined behavior }
Если компилятор не знает определения
PrintToConsole
, он не может удалитьif (num == 3)
условное выражение . Предположим, у вас естьLongAndCamelCaseStdio.h
системный заголовок со следующим объявлениемPrintToConsole
.void PrintToConsole(int);
Ничего особенного, хорошо. Теперь давайте посмотрим, насколько злой (или, возможно, не такой злой, неопределенное поведение могло быть хуже) поставщик, проверив фактическое определение этой функции.
int printf(const char *, ...); void exit(int); void PrintToConsole(int num) { printf("%d\n", num); exit(0); }
На самом деле компилятор должен предположить, что любая произвольная функция, которую компилятор не знает, что она делает, может завершиться или вызвать исключение (в случае C ++). Вы можете заметить, что
*((char*)NULL) = 0;
он не будет выполнен, так как выполнение не будет продолжено послеPrintToConsole
вызова.Неопределенное поведение проявляется, когда
PrintToConsole
действительно возвращается. Компилятор ожидает, что этого не произойдет (так как это заставит программу выполнять неопределенное поведение, несмотря ни на что), поэтому может произойти все, что угодно.Однако давайте рассмотрим другое. Допустим, мы выполняем нулевую проверку и используем переменную после нулевой проверки.
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
является энергонезависимым, поэтому он не может волшебным образом изменить свое значение в середине функции (если это так, это неопределенное поведение). Вызовlol_null_check(NULL)
вызовет неопределенное поведение, которое может привести к тому, что переменная не будет назначена (потому что в этот момент известен факт, что программа выполняет неопределенное поведение).Однако неопределенное поведение означает, что программа может делать все, что угодно. Следовательно, ничто не мешает неопределенному поведению вернуться в прошлое и привести к сбою вашей программы до выполнения первой строки
int main()
. Это неопределенное поведение, оно не должно иметь смысла. Он также может дать сбой после ввода 3, но поведение undefined вернется в прошлое и выйдет из строя еще до того, как вы даже наберете 3. И кто знает, возможно, поведение undefined перезапишет вашу системную оперативную память и вызовет сбой вашей системы через две недели, пока ваша неопределенная программа не запущена.источник
PrintToConsole
это моя попытка вставить внешний побочный эффект программы, который виден даже после сбоев и строго упорядочен. Я хотел создать ситуацию, когда мы можем точно сказать, оптимизирован ли этот оператор. Но вы правы в том, что он может никогда не вернуться; Ваш пример записи в глобальный может быть предметом других оптимизаций, не связанных с UB. Например, можно удалить неиспользуемый глобал. У вас есть идея создать внешний побочный эффект таким образом, чтобы гарантированно вернуть контроль?volatile
переменную, может законно инициировать операцию ввода-вывода, которая, в свою очередь, может немедленно прервать текущий поток; обработчик прерывания может убить поток до того, как у него появится возможность выполнить что-либо еще. Я не вижу оправдания, по которому компилятор мог бы продвигать неопределенное поведение до этого момента.Если программа достигает оператора, который вызывает неопределенное поведение, никакие требования не предъявляются ни к одному из результатов / поведения программы; не имеет значения, будут ли они иметь место «до» или «после» вызова неопределенного поведения.
Ваши рассуждения по поводу всех трех фрагментов кода верны. В частности, компилятор может обрабатывать любой оператор, который безоговорочно вызывает неопределенное поведение, как GCC обрабатывает
__builtin_unreachable()
: как подсказку оптимизации, что оператор недоступен (и, следовательно, все пути кода, ведущие к нему безоговорочно, также недоступны). Конечно, возможны и другие подобные оптимизации.источник
__builtin_unreachable()
начали проявляться эффекты, движущиеся вперед и назад во времени? Учитывая что-то вроде этого,extern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }
я мог быbuiltin_unreachable()
счесть полезным сообщить компилятору, что он может пропуститьreturn
инструкцию, но это будет несколько отличаться от того, чтобы сказать, что предыдущий код может быть опущен.__builtin_unreachable
достигнуто. Эта программа определена.restrict
указателя, будут записаны с использованиемunsigned char*
.Многие стандарты для многих вещей затрачивают много усилий на описание того, что реализации ДОЛЖНЫ или НЕ ДОЛЖНЫ делать, используя номенклатуру, аналогичную той, которая определена в IETF RFC 2119 (хотя и не обязательно со ссылкой на определения в этом документе). Во многих случаях описания вещей, которые реализации должны выполнять, за исключением случаев, когда они были бы бесполезны или непрактичны , более важны, чем требования, которым должны соответствовать все соответствующие реализации.
К сожалению, стандарты C и C ++ обычно избегают описаний вещей, которые, хотя и не требуются на 100%, тем не менее следует ожидать от качественных реализаций, не документирующих противоположное поведение. Предложение о том, что реализации должны что-то делать, может рассматриваться как подразумевающее, что те, которые этого не делают, являются неполноценными, и в случаях, когда в целом было бы очевидно, какое поведение было бы полезным или практичным, а не непрактичным и бесполезным для данной реализации, было малая осознанная необходимость в Стандарте вмешиваться в такие суждения.
Умный компилятор мог бы соответствовать Стандарту, исключая любой код, который не имел бы никакого эффекта, за исключением случаев, когда код получает входные данные, которые неизбежно вызывают неопределенное поведение, но «умный» и «тупой» не антонимы. Тот факт, что авторы Стандарта решили, что могут быть некоторые виды реализаций, в которых полезное поведение в данной ситуации было бы бесполезным и непрактичным, не подразумевает никакого суждения о том, следует ли считать такое поведение практичным и полезным для других. Если бы реализация могла поддерживать поведенческую гарантию без каких-либо затрат, кроме потери возможности отсечения «мертвой ветви», почти любое значение, которое пользовательский код мог бы получить от этой гарантии, превысило бы стоимость ее предоставления. Устранение мертвых ветвей может подойти в тех случаях, когда не потребуется отказываться от него., Но если в данной ситуации пользовательского кода мог бы справиться практически любое возможное поведение другого , чем устранение мертвой ветви, любое усилие кода пользователь должен затратить , чтобы избежать UB, скорее всего , превысит значение достигнутого с DBE.
источник
x*y < z
когдаx*y
не происходит переполнения, и в случае переполнения выдает 0 или 1 произвольным образом, но без побочных эффектов, на большинстве платформ нет причин, по которым выполнение второго и третьего требований должно быть дороже, чем соответствует первому, но любой способ написания выражения, гарантирующий стандартное поведение во всех случаях, в некоторых случаях приведет к значительным затратам. Написание выражения(int64_t)x*y < z
может более чем в четыре раза увеличить стоимость вычислений ...(int)((unsigned)x*y) < z
, чтобы компилятор не мог использовать то, что в противном случае могло бы быть полезными алгебраическими заменами (например, если он знает этоx
иz
равны и положительны, он может упростить исходное выражение доy<0
, но версия с использованием беззнакового заставит компилятор выполнить умножение). Если компилятор может гарантировать, даже если Стандарт не требует этого, он будет поддерживать требование «yield 0 или 1 без побочных эффектов», пользовательский код может дать компилятору возможности оптимизации, которые он не мог бы получить иначе.x*y
в случае переполнения будет выдаваться нормальное значение, но вообще любое значение. Мне кажется важным настраиваемый UB на C / C ++.