Почему.NET ведет себя так плохо, когда генерируется StackruException?
Я знаю, что StackruExceptions в.NET не может быть перехвачен, завершает их процесс и не имеет трассировки стека. Это официально задокументировано в MSDN. Однако мне интересно, какие технические (или другие) причины стоят за поведением. Все MSDN говорит:
В предыдущих версиях.NET Framework ваше приложение могло перехватывать объект StackruException (например, для восстановления после неограниченной рекурсии). Однако такая практика в настоящее время не рекомендуется, поскольку требуется значительный дополнительный код, чтобы надежно перехватить исключение переполнения стека и продолжить выполнение программы.
Что это за "существенный дополнительный код"? Существуют ли другие задокументированные причины такого поведения? Даже если мы не можем поймать SOE, почему мы не можем хотя бы получить трассировку стека? Несколько коллег и я просто потратили несколько часов на отладку производственного исключения StackruException, которое заняло бы минуты с трассировкой стека, поэтому мне интересно, есть ли веская причина для моих страданий.
3 ответа
Стек потока создается 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, C++ или Delphi, код генерируется во время сборки. Однако это все еще очень трудная ошибка, аварийное пространство в стеке взорвано, поэтому не существует сценария, при котором продолжение выполнения кода в потоке является безопасным. Вероятность того, что программа может продолжать работать правильно с потоком, прерванным в совершенно случайном месте и довольно поврежденном состоянии, весьма маловероятна.
Если были предприняты какие-либо усилия, чтобы рассмотреть возможность создания события в другом потоке или удалить ограничение в winapi (количество защитных страниц не настраивается), то это либо очень хорошо держится в секрете, либо просто не считается полезным. Я подозреваю, что последнее, я не знаю это точно.
Стек - это место, где хранится практически все о состоянии программы. Адрес каждого сайта возврата при вызове методов, локальные переменные, параметры метода и т. Д. Если метод переполняет стек, его выполнение должно по необходимости немедленно прекратиться (поскольку для продолжения работы стека не осталось места), Затем, чтобы изящно восстановиться, кто-то должен очистить все, что этот метод сделал со стеком, прежде чем он умер. Это значит знать, как выглядит стек до вызова метода. Это влечет за собой некоторые накладные расходы.
И если вы не можете очистить стек, то вы также не сможете получить трассировку стека, потому что информация, необходимая для генерации трассировки, исходит из "развертывания" стека, чтобы определить, какие методы были вызваны.
Чтобы корректно обработать условия переполнения стека или нехватки памяти, необходимо несколько раз вызвать исключение, прежде чем стек фактически переполнится или память кучи полностью исчерпана, в то время как доступные ресурсы стека и кучи будут достаточны для выполнения любого код очистки, который нужно будет выполнить до того, как будут обнаружены исключения. В случае исключений переполнения стека, для их аккуратной обработки в основном потребуется проверка указателя стека при входе в каждый метод (что на самом деле не должно быть слишком дорогим). Обычно они обрабатываются путем установки ловушки нарушения доступа сразу за концом стека, но проблема с этим заключается в том, что ловушка не срабатывает до тех пор, пока не станет слишком поздно для чистой обработки. Можно установить срабатывание ловушки в последнем блоке памяти стека, а не в прошлом, и заставить систему заменить ловушку на блок после стека после того, как она сработает и вызовет StackruException
, но проблема в том, что не было бы хорошего способа гарантировать, что ловушка "почти вне стека" была повторно включена после того, как стек развернулся так далеко.
При этом альтернативный подход состоял бы в том, чтобы позволить потокам установить делегат для того, что должно произойти, если поток разрушает свой стек, а затем сказать, что в случае StackruException
стек потока будет очищен, и он будет запускать предоставленный делегат. Ловушка может быть восстановлена перед выполнением делегата (в этот момент стек будет пустым), и код может поддерживать объект состояния потока, который делегат может использовать, чтобы узнать, является ли какой-либо важный объект. finally
блоки были пропущены.