stackoverflow что это за ошибка

Что такое StackOverflow ошибка: раскрываем тайну

stackoverflow что это за ошибка. Смотреть фото stackoverflow что это за ошибка. Смотреть картинку stackoverflow что это за ошибка. Картинка про stackoverflow что это за ошибка. Фото stackoverflow что это за ошибка

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

Ошибка « stack overflow »

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

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

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

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

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

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

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

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

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

int factorial (int number)

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

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

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

int Object::getNumber(int index)

return getNumber(index, noValue);

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

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

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

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

void eliminateList(struct Item* that)

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

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

Заключение

Мы будем очень благодарны

если под понравившемся материалом Вы нажмёте одну из кнопок социальных сетей и поделитесь с друзьями.

Источник

В самом популярном фрагменте кода за всю историю StackOverflow ошибка!

Недавнее исследование «Использование и атрибуция сниппетов кода Stack Overflow в проектах GitHub» внезапно обнаружило, что чаще всего в опенсорсных проектах встречается мой ответ, написанный почти десять лет назад. По иронии судьбы, там баг.

Давным-давно…

Еще в 2010 году я сидел в своём офисе и занимался ерундой: развлекался код-гольфингом и накручивал рейтинг на Stack Overflow.

Моё внимание привлёк следующий вопрос: как вывести количество байт в удобочитаемом формате? То есть как преобразовать что-то вроде 123456789 байт в «123,5 МБ».

stackoverflow что это за ошибка. Смотреть фото stackoverflow что это за ошибка. Смотреть картинку stackoverflow что это за ошибка. Картинка про stackoverflow что это за ошибка. Фото stackoverflow что это за ошибка
Старый добрый интерфейс 2010 года, спасибо The Wayback Machine

Неявно подразумевалось, что результатом будет число между 1 и 999,9 с соответствующей единицей измерения.

Уже был один ответ с циклом. Идея простая: проверять все степени с самой большой единицы (ЭБ = 10 18 байт) до самой маленькой (Б = 1 байт) и применить первую, которая меньше числа байт. В псевдокоде это выглядит примерно так:

Обычно при наличии правильного ответа с положительной оценкой его трудно догнать. На жаргоне Stack Overflow это называется проблемой самого быстрого стрелка на Западе. Но здесь у ответа было несколько недостатков, поэтому я всё равно надеялся его превзойти. По крайней мере, код с циклом можно значительно сократить.

Это ж алгебра, всё просто!

Тут меня осенило. Приставки кило-, мега-, гига-,… — ни что иное, как степени 1000 (или 1024 в стандарте МЭК), так что правильную приставку можно определить с помощью логарифма, а не цикла.

Основываясь на этой идее, я опубликовал следующее:

Конечно, это не очень читабельно, и log/pow уступает по эффективности другим вариантам. Но никакого цикла и почти нет ветвлений, так что результат получился довольно красивым, на мой взгляд.

В API нет простого выражения log1000, но мы можем выразить его в терминах натурального логарифма следующим образом s = log(byteCount) / log(1000). Затем преобразуем s в int, так что если у нас, например, более одного мегабайта (но не полный гигабайт), то в качестве единицы измерения будет использоваться МБ.

Получается, что если s = 1, то используется размерность килобайт, если s = 2 — мегабайт и так далее. Делим byteCount на 1000 s и шлёпаем соответствующую букву в префикс.

Оставалось только подождать и посмотреть, как сообщество воспримет ответ. Я подумать не мог, что этот фрагмент кода станет самым тиражирумым в истории Stack Overflow.

Исследование по атрибуции

Перенесёмся в 2018 год. Аспирант Себастьян Балтес публикует в научном журнале Empirical Software Engineering статью под названием «Использование и атрибуция сниппетов кода Stack Overflow в проектах GitHub». Тема его исследования — насколько соблюдается лицензия Stack Overflow CC BY-SA 3.0, то есть указывают ли авторы ссылки на Stack Overflow как источник кода.

