CLR and Unhandled Exception Filters
I was recently asked how the CLR and Win32 unhandled exception filters interoperate. Specifically, the original question was (edited for clarity):
We’ve registered to the current AppDomain's UnhandledException event handler. From unmanaged code, we’ve called the SetUnhandledExceptionFilter Win32 API function. In the face of an exception, all unhandled exceptions are caught by the unmanaged exception handler, and the call stack of managed exceptions is shown corrupted. Removing the unmanaged registration catches managed exceptions correctly, but then unmanaged exceptions are lost.
From looking at the SSCLI implementation and running a couple of tests I was able to conclude the following:
- The CLR relies on the SEH unhandled exception filter mechanism to catch unhandled exceptions.
- The CLR's exception filter does the following:
- Invokes the previous exception filter in the chain;
- Invokes the delegates registered to AppDomain.UnhandledException in the AppDomain;
- Migrates the thread to the default AppDomain and again invokes delegates registered to AppDomain.UnhandledException.
So if you install your own unhandled exception filter, there are two options:
- The CLR has installed its exception filter before you have installed your exception filter. In this case, the CLR will invoke your exception filter before performing its own exception processing (because it's a good citizen of Win32);
- The CLR has installed its exception filter after you have installed your exception filter. In this case, you should invoke the CLR's exception filter before performing your processing (because you are a good citizen of Win32).
This shows that one way or another, you get a chance to process the unhandled exception before the CLR performs its processing. If you want to specifically filter out exceptions that are raised by the CLR itself, you can pass to the CLR any exceptions with the exception code 0xE0434F4E ('COM'+1), which is the SEH exception code for CLR exceptions. However, this is not enough (because not all CLR-handled exceptions have the CLR SEH exception code), as the following program demonstrates.
(If all you were interested in is just getting your unmanaged exception filter and the CLR exception filter working in unison, then you can stop reading right now. However, if you're curious, read on...)
The demo consists of two assemblies, a C# console application and a C++/CLI class library used to register an unmanaged exception filter and raise an unmanaged exception (this could be done on either side; fully in C++/CLI by throwing a managed exception, or fully on the C# side by using unsafe code and dereferencing an invalid pointer). In the C# application:
In the C++/CLI class library:
What happens now depends on which kind of exception was thrown. If an unmanaged access violation occurred (SEHInstaller.CauseUnmanagedException), then the output looks as follows - note that the native exception code is 0xC0000005 which is the exception code for an access violation. The unmanaged exception filter was invoked first.
If a managed exception occurred (ThrowApplicationException), then the output looks as follows - note that the exception code is 0xE0434F4D which is the exception code for exceptions raised by the CLR. The unmanaged exception filter was invoked first.
Finally, if a managed exception manifests as dereferencing a null reference (p.ToString), then the output looks as follows - note that the exception code is 0xC0000005 again which means it's not a CLR-induced exception - it's a native exception that the CLR itself swallows and turns into a NullReferenceException by the time the AppDomain's unhandled exception handler executes!
This does make sense, because here's the sequence of assembly instructions for calling the Program.ToString method:
The second statement dereferences the ECX register, because it's necessary in order to find the virtual method slot for the ToString method. This dereference is not guarded by a SEH try block, so it manifests as an unhandled exception and later translated by the CLR, as we have seen.
(You can find the demo code here - 9KB, Visual Studio 2008 solution.)