Что означают разные классификации поведения undefined?

Я читал стандарт C11. В соответствии со стандартом C11 undefined поведение классифицируется на четыре разных типа. Номера в скобках относятся к подпункту C-стандарта (C11), который идентифицирует поведение undefined.

Пример 1: Программа пытается изменить строковый литерал (6.4.5). Это поведение undefined классифицируется как: undefined Поведение (требуется информация/подтверждение)

Пример 2: lvalue не определяет объект при оценке (6.3.2.1). Это поведение undefined классифицируется как: Критическое undefined Поведение

Пример 3: Объект имеет сохраненное значение, доступное иначе, чем lvalue допустимого типа (6.5). Это поведение undefined классифицируется как: Ограниченный undefined Поведение

Пример 4. Строка, на которую указывает аргумент mode при вызове функции fopen, точно не соответствует одной из указанных последовательностей символов (7.21.5.3). Это поведение undefined классифицируется как: Возможное соответствие языкового расширения

В чем смысл классификаций? Что передают эти классификации программисту?

Ответы

Ответ 1

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

Кажется, что ключевой идеей здесь является "запись за пределами границ", которая определяется как операция записи, которая изменяет данные, которые иначе не выделяются как часть объекта. Например, если вы случайно забиваете байты существующей переменной, то это не за пределами записи, но если вы прыгнули в произвольную область памяти и украсили ее своим любимым битовым шаблоном, вы должны выполнить за пределами записи.

Конкретное поведение ограничено поведением undefined, если результат undefined, но никогда не будет писать за пределами границ. Другими словами, поведение undefined, но вы не переходите на случайный адрес, не связанный с каким-либо объектом или выделенным пространством, и помещаете туда байты. Поведение является критическим поведением undefined, если вы получаете поведение undefined, которое не может обещать, что он не будет писать вне границ.

Затем в стандарте говорится о том, что может привести к критическому поведению undefined. По умолчанию undefined поведение ограничено undefined поведениями, но есть исключения для UB, которые возникают из-за ошибок памяти, таких как доступ к освобожденной памяти или использование неинициализированного указателя, которые имеют критическое поведение undefined. Помните, однако, что эти классификации существуют и имеют смысл в контексте реализаций C, которые предпочитают конкретно выделять подобные виды поведения. Если ваша среда C не гарантирует его анализ, все поведение undefined может потенциально сделать абсолютно что угодно!

Моя догадка заключается в том, что это предназначено для таких сред, как создание драйверов или плагинов ядра, где вы хотели бы анализировать фрагмент кода и сказать "хорошо, если вы собираетесь снимать кого-то в ногу, будь твоей ногой, что ты стреляешь, а не моя!" Если вы скомпилируете программу C с этими ограничениями, среда выполнения может управлять очень немногими операциями, которые могут быть критическими поведением undefined и иметь эту операцию в ОС, и предположить, что все остальные действия undefined будут не более уничтожить память, которая специально связана с самой программой.

Ответ 2

Все это случаи, когда поведение undefined, то есть стандартное "не требует никаких требований" . Традиционно, в рамках поведения undefined и рассматривая одну реализацию (то есть C-компилятор + C-стандартная библиотека), можно увидеть два вида поведения undefined:

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

Иногда их можно контролировать с помощью компиляторов. Например. пример 1 обычно всегда вызывает плохое поведение - ловушку или сбой или изменяет общую ценность. Более ранние версии GCC позволяли иметь модифицируемые строковые литералы с -fwritable-strings; поэтому, если этот ключ был указан, реализация определяла поведение в этом случае.

C11 добавила необязательную ортогональную классификацию: ограниченное поведение undefined и критическое поведение undefined. Ограниченное поведение undefined - это то, что не выполняет хранилище вне границ, то есть оно не может приводить к тому, что значения записываются в произвольных местах в памяти, Любое поведение undefined, которое не является ограниченным поведением undefined, является критическим поведением undefined.

Iff __STDC_ANALYZABLE__, реализация будет соответствовать приложению окончательный список критического undefined поведения:

  • Объект упоминается вне его срока службы (6.2.4).
  • Хранилище выполняется объекту с двумя несовместимыми объявлениями (6.2.7),
  • Указатель используется для вызова функции, тип которой несовместим со ссылочным типом (6.2.7, 6.3.2.3, 6.5.2.2).
  • Значение lvalue не определяет объект при оценке (6.3.2.1).
  • Программа пытается изменить строковый литерал (6.4.5).
  • Операнд унарного оператора * имеет недопустимое значение (6.5.3.2).
  • Сложение или вычитание указателя на объект массива или целочисленного типа или только за его пределами приводит к результату, который указывает только за объектом массива и используется как операнд унарного * оператор, который оценивается (6.5.6).
  • Делается попытка изменить объект, определенный с использованием типа const, используя значение l с неконстантно-квалифицированный тип (6.7.3).
  • Аргумент функции или макроса, определенного в стандартной библиотеке, имеет недопустимое значение или тип, не ожидаемый функцией с переменным числом аргументов (7.1.4).
  • Функция longjmp вызывается с аргументом jmp_buf, где самый последний вызов макроса setjmp в том же вызове программа с соответствующим аргументом jmp_buf не существует, или вызов был из другого потока выполнения, или функция, содержащая вызов, прекратила выполнение в промежуточный или вызов был в рамках идентификатора с измененный тип и исполнение оставил эту область в промежуточный (7.13.2.1).
  • Используется значение указателя, которое ссылается на пространство, освобожденное вызовом функции free или realloc (7.22.3).
  • Строковая или широкая функция командной строки обращается к массиву за пределами объекта (7.24.1, 7.29.4).

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

Пример 1: модификация строкового литерала также. классифицируется как критическое поведение undefined. Пример 4 - это критическое поведение undefined - значение не ожидается стандартной библиотекой.


Например, 4 стандартных указателя на то, что при поведении undefined в случае режима, который не определен стандартом, существуют реализации, которые могут определять поведение для других флагов. Например, glibc поддерживает еще много флагов режима, например c, e, m и x, и разрешить установку кодирование символа ввода с помощью модификатора ,ccs=charset (и сразу же поместить поток в широкий режим).

Ответ 3

Согласно cppreference:

Критическое поведение undefined

Критический UB - это поведение undefined, которое может выполнять запись в памяти или энергозависимая память, считываемая за пределы любого объекта. Программа, критическое поведение undefined может быть восприимчивым к эксплойтам безопасности.

Критически важны только следующие действия undefined:

  • доступ к объекту за пределами его срока службы (например, через висячий указатель)
  • записать объект, чьи объявления несовместимы
  • вызов функции через указатель функции, тип которого несовместим с типом функции, которую он указывает на
  • выражение lvalue оценивается, но не указывает объект, который пытается изменить строковый литерал
  • разыменовывание недействительного (нулевого, неопределенного и т.д.) или указателя прошлого конца
  • модификация объекта const с помощью указателя не-const
  • вызов стандартной библиотечной функции или макроса с недопустимым аргументом
  • вызов функции переменной вариационной стандартной библиотеки с неожиданным типом аргумента (например, вызов printf с аргументом типа, который не соответствует его спецификатору преобразования)
  • longjmp, где нет setjmp до области вызова, по потокам или из области действия типа VM.
  • любое использование указателя, освобожденного свободным или realloc
  • любая строка или широкая функция библиотеки строк обращается к массиву за пределами границ

Ограниченное поведение undefined

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

Все undefined поведение, не указанное как критическое, ограничено, включая

  • многопоточные расы данных
  • использование неопределенных значений с автоматической продолжительностью хранения
  • строгое нарушение прав на псевдонимы
  • доступ к несортированному объекту
  • подписанное целочисленное переполнение
  • Непоследовательные побочные эффекты изменяют один и тот же скаляр или изменяют и читают один и тот же скаляр
  • переполнение с плавающей точкой в ​​целое число или преобразование конвертера в целое число
  • побитовый сдвиг по отрицательному или слишком большому количеству бит
  • целочисленное деление на ноль
  • использование выражения void
  • прямое назначение или memcpy неточно перекрытых объектов
  • ограничить нарушения
  • и т.д. ВСЕ undefined поведение, которое не входит в критический список.

Ответ 4

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

Рассмотрим функцию (предположим, что "int" - 32 бита):

int32_t triplet_may_be_interesting(int32_t a, int32_t b, int32_t c)
{ 
  return a*b > c;
}

