Working on the CLR’s exception handling subsystem is not just challenging but also gives unique opportunities to understand how the various exception handling subsystems (e.g. SEH, C++ EH, etc) work, and in particular, how they interoperate with one another. It was such case that had me discuss one such issue with Kevin, development lead of the JIT, that he mentions here.
That discussion got me exploring the interoperability aspects of EH further and resulted in the bullets listed below. Incase you are interested, download the PDF version of this article from here and the accompanying source code from here. For those uninitiated to exception handling, Matt Pietrek’s must read article is here.
Now, onto the interoperability aspects of EH.
Note: the following discussion is in context of the x86 platform
- Catching an exception using __try/__except SEH handler will result in RtlUnwind being called without exception record
Structured Exception Handling (SEH) is built into the Windows OS and it is how the OS processes the exceptions. Compilers offer their own specific ways of setting up a SEH handler. To setup one using Microsoft Visual C++, we use __try/__except keywords. For example:__try
{
printf(“Calling the /EHSc function\n“);
InvokeCPPFunction();
}
__except (IsForUs(GetExceptionInformation()))
{
printf(“In __except\n“);
}If the InvokeCPPFunction throws an exception, the OS will invoke the filter function (the IsForUs function referenced in the braces of __except) and check its return value. The filter function can be passed exception details that can be retrieved using the GetExceptionInformation function and using that information, it can decide whether it wants to process the exception or not.
If the filter function returns EXCEPTION_CONTINUE_EXECUTION, the OS retries the instruction that created the exception. However, if the return value is EXCEPTION_CONTINUE_SEARCH, it tells the OS that the filter function has declined to process the exception and the OS continues its search for the handler in the SEH handler chain (that is available from FS:0 on x86 machines for each OS thread).
The filter function can also return EXCEPTION_EXECUTE_HANDLER to tell the OS that it wants to handle the exception and the OS proceeds to invoke the funclet within the __except block. But before invoking the funclet, the OS has to unwind the call stack. On x86, the code generated by VC++ compiler does this by invoking RtlUnwind API, with NULL for the third argument – this argument is a pointer to the exception record for the exception being processed. This is done by the compiler by invoking GlobalUnwind (an internal function) after doing some processing. GlobalUnwind makes the call to RtlUnwind as shown below:
@_EH4_GlobalUnwind@4:
70508FB2 push ebp
70508FB3 mov ebp,esp
70508FB5 push ebx
70508FB6 push esi
70508FB7 push edi
70508FB8 push 0
70508FBA push 0
70508FBC push offset ReturnPoint (70508FC7h)
70508FC1 push ecx
70508FC2 call RtlUnwind (70526620h)
The highlighted red line is the one where NULL is pushed for exception record.
What this implies that all SEH handlers that are in the x86 FS:0 chain, prior to the one that agreed to process the exception, will be called by the OS once again to give them a chance to do some cleanup (e.g. release resources) – this is typically known as unwinding of the stack. And since the unwind was initiated with a NULL for the exception record, if your SEH handler relies on checking the exception record for details before deciding to do the cleanup, it can potentially fail (e.g. not end up doing the cleanup)!
- Corollary Lesson: Don’t throw (and expect to catch correctly) exception across environment boundaries. E.g. throwing a C++ exception and trying to catch it using a SEH handler
Since a C++ application can have a mix of both C++ exception handling constructs and SEH exception handing constructs, it is easy to commit the mistake of writing code that throws an exception from C++ EH and excepts that to be correctly caught and processed in SEH EH constructs – the focus is on correct processing.
Let’s extend the previous example. Assume that InvokeCPPFunction is a function in a DLL that was compiled to use C++ EH (using the /EHSc switch) and implemented as shown below:
class CPPClass2
{
public:
CPPClass2()
{
printf(“CPPClass2 constructor\n“);
}
~CPPClass2()
{
printf(“CPPClass2 destructor\n“);
}
};class CPPClass
{
public:
CPPClass()
{
printf(“CPPClass constructor\n“);
}
void ThrowException()
{
CPPClass2 cpp2;
throw 1;
}
~CPPClass()
{
printf(“CPPClass destructor\n“);
}
};__declspec (dllexport) void InvokeCPPFunction()
{
CPPClass cpp;
cpp.ThrowException();
}
We have two classes, CPPClass & CPPClass2 that have destructors each. InvokeCPPFunction instantiates CPPClass object, cpp, and invokes the ThrowException method, which in turn, instantiates CPPClass2 object, cpp2, and throws an exception.
As per the C++ semantics, when this exception is caught, the destructors should be invoked as they are expected to do the cleanup for the respective class instance. However, this depends upon who catches the exception.
In our previous example, InvokeCPPFunction was invoked from within __try/__except SEH mechanism. Hence, when ThrowException throws an exception, the OS walks the FS:0 chain to look for a handler that will handle the exception. When the OS comes to our __except block, if the filter funclet returns EXECEPTION_EXECUTE_HANDLER, as per our last discussion, RtlUnwind is invoked with a NULL for the exception record pointer.
When this happens and the unwind call comes to C++ exception handler, the C++ EH does not process the unwind since the exception record is NULL. This is because C++ EH only processes the unwind when the exception code in the exception record has the C++ exception code (0Xe06d7363). Thus, no destructors are invoked and that is not something you want. Below is the output this example:
Hence, it’s important to catch the exception in the environment/EH-context it was thrown in, since that EH-context will know how to process it correctly.