Для анализа из дампа Stack Overflow были извлечены сниппеты кода и сопоставлены с кодом в публичных репозиториях GitHub. Цитата из реферата:

Представляем результаты крупномасштабного эмпирического исследования, анализирующего использование и атрибуцию нетривиальных фрагментов кода Java из ответов SO в публичных проектах GitHub (GH).

(Спойлер: нет, большинство программистов не соблюдает требования лицензии).

В статье есть такая таблица:

stackoverflow что это за ошибка. Смотреть фото stackoverflow что это за ошибка. Смотреть картинку stackoverflow что это за ошибка. Картинка про stackoverflow что это за ошибка. Фото stackoverflow что это за ошибка

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

stackoverflow что это за ошибка. Смотреть фото stackoverflow что это за ошибка. Смотреть картинку stackoverflow что это за ошибка. Картинка про stackoverflow что это за ошибка. Фото stackoverflow что это за ошибка

Поиск этого фрагмента в своём репозитории:

Забавная история, как я узнал об этом исследовании.

Себастьян нашёл совпадение в репозитории OpenJDK без какой-либо атрибуции, а лицензия OpenJDK не совместима с CC BY-SA 3.0. В списке рассылки jdk9-dev он спросил: это код Stack Overflow скопирован из OpenJDK или наоборот?

Самое смешное то, что я как раз работал в Oracle, в проекте OpenJDK, поэтому мой бывший коллега и друг написал следующее:

Почему бы не спросить напрямую у автора этого сообщения на SO (aioobe)? Он является участником OpenJDK и работал в Oracle, когда этот код появился в исходных репозиториях OpenJDK.

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

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

Держу пари, вы уже задумались об этом. Что же за ошибка в коде?

Может, путаница между единицами СИ и двоичной системой? Нет. В первой версии ответа была путаница, но её исправили довольно быстро.

Может, exp в конечном итоге обнуляется, вызывая сбой charAt(exp-1)? Тоже нет. Первый if-оператор охватывает этот случай. Значение exp всегда будет не менее 1.

Может, какая-то странная ошибка округления в выдаче? Ну вот наконец…

Много девяток

В своё оправдание могу сказать, что на момент написания такая ошибка была во всех 22 опубликованных ответах, включая Apache Commons и библиотеки Android.

Как это исправить? Прежде всего, отметим, что показатель степени (exp) должен измениться с ‘k’ на ‘M’, как только число байт ближе к 1 × 1,000 2 (1 МБ), чем к 999,9 × 1000 1 (999,9 k). Это происходит на 999 950. Точно так же следует переключиться с ‘M’ на ‘G’, когда мы проходим 999 950 000 и так далее.

С этим изменением код работает хорошо до тех пор, пока количество байт не приблизится к 1 ЭБ.

Ещё больше девяток

Введение в арифметику с плавающей запятой

Проблему представляют два вычисления:

Уменьшение промежуточных значений

Настройка наименее значимых битов

Для решения второй проблемы нам важны наименее значимые биты (у 99994999. 9 и 99995000. 0 должны быть разные степени), поэтому придётся найти иное решение.

Сначала отметим, что существует 12 различных пороговых значений (по 6 для каждого режима), и только одно из них приводит к ошибке. Неправильный результат можно однозначно идентифицировать, потому что он заканчивается на D0016. Значит, можно исправить его напрямую.

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

Отрицательные значения на входе

Окончательная версия

Вот окончательная версия кода, сокращённая и уплотнённая в духе оригинальной версии:

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

Для обновлённой версии продакшн-качества см. отдельную статью: «Форматирование размера байт в удобочитаемый формат».

Источник

Stack Overflow Exception Класс

Определение

Некоторые сведения относятся к предварительной версии продукта, в которую до выпуска могут быть внесены существенные изменения. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

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

Примеры

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

Комментарии

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

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

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

Конструкторы

Инициализирует новый экземпляр класса StackOverflowException, устанавливая в качестве значения свойства нового экземпляра Message системное сообщение с описанием ошибки, например: «Запрашиваемая операция вызывает переполнение стека». Это сообщение учитывает культуру текущей системы.

Инициализирует новый экземпляр класса StackOverflowException с указанным сообщением об ошибке.

Инициализирует новый экземпляр класса StackOverflowException указанным сообщением об ошибке и ссылкой на внутреннее исключение, вызвавшее данное исключение.

Свойства

Возвращает коллекцию пар «ключ-значение», предоставляющую дополнительные сведения об исключении.

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

Возвращает или задает HRESULT — кодированное числовое значение, присвоенное определенному исключению.

Возвращает экземпляр класса Exception, который вызвал текущее исключение.

Возвращает сообщение, описывающее текущее исключение.

Возвращает или задает имя приложения или объекта, вызывавшего ошибку.

Получает строковое представление непосредственных кадров в стеке вызова.

Возвращает метод, создавший текущее исключение.

Методы

Определяет, равен ли указанный объект текущему объекту.

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

Служит хэш-функцией по умолчанию.

При переопределении в производном классе задает объект SerializationInfo со сведениями об исключении.

Возвращает тип среды выполнения текущего экземпляра.

Создает неполную копию текущего объекта Object.

Создает и возвращает строковое представление текущего исключения.

События

Возникает, когда исключение сериализовано для создания объекта состояния исключения, содержащего сериализованные данные об исключении.

Источник

Можете объяснить, когда возникает эта ошибка и как от нее избавиться?

stackoverflow что это за ошибка. Смотреть фото stackoverflow что это за ошибка. Смотреть картинку stackoverflow что это за ошибка. Картинка про stackoverflow что это за ошибка. Фото stackoverflow что это за ошибка

2 ответа 2

Это означает, что в стеке недостаточно места.

Как избавиться? Опять же, можно просто в настройках компилятора поднять размер стека.

практически гарантированно даст переполнение стека. В отличие от

Словом, смотрите, кто съедает много стековой памяти, и избавляйтесь от него.

stackoverflow что это за ошибка. Смотреть фото stackoverflow что это за ошибка. Смотреть картинку stackoverflow что это за ошибка. Картинка про stackoverflow что это за ошибка. Фото stackoverflow что это за ошибка

Про стек и другие типы данных:

У 32-битного приложения запущенного в 32-битной Windows суммарный размер всех перечисленных типов данных не должен превышать 2 GB. (Практически ограничение равно 1.75GB из-за требований к памяти самой операционной системы) 32-битная программа, собранная с ключом /LARGEADDRESSAWARE:YES может выделять до 3-х гигабайт памяти, если 32-битная операционная система Windows запущена с ключом /3gb. Эта же 32-битная программа, запущенная на 64-битной системе, может выделить почти 4 GB памяти (на практике около 3.5 GB).

Ограничения на максимальный размер статически-выделяемой и стековой памяти одинаковы для 32-х и 64-х битных Windows приложений. Это связано с форматом типа файлов Portable Executable (PE), который используется в Windows для описания exe и dll файлов. Статические и стековые данные располагаются в первых 2-х GB адресного пространства приложения. Стоит помнить, что данные ограничения накладываются самой операционной системой и не зависят от используемого компилятора.

Это означает, что в стеке недостаточно места.

Источник

Под катом расшифровка доклада Евгения (epeshk) Пешкова с нашей конференции DotNext 2018 Piter, где он рассказал про эти и другие особенности исключений.

Привет! Меня зовут Евгений. Я работаю в компании СКБ Контур и занимаюсь разработкой системы хостинга и деплоя приложений под Windows. Суть в том, что у нас есть много продуктовых команд, которые пишут собственные сервисы и хостят их у нас. Мы предоставляем им легкое и простое решение разнообразных инфраструктурных задач. Например, проследить за потреблением системных ресурсов или докинуть реплик к сервису.

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

Access Violation

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

Попробуем получить это исключение, используя C#. Для этого запишем байт 42 по адресу 1000 (будем считать, что 1000 — это достаточно случайный адрес и у нашего приложения, скорее всего, доступа к нему нет).

WriteByte делает как раз то, что нам нужно: записывает байт по заданному адресу. Мы ожидаем, что этот вызов выбросит AccessViolationException. Этот код действительно выбросит это исключение, его удастся обработать и приложение продолжит работать. Теперь немного изменим код:

Если вместо WriteByte использовать метод Copy и скопировать байт 42 по адресу 1000, то, используя try-catch, AccessViolation поймать не получится. При этом на консоль будет выведено сообщение о том, что приложение завершено из-за необработанного AccessViolationException.

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

Начнем с метода Copy.

Теперь поймём, почему удалось обработать AccessViolation при использовании метода WriteByte. Посмотрим на код этого метода:

Этот метод реализован полностью в managed-коде. Здесь используется C#-pointer, чтобы писать данные по нужному адресу, а также перехватывается NullReferenceException. Если перехватили NRE — выбрасывается AccessViolationException. Так нужно из-за спецификации. При этом все исключения, выброшенные конструкцией throw — обрабатываемые. Соответственно, если при выполнении кода внутри WriteByte произойдёт NullReferenceException — мы сможем поймать AccessViolation. Мог ли произойти NRE, в нашем случае, при обращении не к нулевому адресу, а к адресу 1000?

Перепишем код с использованием C# pointers напрямую, и увидим, что при обращении к ненулевому адресу действительно выбрасывается NullReferenceException:

Давайте разберемся, почему NullReference выбрасывается не только при обращении по нулевому адресу. Представьте, что вы обращаетесь к полю объекта ссылочного типа, и ссылка на этот объект нулевая:

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

Но что же происходит, если мы обращаемся к полю объекта, а сам этот объект занимает больше, чем 64 КБ?

Можем ли мы в этом случае получить AccessViolation? Проведем эксперимент. Создадим очень большой объект и будем обращаться к его полям. Одно поле – в начале объекта, второе – в конце:

Оба метода выбросят NullReferenceException. Никакого AccessViolationException не произойдет.
Посмотрим на инструкции, которые будут сгенерированы для этих методов. Во втором случае JIT-компилятор добавил дополнительную инструкцию cmp, которая обращается к адресу самого объекта, тем самым вызывая AccessViolation с нулевым адресом, который будет преобразован рантаймом в NullReferenceException.

Стоит отметить, что для этого эксперимента недостаточно использовать в качестве большого объекта массив. Почему? Оставим этот вопрос читателю, пишите идеи в комментариях 🙂

Подведем краткий итог экспериментов с AccessViolation.

AccessViolationException ведёт себя по-разному в зависимости от того, где исключение произошло (в managed-коде или в нативном). Кроме того, если исключение произошло в managed-коде, то будет проверяться адрес объекта.

В нашем продакшене была вот такая ситуация:

В этот хелпер мы передавали action из нашего приложения. Так получилось, что он падал с AccessViolation. В результате наше приложение постоянно логгировало AccessViolation, вместо того, чтобы упасть, т.к. код в библиотеке под 3.5 мог его поймать. Нужно обратить внимание, что перехватываемость зависит не от версии рантайма, на котором запущено приложение, а от TargetFramework, под который было собрано приложение, и его зависимости.

Подводим итог. Обработка AccessVilolation зависит от того, где он произошел — в нативном или управляемом коде — а также от TargetFramework и настроек рантайма.

Thread Abort

Иногда в коде нужно остановить выполнение одного из потоков. Для этого можно использовать метод thread.Abort();

При вызове метода Abort в останавливаемом потоке выбрасывается исключение ThreadAbortException. Разберём его особенности. Например, такой код:

Абсолютно эквивалентен такому:

Если всё-таки нужно обработать ThreadAbort и выполнить еще какие-то действия в останавливаемом потоке, то можно использовать метод Thread.ResetAbort(); Он прекращает процесс остановки потока и исключение перестаёт прокидываться выше по стеку. Важно понимать, что метод thread.Abort() сам по себе ничего не гарантирует — код в останавливаемом потоке может препятствовать остановке.