вызывается из контекста:

#define SCALE_FACTOR 123456
int my_array[20000];
int32_t foo(uint16_t x, uint16_t y)
{
  if (x < 20000)
    my_array[x]++;
  if (triplet_may_be_interesting(x, SCALE_FACTOR, y))
    return examine_triplet(x, SCALE_FACTOR, y);
  else
    return 0;
}

Когда C89 был написан, наиболее распространенным способом 32-битного компилятора будет обработать этот код, чтобы выполнить 32-битное умножение, а затем выполнить подписанное сравнение с y. Однако возможны несколько оптимизаций, особенно если компилятор встраивает вызов функции:

  • На платформах, где сравнение без знака быстрее, чем подписывается, компилятор может сделать вывод, что, поскольку ни один из a, b или c не может быть отрицательным, арифметическое значение a*b -negative, и поэтому он может использовать сравнение без знака вместо подписанного сравнения. Эта оптимизация была бы допустимой, даже если __STDC_ANALYZABLE__ отлична от нуля.

  • Компилятор также мог бы сделать вывод, что если x отличное от нуля, арифметическое значение x*123456 будет больше, чем любое возможное значение y, а если x равно нулю, то x*123456 не будет больше, чем любое. Таким образом, он мог бы заменить второе условие if простым if (x). Эта оптимизация также допустима, даже если __STDC_ANALYZABLE__ отличен от нуля.

  • Компилятор, авторы которого либо намерены использовать его только с доверенными данными, либо ошибочно считают, что умность и глупость являются антонимами, можно сделать вывод, что, поскольку любое значение x больше 17395 приведет к переполнению целых чисел, x можно смело предположить, что оно равно 17395 или меньше. Таким образом, он мог бы безоговорочно выполнить my_array[x]++;. Компилятор не может определить __STDC_ANALYZABLE__ с ненулевым значением, если он выполнит эту оптимизацию. Это последний вид оптимизации, для которого предназначен приложение L.. Если реализация может гарантировать, что эффект переполнения будет ограничен, чтобы дать возможно бессмысленное значение, это может быть дешевле и проще для код, чтобы иметь дело с возможным значением, которое не имеет смысла, чем для предотвращения переполнения. Если переполнение могло бы привести к тому, что объекты будут вести себя так, как если бы их значения были искажены будущими вычислениями, то не было бы способа, которым программа могла бы обрабатывать такие вещи, как переполнение после факта, даже в тех случаях, когда результат вычисления будет не имеет значения.

В этом примере, если эффект целочисленного переполнения будет ограничен, чтобы дать возможно бессмысленное значение, и если вызов examine_triplet() излишне будет тратить время, но в противном случае будет безвредным, компилятор может с пользой оптимизировать triplet_may_be_interesting способами, которые были бы невозможны, если бы они были написаны, чтобы избежать цельного переполнения любой ценой. агрессивный "оптимизация", таким образом, приведет к менее эффективному коду, чем это было бы возможно с компилятором, который вместо этого использовал свою свободу, чтобы предложить некоторые свободные поведенческие гарантии.

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

Ответ 5

"Я читал стандарт C11. В соответствии со стандартом C11 undefined поведение классифицируется на четыре разных типа."

Интересно, что вы на самом деле читали. В стандарте ISO C 2011 года не упоминаются эти четыре разные классификации поведения undefined. На самом деле он достаточно ясен, не делая различий между различными типами поведения undefined.

Здесь пункт 4 раздела 2 раздела ISO C11:

Если требование "должно" или "не должно", которое появляется за пределами ограничение или ограничение времени выполнения нарушены, поведение undefined. В этом случае поведение undefined указано иначе Международный стандарт словами "undefined поведение" или отсутствие явного определения поведения. Здесь нет разница в акценте среди этих трех; все они описывают "поведение то есть undefined".

Все примеры, которые вы приводите, - это поведение undefined, которое, что касается Стандарта, означает ничего более или менее:

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

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

Некоторые формулировки в вашем вопросе кажутся похожими на некоторую информацию в C11 Annex L, "Analyzability" (что необязательно для соответствия реализациям C11), но ваш первый пример относится к "undefined Поведение (информация/подтверждение) необходимо)", а слово "подтверждение" нигде не встречается в стандарте ISO C.