Ошибка переполнения стека может приводить к

Стек (или магазин) — структура данных в программировании, работающая по принципу магазина с патронами: последний помещеннный в него объект, обрабатывается первым.

Приработе со стекам часто приходится сталкиваться с двумя типичными ошибками: переполненем стека и опустошением стека.

Переполнение стека (stack overflow) — одна из типичных ошибок при работе со стеком, состоящая в попытке добавить в стек элемент, когда память, отведенная для хранения стека полностью занята.

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

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

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

Опустошение стека (stack underflow) — другая типичная ошибка при работе со стеком, состоящая в попытке извлечь значение пустого стека.

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

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

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

Чтобы избегать ошибок при работе со стеком нужно следовать двум правилам.

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

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

Описанные правила безопасности приводят к тому, что программный код оказывается перегружен проверками. По этой причине более эффективным методом является уведомление об ошибках в работе со стеком при помощи механизма исключений или прерываний (например, конструкция  try … throw в языке С++).

Дополнительно в базе данных Генона: 

  • Что такое стек в программировании?
  • Для чего используются указатели в программировании?

Ссылки по теме:

  • cyberforum.ru — реализация стека на С++ с использованием исключений 
  • ru.wikipedia.org — Википедия: Переполнение буфера
  • codenet.ru — рассматривается уязвимость Windows за счет использования переполнения стека  
  • sdteam.com — статья «Переполнения стека»
  • xakep.ru — статья «Переполнение буфера в стеке», Хакер, №2, 2003

From Wikipedia, the free encyclopedia

In software, a stack overflow occurs if the call stack pointer exceeds the stack bound. The call stack may consist of a limited amount of address space, often determined at the start of the program. The size of the call stack depends on many factors, including the programming language, machine architecture, multi-threading, and amount of available memory. When a program attempts to use more space than is available on the call stack (that is, when it attempts to access memory beyond the call stack’s bounds, which is essentially a buffer overflow), the stack is said to overflow, typically resulting in a program crash.[1]

Causes[edit]

Infinite recursion[edit]

The most-common cause of stack overflow is excessively deep or infinite recursion, in which a function calls itself so many times that the space needed to store the variables and information associated with each call is more than can fit on the stack.[2]

An example of infinite recursion in C.

int foo() 
{
     return foo();
}

The function foo, when it is invoked, continues to invoke itself, allocating additional space on the stack each time, until the stack overflows resulting in a segmentation fault.[2] However, some compilers implement tail-call optimization, allowing infinite recursion of a specific sort—tail recursion—to occur without stack overflow. This works because tail-recursion calls do not take up additional stack space.[3]

Some C compiler options will effectively enable tail-call optimization; for example, compiling the above simple program using gcc with -O1 will result in a segmentation fault, but not when using -O2 or -O3, since these optimization levels imply the -foptimize-sibling-calls compiler option.[4] Other languages, such as Scheme, require all implementations to include tail-recursion as part of the language standard.[5]

Very deep recursion[edit]

A recursive function that terminates in theory but causes a call stack buffer overflow in practice can be fixed by transforming the recursion into a loop and storing the function arguments in an explicit stack (rather than the implicit use of the call stack). This is always possible because the class of primitive recursive functions is equivalent to the class of LOOP computable functions. Consider this example in C++-like pseudocode:

void function (argument) 
{
  if (condition)
    function (argument.next);

}
stack.push(argument);
while (!stack.empty())
{
  argument = stack.pop();
  if (condition)
    stack.push(argument.next);
}

A primitive recursive function like the one on the left side can always be transformed into a loop like on the right side.

A function like the example above on the left would not be a problem in an environment supporting tail-call optimization; however, it is still possible to create a recursive function that may result in a stack overflow in these languages. Consider the example below of two simple integer exponentiation functions.

int pow(int base, int exp) {
    if (exp > 0)
        return base * pow(base, exp - 1);
    else
        return 1;
}
int pow(int base, int exp) {
    return pow_accum(base, exp, 1);
}

int pow_accum(int base, int exp, int accum) {
    if (exp > 0)
        return pow_accum(base, exp - 1, accum * base);
    else
        return accum;
}

Both pow(base, exp) functions above compute an equivalent result, however, the one on the left is prone to causing a stack overflow because tail-call optimization is not possible for this function. During execution, the stack for these functions will look like this:

pow(5, 4)
5 * pow(5, 3)
5 * (5 * pow(5, 2))
5 * (5 * (5 * pow(5, 1)))
5 * (5 * (5 * (5 * pow(5, 0))))
5 * (5 * (5 * (5 * 1)))
625
pow(5, 4)
pow_accum(5, 4, 1)
pow_accum(5, 3, 5)
pow_accum(5, 2, 25)
pow_accum(5, 1, 125)
pow_accum(5, 0, 625)
625

Notice that the function on the left must store in its stack exp number of integers, which will be multiplied when the recursion terminates and the function returns 1. In contrast, the function at the right must only store 3 integers at any time, and computes an intermediary result which is passed to its following invocation. As no other information outside of the current function invocation must be stored, a tail-recursion optimizer can «drop» the prior stack frames, eliminating the possibility of a stack overflow.

Very large stack variables[edit]

The other major cause of a stack overflow results from an attempt to allocate more memory on the stack than will fit, for example by creating local array variables that are too large. For this reason some authors recommend that arrays larger than a few kilobytes should be allocated dynamically instead of as a local variable.[6]

An example of a very large stack variable in C:

int foo() 
{
     double x[1048576];
}

On a C implementation with 8 byte double-precision floats, the declared array consumes 8 megabytes of data; if this is more memory than is available on the stack (as set by thread creation parameters or operating system limits), a stack overflow will occur.

Stack overflows are made worse by anything that reduces the effective stack size of a given program. For example, the same program being run without multiple threads might work fine, but as soon as multi-threading is enabled the program will crash. This is because most programs with threads have less stack space per thread than a program with no threading support. Because kernels are generally multi-threaded, people new to kernel development are usually discouraged from using recursive algorithms or large stack buffers.[7]

See also[edit]

  • Buffer overflow
  • Call stack
  • Heap overflow
  • Stack buffer overflow
  • Double fault

References[edit]

  1. ^ Burley, James Craig (1991-06-01). «Using and Porting GNU Fortran». Archived from the original on 2012-02-06.
  2. ^ a b What is the difference between a segmentation fault and a stack overflow? Archived 2021-09-13 at the Wayback Machine at Stack Overflow
  3. ^ «An Introduction to Scheme and its Implementation». 1997-02-19. Archived from the original on 2007-08-10.
  4. ^ «Using the GNU Compiler Collection (GCC): Optimize Options». Archived from the original on 2017-08-20. Retrieved 2017-08-20.
  5. ^ Richard Kelsey; William Clinger; Jonathan Rees; et al. (August 1998). «Revised5 Report on the Algorithmic Language Scheme». Higher-Order and Symbolic Computation. 11 (1): 7–105. doi:10.1023/A:1010051815785. S2CID 14069423. Archived from the original on 2007-01-05. Retrieved 2012-08-09.
  6. ^ Feldman, Howard (2005-11-23). «Modern Memory Management, Part 2». Archived from the original on 2012-09-20. Retrieved 2007-08-14.
  7. ^ «Kernel Programming Guide: Performance and Stability Tips». Apple Inc. 2014-05-02. Archived from the original on 2014-05-03. Retrieved 2014-05-02.

External links[edit]

  • The reasons why 64-bit programs require more stack memory Archived 2017 November 4 at the Wayback Machine

Переполнение стека

  • Определение
  • Стек программы
  • Последствия ошибки
  • Причины ошибки
  • Примеры
  • Итог
  • Библиографический список

Определение

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

Стек программы

Стек программы — это специальная области памяти, организованная по принципу очереди LIFO (Last in, first out — последним пришел, первым ушел). Название «стек» произошло из-за аналогии принципа его построения со стопкой (англ. stack) тарелок — можно класть тарелки друг на друга (метод добавления в стек, «заталкивание», «push»), а затем забирать их, начиная с верхней (метод получения значения из стека, «выталкивание», «pop»). Стек программы также называют стек вызовов, стек выполнения, машинным стеком (чтобы не путать его со «стеком» — абстрактной структурой данных).

Для чего нужен стек? Он позволяет удобно организовать вызов подпрограмм. При вызове функция получает некоторые аргументы; также она должна где-то хранить свои локальные переменные. Кроме того, надо учесть, что одна функция может вызвать другую функцию, которой тоже надо передавать параметры и хранить свои переменные. Используя стек, при передаче параметров нужно просто положить их в стек, тогда вызываемая функция сможет их оттуда «вытолкнуть» и использовать. Локальные переменные тоже можно хранить там же — в начале своего кода функция выделяет часть памяти стека, при возврате управления — очищает и освобождает. Программисты на высокоуровневых языках обычно не задумываются о таких вещах — весь необходимый рутинный код за них генерирует компилятор.

Последствия ошибки

Теперь мы подошли почти вплотную к проблеме. В абстрактном виде стек представляет собой бесконечное хранилище, в которое можно бесконечно добавлять новые элементы. К сожалению, в нашем мире все конечно — и память под стек не исключение. Что будет, если она закончится, когда в стек заталкиваются аргументы функции? Или функция выделяет память под свои переменные?

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

Чтобы быть совсем точным, следует отметить, что подобное описание событий верно лишь для компиляторов, компилирующих в «родной» (native) код. В управляемых языках у виртуальной машины есть свой стек для управляемых программ, за состоянием которого гораздо проще следить, и можно даже позволить себе при возникновении переполнения передать программе исключение. В языках Си и Си++ на подобную «роскошь» рассчитывать не приходится.

Причины ошибки

Что же может привести к такой неприятной ситуации? Исходя из описанного выше механизма, один из вариантов — слишком большое число вложенных вызовов функций. Особенно вероятен такой вариант развития событий при использовании рекурсии. Бесконечная рекурсия (при отсутствии механизма «ленивых» вычислений) прерывается именно таким образом, в отличие от бесконечного цикла, который иногда имеет полезное применение. Впрочем, при небольшом объеме памяти, отведенной под стек (что, например, характерно для микроконтроллеров), достаточно может быть и простой последовательности вызовов.

Другой вариант — локальные переменные, требующие большого количества памяти. Заводить локальный массив из миллиона элементов, или миллион локальных переменных (мало ли что бывает) — не самая лучшая идея. Даже один вызов такой «жадной» функции легко может вызвать переполнение стека. Для получения больших объемов данных лучше воспользоваться механизмами динамической памяти, которая позволит обработать ошибку её нехватки.

