stringtranslate.com

Неопределенное поведение

В компьютерном программировании неопределенное поведение ( UB ) является результатом выполнения программы, поведение которой предписано быть непредсказуемым в спецификации языка , которой соответствует компьютерный код . Это отличается от неопределенного поведения , для которого спецификация языка не предписывает результат, и поведения, определяемого реализацией, которое зависит от документации другого компонента платформы ( например, ABI или документации переводчика ).

В сообществе программистов на языке неопределенное поведение можно с юмором назвать « носовыми демонами » после публикации на comp.std.c , в которой неопределенное поведение объясняется как возможность компилятору делать все, что он пожелает, даже «заставлять демонов вылетать из вашего компьютера». нос". [1]

Обзор

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

В ранних версиях C основным преимуществом неопределенного поведения было создание производительных компиляторов для широкого спектра машин: конкретная конструкция могла быть сопоставлена ​​с специфичной для машины функцией, и компилятору не приходилось генерировать дополнительный код для среды выполнения. адаптировать побочные эффекты в соответствии с семантикой, заданной языком. Исходный код программы был написан с предварительным знанием конкретного компилятора и платформ , которые он будет поддерживать.

Однако прогрессивная стандартизация платформ сделала это преимущество меньше, особенно в новых версиях C. Теперь случаи неопределенного поведения обычно представляют собой однозначные ошибки в коде, например, индексацию массива за его пределами. По определению, среда выполнения может предполагать, что неопределенное поведение никогда не происходит; поэтому некоторые недопустимые условия не нуждаются в проверке. Для компилятора это также означает, что различные преобразования программы становятся действительными или доказательства их корректности упрощаются; это позволяет проводить различные виды оптимизации, корректность которых зависит от предположения, что состояние программы никогда не удовлетворяет никакому такому условию. Компилятор также может удалять явные проверки, которые могли быть в исходном коде, не уведомляя программиста; например, обнаружение неопределенного поведения путем проверки того, произошло ли оно, по определению не гарантирует работу. Это затрудняет или делает невозможным программирование портативной отказоустойчивой опции (для некоторых конструкций возможны непереносимые решения).

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

Для C и C++ в этих случаях компилятору разрешено выдавать диагностику во время компиляции, но это не обязательно: реализация будет считаться правильной, что бы она ни делала в таких случаях, аналогично терминам безразличия в цифровой логике. . Программист несет ответственность за написание кода, который никогда не вызывает неопределенное поведение, хотя реализации компилятора могут выдавать диагностику, когда это происходит. В настоящее время компиляторы имеют флаги, которые включают такую ​​диагностику, например, -fsanitize=undefinedвключает «дезинфицирующее средство неопределенного поведения» (UBSan) в gcc 4.9 [2] и в clang . Однако этот флаг не установлен по умолчанию, и его включение — выбор человека, создающего код.

При некоторых обстоятельствах могут быть определенные ограничения на неопределенное поведение. Например, в спецификациях набора команд ЦП поведение некоторых форм инструкций может оставаться неопределенным, но если ЦП поддерживает защиту памяти , тогда спецификация, вероятно, будет включать общее правило, гласящее, что ни одна доступная пользователю инструкция не может вызвать дыру в памяти. безопасность операционной системы ; таким образом, реальному ЦП будет разрешено повредить пользовательские регистры в ответ на такую ​​инструкцию, но ему не будет разрешено, например, переключиться в режим супервизора .

Платформа среды выполнения также может предоставлять некоторые ограничения или гарантии неопределенного поведения, если цепочка инструментов или среда выполнения явно документируют, что определенные конструкции, найденные в исходном коде , сопоставляются с конкретными четко определенными механизмами, доступными во время выполнения. Например, интерпретатор может документировать определенное поведение для некоторых операций, которые не определены в спецификации языка, в то время как другие интерпретаторы или компиляторы для того же языка не могут этого сделать. Компилятор создает исполняемый код для определенного ABI , заполняя семантический пробел способами, которые зависят от версии компилятора: документация для этой версии компилятора и спецификация ABI могут предоставлять ограничения на неопределенное поведение . Использование этих деталей реализации делает программное обеспечение непереносимым , но переносимость может не вызывать беспокойства, если программное обеспечение не предполагается использовать за пределами определенной среды выполнения.

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

Преимущества

Документирование операции как неопределенного поведения позволяет компиляторам предполагать, что эта операция никогда не произойдет в соответствующей программе. Это дает компилятору больше информации о коде, и эта информация может привести к увеличению возможностей оптимизации.

Пример для языка C:

int foo ( unsigned char x ) { int value = 2147483600 ; /* предполагая, что 32-битное целое число и 8-битное char */ value += x ; if ( значение < 2147483600 ) бар (); возвращаемое значение ; }                  

Значение xне может быть отрицательным, и, учитывая, что переполнение целого числа со знаком является неопределенным поведением в C, компилятор может предположить, что оно value < 2147483600всегда будет ложным. Таким образом if, оператор, включая вызов функции bar, может быть проигнорирован компилятором, поскольку тестовое выражение в файле ifне имеет побочных эффектов и его условие никогда не будет выполнено. Таким образом, код семантически эквивалентен:

int foo ( беззнаковый символ x ) { int value = 2147483600 ; значение += х ; возвращаемое значение ; }            

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

Людям становится трудно обнаружить такие оптимизации, когда код становится более сложным и имеют место другие оптимизации, такие как встраивание . Например, другая функция может вызвать указанную выше функцию:

void run_tasks ( unsigned char * ptrx ) { int z ; z = foo ( * ptrx ); while ( * ptrx > 60 ) { run_one_task ( ptrx , z ); } }                 

Компилятор здесь может оптимизировать цикл, whileприменяя анализ диапазона значений : проверяя foo(), он знает, что начальное значение, на которое указывает символ, ptrxне может превышать 47 (поскольку любое большее значение вызовет неопределенное поведение в foo()); следовательно, в соответствующей программе первоначальная проверка *ptrx > 60всегда будет ложной. Идя дальше, поскольку результат zтеперь никогда не используется и foo()не имеет побочных эффектов, компилятор может оптимизировать его run_tasks()до пустой функции, которая немедленно возвращает значение. Исчезновение -loop whileможет быть особенно неожиданным, если foo()он определен в отдельно скомпилированном объектном файле .

Еще одним преимуществом возможности неопределенного переполнения целочисленного числа со знаком является то, что это позволяет хранить и манипулировать значением переменной в регистре процессора , которое больше, чем размер переменной в исходном коде. Например, если тип переменной, указанный в исходном коде, уже ширины собственного регистра (например, intна 64-битной машине, распространенный сценарий), то компилятор может безопасно использовать 64-битное целое число со знаком для переменную в машинном коде, которую он создает, без изменения определенного поведения кода. Если бы программа зависела от поведения переполнения 32-битного целого числа, то компилятору пришлось бы вставлять дополнительную логику при компиляции для 64-битной машины, поскольку поведение переполнения большинства машинных инструкций зависит от ширины регистра. [3]

Неопределенное поведение также позволяет выполнять дополнительные проверки во время компиляции как компиляторами, так и статическим анализом программы . [ нужна цитата ]

Риски

Стандарты C и C++ имеют несколько форм неопределенного поведения, которые обеспечивают большую свободу в реализации компилятора и проверках во время компиляции за счет неопределенного поведения во время выполнения, если оно присутствует. В частности, стандарт ISO для C имеет приложение, в котором перечислены распространенные источники неопределенного поведения. [4] Более того, компиляторы не обязаны диагностировать код, который использует неопределенное поведение. Следовательно, программисты, даже опытные, часто полагаются на неопределенное поведение либо по ошибке, либо просто потому, что они плохо разбираются в правилах языка, который может занимать сотни страниц. Это может привести к ошибкам, которые проявляются при использовании другого компилятора или других настроек. Тестирование или фаззинг с включенными динамическими проверками неопределенного поведения, например дезинфицирующими средствами Clang , может помочь обнаружить неопределенное поведение, не диагностируемое компилятором или статическими анализаторами. [5]

Неопределенное поведение может привести к уязвимостям безопасности в программном обеспечении. Например, переполнение буфера и другие уязвимости безопасности в основных веб-браузерах возникают из-за неопределенного поведения. Когда в 2008 году разработчики GCC изменили свой компилятор таким образом, что он пропустил некоторые проверки переполнения, основанные на неопределенном поведении, 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]

интервал х = 1 ; вернуть х / 0 ; // неопределенное поведение       

Определенные операции с указателями могут привести к неопределенному поведению: [12]

int arr [ 4 ] = { 0 , 1 , 2 , 3 }; int * p = arr + 5 ; // неопределенное поведение при индексации за пределами границ p = NULL ; интервал а = * п ; // неопределенное поведение при разыменовании нулевого указателя                  

В C и C++ реляционное сравнение указателей на объекты (для сравнения «меньше» или «больше») строго определено только в том случае, если указатели указывают на члены одного и того же объекта или элементы одного и того же массива . [13] Пример:

int main ( void ) { int a = 0 ; интервал б = 0 ; возврат & а < & б ; /* неопределенное поведение */ }              

Достижение конца функции, возвращающей значение (кроме main()), без оператора return приводит к неопределенному поведению, если значение вызова функции используется вызывающей стороной: [14]

int f () { } /* неопределенное поведение, если используется значение вызова функции*/  

Изменение объекта между двумя точками последовательности более одного раза приводит к неопределенному поведению. [15] Начиная с C++11, произошли значительные изменения в причинах неопределенного поведения в отношении точек последовательности. [16] Современные компиляторы могут выдавать предупреждения, когда сталкиваются с несколькими неупорядоченными модификациями одного и того же объекта. [17] [18] Следующий пример приведет к неопределенному поведению как в C, так и в C++.

int f ( int я ) { return я ++ + я ++ ; /* неопределенное поведение: две неупорядоченные модификации i */ }        

При изменении объекта между двумя точками последовательности чтение значения объекта для любой другой цели, кроме определения сохраняемого значения, также является неопределенным поведением. [19]

а [ я ] = я ++ ; // неопределенное поведение printf ( "%d %d \n " , ++ n , power ( 2 , n )); // также неопределенное поведение       

В C/C++ побитовый сдвиг значения на число битов, которое либо является отрицательным числом, либо больше или равно общему количеству битов в этом значении, приводит к неопределенному поведению. Самый безопасный способ (независимо от производителя компилятора) — всегда сохранять количество смещаемых битов (правый операнд побитовых операторов<< и ) в пределах диапазона: [ ] (где находится левый операнд).>> 0, sizeof value * CHAR_BIT - 1value

интервал число = -1 ; беззнаковое целое значение = 1 << число ; // сдвиг на отрицательное число - неопределенное поведение          число = 32 ; // или любое другое число больше 31 val = 1 << num ; // литерал '1' вводится как 32-битное целое число - в этом случае сдвиг более чем на 31 бит является неопределенным поведением        число = 64 ; // или любое другое число больше 63 unsigned long long val2 = 1ULL << num ; // литерал '1ULL' вводится как 64-битное целое число - в этом случае сдвиг более чем на 63 бита является неопределенным поведением           

Примеры в Rust

Хотя неопределенное поведение никогда не присутствует в безопасном Rust , в небезопасном Rust можно вызвать неопределенное поведение разными способами. [20] Например, создание недопустимой ссылки (ссылки, которая не ссылается на допустимое значение) вызывает немедленное неопределенное поведение:

fn  main () { // Следующая строка вызывает немедленное неопределенное поведение. let _null_reference : & i32 = unsafe { std :: mem :: zeroed () }; }         

Обратите внимание, что нет необходимости использовать ссылку; неопределенное поведение вызывается просто при создании такой ссылки.

Смотрите также

Рекомендации

  1. ^ "Носовые демоны". Файл жаргона . Проверено 12 июня 2014 г.
  2. ^ Дезинфицирующее средство для неопределенного поведения GCC - ubsan
  3. ^ «Немного информации о компиляторах, использующих знаковое переполнение» .
  4. ^ ISO/IEC 9899:2011 §J.2.
  5. ^ Джон Регер. «Неопределенное поведение в 2017 году, cppcon 2017». YouTube .
  6. ^ «Примечание об уязвимости VU#162289 — gcc молча отменяет некоторые циклические проверки» . База данных заметок об уязвимостях . СЕРТ. 4 апреля 2008 г. Архивировано из оригинала 9 апреля 2008 г.
  7. Джонатан Корбет (16 апреля 2008 г.). «GCC и переполнение указателя». Еженедельные новости Linux .
  8. ^ «Примечание об уязвимости VU#162289 — компиляторы C могут молча отбрасывать некоторые циклические проверки» . База данных заметок об уязвимостях . СЕРТ. 8 октября 2008 г. [4 апреля 2008 г.].
  9. Паскаль Куок и Джон Регер (4 июля 2017 г.). «Неопределенное поведение в 2017 году, встроенное в академический блог».
  10. ^ ИСО / МЭК (2003). ISO/IEC 14882:2003(E): Языки программирования – C++ §2.13.4 Строковые литералы [lex.string] параграф. 2
  11. ^ ИСО / МЭК (2003). ISO/IEC 14882:2003(E): Языки программирования – C++ §5.6 Мультипликативные операторы [expr.mul], параграф. 4
  12. ^ ИСО / МЭК (2003). ISO/IEC 14882:2003(E): Языки программирования – C++ §5.7 Аддитивные операторы [expr.add], параграф. 5
  13. ^ ИСО / МЭК (2003). ISO/IEC 14882:2003(E): Языки программирования – C++ §5.9 Операторы отношения [expr.rel], параграф. 2
  14. ^ ИСО / МЭК (2007). ISO/IEC 9899:2007(E): Языки программирования – C §6.9 Внешние определения, параграф. 1
  15. ^ ANSI X3.159-1989 Язык программирования C , сноска 26
  16. ^ «Порядок оценки — cppreference.com» . ru.cppreference.com . Проверено 9 августа 2016 г.
  17. ^ «Параметры предупреждения (с использованием коллекции компиляторов GNU (GCC))» . GCC, Коллекция компиляторов GNU — Проект GNU — Фонд свободного программного обеспечения (FSF) . Проверено 9 июля 2021 г.
  18. ^ «Диагностические флаги в Clang» . Документация Clang 13 . Проверено 9 июля 2021 г.
  19. ^ ИСО / МЭК (1999). ISO/IEC 9899:1999(E): Языки программирования – C §6.5 Выражения, параграф. 2
  20. ^ «Поведение считается неопределенным» . Справочник по ржавчине . Проверено 28 ноября 2022 г.

дальнейшее чтение

Внешние ссылки