Обновить

Избавляемся от ошибок Segmentation fault из-за переполнения стека в С++

Уровень сложностиСложный
Время на прочтение10 мин
Охват и читатели9.4K
Всего голосов 5: ↑3 и ↓2+3
Комментарии22

Комментарии 22

Стек и рекурсия по-сути две стороны одной медали. Плюс многопроцессорность и безопасность стека при работе с общей памятью, плюс кеш, плюс каналы DMA, там же вызов прерываний, если ещё имеется защищённый режим - своп при очень большом стеке (да, часть стека скидывается на диск как при malloc(1 GB) ), особенно когда имеется режим реалтайм это создаёт дополнительные вопросы, поскольку время чтения/записи стека не гарантировано нигде. Поэтому лучшее решение проблем - гарантированное управление памятью по-соседству с исполняемым кодом, исключать любые лямбды и впихивание в стек гигабайтных объектов за счёт копирования контекста, особенно при применении стек-очередей. Вообще говоря отсутствие управления стека, регистров аргументов функций на уровне компилятора и статических языковых конструкций - это боль длящаяся ещё из 80-х, когда зачем-то эту опцию выпилили якобы языки высокого уровня не должны так много знать, в большинстве случаев это загоняют в статические командные файлы линкёра чтобы прибить стек в микроконтроллере, но для систем более высокого уровня всё-таки должны быть инструменты на уровне языка а не стандарта, регламентирующего костыли и аддоны.

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

ну кстати я тут начал Раст изучать, да-да знаю, что разные языки, но с кешем пока не встретил проблем, когда в С например надо буквально мыслить как указатель например

Я бы поступил проще - добавить к любому статическому объекту атрибут. Ну как 0xABC... тоже самое что ABC.hex равно как 0b10 = 10.bin, побольше синтаксического сахара где он уместен. То есть количество информации небольшое, но оно легко парсится, достойно скармливается ИИ, так как влезает в контекстное окно и много других плюшек. Что то типа void f(void) {}.stack = {придумайте атрибуты настроек}; вполне себе укладывается в синтаксис. Раст - это надстройка над С, как С++ над ним же. По сути любую прогу из Раст можно транслировать на С со всеми плюшками. Если посмотрите ассемблерный выход то там ничего кроме ++/-- счётчиков-флажков и проверок указателей / переменных в стеке больше и нет. Шума много, а по сути маловато. Поэтому язык что то вяло продвигается чтобы стать стандартом ISO/IEC/ANSI.

"Если посмотрите ассемблерный выход то там ничего кроме ++/-- счётчиков-флажков и проверок указателей / переменных в стеке больше и нет. "

Если вкратце Rust вам не даст собрать чушь (без unsafe), он прямо скажет в чём проблема, и не даст никакого ассемблерного выхода, куда вы смотреть собрались, он научит правильно мыслить, а плюсы - приватное виртуальное наследование? исключение из исключения? Use after free? Конечно, дорогой, вот тебе бинарничек, используй на здоровье

Насколько я знаю, переполнение стека не входит в обеспечение гарантий безопасной работы с памятью в Rust, и в нем стек может переполниться как и в С++ не зависимо от safe или unsafe блоков.

Мне кажется, решаемвя проблема не особо актуальна.

Для обычной разработки переполнение стека я видел еще под Дос16.

А для эмбеда и Плк применяются другие технологии, исключающие проблему в принципе.

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

Рекурсия, понятно, запрещена.

В остальных случаях строится граф вызовов с известным размером стека.

Каким образом это можно сделать в случае, когда граф вызовов зависит от входных данных или от создаваемых динамически в рантайме?

Всегда есть максимальный путь

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

Все просто.

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

  2. Устанавливаем размер стека

    Ничего избегать не нужно и бесполезно =)

И как вы собрались считать глубину стека при вызовах по указателям?

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

И спасибо за идею, так как в последнем варианте отпадает необходимость в анализе AST!

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

Выше и написано, что это не для любых проектов, а для

для эмбеда и Плк применяются другие технологии

все эти рассуждения пустые, в реальности невозможно посчитать размер стека практически ни для какой программы. Только для самой тривиальной и не использующей ОС/dll

И даже для этого потребуется глобальный анализ исходного кода (cpp/hpp), который всё равно будет выдавать ошибки, например

// функция "рекурсивна",
// но никогда не приведёт к бесконечной рекурсии
// но анализ этого нетривиален и даже невозможен,
// если рекурсия косвенная
void foo(bool b) {
  if (b) foo(!b);
  return;
}

Про корутины и .resume и динамический goto - промолчу, не поддаётся анализу

Мне кажется, кто то не разбирался с АБИ. Второе сообщение не в тему.

Прошу пример, где это невозможно. Кроме рекурсии.

Ну с помощью alloca можно сломать, её тоже придётся запретить, как и прочее VLA и...

Прошу пример, где это невозможно. Кроме рекурсии

Все системы, где у нас нет полного исходного кода.
Например, любое приложение Windows (нет исходников DLL и ядра).

Либо кода столько много и он настолько сложный, что анализ всегда зайдёт в тупик/рекурсию.
Например, linux-системы.

все эти рассуждения пустые, в реальности невозможно посчитать размер стека практически ни для какой программы. Только для самой тривиальной и не использующей ОС/dll

Так с самого начала написали, что работает в микроконтроллерах и embed, в больших ОС - "проблема не актуальна".

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

Удивительно. В черном ящике возможны неизвестные ошибки

В чем проблема? Вызов по указателю ничем не отличается. Известен размер стека, передаются параметры.

Ну вообще в микроконтроллерах переполнение стека - очень распространенная проблема, особенно при использовании RTOS. И гарантированного способа отловить эту проблему до того, как она случится, в общем случае, по-моему, нет.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации