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

Является ли поведение undefined распределением перекрывающихся структур стека?

Это вопрос спецификации C.

Мы все знаем, что это законный C и должен работать нормально на любой платформе:

/* Stupid way to count the length of a number */
int count_len(int val) {
    char buf[256];
    return sprintf(buf, "%d", val);
}

Но это почти гарантированно сбой:

/* Stupid way to count the length of a number */
int count_len(int val) {
    char buf[256000000];
    return sprintf(buf, "%d", val);
}

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

В соответствии с спецификацией C, является ли последняя программа фактически undefined поведением? Если да, то что отличает его от первого? Если нет, то что в спецификации C говорит, что это нормально для аварийной реализации?

(Если это отличается от C89/C99/C11/С++ *, это тоже было бы интересно).

4b9b3361

Ответ 1

Языковые стандарты для C (89, 99, 11) начните с раздела области с этой формулировкой (также можно найти в некоторых стандартах С++, С#, Fortran и Pascal):

В этом международном стандарте не указывается

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

Компилятор gcc предлагает возможность проверки во время выполнения

21.1 Проверка

Для большинства операционных систем gcc не выполняет проверку по умолчанию. Это означает, что если основная задача среды или какая-либо другая задача превышает доступное пространство стека, произойдет непредсказуемое поведение. Большинство собственных систем предлагают некоторую степень защиты, добавляя страницу защиты в конце каждого стека задач. Обычно этого механизма недостаточно для правильной работы с ситуациями, поскольку большая локальная переменная может "прыгать" над защитной страницей. Кроме того, когда пострадает страница защиты, в стеке не может быть места для выполнения кода распространения исключений. Включение проверки стека позволяет избежать таких ситуаций. Чтобы активировать проверку стека, скомпилируйте все блоки с помощью опции gcc -fstack-check. Например:

gcc -c -fstack-check package1.adb

Единицы, скомпилированные с этой опцией, генерируют дополнительные инструкции для проверки того, что любое использование стека (для вызовов процедур или для объявления локальных переменных в блоках объявления) не превышает доступное пространство стека. Если пробел превышен, возникает исключение Storage_Error.

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

Обоснование было

Определение соответствия всегда было проблемой с C Стандарт, описываемый одним автором как "даже не резиновые зубы, больше как резиновые резины". Хотя в C9X есть улучшения по сравнению с C89, многие проблемы остаются.

В этом документе предлагаются изменения, которые, хотя и не идеальны, надеемся улучшить ситуация.

Для включения в раздел 5.2.4.1 была предложена следующая формулировка:

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

    5.2.4.1. Реализация всегда позволяет утверждать, что данная программа слишком большой или слишком сложный для перевода или исполнения. Однако, чтобы остановить это способ заявить о соответствии, не предоставляя никаких полезных возможностей что разработчик должен показать, предоставляет способ определить, программа, вероятно, превысит пределы. Метод не должен быть совершенным, поэтому пока он ошибается на стороне осторожности. Один из способов сделать это - иметь формулу, которая преобразует значения, такие как как количество переменных в, скажем, объем памяти, компилятор понадобиться. Точно так же, если существует ограничение на пространство стека, нужна формула только показать, как определить требования к стеку для каждого вызова функции (предполагая, что это единственное место, куда выделяется стек) и не нужно работать через все возможные пути выполнения (что было бы невозможно в лицо рекурсии). У компилятора может быть даже режим, который выводит значение для каждой функции в программе.

Предложенная формулировка не попала в стандарт C99, и поэтому эта область оставалась вне рамок стандарта. В разделе 5.2.4.1 из C99 перечислены эти ограничения

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

  • 127 уровней вложенности блоков
  • 63 уровней вложенности условного включения
  • 12 деклараторов указателей, массивов и функций (в любых комбинациях), изменяющих арифметику, структуру, объединение или неполный тип в объявлении
  • 63 уровней вложенности объявлений в скобках в полном объявлении
  • 63 уровней вложенности выражений в скобках в полном выражении
  • 63 важных начальных символа во внутреннем идентификаторе или имени макроса (каждое универсальное имя символа или расширенный символ источника считается единственным символом)
  • 31 значимых начальных символов во внешнем идентификаторе (каждое универсальное имя символа, определяющее короткий идентификатор 0000FFFF или менее, считается 6 символами, каждое универсальное имя символа, определяющее короткий идентификатор 00010000 или более, считается 10 символами, а каждый расширенный источник символ считается тем же числом символов, что и соответствующее универсальное имя символа, если оно есть)
  • 4095 внешних идентификаторов в одной единице перевода
  • 511 идентификаторов с объемом блока, объявленным в одном блоке
  • 4095 макроидентификаторов, определенных одновременно в одном блоке перевода препроцессора
  • 127 параметров в одном определении функции
  • 127 аргументов в одном вызове функции
  • 127 параметров в одном определении макроса
  • 127 аргументов в одном вызове макроса
  • 4095 символов в логической строке источника
  • 4095 символов в символьной строке литерала или широкоформатного литерала (после конкатенации)
  • 65535 байт в объекте (только в размещенной среде)
  • 15 уровней вложенности для # включенных файлов
  • 1023 ярлыки case для оператора switch (за исключением тех, которые относятся к любым вложенным операторам switch)
  • 1023 члена в одной структуре или объединении
  • 1023 констант перечисления в одном перечислении
  • 63 уровня вложенных структур или определений объединений в одном списке struct-declaration

Ответ 2

В С++ приложение B указывает, что максимальный размер объекта является конечным числом, специфичным для реализации. Это будет иметь тенденцию ограничивать массивы с помощью класса автоматического хранения.

Тем не менее, я не вижу что-то специально для пространства, накопленного всеми автоматическими переменными в стеке вызовов, в котором должно возникать переполнение стека. Я также не вижу предела рекурсии в Приложении B, хотя это будет тесно связано.

Ответ 3

В стандарте C не говорится обо всех проблемах, связанных с переполнением стека. Это немного странно, поскольку он очень вокал практически в любом другом углу программирования на C. AFAIK нет спецификации того, что должно быть доступно определенное количество автоматического хранилища, а также нет способа обнаружения или восстановления из исчерпания пространства, доступного для автоматического хранения. Предполагается, что абстрактная машина имеет неограниченное количество автоматического хранения.

Ответ 4

Я считаю, что поведение undefined бездействия - если локальный объект размером 250 000 000 байт фактически превышает пропускную способность реализации.

Цитата из стандарта ISO C 2011 года, раздел 1 (Область применения), пункт 2:

В этом международном стандарте не указывается [...]
- размер или сложность программы и ее данных, которые превышают емкость любого конкретной системы обработки данных или емкости конкретного процессора.

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

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

Поскольку ничто в стандарте не определяет поведение такой программы, поведение undefined. Это указано в разделе 4 (Соответствие), пункт 2:

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

Конечно, реализация на большинстве современных компьютеров могла бы легко выделить 250 миллионов байт памяти; например, только небольшая часть доступной ОЗУ на компьютере, на которую я печатаю это, например. Но многие операционные системы устанавливают довольно низкое ограничение на количество пространства стека, которое может выделить программа.

(Кстати, я предполагаю, что код в вопросе является фрагментом какой-то полной программы, которая фактически вызывает функцию. Как бы то ни было, код не имеет никакого поведения, так как нет вызова count_len, а также нет там есть функция main. Обычно я не могу сказать об этом, но вы использовали тег "language-lawyer".)

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