Еще одна особенность thread.Abort() заключается в том, что он не сможет прервать код в том случае, если он находится в блоках catch и finally.

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

Также вызов метода thread.Abort() дожидается выброса ThreadAbortException. Объединим эти два факта и получим, что метод thread.Abort() может заблокировать вызывающий поток.

В реальности с этим можно столкнуться при использовании конструкции using. Она разворачивается в try/finally, внутри finally вызывается метод Dispose. Он может быть сколь угодно сложным, содержать вызовы обработчики событий, использовать блокировки. И если thread.Abort был вызван во время выполнения Dispose — thread.Abort() будет его ждать. Так мы получаем блокировку почти на пустом месте.

OUT OF MEMORY

Это исключение можно получить, если памяти на машине оказалось меньше, чем требуется. Или когда мы уперлись в ограничения 32-битного процесса. Но получить его можно, даже если на компьютере много свободной памяти, а процесс — 64-битный.

Код выше выкинет OutOfMemory. Все дело в том, что в дотнете по умолчанию не разрешены объекты более 2 ГБ. Это можно исправить настройкой gcAllowVeryLargeObjects в App.config. В этом случае массив размером 4 ГБ создастся.

А теперь попробуем создать массив ещё больше.

Это реализация метода string.Concat. Если длина строки-результата будет больше, чем int.MaxValue, то сразу выбрасывается OutOfMemoryException.

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

Сначала мы ограничиваем память нашего процесса в 64 мБ. Далее внутри цикла выделяем новые массивы байтов, сохраняем их в какой-то лист, чтобы GC их не собирал, и пытаемся поймать OutOfMemory.

В этом случае может произойти все что угодно:

В виртуальной памяти страницы могут быть не только отображены в физическую память, но и могут быть зарезервированными (reserved). Если страница зарезервирована, то приложение отметило, что собирается её использовать. Если страница уже отображена в реальную память или своп, то она называется «закоммиченной» (committed). Стек использует такую возможность разделять память на зарезервированную и закомиченную. Выглядит это примерно так:

Получается, что мы вызываем метод WriteLine, который занимает какое-то место на стеке. Так получается, что уже вся закоммиченная память закончилась, значит операционная система в этот момент должна взять еще одну зарезервированную страницу стека и отобразить ее в реальную физическую память, которая уже заполнена массивами байтов. Это и приводит к исключению StackOverflow.

Следующий код позволит на старте потока закоммитить всю память под стек сразу.

Кроме того, можно использовать настройку рантайма disableCommitThreadStack. Её нужно отключить, чтобы стек потока коммитился заранее. Стоит отметить, что поведение по умолчанию описанное в документации и наблюдаемое в реальности — различно.

Stack Overflow

Разберёмся подробнее со StackOverflowException. Посмотрим на два примера кода. В одном из них мы запускаем бесконечную рекурсию, которая приводит к переполнению стека, во втором мы просто выбрасываем это исключение с помощью throw.

Так как все исключения, выброшенные с помощью throw, обрабатываемы, то во втором случае мы поймаем исключение. А с первым случаем все интереснее. Обратимся к MSDN:

«You cannot catch stack overflow exceptions, because the exception-handling code may require the stack.»
MSDN

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

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

В C# 7.2 появилась возможность использовать Span и stackallock вместе без использования unsafe-кода. Возможно, благодаря этому stackalloc станет использоваться в коде чаще и будет полезно иметь способ защититься от StackOverflow при его использовании, выбирая, где именно выделить память. В качестве такого способа предложены метод, проверяющий возможность аллокации на стеке и конструкция trystackalloc.

Вернёмся к документации по StackOverflow на MSDN

Instead, when a stack overflow occurs in a normal application, the Common Language Runtime (CLR) terminates the process.»
MSDN

Если есть «normal» application, которые падают при StackOverflow, значит есть и не-«normal» application, которые не падают? Для того, чтобы ответить на этот вопрос придется спуститься на уровень ниже с уровня управляемого приложения на уровень CLR.

Приложение, которое хостит CLR может переопределить поведение при переполнении стека так, чтобы вместо завершения всего процесса выгружался Application Domain, в потоке котором это переполнение произошло. Таким образом, мы можем превратить StackOverflowException в AppDomainUnloadedException.

Следующий код конфигурирует CLR-host так, чтобы при StackOverflow выгружался AppDomain (C++):

Хороший ли это способ спастись от StackOverflow? Наверно, не очень. Во-первых, нам пришлось написать код на C++, чего делать не хотелось бы. Во-вторых, мы должны поменять свой C#-код так, чтобы та функция, которая может выбросить StackOverflowException выполнялась в отдельном AppDomain’е и в отдельном потоке. Наш код сразу превратится вот в такую лапшу:

Ради того, чтобы вызвать метод InfiniteRecursion, мы написали кучу строк. В-третьих, мы начали использовать AppDomain. А это почти гарантирует кучу новых проблем. В том числе, с исключениями. Рассмотри пример:

Так как наше исключение не помечено как сериализуемое, то наш код упадет с исключением SerializationException. И чтобы исправить эту проблему, нам недостаточно пометить наше исключение атрибутом Serializable, еще потребуется реализовать дополнительный конструктор для сериализации.

Это все получается не очень красиво, поэтому идём дальше — на уровень операционной системы и хаков, которые не стоит использовать в продакшене.

SEH/VEH

Обратите внимание, что если между Managed и CLR летали Managed-exceptions, то между CLR и Windows летают SEH-exceptions.

SEH – Structured Exception Handling

На самом деле, конструкция throw тоже работает через SEH.

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

VEH — это векторная обработка исключений, расширение SEH, но работающее на уровне процесса, а не на уровне одного потока. Если SEH по семантике схож с try-catch, то VEH по семантике схож с обработчиком прерываний. Мы просто задаем свой обработчик и можем получать информацию обо всех исключениях, которые происходят в нашем процессе. Интересная возможность VEH — это то, что он позволяет изменить SEH-исключение до того, как оно попадет в обработчик.

С VEH можно взаимодействовать через WinApi:

В Context находится информация о состоянии всех регистров процессора в момент исключения. Нас же будет интересовать EXCEPTION_RECORD и поле ExceptionCode в нем. Мы можем подменить его на собственный код исключения, о котором CLR вообще ничего не знает. Векторный обработчик выглядит так:

Теперь сделаем обёртку, устанавливающую векторный обработчик в виде метода HandleSO, который принимает в себя делегат, который потенциально может упасть со StackOverflowException (для наглядности в коде нет обработки ошибок функций WinApi и удаления векторного обработчика).

Внутри него также используется метод SetThreadStackGuarantee. Этот метод резервирует место на стеке под обработку StackOverflow.

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

Но, что произойдет, если вызвать HandleSO дважды в одном потоке?

А произойдёт AccessViolationException. Вернемся к устройству стека.

Операционная система умеет детектировать переполнение стека. В самом верху стека лежит специальная страница, помеченная флагом Guard page. При первом обращении к этой странице произойдет другое исключение – STATUS_GUARD_PAGE_VIOLATION, а флаг Guard page со страницы снимается. Если просто перехватить это переполнение, то этой страницы на стеке больше не будет – при следующем переполнении операционная система не сможет этого понять и stack-pointer выйдет за границы памяти, выделенной под стек. Как итог — произойдет AccessViolationException. Значит нужно восстанавливать флаги страниц после обработки StackOverflow – cамый простой способ это сделать – использовать метод _resetstkoflw из библиотеки рантайма C (msvcrt.dll).

Изучив практические вопросы, подведем общие итоги:

Ссылки

22-23 ноября Евгений выступит на DotNext 2018 Moscow с докладом «Системные метрики: собираем подводные камни». А еще в Москву приедут Джеффри Рихтер, Грег Янг, Павел Йосифович и другие не менее интересные спикеры. Темы докладов можно посмотреть здесь, а купить билеты — здесь. Присоединяйтесь!

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *