Меню

Что такое ошибка переполнения стека

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]

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

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

Определение

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

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

Стек программы — это специальная области памяти, организованная по принципу очереди 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».

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

Что означает ошибка 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

Вёрстка:

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

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

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

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

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

В мире программистов ошибка «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» возникла, значит, программист где-то что-то упустил или не доглядел.

Содержание:

1.      Что такое стек в конфигурации программы 1С?

2.      Для чего нужен стек и откуда появляется ошибка переполнения стека?

3.      Причины, вызывающие переполнение стека в системе программы 1С: Предприятие  

1.      Что такое стек в конфигурации программы 1С?

При работе в конфигурации системы 1С, после обновления конфигурации или платформы, или по каким-то другим причинам, все мы сталкиваемся с различными всплывающими ошибками. Разберемся сегодня, что можно сделать, если столкнуться с ошибкой – «Переполнение стека встроенного языка на сервере». Разберемся, что вызывает эту ошибку:

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

Стек – это по сути стопка элементов, в которой эти элементы «укладываются» один поверх другого. Работает этот механизм по принципу LIFO (Last In, First Out – последним вошел/первым вышел). Поэтому добраться мы можем в первую очередь до того элемента, который положили последним, потом предпоследним, и т.д.  

2.      Для чего нужен стек и откуда появляется ошибка переполнения стека?

Главное назначение стека — чтобы организовать команду вызова подпрограмм удобным образом. Например, вызывается «функция1» с определенными переменными, и которая при этом передает определенные параметры и вызывает «функцию2» со своими переменными. В свою очередь «функция2» передает свои параметры и вызывает «функцию3», и т.д. Все эти параметры и переменные нужно сохранять (куда-то складывать), и затем по порядку вызывать. Для этого как раз удобно использовать стек.

Если рассматривать стек абстрактно – это «безразмерный склад» для 1С хранения информации. Но фактически – это не так.

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

3.      Причины, вызывающие переполнение стека в системе программы 1С: Предприятие

1. Самый распространенный вариант – это рекурсия. И если происходит бесконечная рекурсия, то прерывается она таким образом.

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

Процедура ИмяПроцедурыВызываемойРекурсивно()

 ИмяПроцедурыВызываемойРекурсивно();

КонецПроцедуры

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

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

1.      Если есть возможность – не используем в работе рекурсию.

2.      Не выполнять вручную ту работу, которую может выполнить компилятор. 

Специалист компании «Кодерлайн»
Марина Анапольская

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Перевод публикуется с сокращениями, автор оригинальной статьи Megan Kaczanowski.

C, C++ и Objective-C являются ключевыми языками, имеющими уязвимости переполнения буфера, поскольку они работают с памятью более непосредственно чем многие интерпретируемые языки.

Даже если код написан на
«безопасном» языке (например, на Python), если используются любые
написанные на C, C++ или Objective C библиотеки, он все равно может быть уязвим для
переполнения буфера.

Выделение памяти

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

  • Объявление переменной в стеке: int numberPoints = 10.
  • Объявление переменной в куче: int* ptr = malloc (10 * sizeof(int)).

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

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

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

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

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

Схема стека:

❓ Что такое переполнение буфера и как с ним бороться

Что такое stackframe?

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

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

  • Stack Pointer: указывает на топ стека вызовов процесса (или на последний помещенный в стек элемент).
  • Instruction Pointer: указывает на адрес следующей инструкции процессора, которая будет выполнена.
  • Base Pointer (BP): (также известный как указатель кадра) указывает на основание текущего кадра стека. Он остается постоянным до тех пор, пока программа выполняет текущий стекфрейм (хотя указатель стека может измениться).

Для примера рассмотрим
следующий код:

        int main() {
    int j = firstFunction(5);
    return 0;
}
    
int firstFunction(int z) {
    int x = 1 + z;
    return x;
}
    

Стек вызовов будет
выглядеть следующим образом, сразу после вызова firstFunction и выполнения
оператора int x = 1+z:

❓ Что такое переполнение буфера и как с ним бороться

Здесь main вызывает
firstFunction (которая в данный момент выполняется), поэтому она находится в
верхней части стека вызовов. Возвращаемый адрес – это адрес в памяти,
относящийся к функции, которая его вызвала (он удерживается указателем
инструкции при создании стекфрейма). Локальные переменные, которые все еще
находятся в области видимости, также находятся в стеке вызовов. Когда они
выполняются и выходят за пределы области действия, они удаляются из верха
стека.

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

Пример уязвимости переполнения
буфера:

        int main() {
    bufferOverflow();
 }
 
 bufferOverflow() {
    char textLine[10];
    printf("Enter your line of text: ");
    gets(textLine);
    printf("You entered: ", textLine);
    return 0;
 }
    

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

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

Почему происходит переполнение буфера?

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

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

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

Как уменьшить влияние переполнения
буфера:

  • Используйте интерпретируемый язык, который не подвержен этим проблемам.
  • Избегайте использования функций, которые не выполняют проверку буфера (например, в C вместо функции gets() используйте функцию fgets()).
  • Применяйте компиляторы, которые помогают определить небезопасные функции или найти ошибки.
  • Используйте canaries, которые могут помочь предотвратить переполнение буфера. Они вставляются перед обратным адресом в стеке и проверяются перед обращением к нему. Если программа обнаружит изменение значения canary, она прервет процесс, не позволив злоумышленнику пробиться. Значение canary является либо случайным (поэтому злоумышленнику очень трудно его угадать), либо строкой, которую по техническим причинам невозможно перезаписать.
  • Переставляйте локальные переменных таким образом, чтобы скалярные (отдельные объекты данных фиксированного размера) были выше переменных массива, содержащих несколько значений. Это означает, что если переменные массива переполняются, они не будут влиять на скалярные переменные. Этот метод в сочетании с canary-значениями очень помогает.
  • Сделайте стек неисполняемым, установив бит NX (No-eXecute), чтобы злоумышленник не вставлял шелл-код непосредственно в стек и не выполнял его там. Это неидеальное решение, так как даже неисполняемые стеки могут стать жертвами атак переполнения буфера, вроде return-to-libc. Эта атака происходит, когда обратный адрес стекового фрейма заменяется адресом библиотеки, уже находящейся в адресном пространстве процесса. К тому же не все процессоры позволяют установить бит NX.
  • ASLR (рандомизация расположения адресного пространства) может служить общей защитой, а также специфической защитой от атак return-to-libc. Это означает, что всякий раз, когда файл библиотеки или другая функция вызывается запущенным процессом, ее адрес сдвигается на случайное число. Это делает практически невозможным связать фиксированный адрес памяти процесса с функциями, из чего следует, что злоумышленнику может быть трудно узнать, откуда вызывать определенные функции. ASLR включен по умолчанию во многих версиях Linux, OS X и Android.

Stack Underflow

Такая уязвимость возникает, когда две части программы по-разному обрабатывают один и
тот же блок памяти. Например, если вы выделите массив размером X, но заполните
его массивом размером x < X, а затем попытаетесь извлечь все X байтов, скорее
всего вы получите «грязные» данные для X – x байтов.

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

Заключение

Рассмотренная уязвимость
является очень серьезной угрозой стабильной работе любого продукта.
Необходимо приложить все усилия и проверить ваши проекты на ее наличие, т. к. последствия могут быть весьма плачевными (уже упоминался
Ransome) и болезненными. Используйте советы из статьи и вы уменьшите вероятность успешного проникновения
злоумышленников в ваш код. Удачи в обучении!

Дополнительные материалы:

  • Алгоритмы в C++: запросы к статическим массивам
  • ТОП-10 трюков на C++, которые облегчат вам жизнь
  • Сайт на C++ своими руками с помощью библиотеки cgicc
  • 5 шагов для создания простой формы входа на C#
  • 10 самых популярных алгоритмов сортировки на C#

Источники

  • https://www.freecodecamp.org/news/buffer-overflow-attacks/

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии

А вот еще интересные материалы:

  • Яшка сломя голову остановился исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного где ошибка
  • Что такое ошибка передвижения валорант
  • Что такое ошибка первого рода при диагностировании