Однако динамическая память является довольно медленной в плане выделения и освобождения (поскольку этим занимается операционная система), кроме того, при прямом доступе приходится вручную выделять её и освобождать. Память же в стеке выделяется очень быстро (по сути, надо лишь изменить значение одного регистра), кроме того, у объектов, выделенных в стеке, автоматически вызываются деструкторы при возврате управления функцией и очистке стека. Разумеется, тут же возникает желание получить память из стека. Поэтому третий путь к переполнению — самостоятельное выделение в стеке памяти программистом. Специально для этой цели библиотека языка Си предоставляет функцию alloca. Интересно заметить, что если у функции для выделения динамической памяти malloc есть свой «близнец» для её освобождения free, то у функции alloca его нет — память освобождается автоматически после возврата управления функцией. Возможно, это только осложняет ситуацию — ведь до выхода из функции освободить память не получится. Даже несмотря на то, что согласно man-странице «функция alloca зависит от машины и компилятора; во многих системах ее реализация проблематична и содержит много ошибок; ее использование очень несерьезно и не одобряется» — она все равно используется.

Примеры

В качестве примера рассмотрим код для рекурсивного поиска файлов, расположенный на MSDN:

void DirSearch(String* sDir)
 {
     try
     {
         // Find the subfolders in the folder that is passed in.
         String* d[] = Directory::GetDirectories(sDir);
         int numDirs = d->get_Length();
         
         for (int i=0; i < numDirs; i++)
         {
             // Find all the files in the subfolder.
             String* f[] = Directory::GetFiles(d[i],textBox1->Text);
             int numFiles = f->get_Length();

             for (int j=0; j < numFiles; j++)
             {
                 listBox1->Items->Add(f[j]);
             }
             DirSearch(d[i]);
         }
     }
     catch (System::Exception* e)
     {
         MessageBox::Show(e->Message);
     }
 }

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

Пример второго подхода, взятый из вопроса «Почему происходит переполнение стека?» с сайта под названием Stack Overflow (сайт является сборником вопросов и ответов на любые программистские темы, а не только по переполнению стека, как может показаться):

#define W 1000
#define H 1000
#define MAX 100000 
//...
int main()
{
    int image[W*H];
    float dtr[W*H];
    initImg(image,dtr);
    return 0;
}

Как видно, в функции main выделяется память в стеке под массивы типов int и float по миллиону элементов каждый, что в сумме дает чуть менее 8 мегабайт. Если учесть, что по умолчанию Visual C++ резервирует под стек лишь 1 мегабайт, то ответ становится очевидным.

А вот пример, взятый из GitHub-репозитория проекта Flash-плеера Lightspark:

DefineSoundTag::DefineSoundTag(/* ... */)
{
    // ...
    unsigned int soundDataLength = h.getLength()-7;
    unsigned char *tmp = (unsigned char *)alloca(soundDataLength);
    // ...
}

Можно надеятся, что h.getLength()-7 не будет слишком большим числом, чтобы на следующей строчке не произошло переполнения. Но стоит ли сэкономленное на выделении памяти время «потенциального» вылета программы?

Итог

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

Библиографический список

  • Э. Таненбаум. Архитектура компьютера.
  • Wikipedia. Stack Overflow.
  • man 3 alloca.
  • MSDN. How to recursively search folders by using Visual C++.
  • Stack Overflow. Stack Overflow C++.
  • GitHub. Lightspark — «tags.cpp».

Присылаем лучшие статьи раз в месяц

To describe this, first let us understand how local variables and objects are stored.

Local variable are stored on the stack:

Enter image description here

If you looked at the image you should be able to understand how things are working.

When a function call is invoked by a Java application, a stack frame is allocated on the call stack. The stack frame contains the parameters of the invoked method, its local parameters, and the return address of the method. The return address denotes the execution point from which, the program execution shall continue after the invoked method returns. If there is no space for a new stack frame then, the StackOverflowError is thrown by the Java Virtual Machine (JVM).

The most common case that can possibly exhaust a Java application’s stack is recursion. In recursion, a method invokes itself during its execution. Recursion is considered as a powerful general-purpose programming technique, but it must be used with caution, to avoid StackOverflowError.

An example of throwing a StackOverflowError is shown below:

StackOverflowErrorExample.java:

public class StackOverflowErrorExample {

    public static void recursivePrint(int num) {
        System.out.println("Number: " + num);
        if (num == 0)
            return;
        else
            recursivePrint(++num);
        }

    public static void main(String[] args) {
        StackOverflowErrorExample.recursivePrint(1);
    }
}

In this example, we define a recursive method, called recursivePrint that prints an integer and then, calls itself, with the next successive integer as an argument. The recursion ends until we pass in 0 as a parameter. However, in our example, we passed in the parameter from 1 and its increasing followers, consequently, the recursion will never terminate.

A sample execution, using the -Xss1M flag that specifies the size of the thread stack to equal to 1 MB, is shown below:

Number: 1
Number: 2
Number: 3
...
Number: 6262
Number: 6263
Number: 6264
Number: 6265
Number: 6266
Exception in thread "main" java.lang.StackOverflowError
        at java.io.PrintStream.write(PrintStream.java:480)
        at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
        at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
        at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
        at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
        at java.io.PrintStream.write(PrintStream.java:527)
        at java.io.PrintStream.print(PrintStream.java:669)
        at java.io.PrintStream.println(PrintStream.java:806)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:4)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
        ...

Depending on the JVM’s initial configuration, the results may differ, but eventually the StackOverflowError shall be thrown. This example is a very good example of how recursion can cause problems, if not implemented with caution.

How to deal with the StackOverflowError

  1. The simplest solution is to carefully inspect the stack trace and
    detect the repeating pattern of line numbers. These line numbers
    indicate the code being recursively called. Once you detect these
    lines, you must carefully inspect your code and understand why the
    recursion never terminates.

  2. If you have verified that the recursion
    is implemented correctly, you can increase the stack’s size, in
    order to allow a larger number of invocations. Depending on the Java
    Virtual Machine (JVM) installed, the default thread stack size may
    equal to either 512 KB, or 1 MB. You can increase the thread stack
    size using the -Xss flag. This flag can be specified either via the
    project’s configuration, or via the command line. The format of the
    -Xss argument is:
    -Xss<size>[g|G|m|M|k|K]

To describe this, first let us understand how local variables and objects are stored.

Local variable are stored on the stack:

Enter image description here

If you looked at the image you should be able to understand how things are working.

When a function call is invoked by a Java application, a stack frame is allocated on the call stack. The stack frame contains the parameters of the invoked method, its local parameters, and the return address of the method. The return address denotes the execution point from which, the program execution shall continue after the invoked method returns. If there is no space for a new stack frame then, the StackOverflowError is thrown by the Java Virtual Machine (JVM).

The most common case that can possibly exhaust a Java application’s stack is recursion. In recursion, a method invokes itself during its execution. Recursion is considered as a powerful general-purpose programming technique, but it must be used with caution, to avoid StackOverflowError.

An example of throwing a StackOverflowError is shown below:

StackOverflowErrorExample.java:

public class StackOverflowErrorExample {

    public static void recursivePrint(int num) {
        System.out.println("Number: " + num);
        if (num == 0)
            return;
        else
            recursivePrint(++num);
        }

    public static void main(String[] args) {
        StackOverflowErrorExample.recursivePrint(1);
    }
}

In this example, we define a recursive method, called recursivePrint that prints an integer and then, calls itself, with the next successive integer as an argument. The recursion ends until we pass in 0 as a parameter. However, in our example, we passed in the parameter from 1 and its increasing followers, consequently, the recursion will never terminate.

A sample execution, using the -Xss1M flag that specifies the size of the thread stack to equal to 1 MB, is shown below:

Number: 1
Number: 2
Number: 3
...
Number: 6262
Number: 6263
Number: 6264
Number: 6265
Number: 6266
Exception in thread "main" java.lang.StackOverflowError
        at java.io.PrintStream.write(PrintStream.java:480)
        at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
        at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
        at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
        at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
        at java.io.PrintStream.write(PrintStream.java:527)
        at java.io.PrintStream.print(PrintStream.java:669)
        at java.io.PrintStream.println(PrintStream.java:806)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:4)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
        ...

Depending on the JVM’s initial configuration, the results may differ, but eventually the StackOverflowError shall be thrown. This example is a very good example of how recursion can cause problems, if not implemented with caution.

How to deal with the StackOverflowError

  1. The simplest solution is to carefully inspect the stack trace and
    detect the repeating pattern of line numbers. These line numbers
    indicate the code being recursively called. Once you detect these
    lines, you must carefully inspect your code and understand why the
    recursion never terminates.

  2. If you have verified that the recursion
    is implemented correctly, you can increase the stack’s size, in
    order to allow a larger number of invocations. Depending on the Java
    Virtual Machine (JVM) installed, the default thread stack size may
    equal to either 512 KB, or 1 MB. You can increase the thread stack
    size using the -Xss flag. This flag can be specified either via the
    project’s configuration, or via the command line. The format of the
    -Xss argument is:
    -Xss<size>[g|G|m|M|k|K]

В мире программистов ошибка «stack overflow» очень известна благодаря тому, что этот вид ошибки довольно распространен. А сам термин «stack overflow» известен еще больше, чем ошибка, благодаря одноименному англоязычному ресурсу «StackOverflow». Это сообщество программистов международного масштаба, и еще куча всего интересного. Поэтому не нужно путать ошибку «stack overflow» и веб-ресурс с таким же названием. В нашей статье речь пойдет об ошибке.

Ошибка «stack overflow» связана с переполнением стека. Она появляется в том случае, когда в стеке должно сохраниться больше информации, чем он может уместить. Объем памяти стека задает программист при запуске программы. Если в процессе выполнения программы стек переполняется, тогда возникает ошибка «stack overflow» и программа аварийно завершает работу. Причин возникновения подобной ошибки может быть несколько.

Ошибка «stack overflow»

Нужно отметить, что ошибка «stack overflow» не связана с конкретным языком программирования, то есть она может возникнуть в программах на Java, C++, C, C# и других компилируемых языках.

Причин ее возникновения может быть несколько. К самым распространенным причинам относятся:

  • бесконечная рекурсия;

  • глубокая рекурсия;

  • проблемы с переменными в стеке.

Бесконечная рекурсия и ошибка «stack overflow» 

Бесконечная рекурсия редко возникает самостоятельно и по неизвестным причинам. Обычно программист:

  • забывает прописывать условие для выхода из рекурсии;

  • пишет неосознанную косвенную рекурсию.

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

Вот как это выглядит на С:

int factorial (int number)

{

  if (number == 0)

    return 1;

  return number * factorial(number — 1);

}

В описанном примере прописаны условия выхода из рекурсии, однако они никогда не сработают, если «number» будет отрицательным. Поэтому через несколько миллионов вызовов стек будет переполнен и возникнет ошибка «stack overflow». В некоторых языках программирования предусмотрена «защита» от таких рекурсий. В них рекурсия из конца функции конвертируется в цикл, что не будет расходовать стековую память. Но подобная «оптимизация» вызовет другую, менее опасную проблему «зацикливание».

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

