Почему .NET ведет себя так плохо, когда StackOverflowException выбрасывается?

Я знаю, что StackOverflowExceptions в .NET нельзя поймать, удалить свой процесс и не иметь трассировки стека. Это официально зарегистрировано в MSDN. Тем не менее, мне интересно, каковы технические (или другие) причины этого поведения. Все MSDN говорит:

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

Что это за "значительный дополнительный код"? Существуют ли другие документированные причины такого поведения? Даже если мы не можем поймать SOE, почему мы не можем получить трассировку стека? Несколько сотрудников, и я просто потратил несколько часов на отладку производственного StackOverflowException, которое потребовало бы минут с трассировкой стека, поэтому мне интересно, есть ли веская причина для моих страданий.

Ответ 1

Стек потока создается Windows. Он использует так называемые защитные страницы для обнаружения. Функция, которая обычно доступна для кода режима пользователя, как описано в этой статье библиотеки MSDN. Основная идея заключается в том, что последние две страницы стека (2 x 4096 = 8192 байта) зарезервированы, и любой доступ к ним процессора вызывает ошибку страницы, которая превратилась в исключение SEH, STATUS_GUARD_PAGE_VIOLATION.

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

Это исключение, в свою очередь, перехвачено CLR. В этот момент осталось около 3 килобайт пространства стека. Этого недостаточно для запуска Just-in-time компилятора (JITter) для компиляции кода, который мог бы обрабатывать исключение в вашем программы, JITTER требует гораздо больше места, чем это. Таким образом, CLR не может делать ничего, кроме грубого прерывания потока. И по политике .NET 2.0, которая также завершает процесс.

Обратите внимание, что это не проблема в Java, у него есть интерпретатор байт-кода, поэтому есть гарантия, что исполняемый код пользователя может работать. Или в не управляемой программе, написанной на таких языках, как C, С++ или Delphi, код генерируется во время сборки. Тем не менее, все же очень сложная проблема, с которой приходится иметь дело, аварийное место в стеке выдувается, поэтому нет сценария, в котором безопасный запуск кода в потоке безопасен. Вероятность того, что программа может продолжать корректно работать с потоком, прерванным в совершенно случайном месте и довольно поврежденным состоянием, маловероятна.

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

Ответ 2

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

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

Ответ 3

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

Было сказано, что альтернативным подходом было бы позволить потокам устанавливать делегат для того, что должно произойти, если поток ударит его стек, а затем скажет, что в случае StackOverflowException стек потока будет очищен, и он будет запустите предоставленный делегат. Ловушка может быть восстановлена ​​до запуска делегата (стек будет пустым в этой точке), а код может поддерживать объект статуса потока, который делегат мог бы использовать, чтобы узнать, не прошли ли какие-либо важные блоки finally.