Может ли переполнение стека привести к чему-то другому, кроме ошибки сегментации?

В скомпилированной программе (скажем, C или C++, но я предполагаю, что этот вопрос может распространяться на любой не-VM-ish язык со стеком вызовов) - очень часто при переполнении стека возникает ошибка сегментации:

Переполнение стека является [a] причиной, результатом является ошибка сегментации.

Это всегда так? Может ли переполнение стека привести к другим видам поведения программы/ОС?

Я также спрашиваю о не-Linux, не-ОС Windows и аппаратном обеспечении, отличном от X86. (Конечно, если у вас нет поддержки аппаратной памяти или поддержки ОС (например, MS-DOS), тогда нет такой вещи, как ошибка сегментации, я спрашиваю о случаях, когда вы можете получить ошибку сегментации, но что-то еще происходит).

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

Ответ 1

Да, даже на стандартной ОС (Linux) и стандартном оборудовании (x86).

void f(void) {
    char arr[BIG_NUMBER];
    arr[0] = 0; // stack overflow
}

Обратите внимание, что на x86 стек растет, поэтому мы назначаем начало массива для запуска переполнения. Обычные заявления об отказе применяются... точное поведение зависит от большего количества факторов, чем обсуждается в этом ответе, включая сведения о вашем компиляторе C.

Если BIG_NUMBER едва ли достаточно большой, чтобы переполнить, вы столкнетесь с защитой стека и получите ошибку сегментации. Это то, для чего существует защита стека, и она может быть такой же маленькой, как и одна страница 4 KiB (но не меньше, и этот размер 4 KiB используется до Linux 4.12), или он может быть больше (1 MiB по умолчанию для Linux 4.12, см. мм: большой защитный зазор в стеке), но он всегда имеет определенный размер.

Если BIG_NUMBER достаточно велик, переполнение может пропустить над защитой стека и приземлиться на какой-либо другой фрагмент памяти, что потенциально возможно. Это может привести к неправильной работе вашей программы, но не к сбою, что в основном относится к наихудшему сценарию: мы хотим, чтобы наши программы выходили из строя, когда они были неправильными, а не делали что-то непреднамеренное.

Ответ 2

Одна вещь - это то, что происходит во время выполнения, когда вы переполняете стек, что может быть много. В том числе, но не ограничивается; ошибка сегментации, переписывание переменных, следующих за тем, что вы переполняете, вызывая незаконную инструкцию, ничего вообще и многое другое. "Старая" классическая статья Smashing The Stack For Fun and Profit описывает множество способов "весело" с этим материалом.

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

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

Что должен знать каждый программист C о неопределенном поведении № 1/3

Что должен знать каждый программист C о неопределенном поведении № 2/3

Что должен знать каждый программист C о неопределенном поведении № 3/3

Руководство по неопределенному поведению в C и C++, часть 1

Руководство по неопределенному поведению в C и C++, часть 2

Руководство по неопределенному поведению в C и C++, часть 3

Ответ 3

Другие ответы довольно хорошо охватывали ПК. Я затрону некоторые проблемы во встроенном мире.

Встроенный код имеет нечто похожее на segfault. Код хранится в каком-то энергонезависимом хранилище (как правило, в эти дни, но в некотором роде ROM или PROM). Для этого требуется специальные операции по его настройке; нормальный доступ к памяти может считываться из него, но не записывать на него. Кроме того, встроенные процессоры обычно имеют большие разрывы в своих картах памяти. Если процессор получает запрос на запись для памяти, который доступен только для чтения, или если он получает запрос на чтение или запись для адреса, который физически не существует, процессор обычно выдает аппаратное исключение. Если у вас подключен отладчик, вы можете проверить состояние системы, чтобы найти, что пошло не так, как с дампом ядра.

Там нет гарантии, что это произойдет для. Стек можно разместить в любом месте в ОЗУ, и это обычно будет рядом с другими переменными. Результатом, как правило, является повреждение этих переменных.

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

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

Встроенный код имеет общий шаблон, чтобы справиться с этим, то есть "водяным знаком" стека, инициализируя каждый байт до известного значения. Иногда компилятор может это сделать; или иногда вам может потребоваться реализовать его самостоятельно в стартовом коде перед main(). Вы можете оглянуться назад с конца стека, чтобы найти, где он больше не установлен для этого значения, и в этот момент вы знаете отметку с высоким уровнем воды для использования стека; или, если все это неверно, вы знаете, что у вас переполнение. Общепринятым (и хорошей практикой) для встроенных приложений является постоянное голосование в качестве фоновой операции и возможность сообщать об этом в диагностических целях.

Предоставив возможность отслеживать использование стека, большинство компаний установит приемлемую наихудшую маржу, чтобы избежать переполнения. Обычно это где-то от 75% до 90%, но всегда будет запас. Это не только позволяет предположить, что есть худший худший случай, который вы еще не видели, но также облегчает жизнь для будущего развития, когда нужно добавить новый код, который использует больше стека.

Ответ 4

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