Net stack overflow exception



Stack Overflow Exception Class

Definition

Some information relates to prerelease product that may be substantially modified before it’s released. Microsoft makes no warranties, express or implied, with respect to the information provided here.

The exception that is thrown when the execution stack exceeds the stack size. This class cannot be inherited.

Examples

The following example uses a counter to ensure that the number of recursive calls to the Execute method do not exceed a maximum defined by the MAX_RECURSIVE_CALLS constant.

Remarks

StackOverflowException is thrown for execution stack overflow errors, typically in case of a very deep or unbounded recursion. So make sure your code doesn’t have an infinite loop or infinite recursion.

StackOverflowException uses the HRESULT COR_E_STACKOVERFLOW, which has the value 0x800703E9. The Localloc intermediate language (IL) instruction throws StackOverflowException . For a list of initial property values for a StackOverflowException object, see the StackOverflowException constructors.

Starting with the .NET Framework 2.0, you can’t catch a StackOverflowException object with a try / catch block, and the corresponding process is terminated by default. Consequently, you should write your code to detect and prevent a stack overflow. For example, if your app depends on recursion, use a counter or a state condition to terminate the recursive loop. See the Examples section for an illustration of this technique.

Applying the HandleProcessCorruptedStateExceptionsAttribute attribute to a method that throws a StackOverflowException has no effect. You still cannot handle the exception from user code.

If your app hosts the common language runtime (CLR), it can specify that the CLR should unload the application domain where the stack overflow exception occurs and let the corresponding process continue. For more information, see ICLRPolicyManager Interface.

Constructors

Initializes a new instance of the StackOverflowException class, setting the Message property of the new instance to a system-supplied message that describes the error, such as «The requested operation caused a stack overflow.» This message takes into account the current system culture.

Initializes a new instance of the StackOverflowException class with a specified error message.

Initializes a new instance of the StackOverflowException class with a specified error message and a reference to the inner exception that is the cause of this exception.

Properties

Gets a collection of key/value pairs that provide additional user-defined information about the exception.

(Inherited from Exception) HelpLink

Gets or sets a link to the help file associated with this exception.

(Inherited from Exception) HResult

Gets or sets HRESULT, a coded numerical value that is assigned to a specific exception.

(Inherited from Exception) InnerException

Gets the Exception instance that caused the current exception.

(Inherited from Exception) Message

Gets a message that describes the current exception.

(Inherited from Exception) Source

Gets or sets the name of the application or the object that causes the error.

(Inherited from Exception) StackTrace

Gets a string representation of the immediate frames on the call stack.

(Inherited from Exception) TargetSite

Gets the method that throws the current exception.

(Inherited from Exception)

Methods

Determines whether the specified object is equal to the current object.

(Inherited from Object) GetBaseException()

When overridden in a derived class, returns the Exception that is the root cause of one or more subsequent exceptions.

(Inherited from Exception) GetHashCode()

Serves as the default hash function.

(Inherited from Object) GetObjectData(SerializationInfo, StreamingContext)

When overridden in a derived class, sets the SerializationInfo with information about the exception.

(Inherited from Exception) GetType()

Gets the runtime type of the current instance.

(Inherited from Exception) MemberwiseClone()

Creates a shallow copy of the current Object.

(Inherited from Object) ToString()

Creates and returns a string representation of the current exception.

(Inherited from Exception)

Events

Occurs when an exception is serialized to create an exception state object that contains serialized data about the exception.

Источник

Stack Overflow Exception Класс

Определение

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

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

Примеры

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

Комментарии

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

StackOverflowException использует COR_E_STACKOVERFLOW HRESULT, который имеет значение 0x800703E9. Вызывается Localloc StackOverflowException инструкция промежуточного языка (IL). Список начальных значений свойств для StackOverflowException объекта см. в StackOverflowException конструкторах.

Начиная с платформа .NET Framework 2.0, невозможно перехватывать StackOverflowException объект с блоком try / catch , и соответствующий процесс завершается по умолчанию. Следовательно, необходимо написать код для обнаружения и предотвращения переполнения стека. Например, если приложение зависит от рекурсии, используйте счетчик или условие состояния для завершения рекурсивного цикла. Примеры см. в разделе «Примеры » для иллюстрации этого метода.

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

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

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

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

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

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

Свойства

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

(Унаследовано от Exception) HelpLink

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

(Унаследовано от Exception) HResult

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

(Унаследовано от Exception) InnerException

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

(Унаследовано от Exception) Message

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

(Унаследовано от Exception) Source

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

(Унаследовано от Exception) StackTrace

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

(Унаследовано от Exception) TargetSite

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

(Унаследовано от Exception)

Методы

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

(Унаследовано от Object) GetBaseException()

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

(Унаследовано от Exception) GetHashCode()

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

(Унаследовано от Object)

Читайте также:  Error code accessdenied code message access denied message requestid
GetObjectData(SerializationInfo, StreamingContext)

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

(Унаследовано от Exception) GetType()

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

(Унаследовано от Exception) MemberwiseClone()

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

(Унаследовано от Object) ToString()

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

(Унаследовано от Exception)

События

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

Источник

Особые исключения в .NET и как их готовить

У разных исключений в .NET есть свои особенности, и знать их бывает очень полезно. Как обмануть CLR? Как остаться в живых в рантайме, поймав StackOverflowException? Какие исключения перехватить вроде бы нельзя, но если очень хочется, то можно?

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

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

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

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

  1. Поведение исключений в .NET
  2. Обработка исключений в Windows и хаки

Всё рассмотренное далее верно для Windows. Все примеры тестировались на последней версии полного фреймворка .NET 4.7.1. Также будет немного упоминаний .NET Core.

Access Violation

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

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

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

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

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

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

Единственное, что делает метод Copy — вызывает метод CopyToNative, реализованный внутри .NET. Если наше приложение все-таки падает и исключение где-то происходит, то это может происходить только внутри CopyToNative. Отсюда можно сделать первое наблюдение: если .NET-код вызвал нативный код и внутри него произошел AccessViolation, то .NET-код это исключение по какой-то причине обработать не может.

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

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

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

Чтобы понять, почему так происходит, нам нужно вспомнить как устроена память процесса. В памяти процесса все адреса – виртуальные. Это значит, что у приложения есть большое адресное пространство и лишь некоторые страницы из него отображаются в реальной физической памяти. Но есть особенность: первые 64 КБ адресов никогда не отображаются в физическую память и не отдаются приложению. Рантайм .NET об этом знает и использует это. Если AccessViolation произошел в managed-коде, то рантайм проверяет, по какому именно адресу в памяти происходило обращение, и генерирует соответствующее исключение. Для адресов от 0 до 2^16 — NullReference, для всех остальных – AccessViolation.

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

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

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

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

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

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

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

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

Возникает вопрос: можем ли мы обработать AccessViolationException, который произошел в нативном коде или в управляемом, но не преобразованный в NullReference и не выброшенный с использованием throw? Это иногда полезная возможность, особенно при работе с unsafe-кодом. Ответ на этот вопрос зависит от версии .NET.

В .NET 1.0 вообще не было никакого AccessViolationException. Все ссылки считались либо валидными, либо нулевыми. Ко времени .NET 2.0 стало понятно, что без прямой работы с памятью – никак, и AccessViolation появился, при этом был обрабатываемым. В 4.0 и выше он по-прежнему остался обрабатываемым, но обработать его уже не так просто. Для перехвата этого исключения теперь нужно пометить метод, в котором находится блок catch атрибутом HandleProcessCorruptedStateException. Видимо, разработчики так сделали, потому что посчитали, что AccessViolationException — это не то исключение, которое надо ловить в обычном приложении.
Кроме того, для обратной совместимости есть возможность использовать настройки рантайма:

  • legacyNullReferenceExceptionPolicy возвращает поведение .NET 1.0 – все AV превращаются в NRE
  • legacyCorruptedStateExceptionsPolicy возвращает поведение .NET 2.0 – все AV перехватываемы
Читайте также:  Print exception text python

В .NET Core AccessViolation не обрабатывается вообще.

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

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

В этот хелпер мы передавали 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() будет его ждать. Так мы получаем блокировку почти на пустом месте.

В .NET Core метод thread.Abort() выбрасывает PlatformNotSupportedException. И я считаю, что это очень хорошо, потому что мотивирует пользоваться не thread.Abort(), а неинвазивными методами остановки выполнения кода, например с помощью CancellationToken.

OUT OF MEMORY

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

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

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

Теперь даже gcAllowVeryLargeObjects не поможет. Все из-за того, что в .NET есть ограничение на максимальный индекс в массиве. Это ограничение меньше, чем int.MaxValue.

Max array index:

  • byte arrays – 0x7FFFFFC7
  • other arrays – 0X7FEFFFFF

В этом случае произойдёт OutOfMemoryException, хотя на самом деле мы уперлись в ограничение типа данных, а не в недостаток памяти.

Иногда OutOfMemory явно выбрасывается управляемым кодом внутри .NET фреймворка:

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

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

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

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

  • Исключение обработается
  • Процесс упадёт
  • Зайдём в catch, но исключение вылетит снова
  • Зайдём в catch, но вылетит StackOverflow

В этом случае программа получится абсолютно недетерминированной. Разберем все варианты:

  1. Исключение может обработаться. Внутри .NET ничто не мешает обрабатывать OutOfMemoryException.
  2. Процесс может упасть. Не нужно забывать, что у нас managed-приложение. Это означает, что внутри него выполняется не только наш код, но и код рантайма. Например, GC. Таким образом, может случиться ситуация, когда рантайм захочет себе выделить память, но не сможет это сделать, тогда мы не сможем перехватить исключение.
  3. Зайдем в catch, но исключение вылетит снова. Внутри catch мы тоже выполняем работу, при которой нам понадобится память (печатаем исключение на консоль), а это может вызвать новое исключение.
  4. Зайдем в catch, но вылетит StackOverflow. Сам StackOverflow происходит при вызове метода WriteLine, но переполнения стека здесь нет, а происходит другая ситуация. Разберём её подробнее.

В виртуальной памяти страницы могут быть не только отображены в физическую память, но и могут быть зарезервированными (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:

  • «Ensures that the remaining stack space is large enough to execute the average .NET Framework function.» — MSDN
  • InsufficientExecutionStackException
  • 512 KB – x86, AnyCPU, 2 MB – x64 (half of stack size)
  • 64/128 KB — .NET Core
  • Check only stack address space
Читайте также:  Unknown download error please reinstall from google play что делать

В документации по этому методу сказано, что он проверяет, что на стеке достаточно места для выполнения средней функции .NET. Но что же такое средняя функция? На самом деле в .NET Framework этот метод проверяет, что на стеке свободна хотя бы половина от его размера. В .NET Core он проверяет чтобы было свободно 64 КБ.

Также в .NET Core появился аналог: RuntimeHelpers.TryEnsureSufficientExecutionStack() возвращающий bool, а не бросающий исключение.

В 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.

«An application that hosts the CLR can change the default behavior and specify that the CLR unload the application domain where the exception occurs, but lets the process continue.» — MSDN
StackOverflowException -> AppDomainUnloadedException

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

При запуске managed-приложение, автоматически запускается рантайм .NET. Но можно пойти по другому пути. Например, написать unmanaged-приложение (на С++ или другом языке), которое будет использовать специальное API для того, чтобы поднять CLR и запустить наше приложение. Приложение, которое запускает внутри себя CLR будем называть CLR-host. Написав его, мы можем сконфигурировать многие вещи в рантайме. Например, подменить менеджер памяти и менеджер потоков. Мы в продакшене используем CLR-host для того, чтобы избежать попадания страниц памяти в своп.

Следующий код конфигурирует 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

  • Механизм обработки исключений в Windows
  • Единообразная обработка software и hardware исключений
  • C# исключения реализованы поверх SEH

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

Рантайм .NET знает о SEH-исключениях и умеет их конвертировать в managed-исключения:

  • EXCEPTION_STACK_OVERFLOW -> Crash
  • EXCEPTION_ACCESS_VIOLATION -> AccessViolationException
  • EXCEPTION_ACCESS_VIOLATION -> NullReferenceException
  • EXCEPTION_INT_DIVIDE_BY_ZERO -> DivideByZeroException
  • Unknown SEH exceptions -> SEHException

Взаимодействовать с SEH мы можем через WinApi.

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

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

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

Мы можем поставить между операционной системой и рантаймом собственный векторный обработчик, который будет обрабатывать SEH-исключения и при встрече с EXCEPTION_STACK_OVERFLOW изменять его так, чтобы рантайм .NET не крэшил процесс.

С 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).

Аналогичным способом можно перехватить AccessViolationException в .NET Core под Windows, который приводит к падению процесса. При этом понадобиться учесть порядок вызова векторных обработчиков и установить свой обработчик в начало цепочки, так как .NET Core также использует VEH при обработке AccessViolation. За порядок вызова обработчиков отвечает первый параметр функции AddVectoredExceptionHandler:

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

  • Исключения не так просты, как кажутся;
  • Не все исключения обрабатываются одинаково;
  • Обработка исключений происходит на разных уровнях абстракции;
  • Можно вмешаться в процесс обработки исключений и заставить рантайм .NET работать не так, как было задумано изначально.

Ссылки

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

Источник

Оцените статью
toolgir.ru
Adblock
detector