Как происходит "переполнение стека" и как его предотвратить?
Как происходит переполнение стека и каковы наилучшие способы убедиться, что этого не происходит, или способы предотвратить это, особенно на веб-серверах, но другие примеры также были бы интересны?
9 ответов
стек
стек, в этом контексте, является последним буфером in, first out, который вы размещаете данные во время выполнения программы. Last in, first out (LIFO) означает, что последнее, что вы кладете, всегда первое, что вы получаете обратно - если вы нажимаете 2 элемента в стеке, "A", а затем "B", то первое, что вы выскочите из стека, будет "B", а следующее - "A".
при вызове функции в коде следующая инструкция после вызова функции сохраняется на стек и любое пространство хранения, которое может быть перезаписано вызовом функции. Вызываемая функция может использовать больше стека для собственных локальных переменных. Когда это сделано, он освобождает пространство стека локальной переменной, которое он использовал, а затем возвращается к предыдущей функции.
переполнение стека
переполнение стека, когда вы использовали больше памяти для стека, чем ваша программа должна использовать. Во встроенных системах может быть только 256 байт для стека, и если каждый функция занимает 32 байта, тогда вы можете иметь только вызовы функций 8 глубоких функций 1 вызывает функцию 2 кто вызывает функцию 3 кто вызывает функцию 4 .... кто вызывает функцию 8, кто вызывает функцию 9, но функция 9 перезаписывает память вне стека. Это может перезаписать память, код и т. д.
многие программисты делают эту ошибку, вызывая функцию A, которая затем вызывает функцию B, которая затем вызывает функцию C, которая затем вызывает функцию A. Это может работать большую часть времени, но только один раз неправильный ввод заставит его идти по этому кругу навсегда, пока компьютер не распознает, что стек раздут.
рекурсивные функции также являются причиной этого, но если вы пишете рекурсивно (т. е. ваша функция вызывает себя), вам нужно знать об этом и использовать статические/глобальные переменные для предотвращения бесконечной рекурсии.
Как правило, ОС и язык программирования, который вы используете, управляют стеком, и это не в ваших руках. Вы должны посмотреть на ваш звонок graph (древовидная структура, которая показывает из вашего main, что вызывает каждая функция), чтобы увидеть, как глубоко ваши вызовы функций идут, и обнаружить циклы и рекурсию, которые не предназначены. Преднамеренные циклы и рекурсия должны быть искусственно проверены на ошибку, если они вызывают друг друга слишком много раз.
помимо хороших методов программирования, статического и динамического тестирования, вы мало что можете сделать в этих системах высокого уровня.
встраиваемых систем
во встроенном мир, особенно в коде высокой надежности (автомобильный, самолет, космос) Вы делаете обширные обзоры кода и проверку, но вы также делаете следующее:
- запретить рекурсию и циклы-принудительно с помощью политики и тестирования
- держите код и стек далеко друг от друга (код во вспышке, стек в ОЗУ, и никогда два не должны встретиться)
- поместите полосы защиты вокруг стека-пустая область памяти, которую вы заполняете магическим числом (обычно прерывание программного обеспечения инструкция, но здесь есть много вариантов), и сотни или тысячи раз в секунду вы смотрите на полосы охраны, чтобы убедиться, что они не были перезаписаны.
- использовать защиту памяти (т. е. не выполнять в стеке, не читать и не писать только вне стека)
- прерывания не вызывают вторичные функции - они устанавливают флаги, копируют данные и позволяют приложению заботиться об их обработке (в противном случае вы можете получить 8 глубоко в дереве вызовов функций, иметь прерывание и затем выйдите еще несколько функций внутри прерывания, вызывая выброс). У вас есть несколько деревьев вызовов - по одному для основных процессов и по одному для каждого прерывания. Если ваши прерывания могут прерывать друг друга... ну, есть драконы...
языки и системы высокого уровня
но в языках высокого уровня, работающих в операционных системах:
- уменьшите локальное хранилище переменных (локальные переменные хранятся в стеке-хотя компиляторы довольно умный об этом и иногда будет помещать больших местных жителей в кучу, если ваше дерево вызовов мелкое)
- избегайте или строго ограничить рекурсию
- не разбивайте свои программы слишком далеко на все меньшие и меньшие функции-даже без подсчета локальных переменных каждый вызов функции потребляет целых 64 байта в стеке (32-битный процессор, сохраняя половину регистров процессора, флаги и т. д.)
- держите дерево вызовов неглубоким (аналогично приведенному выше заявление)
веб-сервера
это зависит от "песочницы" у вас есть, можете ли вы контролировать или даже видеть стек. Скорее всего, вы можете относиться к веб - серверам, как к любому другому языку высокого уровня и операционной системе-это в основном из ваших рук, но проверьте язык и стек серверов, которые вы используете. Это is можно взорвать стек на вашем SQL server, например.
Адам
переполнение стека в реальном коде происходит очень редко. Большинство ситуаций, в которых это происходит, являются рекурсиями, в которых завершение было забыто. Однако это может редко происходить в сильно вложенных структурах, например, особенно больших XML-документах. Единственная реальная помощь здесь-рефакторинг кода для использования явного объекта стека вместо стека вызовов.
бесконечная рекурсия-это простой способ получить ошибку переполнения стека. Чтобы предотвратить - всегда убедитесь, что есть путь выхода, что будет быть хитом. :-)
другой способ получить переполнение стека (по крайней мере, в C/C++) - объявить некоторую огромную переменную в стеке.
char hugeArray[100000000];
этого достаточно.
большинство людей скажут вам, что переполнение стека происходит с рекурсией без пути выхода - хотя в основном это правда, если вы работаете с достаточно большими структурами данных, даже правильный путь выхода рекурсии не поможет вам.
некоторые опции в этом случае:
- поиск в ширину
- хвостовая рекурсия, .Net-specific great блоге (извините, 32-бит .Net)
обычно переполнение стека является результатом бесконечного рекурсивного вызова (учитывая обычный объем памяти в настоящее время для обычных компьютеров).
при вызове метода, функции или процедуры "стандартный" способ или вызов состоит в следующем:
- нажатие направления возврата для вызова в стек (это следующее предложение после вызова)
- обычно пространство для возвращаемого значения резервируется в стек
- нажатие каждого параметра в стек (порядок расходится и зависит от каждого компилятора, также некоторые из них иногда хранятся в регистрах процессора для повышения производительности)
- выполнение фактического вызова.
таким образом, обычно это занимает несколько байтов, уменьшающих количество и тип параметров, а также архитектуру машины.
вы увидите, что если вы начнете делать рекурсивные вызовы, стек начнет расти. Сейчас, стек обычно резервируется в памяти таким образом, что он растет в противоположном направлении к куче, поэтому при большом количестве вызовов без "возврата" стек начинает заполняться.
теперь, в старые времена переполнение стека может произойти просто потому, что вы exausted всю доступную память, просто так. С моделью виртуальной памяти (до 4 ГБ в системе X86), которая была вне области, поэтому обычно, если вы получаете ошибку переполнения стека, ищите бесконечный рекурсивный вызов.
переполнение стека происходит, когда Джефф и Джоэл хотите дать миру лучшее место, чтобы получить ответы на технические вопросы. Слишком поздно предотвращать переполнение стека. Этот "другой сайт" мог бы предотвратить это, не будучи scuzzy. ;)
Помимо формы переполнения стека, которую вы получаете от прямой рекурсии (например,Fibonacci(1000000)
), более тонкой формой этого, которую я испытывал много раз, является косвенная рекурсия, где функция вызывает другую функцию, которая вызывает другую, а затем одна из этих функций снова вызывает первую.
это обычно может происходить в функциях, которые вызываются в ответ на события, но которые сами могут генерировать новые события, например:
void WindowSizeChanged(Size& newsize) {
// override window size to constrain width
newSize.width=200;
ResizeWindow(newSize);
}
в этом случае вызов ResizeWindow
может привести к WindowSizeChanged()
обратный вызов должен быть запущен снова, который вызывает ResizeWindow
снова, пока у вас не закончится стек. В таких ситуациях вам часто нужно отложить ответ на событие до тех пор, пока не вернется кадр стека, например, отправив сообщение.
что? Никто не любит тех, кого окружает бесконечная петля?
do
{
JeffAtwood.WritesCode();
} while(StackOverflow.MakingMadBank.Equals(false));
учитывая, что это было помечено как "взлом", я подозреваю, что" переполнение стека", на которое он ссылается, - это переполнение стека вызовов, а не переполнение стека более высокого уровня, например, те, на которые ссылаются в большинстве других ответов здесь. Он не применяется к любым управляемым или интерпретируемым средам, таким как .NET, Java, Python, Perl, PHP и т. д., в которых обычно записываются веб-приложения, поэтому ваш единственный риск-это сам веб-сервер, который, вероятно, написан на C или c++.
проверить это нить:
https://stackoverflow.com/questions/7308/what-is-a-good-starting-point-for-learning-buffer-overflow