As várias exceções no .NET têm suas próprias características e pode ser muito útil conhecê-las. Como enganar o CLR? Como permanecer vivo em tempo de execução capturando uma StackOverflowException? Que exceções parece impossível captar, mas se você realmente quiser, pode?

Sob o corte, a transcrição do relatório de Eugene (
epeshk ) Peshkov da nossa conferência
DotNext 2018 Piter , onde ele falou sobre essas e outras características das exceções.
Oi Meu nome é Eugene. Trabalho na SKB Kontur e desenvolvo um sistema de hospedagem e implanto aplicativos para Windows. O resultado final é que temos muitas equipes de produtos que escrevem seus próprios serviços e os hospedam conosco. Fornecemos a eles uma solução fácil e simples para uma variedade de tarefas de infraestrutura. Por exemplo, para monitorar o consumo de recursos do sistema ou concluir réplicas para o serviço.
Às vezes, os aplicativos hospedados em nosso sistema desmoronam. Vimos muitas maneiras de como um aplicativo pode falhar em tempo de execução. Um desses métodos é lançar alguma exceção inesperada e encantadora.
Hoje vou falar sobre os recursos das exceções no .NET. Encontramos alguns desses recursos na produção e outros no decorrer de experimentos.
Planejar
- Comportamento de exceção do .NET
- Manipulação de exceção do Windows e hacks
Tudo o que se segue é verdadeiro para Windows. Todos os exemplos foram testados na versão mais recente da estrutura completa do .NET 4.7.1. Também haverá algumas referências ao .NET Core.
Violação de acesso
Essa exceção ocorre durante operações incorretas de memória. Por exemplo, se um aplicativo tentar acessar uma área de memória à qual não tem acesso. A exceção é de nível baixo e, geralmente, se isso acontecer, será necessária uma depuração muito longa.
Vamos tentar obter essa exceção usando C #. Para fazer isso, escreveremos o byte 42 no endereço 1000 (presumimos que 1000 seja um endereço aleatório e nosso aplicativo provavelmente não tem acesso a ele).
try { Marshal.WriteByte((IntPtr) 1000, 42); } catch (AccessViolationException) { ... }
WriteByte faz exatamente o que precisamos: escreve um byte no endereço fornecido. Esperamos que esta chamada gere uma AccessViolationException. Esse código realmente lançará essa exceção, ele poderá manipular e o aplicativo continuará funcionando. Agora vamos mudar um pouco o código:
try { var bytes = new byte[] {42}; Marshal.Copy(bytes, 0, (IntPtr) 1000, bytes.Length); } catch (AccessViolationException) { ... }
Se, em vez de WriteByte, você usar o método Copy e copiar o byte 42 para o endereço 1000, usando try-catch, o AccessViolation não poderá ser capturado. Ao mesmo tempo, uma mensagem será exibida no console informando que o aplicativo foi encerrado devido a uma AccessViolationException não tratada.
Marshal.Copy(bytes, 0, (IntPtr) 1000, bytes.Length); Marshal.WriteByte((IntPtr) 1000, 42);
Acontece que temos duas linhas de código, enquanto a primeira trava o aplicativo inteiro com o AccessViolation e a segunda gera uma exceção processada do mesmo tipo. Para entender por que isso acontece, veremos como esses métodos são organizados por dentro.
Vamos começar com o método Copy.
static void Copy(...) { Marshal.CopyToNative((object) source, startIndex, destination, length); } [MethodImpl(MethodImplOptions.InternalCall)] static extern void CopyToNative(object source, int startIndex, IntPtr destination, int length);
A única coisa que o método Copy faz é chamar o método CopyToNative, implementado dentro do .NET. Se nosso aplicativo ainda travar e ocorrer uma exceção em algum lugar, isso só poderá ocorrer dentro de CopyToNative. A partir daqui, podemos fazer a primeira observação: se o código .NET chamado código nativo e AccessViolation ocorrerem dentro dele, o código .NET não poderá lidar com essa exceção por algum motivo.
Agora vamos entender por que foi possível processar o AccessViolation usando o método WriteByte. Vamos dar uma olhada no código para este método:
unsafe static void WriteByte(IntPtr ptr, byte val) { try { *(byte*) ptr = val; } catch (NullReferenceException) {
Este método é totalmente implementado no código gerenciado. Ele usa o ponteiro do C # para gravar dados no endereço desejado e também captura uma NullReferenceException. Se o NRE for interceptado, uma AccessViolationException será lançada. Portanto, é necessário por causa da
especificação . Nesse caso, todas as exceções lançadas pela construção throw são tratadas. Portanto, se uma NullReferenceException ocorrer durante a execução do código dentro do WriteByte, podemos capturar o AccessViolation. Poderia ocorrer um NRE, no nosso caso, ao acessar o endereço 1000 em vez do endereço zero?
Reescrevemos o código usando ponteiros C # diretamente e vemos que, ao acessar um endereço diferente de zero, uma NullReferenceException é realmente lançada:
*(byte*) 1000 = 42;
Para entender por que isso acontece, precisamos lembrar como a memória do processo funciona. Na memória do processo, todos os endereços são virtuais. Isso significa que o aplicativo possui um grande espaço de endereço e apenas algumas páginas são exibidas na memória física real. Mas há uma peculiaridade: os primeiros 64 KB de endereços nunca são mapeados para a memória física e não são fornecidos ao aplicativo. O Rantime .NET sabe disso e o usa. Se o AccessViolation ocorreu no código gerenciado, o tempo de execução verifica qual endereço na memória foi acessado e gera uma exceção apropriada. Para endereços de 0 a 2 ^ 16 - NullReference, para todos os outros - AccessViolation.

Vamos ver por que o NullReference é lançado não apenas ao acessar o endereço zero. Imagine que você está acessando um campo de um objeto de um tipo de referência e a referência a esse objeto é nula:

Nessa situação, esperamos obter uma NullReferenceException. O acesso ao campo de um objeto ocorre por deslocamento em relação ao endereço desse objeto. Acontece que nos voltaremos para um endereço próximo do zero (lembre-se de que o link para o objeto original é zero). Com esse comportamento de tempo de execução, obtemos a exceção esperada sem verificação adicional do endereço do próprio objeto.
Mas o que acontece se nos voltarmos para o campo de um objeto, e esse objeto em si ocupa mais de 64 KB?

Podemos obter o AccessViolation neste caso? Vamos fazer um experimento. Vamos criar um objeto muito grande e nos referiremos a seus campos. Um campo no início do objeto, o segundo no final:

Ambos os métodos lançarão uma NullReferenceException. Nenhuma AccessViolationException ocorrerá.
Vejamos as instruções que serão geradas para esses métodos. No segundo caso, o compilador JIT adicionou uma instrução cmp adicional que acessa o endereço do próprio objeto, chamando AccessViolation com um endereço zero, que será convertido pelo tempo de execução em um NullReferenceException.
Vale ressaltar que, para este experimento, não é suficiente usar uma matriz como um objeto grande. Porque Deixe essa pergunta para o leitor, escreva idéias nos comentários :)
Vamos resumir os experimentos com o AccessViolation.

AccessViolationException se comporta de maneira diferente dependendo de onde a exceção ocorreu (no código gerenciado ou no nativo). Além disso, se uma exceção ocorreu no código gerenciado, o endereço do objeto será verificado.
A questão é: podemos lidar com uma AccessViolationException que ocorreu no código nativo ou no código gerenciado, mas não convertido em NullReference e não lançado usando throw? Às vezes, esse é um recurso útil, especialmente ao trabalhar com código não seguro. A resposta a esta pergunta depende da versão do .NET.

No .NET 1.0, não havia AccessViolationException. Todos os links foram considerados válidos ou nulos. Na época do .NET 2.0, ficou claro que, sem o trabalho direto com a memória - de jeito nenhum, o AccessViolation aparecia enquanto era processável. No 4.0 e acima, ele ainda era praticável, mas o processamento não é tão simples. Para capturar essa exceção, agora você precisa marcar o método no qual o bloco catch está localizado com o atributo HandleProcessCorruptedStateException. Aparentemente, os desenvolvedores fizeram isso porque pensavam que AccessViolationException não era a exceção que deveria ser capturada em um aplicativo regular.
Além disso, para compatibilidade com versões anteriores, é possível usar as configurações de tempo de execução:
- legacyNullReferenceExceptionPolicy retorna o comportamento do .NET 1.0 - todos os AVs se transformam em NRE
- legacyCorruptedStateExceptionsPolicy retorna o comportamento do .NET 2.0 - todos os antivírus são interceptados
No .NET, o Core AccessViolation não é tratado.
Em nossa produção, havia uma situação assim:

Um aplicativo criado no .NET 4.7.1 usou uma biblioteca de códigos compartilhada criada no .NET 3.5. Havia um auxiliar nesta biblioteca para executar uma ação periódica:
while (isRunning) { try { action(); } catch (Exception e) { log.Error(e); } WaitForNextExecution(... ); }
Passamos a ação do nosso aplicativo para esse ajudante. Aconteceu que ele caiu com o AccessViolation. Como resultado, nosso aplicativo registrava constantemente o AccessViolation, em vez de travar porque o código da biblioteca em 3.5 pode pegá-lo. Deve-se observar que a interceptação não depende da versão do tempo de execução na qual o aplicativo está sendo executado, mas do TargetFramework, no qual o aplicativo foi construído, e de suas dependências.
Para resumir. O processamento do AccessVilolation depende de onde ele se originou - no código nativo ou gerenciado -, bem como nas configurações TargetFramework e de tempo de execução.
Interrupção de thread
Às vezes, no código, você precisa interromper a execução de um dos threads. Para fazer isso, você pode usar o thread.Abort ();
var thread = new Thread(() => { try { ... } catch (ThreadAbortException e) { ... Thread.ResetAbort(); } }); ... thread.Abort();
Quando o método Abort é chamado em um thread parado, é lançada uma ThreadAbortException. Vamos analisar suas características. Por exemplo, um código como este:
var thread = new Thread(() => { try { … } catch (ThreadAbortException e) { … } }); ... thread.Abort();
Absolutamente equivalente a isso:
var thread = new Thread(() => { try { ... } catch (ThreadAbortException e) { ... throw; } }); ... thread.Abort();
Se você ainda precisar processar o ThreadAbort e executar algumas outras ações no segmento parado, poderá usar o método Thread.ResetAbort (); Ele interrompe o processo de interrupção do fluxo e a exceção para de subir mais alto na pilha. É importante entender que o próprio método thread.Abort () não garante nada - o código no thread parado pode impedir que ele pare.
Outro recurso do thread.Abort () é que ele não poderá interromper o código se estiver no catch e finalmente bloqueia.
Dentro do código da estrutura, geralmente é possível encontrar métodos em que o bloco try está vazio e toda a lógica está finalmente dentro. Isso é feito apenas para impedir que esse código seja lançado por um ThreadAbortException.
Além disso, uma chamada para o método thread.Abort () aguarda o lançamento de um ThreadAbortException. Combine esses dois fatos e faça com que o método thread.Abort () possa bloquear o thread de chamada.
var thread = new Thread(() => { try { } catch { }
Na realidade, isso pode ser encontrado ao usar o uso. Ele é implantado em try / finalmente, dentro do finalmente, o método Dispose é chamado. Pode ser arbitrariamente complexo, conter manipuladores de eventos, usar bloqueios. E se thread.Abort foi chamado em tempo de execução, Dispose - thread.Abort () aguardará por isso. Então, temos uma trava quase do zero.
No .NET Core, o método thread.Abort () lança uma PlatformNotSupportedException. E acho que isso é muito bom, porque me motiva a não usar thread.Abort (), mas métodos não invasivos para interromper a execução do código, por exemplo, usando o CancellationToken.
SEM MEMÓRIA
Essa exceção pode ser obtida se a memória na máquina for menor que o necessário. Ou quando encontramos as limitações de um processo de 32 bits. Mas você pode obtê-lo mesmo que o computador tenha muita memória livre e o processo seja de 64 bits.
var arr4gb = new int[int.MaxValue/2];
O código acima lançará OutOfMemory. O fato é que, por padrão, objetos maiores que 2 GB não são permitidos. Isso pode ser corrigido definindo gcAllowVeryLargeObjects em App.config. Nesse caso, uma matriz de 4 GB é criada.
Agora vamos tentar criar uma matriz ainda mais.
var largeArr = new int[int.MaxValue];
Agora, mesmo o gcAllowVeryLargeObjects não ajudará. Isso ocorre porque o .NET tem um
limite no índice máximo em uma matriz . Essa restrição é menor que int.MaxValue.
Índice máximo da matriz:
- matrizes de bytes - 0x7FFFFFC7
- outras matrizes - 0X7F E FFFFF
Nesse caso, ocorrerá uma OutOfMemoryException, embora na verdade tenhamos enfrentado uma restrição de tipo de dados, não uma falta de memória.
Às vezes, o OutOfMemory é explicitamente descartado pelo código gerenciado dentro da estrutura .NET:

Esta é uma implementação do método string.Concat. Se o comprimento da sequência de resultados for maior que int.MaxValue, uma OutOfMemoryException será lançada imediatamente.
Vamos para a situação em que OutOfMemory surge no caso em que a memória realmente se esgota.
LimitMemory(64.Mb()); try { while (true) list.Add(new byte[size]); } catch (OutOfMemoryException e) { Console.WriteLine(e); }
Primeiro, limitamos a memória do nosso processo a 64 MB. Em seguida, dentro do loop, selecione novas matrizes de bytes, salve-as em alguma planilha para que o GC não as colete e tente capturar OutOfMemory.
Nesse caso, tudo pode acontecer:
- Exceção tratada
- Processo cairá
- Vamos pegar, mas a exceção falhará novamente
- Vamos pegar, mas o StackOverflow falhará
Nesse caso, o programa será completamente não determinístico. Vamos analisar todas as opções:
- Uma exceção pode ser tratada. Dentro do .NET, não há nada que o impeça de manipular uma OutOfMemoryException.
- O processo pode cair. Não esqueça que temos um aplicativo gerenciado. Isso significa que dentro dele é executado não apenas o nosso código, mas também o código de tempo de execução. Por exemplo, GC. Portanto, pode ocorrer uma situação em que o tempo de execução deseja alocar memória para si mesmo, mas não pode fazê-lo; portanto, não conseguiremos capturar a exceção.
- Vamos direto ao ponto, mas a exceção falhará novamente. Dentro da captura, também fazemos o trabalho em que precisamos de memória (imprimimos uma exceção no console), e isso pode causar uma nova exceção.
- Vamos pegar, mas o StackOverflow falhará. O StackOverflow em si ocorre quando o método WriteLine é chamado, mas não há excesso de pilha aqui, mas ocorre uma situação diferente. Vamos analisá-lo em mais detalhes.

Na memória virtual, as páginas podem não apenas ser mapeadas para a memória física, mas também podem ser reservadas. Se a página estiver reservada, o aplicativo notou que iria usá-la. Se a página já estiver mapeada para memória real ou troca, será chamada de "confirmada" (confirmada). A pilha usa essa capacidade de dividir a memória em reservada e confirmada. Parece algo como isto:

Acontece que chamamos o método WriteLine, que ocupa algum lugar na pilha. Acontece que toda a memória bloqueada já terminou, o que significa que o sistema operacional neste momento deve pegar outra página reservada na pilha e mapeá-la para a memória física real, que já está cheia de matrizes de bytes. Isso leva à exceção do StackOverflow.
O código a seguir permitirá que você comprometa toda a memória da pilha no início do fluxo imediatamente.
new Thread(() => F(), 4*1024*1024).Start();
Como alternativa, você pode usar a
configuração de tempo de execução disableCommitThreadStack. Ele precisa ser desativado para que a pilha de encadeamentos seja confirmada com antecedência. Vale ressaltar que o comportamento padrão descrito na documentação e observado na realidade é diferente.

Estouro de pilha
Vamos dar uma olhada em StackOverflowException. Vejamos dois exemplos de código. Em um deles, executamos uma recursão infinita, o que leva a um estouro de pilha; no segundo, lançamos essa exceção com throw.
try { InfiniteRecursion(); } catch (Exception) { ... }
try { throw new StackOverflowException(); } catch (Exception) { ... }
Como todas as exceções lançadas com throw são tratadas, no segundo caso, capturaremos a exceção. E com o primeiro caso, tudo é mais interessante. Vá para o
MSDN :
"Você não pode capturar exceções de estouro de pilha, porque o código de tratamento de exceção pode exigir a pilha."
MSDN
Ele diz aqui que não conseguiremos capturar uma StackOverflowException, pois a própria interceptação pode exigir espaço adicional na pilha que já foi finalizado.
Para proteger de alguma forma contra essa exceção, podemos fazer o seguinte. Primeiro, você pode limitar a profundidade da recursão. Em segundo lugar, você pode usar os métodos da classe RuntimeHelpers:
RuntimeHelpers.EnsureSufficientExecutionStack ();
- "Garante que o espaço restante da pilha seja grande o suficiente para executar a função média do .NET Framework." - MSDN
- InsufficientExecutionStackException
- 512 KB - x86, Qualquer CPU, 2 MB - x64 (metade do tamanho da pilha)
- 64/128 KB - .NET Core
- Verifique apenas o espaço de endereço da pilha
A documentação para esse método diz que verifica se há espaço suficiente na pilha para executar a função .NET
média . Mas qual é a função
média ? De fato, no .NET Framework, esse método verifica se pelo menos metade do seu tamanho está livre na pilha. No .NET Core, ele verifica 64K gratuitamente.
Um análogo também apareceu no .NET Core: RuntimeHelpers.TryEnsureSufficientExecutionStack () que retorna um bool, em vez de lançar uma exceção.
O C # 7.2 introduziu a capacidade de usar Span e blockallock juntos sem usar código não seguro. Talvez por isso, o stackalloc seja usado com mais frequência no código e será útil ter uma maneira de se proteger do StackOverflow ao usá-lo, escolhendo onde alocar memória. Como tal, é proposto um método
que verifica a possibilidade de alocação na pilha e na construção
trystackalloc .
Span<byte> span; if (CanAllocateOnStack(size)) span = stackalloc byte[size]; else span = new byte[size];
Voltar para a documentação StackOverflow no MSDN
Em vez disso, quando ocorre um estouro de pilha em um aplicativo normal , o Common Language Runtime (CLR) termina o processo. ”
MSDN
Se houver um aplicativo "normal" que cai durante o StackOverflow, existem aplicativos não normais que não caem? Para responder a essa pergunta, você precisará descer um nível do nível de aplicativo gerenciado para o nível CLR.

"Um aplicativo que hospeda o CLR pode alterar o comportamento padrão e especificar que o CLR descarregue o domínio do aplicativo onde a exceção ocorre, mas permite que o processo continue." - MSDN
StackOverflowException -> AppDomainUnloadedException
Um aplicativo que hospeda o CLR pode redefinir o comportamento do estouro de pilha para que, em vez de concluir todo o processo, o Domínio do Aplicativo seja descarregado, no fluxo no qual esse estouro ocorreu. Assim, podemos transformar um StackOverflowException em um AppDomainUnloadedException.
Quando um aplicativo gerenciado é iniciado, o tempo de execução do .NET é iniciado automaticamente. Mas você pode seguir o outro caminho. Por exemplo, escreva um aplicativo não gerenciado (em C ++ ou outro idioma) que usará uma API especial para aumentar o CLR e iniciar nosso aplicativo. Um aplicativo que executa o CLR internamente será chamado CLR-host. Ao escrevê-lo, podemos configurar muitas coisas em tempo de execução. Por exemplo, substitua o gerenciador de memória e o gerenciador de threads. Em produção, usamos o host CLR para evitar a troca de páginas de memória.
O código a seguir configura o host CLR para que o AppDomain (C ++) seja descarregado durante o StackOverflow:
ICLRPolicyManager *policyMgr; pCLRControl->GetCLRManager(IID_ICLRPolicyManager, (void**) (&policyMgr)); policyMgr->SetActionOnFailure(FAIL_StackOverflow, eRudeUnloadAppDomain);
Essa é uma boa maneira de escapar do StackOverflow? Provavelmente não muito. Primeiro, tivemos que escrever código C ++, o que não queremos fazer. Em segundo lugar, devemos alterar nosso código C # para que a função que pode lançar uma StackOverflowException seja executada em um AppDomain separado e em um thread separado. Nosso código se transformará imediatamente em tal macarrão:
try { var appDomain = AppDomain.CreateDomain("..."); appDomain.DoCallBack(() => { var thread = new Thread(() => InfiniteRecursion()); thread.Start(); thread.Join(); }); AppDomain.Unload(appDomain); } catch (AppDomainUnloadedException) { }
Para chamar o método InfiniteRecursion, escrevemos várias linhas. Terceiro, começamos a usar o AppDomain. E isso quase garante um monte de novos problemas. Incluindo com exceções. Considere um exemplo:
public class CustomException : Exception {} var appDomain = AppDomain.CreateDomain( "..."); appDomain.DoCallBack(() => throw new CustomException()); System.Runtime.Serialization.SerializationException: Type 'CustomException' is not marked as serializable. at System.AppDomain.DoCallBack(CrossAppDomainDelegate callBackDelegate)
Como nossa exceção não está marcada como serializável, nosso código será descartado com uma SerializationException. E para corrigir esse problema, não basta marcar nossa exceção com o atributo Serializable, ainda precisamos implementar um construtor adicional para serialização.
[Serializable] public class CustomException : Exception { public CustomException(){} public CustomException(SerializationInfo info, StreamingContext ctx) : base(info, context){} } var appDomain = AppDomain.CreateDomain("..."); appDomain.DoCallBack(() => throw new CustomException());
Tudo não é muito bonito, então vamos além - ao nível do sistema operacional e dos hacks, que não devem ser usados na produção.
Seh / veh

Observe que, enquanto as exceções gerenciadas voavam entre o gerenciado e o CLR, as exceções SEH voam entre o CLR e o Windows.
SEH - Tratamento Estruturado de Exceções
- Mecanismo de manipulação de exceção do Windows
- Tratamento uniforme de exceções de software e hardware
- Exceções de C # implementadas no topo do SEH
O SEH é um mecanismo de manipulação de exceções no Windows, que permite lidar igualmente de maneira uniforme com as exceções que vieram, por exemplo, do nível do processador ou foram associadas à lógica do próprio aplicativo.
O Rantime .NET conhece as exceções SEH e pode convertê-las em exceções gerenciadas:
- EXCEPTION_STACK_OVERFLOW -> Travamento
- EXCEPTION_ACCESS_VIOLATION -> AccessViolationException
- EXCEPTION_ACCESS_VIOLATION -> NullReferenceException
- EXCEPTION_INT_DIVIDE_BY_ZERO -> DivideByZeroException
- Exceções de SEH desconhecidas -> SEHException
Podemos interagir com o SEH através do WinApi.
[DllImport("kernel32.dll")] static extern void RaiseException(uint dwExceptionCode, uint dwExceptionFlags, uint nNumberOfArguments,IntPtr lpArguments);
, throw SEH.
throw -> RaiseException(0xe0434f4d, ...)
, CLR-exception , , .
VEH — , SEH, , . SEH try-catch, VEH . , . VEH — , SEH- , .

, SEH- EXCEPTION_STACK_OVERFLOW , .NET .
VEH WinApi:
[DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr AddVectoredExceptionHandler(IntPtr FirstHandler, VECTORED_EXCEPTION_HANDLER VectoredHandler); delegate VEH PVECTORED_EXCEPTION_HANDLER(ref EXCEPTION_POINTERS exceptionPointers); public enum VEH : long { EXCEPTION_CONTINUE_SEARCH = 0, EXCEPTION_EXECUTE_HANDLER = 1, EXCEPTION_CONTINUE_EXECUTION = -1 } delegate VEH PVECTORED_EXCEPTION_HANDLER(ref EXCEPTION_POINTERS exceptionPointers); [StructLayout(LayoutKind.Sequential)] unsafe struct EXCEPTION_POINTERS { public EXCEPTION_RECORD* ExceptionRecord; public IntPtr Context; } delegate VEH PVECTORED_EXCEPTION_HANDLER(ref EXCEPTION_POINTERS exceptionPointers); [StructLayout(LayoutKind.Sequential)] unsafe struct EXCEPTION_RECORD { public uint ExceptionCode; ... }
Context . EXCEPTION_RECORD ExceptionCode . , CLR . :
static unsafe VEH Handler(ref EXCEPTION_POINTERS e) { if (e.ExceptionRecord == null) return VEH. EXCEPTION_CONTINUE_SEARCH; var record = e. ExceptionRecord; if (record->ExceptionCode != ExceptionStackOverflow) return VEH. EXCEPTION_CONTINUE_SEARCH; record->ExceptionCode = 0x01234567; return VEH. EXCEPTION_EXECUTE_HANDLER; }
, HandleSO, , StackOverflowException ( WinApi ).
HandleSO(() => InfiniteRecursion()) ; static T HandleSO<T>(Func<T> action) { Kernel32. AddVectoredExceptionHandler(IntPtr.Zero, Handler); Kernel32.SetThreadStackGuarantee(ref size); try { return action(); } catch (Exception e) when ((uint) Marshal. GetExceptionCode() == 0x01234567) {} return default(T); } HandleSO(() => InfiniteRecursion());
SetThreadStackGuarantee. StackOverflow.
. , .
, , HandleSO ?
HandleSO(() => InfiniteRecursion()); HandleSO(() => InfiniteRecursion());
AccessViolationException. .

. , Guard page. – STATUS_GUARD_PAGE_VIOLATION, Guard page . , – stack-pointer , . — AccessViolationException. StackOverflow – c – _resetstkoflw C (msvcrt.dll).
[DllImport("msvcrt.dll")] static extern int _resetstkoflw();
AccessViolationException .NET Core Windows, . , .NET Core VEH AccessViolation. AddVectoredExceptionHandler:
Kernel32.AddVectoredExceptionHandler(FirstHandler: (IntPtr) 1, handler);
, :
Referências
→
→
Dotnext 2016 Moscow — Adam Sitnik — Exceptional Exceptions in .NET→
DotNetBook: Exceptions→
.NET Inside Out Part 8 — Handling Stack Overflow Exception in C# with VEH — StackOverflow.
22-23 DotNext 2018 Moscow « : » . , , . , — . !