В коде это выглядит так:

 int Object::getNumber(int index, bool& isChangeable)

{

  isChangeable = true;

  return getNumber(index);

}

int Object::getNumber(int index)

{

  bool noValue;

  return getNumber(index, noValue);

}

Глубокая рекурсия и ошибка «stack overflow»

Глубокая рекурсия — это такая рекурсия, которая имеет свое окончание через определенное время, поэтому она не бесконечная. Однако памяти стека не хватит для завершения такой рекурсии, поэтому ошибка «stack overflow» обеспечена. Обычно такая ситуация заранее просчитывается программистом,поэтому ее можно решить. Например, можно:

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

  • «вынести» рекурсию за пределы аппаратного стека в динамический;

  • и другое.

Глубокая рекурсия выглядит так:

void eliminateList(struct Item* that)

{

    if (that == NULL)

        return;

    eliminateList(that->next);

    free(that);

}

Проблемы с переменными в стеке и ошибка «stack overflow»

Если взглянуть на популярность возникновения «stack overflow error», то причина с проблемными переменными в стеке стоит на первом месте. Кроется она в том, что программист изначально выделяет слишком много памяти локальной переменной.

Например:

int function() {

     double b[1000000]

}

В данном случае может возникнуть такая ситуация, что массиву потребуется объем памяти, который стек не способен будет обеспечить, а значит, возникнет ошибка «stack overflow». 

Заключение

Ошибка «stack overflow» возникает довольно часто. Каждый конкретный случай ее возникновения требует собственного решения. Одна причина объединяет возникновение такой ошибки — невнимательность программиста. Если «stack overflow error» возникла, значит, программист где-то что-то упустил или не доглядел.

Что означает ошибка Uncaught RangeError: Maximum call stack size exceeded

Что означает ошибка Uncaught RangeError: Maximum call stack size exceeded

Это когда вызывается слишком много вложенных функций

Это когда вызывается слишком много вложенных функций

Ситуация: заказчик попросил разместить на странице кликабельную картинку, а чтобы на неё обратило внимание больше посетителей, попросил сделать вокруг неё моргающую рамку. Логика моргания в скрипте очень простая:

  1. В первой функции находим на странице нужный элемент.
  2. Добавляем рамку с какой-то задержкой (чтобы она какое-то время была на экране).
  3. Вызываем функцию убирания рамки.
  4. Внутри второй функции находим тот же элемент на странице.
  5. Убираем рамку с задержкой.
  6. Вызываем первую функцию добавления рамки.

Код простой, поэтому делаем всё в одном файле:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title>Pulse</title>
	<style type="text/css">
	/*	рамка, которая будет моргать	*/
		.pulse { box-shadow: 0px 0px 4px 4px #AEA79F; }
	</style>
</head>
<body>
	<div id="pulseDiv"> 
    	<a href="#">
      		<div id="advisersDiv">
        		<img src="https://thecode.media/wp-content/uploads/2020/08/photo_2020-08-05-12.04.57.jpeg">
        	</div>
      	</a>
</div>
<!-- подключаем jQuery -->
<script src="https://yastatic.net/jquery/3.3.1/jquery.min.js" type="text/javascript"></script>
<!-- наш скрипт -->
<script type="text/javascript">
	// добавляем рамку
	function fadeIn() {
		// находим нужный элемент и добавляем рамку с задержкой
  	$('#pulseDiv').find('div#advisersDiv').delay(400).addClass("pulse");
  	// затем убираем рамку
  	fadeOut();
	};
	// убираем рамку
	function fadeOut() {
		// находим нужный элемент и убираем рамку с задержкой
   	$('#pulseDiv').find('div#advisersDiv').delay(400).removeClass("pulse");
   	// затем добавляем 
   	fadeIn();
	};
	// запускаем моргание рамки
	fadeIn();
</script>

</body>
</html>

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

❌ Uncaught RangeError: Maximum call stack size exceeded

Что это значит: в браузере произошло переполнение стека вызовов и из-за этого он не может больше выполнять этот скрипт.

Переполнения стека простыми словами означает вот что:

  1. Когда компьютер что-то делает, он это делает последовательно — 1, 2, 3, 4.
  2. Иногда ему нужно отвлечься от одного и сходить сделать что-то другое — а, б, в, г, д. Получается что-то вроде 1, 2, 3 → а, б, в, г, д → 4.
  3. Вот эти переходы 3 → а и д → 4 — это компьютеру нужно запомнить, что он выполнял пункт 3, и потом к нему вернуться.
  4. Каждое запоминание, что компьютер бросил и куда ему нужно вернуться, — это называется «вызов».
  5. Вызовы хранятся в стеке вызовов. Это стопка таких ссылок типа «когда закончишь вот это, вернись туда».
  6. Стек не резиновый и может переполняться.

Что делать с ошибкой Uncaught RangeError: Maximum call stack size exceeded

Эта ошибка — классическая ошибка переполнения стека во время выполнения рекурсивных функций.

Рекурсия — это когда мы вызываем функцию внутри самой себя, но чуть с другими параметрами. Когда параметр дойдёт до конечного значения, цепочка разматывается обратно и функция собирает вместе все значения. Это удобно, когда у нас есть чёткий алгоритм подсчёта с понятными правилами вычислений.

В нашем случае рекурсия возникает, когда в конце обеих функций мы вызываем другую: 

  1. Функции начинают бесконтрольно вызывать себя бесконечное число раз.
  2. Стек вызовов начинает запоминать вызов каждой функции, чтобы, когда она закончится, вернуться к тому, что было раньше.
  3. Стек — это определённая область памяти, у которой есть свой объём.
  4. Вызовы не заканчиваются, и стек переполняется — в него больше нельзя записать вызов новой функции, чтобы потом вернуться обратно.
  5. Браузер видит всё это безобразие и останавливает скрипт.

То же самое будет, если мы попробуем запустить простую рекурсию слишком много раз:

Что означает ошибка Uncaught RangeError: Maximum call stack size exceeded

Как исправить ошибку Uncaught RangeError: Maximum call stack size exceeded

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

В нашем случае проблема возникает из-за того, что мы вызывали вторые функции бесконтрольно, поэтому они множились без ограничений. Решение — ограничить вызов функции одной секундой — так они будут убираться из стека и переполнения не произойдёт:

<script type="text/javascript">
// добавляем рамку
function fadeIn() {
	// находим нужный элемент и добавляем рамку с задержкой
	$('#pulseDiv').find('div#advisersDiv').delay(400).addClass("pulse");
	// через секунду убираем рамку
	setTimeout(fadeOut,1000)
};
// убираем рамку
function fadeOut() {
	// находим нужный элемент и убираем рамку с задержкой
 	$('#pulseDiv').find('div#advisersDiv').delay(400).removeClass("pulse");
 	// через секунду добавляем рамку
	setTimeout(fadeIn,1000)
};
// запускаем моргание рамки
fadeIn();
</script>

Что означает ошибка Uncaught RangeError: Maximum call stack size exceeded

Вёрстка:

Кирилл Климентьев

Получите ИТ-профессию

В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.

Начать карьеру в ИТ

Получите ИТ-профессию
Получите ИТ-профессию
Получите ИТ-профессию
Получите ИТ-профессию

serjufa1

1 / 1 / 0

Регистрация: 15.10.2007

Сообщений: 83

1

27.02.2021, 14:38. Показов 2862. Ответов 4

Метки нет (Все метки)


здравствуйте.
Выходит ошибка «Ошибка времени выполнения: StackOverflowException: Программа завершена из-за переполнения программного стека
«
Вот задача
(Е. Джобс) Алгоритм вычисления функции F(n), где n – натуральное число, задан следующими соотношениями:
F(n) = n + 1 при n < 3,
F(n) = n + 2*F(n + 2), когда n ≥ 3 и четно,
F(n) = F(n – 2) + n – 2, когда n ≥ 3 и нечетно.
Сколько существует чисел n, для которых значение F(n) будет трехзначным

ВОТ РЕШЕНИЕ

PureBasic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Функция F
function F(n: integer): integer;
begin
  if n < 3 then 
    F := n + 1;  
  if (n >= 3) and (n mod 2 = 0) then
    F := n + 2 * F(n + 2);
  if (n >= 3) and (n mod 2 <> 0) then
    F := n + 2 * F(n + 2);  
  
end;
 
var
  cikl, kolvo: integer;
//Основная часть программы, где запускаем функцию.
begin
  kolvo := 0;
  for cikl := 1 to 1000 do
  begin
    if (F(cikl) >= 100) and (F(cikl) <= 999) then
      kolvo := kolvo + 1; 
  end;
  
  WriteLn(cikl);
end.

__________________
Помощь в написании контрольных, курсовых и дипломных работ, диссертаций здесь

0

Programming

Эксперт

94731 / 64177 / 26122

Регистрация: 12.04.2006

Сообщений: 116,782

27.02.2021, 14:38

4

canadamoscow

1045 / 404 / 187

Регистрация: 23.03.2020

Сообщений: 980

Записей в блоге: 1

27.02.2021, 16:37

2

Pascal
1
2
3
4
5
6
7
8
9
10
11
12
function F(n: integer): integer;
begin
  if n < 3 then  F := n + 1
  else if odd(n) then F := F(n-2) + n-2
end;
 
begin
  var kolvo := 0;
  for var n := -2000 to 2000 do  
    if (abs(f(n)) >= 100) and (abs(f(n)) < 1000) then inc(kolvo);
  Writeln(kolvo);
end.

0

serjufa1

1 / 1 / 0

Регистрация: 15.10.2007

Сообщений: 83

27.02.2021, 20:25

 [ТС]

3

Прошу прощения, я неправильно указал часть кода.
Сейчас поправил код. Теперь он совпадает с заданием. Но, ошибка осталась.
Наверное, Ваш ответ тоже изменится.

PureBasic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Функция F
function F(n: integer): integer;
begin
  if n < 3 then 
    F := n + 1
    else
  if ((n mod 2) = 0) then
    F := n + 2 * F(n + 2)
  else
         F := F(n - 2) + n - 2;   
end;
 
var
  cikl, kolvo: integer;
//Основная часть программы, где запускаем функцию.
begin
  kolvo := 0;
  for cikl := 1 to 100 do
  begin
    if (F(cikl) >= 100) and (F(cikl) <= 999) then
      kolvo := kolvo + 1; 
  end;
  
  WriteLn(cikl);
end.

Также я не понял мысль про

PureBasic
1
for var n := -2000 to 2000 do

0

2109 / 1258 / 476

Регистрация: 07.04.2017

Сообщений: 4,444

27.02.2021, 20:57

4

Цитата
Сообщение от serjufa1
Посмотреть сообщение

F(n) = n + 2*F(n + 2)

Это принципиально не будет работать. Для того чтоб посчитать F(n) вам надо посчитать F(n+2). n+2 тоже будет чётным, раз n чётно. А значит чтоб посчитать F(n+2) — надо сначала посчитать F(n+4). И так до бесконечности.

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

Выясняйте у преподавателя, потому что задание неправильное.

1

2801 / 1480 / 594

Регистрация: 19.03.2019

Сообщений: 4,904

01.03.2021, 12:31

5

Цитата
Сообщение от Sun Serega
Посмотреть сообщение

Это принципиально не будет работать. Для того чтоб посчитать F(n) вам надо посчитать F(n+2). n+2 тоже будет чётным, раз n чётно. А значит чтоб посчитать F(n+2) — надо сначала посчитать F(n+4). И так до бесконечности.

Абсолютно согласен.

Это очередная кривая задача из сборника задач Полякова.

Цитата
Сообщение от Иванин
Посмотреть сообщение

Эта задача №67

Ошибка времени выполнения: StackOverflowException: Программа завершена из-за переполнения программного стека

Решения, как и некоторые другие (см. обсуждение здесь — Не могу найти ошибку!) НЕ ИМЕЕТ.

0

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

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

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

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

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

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

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

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

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

Содержание

  • 1 Использование переполнения буфера стека
  • 2 Различия, связанные с платформой
    • 2.1 Растущие стеки
  • 3 Схемы защиты
    • 3.1 Канарейки стека
    • 3.2 Неисполняемый стек
    • 3.3 Рандомизация
  • 4 Примечательные примеры
  • 5 См. Также
  • 6 Ссылки

Использование буфера стека overflows

Канонический метод использования переполнения буфера на основе стека — перезапись адреса возврата функции указателем на данные, контролируемые злоумышленником (обычно в самом стеке). Это проиллюстрировано с помощью strcpy() в следующем примере:

#include void foo (char * bar) {char c [12]; strcpy (c, bar); // без проверки границ} int main (int argc, char ** argv) {foo (argv [1]); возврат 0; }

Этот код берет аргумент из командной строки и копирует его в локальную переменную стека c. Это отлично работает для аргументов командной строки меньше 12 символов (как вы можете видеть на рисунке B ниже). Любые аргументы длиной более 11 символов приведут к повреждению стека. (Максимальное количество безопасных символов на единицу меньше размера буфера здесь, потому что в языке программирования C строки заканчиваются нулевым байтовым символом. Таким образом, для ввода из двенадцати символов требуется тринадцать байтов для хранения, за вводом следует нулевым байтом контрольного значения. Нулевой байт затем завершается перезаписью области памяти, которая на один байт выходит за пределы буфера.)

Программный стек в foo ()с различными входами:

Обратите внимание, что на рисунке C выше, когда в командной строке указан аргумент размером более 11 байт foo ()перезаписывает данные локального стека, сохраненный кадр указатель, а главное — адрес возврата. Когда foo ()возвращает, он выталкивает адрес возврата из стека и переходит к этому адресу (т.е. начинает выполнять инструкции с этого адреса). Таким образом, злоумышленник перезаписал адрес возврата указателем на буфер стека char c [12], который теперь содержит данные, предоставленные злоумышленником. При фактическом использовании переполнения буфера стека строка «A» вместо этого будет шеллкодом, подходящим для платформы и желаемой функции. Если у этой программы были особые привилегии (например, бит SUID установлен для запуска от имени суперпользователя ), то злоумышленник мог использовать эту уязвимость, чтобы получить привилегии суперпользователя на уязвимой машине.

Злоумышленник также может изменить значения внутренних переменных, чтобы воспользоваться некоторыми ошибками. В этом примере:

#include #include void foo (char * bar) {float My_Float = 10.5; // Адрес = 0x0023FF4C char c [28]; // Addr = 0x0023FF30 // Напечатает 10.500000 printf ("My Float value =% f  n", My_Float); / * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~ Карта памяти: @: c выделенная память #: выделенная память My_Float * c * My_Float 0x0023FF30 0x0023FF4C | | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.com memcpy поместит 0x1010C042 (little endian) в значение My_Float. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~ * / memcpy (c, bar, strlen (bar)); // без проверки границ... // напечатает 96.031372 printf ("My Float value =% f  n", My_Float); } int main (int argc, char ** argv) {foo ("моя строка слишком длинная !!!!!  x10  x10  xc0  x42"); возврат 0; }

Различия, связанные с платформой

У ряда платформ есть тонкие различия в их реализации стека вызовов, которые могут повлиять на способ работы эксплойта переполнения буфера стека. Некоторые машинные архитектуры хранят адрес возврата верхнего уровня стека вызовов в регистре. Это означает, что любой перезаписанный адрес возврата не будет использоваться до последующего раскручивания стека вызовов. Другой пример специфической для машины детали, которая может повлиять на выбор методов эксплуатации, — это тот факт, что большинство архитектур машин в стиле RISC не допускают невыровненный доступ к памяти. В сочетании с фиксированной длиной для машинных кодов операций это машинное ограничение может сделать переход к технике ESP практически невозможным (за одним исключением, когда программа фактически содержит маловероятный код для явного перехода к регистру стека).

Растущие стеки

В рамках темы переполнения буфера стека часто обсуждается, но редко встречается архитектура, в которой стек растет в противоположном направлении. Это изменение в архитектуре часто предлагается в качестве решения проблемы переполнения буфера стека, поскольку любое переполнение буфера стека, происходящее в том же кадре стека, не может перезаписать указатель возврата. Дальнейшее расследование заявленной защиты показало, что это в лучшем случае наивное решение. Любое переполнение, происходящее в буфере из предыдущего кадра стека, по-прежнему будет перезаписывать указатель возврата и допускать злонамеренное использование ошибки. Например, в приведенном выше примере указатель возврата для fooне будет перезаписан, потому что переполнение фактически происходит внутри фрейма стека для memcpy. Однако, поскольку буфер, который переполняется во время вызова memcpy, находится в предыдущем фрейме стека, указатель возврата для memcpyбудет иметь численно больший адрес памяти, чем буфер. Это означает, что вместо перезаписи указателя возврата для fooбудет перезаписан указатель возврата для memcpy. В лучшем случае это означает, что рост стека в противоположном направлении изменит некоторые детали того, как можно использовать переполнение буфера стека, но это не приведет к значительному сокращению количества уязвимых ошибок.

Схемы защиты

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

  • Обнаружение переполнения буфера стека и, таким образом, предотвращение перенаправления указателя инструкции на вредоносный код.
  • Предотвращение выполнения вредоносного кода из стека без прямого обнаружения переполнение буфера стека.
  • Произвести случайное изменение пространства памяти, чтобы поиск исполняемого кода стал ненадежным.

Стек канареек

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

Невыполняемый стек

Другой подход к предотвращению эксплуатации переполнения буфера стека заключается в применении политики памяти в области памяти стека, которая запрещает выполнение из стека (W ^ X, «Запись XOR Execute»). Это означает, что для выполнения шелл-кода из стека злоумышленник должен либо найти способ отключить защиту выполнения из памяти, либо найти способ разместить полезную нагрузку шелл-кода в незащищенной области памяти. Этот метод становится все более популярным сейчас, когда аппаратная поддержка флага отсутствия выполнения доступна в большинстве процессоров для настольных ПК.

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

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

Вариантом return-to-libc является возвратно-ориентированное программирование (ROP), которое устанавливает серию адресов возврата, каждый из которых выполняет небольшую последовательность выбранной машиной инструкции в рамках существующего программного кода или системных библиотек, последовательность которых заканчивается возвратом. Каждый из этих так называемых гаджетов выполняет простую манипуляцию с регистрами или аналогичное выполнение перед возвратом, и объединение их вместе достигает целей злоумышленника. Можно даже использовать «безвозвратное» программирование, ориентированное на возврат, используя инструкции или группы инструкций, которые ведут себя так же, как инструкция возврата.

Рандомизация

Вместо отделения кода от данных, другой метод смягчения последствий — введение рандомизации в область памяти исполняемой программы. Поскольку злоумышленнику необходимо определить, где находится исполняемый код, который можно использовать, либо предоставляется исполняемая полезная нагрузка (с исполняемым стеком), либо она создается с использованием повторного использования кода, например, в ret2libc или в программировании, ориентированном на возврат (ROP). Рандомизация структуры памяти, как концепция, не позволяет злоумышленнику узнать, где находится какой-либо код. Однако реализации обычно не все рандомизируют; обычно сам исполняемый файл загружается по фиксированному адресу, и, следовательно, даже когда ASLR (рандомизация разметки адресного пространства) комбинируется с невыполняемым стеком, злоумышленник может использовать эту фиксированную область памяти. Следовательно, все программы должны быть скомпилированы с использованием PIE (независимые от позиции исполняемые файлы), чтобы даже эта область памяти была рандомизирована. Энтропия рандомизации отличается от реализации к реализации, и достаточно низкая энтропия сама по себе может быть проблемой с точки зрения грубого форсирования пространства памяти, которое рандомизируется.

Примечательные примеры

  • Червь Morris в 1988 г. частично распространился за счет использования переполнения буфера стека на сервере Unix finger. [1]
  • Червь Witty в 2004 г. распространился, эксплуатируя переполнение буфера стека в Internet Security Systems BlackICE Desktop Agent. [2]
  • Червь Slammer в 2003 г. распространился, эксплуатируя переполнение буфера стека на SQL-сервере Microsoft. [3]
  • Червь Blaster в Распространение 2003 г. за счет использования переполнения буфера стека в службе Microsoft DCOM.
  • Существует несколько примеров Wii, позволяющих запускать произвольный код в неизмененной системе.. «Сумеречный хак», который включает в себя длинное имя лошади главного героя в The Legend of Zelda: Twilight Princess и «Smash Stack» для Super Smash Bros. Brawl, который включает использование SD-карты для загрузки специально подготовленного файла в редактор игровых уровней. Хотя оба могут использоваться для выполнения любого произвольного кода, последний часто используется для простой перезагрузки самой Brawl с применением модификаций.

См. Также

  • Рандомизация компоновки адресного пространства
  • Переполнение буфера
  • Стек вызовов
  • Компьютерная безопасность
  • ExecShield
  • Защита исполняемого пространства
  • Эксплойт (компьютерная безопасность)
  • Атака форматной строки
  • Переполнение кучи
  • Целочисленное переполнение
  • бит NX
  • PaX
  • Возврат-ориентированное программирование
  • Linux с усиленной безопасностью
  • Переполнение стека
  • Нарушение хранилища
  • Уязвимость (вычисления)

Ссылки

Понравилась статья? Поделить с друзьями:
  • Ошибка переполнение стека встроенного языка на сервере
  • Ошибка переполнение водой стиральная машина
  • Ошибка переобогащенная смесь что это
  • Ошибка переобедненная смесь ваз 2112
  • Ошибка переноса с android на iphone