В компьютерном программировании , неопределенное поведение ( UB ) является результатом выполнения программы , чье поведение предписано быть непредсказуемым, в спецификации языка , к которому компьютер код прилипает. Это отличается от неуказанного поведения , для которого спецификация языка не предписывает результат, и поведения, определяемого реализацией, которое подчиняется документации другого компонента платформы (например, ABI или документации переводчика ).
В сообществе C неопределенное поведение можно с юмором назвать « носовыми демонами » после сообщения comp.std.c, в котором поведение undefined объясняется как разрешение компилятору делать все, что он захочет, даже «заставить демонов вылетать из вашего носа» ". [1]
Обзор
Некоторые языки программирования позволяют программе работать иначе или даже иметь другой поток управления, чем исходный код , если он демонстрирует те же видимые для пользователя побочные эффекты , если неопределенное поведение никогда не происходит во время выполнения программы . Неопределенное поведение - это название списка условий, которым программа не должна соответствовать.
В ранних версиях C основным преимуществом неопределенного поведения было создание производительных компиляторов для самых разных машин: конкретная конструкция могла быть сопоставлена с машинно-зависимой функцией, и компилятору не приходилось генерировать дополнительный код для среды выполнения. чтобы адаптировать побочные эффекты к семантике, налагаемой языком. Исходный код программы был написан с предварительным знанием конкретного компилятора и платформ, которые он будет поддерживать.
Однако прогрессивная стандартизация платформ сделала это меньшим преимуществом, особенно в новых версиях C. Теперь случаи неопределенного поведения обычно представляют собой недвусмысленные ошибки в коде, например индексацию массива за пределами его границ. По определению среда выполнения может предполагать, что неопределенное поведение никогда не происходит; поэтому нет необходимости проверять некоторые недопустимые условия. Для компилятора это также означает, что различные преобразования программы становятся действительными или упрощаются доказательства их правильности; это допускает различные виды преждевременной оптимизации и микрооптимизации , которые приводят к некорректному поведению, если состояние программы соответствует любому из таких условий. Компилятор также может удалить явные проверки, которые могли быть в исходном коде, без уведомления программиста; например, обнаружение неопределенного поведения путем проверки того, произошло ли оно, по определению не гарантирует работы. Это затрудняет или делает невозможным программирование переносимого отказоустойчивого варианта (для некоторых конструкций возможны непереносимые решения).
Текущая разработка компилятора обычно оценивает и сравнивает производительность компилятора с тестами, разработанными для микрооптимизации, даже на платформах, которые в основном используются на рынке настольных компьютеров и портативных компьютеров общего назначения (например, amd64). Следовательно, неопределенное поведение предоставляет достаточно возможностей для улучшения производительности компилятора, поскольку исходный код для конкретного оператора исходного кода может быть сопоставлен с чем угодно во время выполнения.
Для C и C ++ компилятору разрешено давать диагностику времени компиляции в этих случаях, но это не обязательно: реализация будет считаться правильной, независимо от того, что она делает в таких случаях, аналогично терминам безразличия в цифровой логике. . Программист несет ответственность за написание кода, который никогда не вызывает неопределенное поведение, хотя реализациям компилятора разрешено выдавать диагностику, когда это происходит. В настоящее время компиляторы имеют флаги, которые включают такую диагностику, например, -fsanitize
включает «дезинфицирующее средство неопределенного поведения» ( UBSan ) в gcc 4.9 [2] и в clang . Однако этот флаг не установлен по умолчанию, и его включение - это выбор того, кто собирает код.
При некоторых обстоятельствах могут быть определенные ограничения на неопределенное поведение. Например, спецификации набора команд ЦП могут оставить поведение некоторых форм инструкций неопределенным, но если ЦП поддерживает защиту памяти, то спецификация, вероятно, будет включать общее правило, в котором говорится, что никакая доступная пользователю инструкция не может вызвать дыру в безопасность операционной системы ; поэтому фактическому процессору будет разрешено повреждать регистры пользователя в ответ на такую инструкцию, но ему не разрешено, например, переключиться в режим супервизора .
Выполнения платформа может также обеспечить некоторые ограничения или гарантию на неопределенном поведении, если набор инструментов или выполнения явно документ , что конкретные конструкции находятся в исходном коде отображается на конкретные четко определенные механизмы , доступных во время выполнения. Например, интерпретатор может задокументировать определенное поведение для некоторых операций, которые не определены в спецификации языка, в то время как другие интерпретаторы или компиляторы для того же языка не могут. Компилятор создает исполняемый код для определенного ABI , заполняя семантический разрыв способов , которые зависят от версии компилятора: документация для этой версии компилятора и спецификации ABI может обеспечить ограничения на неопределенном поведении. Опираясь на эти детали реализации делает программное обеспечение не- портативный , но переносимость не может быть проблемой , если программное обеспечение не должно использоваться за пределами конкретного выполнения.
Неопределенное поведение может привести к сбою программы или даже к сбоям, которые труднее обнаружить и заставить программу выглядеть нормально работающей, например к потере данных без вывода сообщений и выдаче неверных результатов.
Преимущества
Документирование операции как неопределенного поведения позволяет компиляторам предполагать, что эта операция никогда не произойдет в соответствующей программе. Это дает компилятору больше информации о коде, и эта информация может открыть больше возможностей для оптимизации.
Пример для языка C:
int foo ( unsigned char x ) { значение int = 2147483600 ; / * предполагается 32-битное int и 8-битное char * / value + = x ; if ( значение < 2147483600 ) bar (); возвращаемое значение ; }
Значение x
не может быть отрицательным, и, учитывая, что знаковое целочисленное переполнение является неопределенным поведением в C, компилятор может предположить, что оно value < 2147483600
всегда будет ложным. Таким образом, if
оператор, включая вызов функции bar
, может игнорироваться компилятором, поскольку тестовое выражение в if
не имеет побочных эффектов, и его условие никогда не будет выполнено. Таким образом, код семантически эквивалентен:
int foo ( unsigned char x ) { значение int = 2147483600 ; значение + = x ; возвращаемое значение ; }
Если бы компилятор был вынужден предположить, что знаковое целочисленное переполнение имеет циклическое поведение, то приведенное выше преобразование не было бы законным.
Людям становится трудно заметить такие оптимизации, когда код более сложен и имеют место другие оптимизации, такие как встраивание . Например, другая функция может вызывать указанную выше функцию:
void run_tasks ( беззнаковый символ * ptrx ) { int z ; z = foo ( * ptrx ); в то время как ( * ptrx > 60 ) { run_one_task ( ptrx , z ); } }
Компилятор может оптимизировать while
-loop здесь, применив анализ диапазона значений : проверяя foo()
, он знает, что начальное значение, на которое указывает, ptrx
не может превышать 47 (поскольку любое другое значение вызовет неопределенное поведение в foo()
), поэтому начальная проверка *ptrx > 60
будет всегда быть ложным в соответствующей программе. Двигаясь дальше, поскольку результат z
теперь никогда не используется и foo()
не имеет побочных эффектов, компилятор может оптимизировать, run_tasks()
чтобы он был пустой функцией, которая немедленно возвращается. Исчезновение while
-loop может быть особенно неожиданным, если foo()
оно определено в отдельно скомпилированном объектном файле .
Еще одно преимущество от разрешения неопределенного целочисленного переполнения со знаком заключается в том, что это дает возможность хранить и управлять значением переменной в регистре процессора , которое больше размера переменной в исходном коде. Например, если тип переменной, как указано в исходном коде, уже, чем ширина собственного регистра (например, " int " на 64-разрядной машине, распространенный сценарий), то компилятор может безопасно использовать подписанный 64-разрядный битовое целое число для переменной в машинном коде, который он производит, без изменения определенного поведения кода. Если программа зависела от поведения 32-битного целочисленного переполнения, то компилятор должен был бы вставить дополнительную логику при компиляции для 64-битной машины, потому что поведение переполнения большинства машинных инструкций зависит от ширины регистра. [3]
Неопределенное поведение также позволяет проводить больше проверок во время компиляции как компиляторами, так и статическим анализом программы . [ необходима цитата ]
Риски
Стандарты C и C ++ имеют несколько форм неопределенного поведения, которые обеспечивают большую свободу в реализации компилятора и проверок во время компиляции за счет неопределенного поведения во время выполнения, если таковое имеется. В частности, в стандарте ISO для C есть приложение, в котором перечислены общие источники неопределенного поведения. [4] Более того, компиляторы не обязаны диагностировать код, основанный на неопределенном поведении. Следовательно, программисты, даже опытные, часто полагаются на неопределенное поведение либо по ошибке, либо просто потому, что они плохо разбираются в правилах языка, который может охватывать сотни страниц. Это может привести к ошибкам, которые обнаруживаются при использовании другого компилятора или других настроек. Тестирование или фаззинг с включенными динамическими проверками неопределенного поведения, например, дезинфицирующими средствами Clang , может помочь выявить неопределенное поведение, не диагностируемое компилятором или статическими анализаторами. [5]
Неопределенное поведение может привести к уязвимостям безопасности в программном обеспечении. Например, переполнение буфера и другие уязвимости безопасности в основных веб-браузерах происходят из-за неопределенного поведения. Год 2038 проблема является еще одним примером благодаря подписанному целочисленного переполнения . Когда разработчики GCC изменили свой компилятор в 2008 году так, что он пропустил определенные проверки переполнения, основанные на неопределенном поведении, CERT выдал предупреждение против более новых версий компилятора. [6] Linux Weekly News указали, что такое же поведение наблюдалось в PathScale C , Microsoft Visual C ++ 2005 и некоторых других компиляторах; [7] предупреждение было позже изменено, чтобы предупреждать о различных компиляторах. [8]
Примеры на C и C ++
Основные формы неопределенного поведения в C можно в целом классифицировать как: [9] нарушения безопасности пространственной памяти, нарушения безопасности временной памяти, целочисленное переполнение , строгие нарушения псевдонимов, нарушения выравнивания, неупорядоченные модификации, гонки данных и циклы, которые не выполняют операции ввода / вывода О, ни прекращать.
В C использование любой автоматической переменной до ее инициализации приводит к неопределенному поведению, так же как и целочисленное деление на ноль , целочисленное переполнение со знаком, индексирование массива за пределами его определенных границ (см. Переполнение буфера ) или разыменование нулевого указателя . В общем, любой экземпляр неопределенного поведения оставляет абстрактную исполнительную машину в неизвестном состоянии и приводит к неопределенному поведению всей программы.
Попытка изменить строковый литерал вызывает неопределенное поведение: [10]
char * p = "википедия" ; // корректный C, не рекомендуется в C ++ 98 / C ++ 03, плохо сформированный в C ++ 11 p [ 0 ] = 'W' ; // неопределенное поведение
Целочисленное деление на ноль приводит к неопределенному поведению: [11]
int x = 1 ; вернуть x / 0 ; // неопределенное поведение
Некоторые операции с указателями могут привести к неопределенному поведению: [12]
int arr [ 4 ] = { 0 , 1 , 2 , 3 }; int * p = arr + 5 ; // неопределенное поведение для индексации за пределами p = 0 ; int a = * p ; // неопределенное поведение при разыменовании нулевого указателя
В C и C ++ реляционное сравнение указателей на объекты (для сравнения « меньше или больше») строго определено, только если указатели указывают на элементы одного и того же объекта или элементы одного и того же массива . [13] Пример:
Int Основной ( недействительными ) { INT = 0 ; int b = 0 ; return & a < & b ; / * неопределенное поведение * / }
Достижение конца функции, возвращающей значение (кроме main()
) без оператора return, приводит к неопределенному поведению, если значение вызова функции используется вызывающей стороной: [14]
int f () { } / * неопределенное поведение, если используется значение вызова функции * /
Изменение объекта между двумя точками следования более одного раза приводит к неопределенному поведению. [15] В C ++ 11 произошли значительные изменения в причинах неопределенного поведения по отношению к точкам следования. [16] Однако следующий пример приведет к неопределенному поведению как в C ++, так и в C.
я = я ++ + 1 ; // неопределенное поведение
При изменении объекта между двумя точками последовательности считывание значения объекта для любой другой цели, кроме определения значения для сохранения, также является неопределенным поведением. [17]
а [ я ] = я ++ ; // неопределенное поведение printf ( "% d% d \ n " , ++ n , power ( 2 , n )); // также неопределенное поведение
В C / C ++ побитовое смещение значения на количество битов, которое является либо отрицательным числом, либо больше или равно общему количеству битов в этом значении, приводит к неопределенному поведению. Самый безопасный способ (независимо от производителя компилятора) - всегда держать количество бит для сдвига (правый операнд <<
и >>
побитовые операторы ) в диапазоне: < > (где - левый операнд).0, sizeof(value)*CHAR_BIT - 1
value
int num = -1 ; беззнаковый int val = 1 << число ; // сдвиг на отрицательное число - неопределенное поведениечисло = 32 ; // или любое другое число больше 31 val = 1 << num ; // литерал '1' вводится как 32-битное целое число - в этом случае сдвиг более чем на 31 бит является неопределенным поведениемчисло = 64 ; // или любое другое число больше 63 unsigned long long val2 = 1ULL << num ; // литерал '1ULL' вводится как 64-битное целое число - в этом случае сдвиг более чем на 63 бита является неопределенным поведением
Смотрите также
- Компилятор
- Остановиться и загореться
- Неустановленное поведение
Рекомендации
- ^ "носовые демоны" . Файл жаргона . Проверено 12 июня 2014 .
- ^ Дезинфицирующее средство для неопределенного поведения GCC - ubsan
- ^ https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759de5a7#file-gistfile1-txt-L166
- ^ ISO / IEC 9899: 2011 §J.2.
- ^ Джон Регер. «Неопределенное поведение в 2017 году, cppcon 2017» .
- ^ «Примечание об уязвимости VU # 162289 - gcc автоматически отменяет некоторые циклические проверки» . База данных заметок об уязвимостях . CERT. 4 апреля 2008 года Архивировано из оригинала 9 апреля 2008 года.
- ^ Джонатан Корбет (16 апреля 2008 г.). «Переполнение GCC и указателя» . Еженедельные новости Linux .
- ^ «Примечание об уязвимости VU # 162289 - компиляторы C могут без уведомления отменить некоторые проверки цикла» . База данных заметок об уязвимостях . CERT. 8 октября 2008 г. [4 апреля 2008 г.].
- ^ Паскаль Куок и Джон Регер (4 июля 2017 г.). «Неопределенное поведение в 2017 году, встроено в академический блог» .
- ^ ISO / IEC (2003). ISO / IEC 14882: 2003 (E): Языки программирования - C ++ §2.13.4 Строковые литералы [lex.string] параграф. 2
- ^ ISO / IEC (2003). ISO / IEC 14882: 2003 (E): Языки программирования - C ++ §5.6 Мультипликативные операторы [expr.mul] para. 4
- ^ ISO / IEC (2003). ISO / IEC 14882: 2003 (E): Языки программирования - C ++ §5.7 Аддитивные операторы [expr.add] para. 5
- ^ ISO / IEC (2003). ISO / IEC 14882: 2003 (E): Языки программирования - C ++ §5.9 Операторы отношения [expr.rel] параграф. 2
- ^ ISO / IEC (2007). ISO / IEC 9899: 2007 (E): Языки программирования - C §6.9 Внешние определения, параграф. 1
- ^ ANSI X3.159-1989 язык программирования C , сноска 26
- ^ «Порядок оценки - cppreference.com» . en.cppreference.com . Проверено 9 августа 2016.
- ^ ISO / IEC (1999). ISO / IEC 9899: 1999 (E): Языки программирования - C §6.5 Выражения, п. 2
дальнейшее чтение
- Питер ван дер Линден , эксперт программирования C . ISBN 0-13-177429-8
- UB Canaries (апрель 2015 г.), Джон Регер (Университет Юты, США)
- Неопределенное поведение в 2017 г. (июль 2017 г.) Паскаль Куок (TrustInSoft, Франция) и Джон Регер (Университет Юты, США)
Внешние ссылки
- Исправленная версия стандарта C99 . Посмотрите раздел 6.10.6 для #pragma