In the previous post, we saw that linking a C++ static
library compiled with /EHs to a mixed mode application prevented the destructor from running when an exception is thrown. Here’s the sample project that demonstrates the behavior, in case you aren’t convinced.
This is the code inside the library.
C::C()
{
cout << "Constructed" << endl;
}
C::~C()
{
cout << "Destructed" << endl;
}
void SomeFunc()
{
C c;
throw std::exception("Gone");
}
And this is the code consuming the library.
int main(array<:string> ^args)
{
try
{
SomeFunc();
}
catch(Exception ^e)
{
Console::WriteLine(e->ToString());
}
return 0;
}
OK, so where do we start? First, let’s see if this happens in the normal (no exception) case as well. Commenting out line 14 in the first code snippet and running the code should show us that. Try it out, and you’ll see that the destructor runs now. So the problem must somehow be related to destruction during exception handling. But we have asked the compiler to emit code for C++ exception handling (/EHs), and we are throwing plain C++ exceptions, so why is this happening?
To understand that, we’ll have to dig deeper into how the VC++ compiler and the CRT implement exceptions. Under the hood, the VC++ compiler uses Win32 SEH (Structured Exception Handling) to implement C++ exceptions. Matt Pietrek’s article explains how SEH works – do give it a quick read. The real quick summary of how it works is this – every function, on entry, creates an exception registration record on the stack that contains the address of the current function’s SEH exception handler and a pointer to the previous exception registration record, and writes the address of the record to the current thread’s TIB (Thread Information Block). When an SEH exception occurs, the OS walks through the registered records once to determine the handler for the exception, and again to allow cleanup code to run. The handler knows why it was called by looking at the exception record that is passed to it – it has a flag that specifies that information.
We’ll look at how the C++ compiler uses SEH by firing up Windbg
and disassembling SomeFunc
.
0:000> x *!SomeFunc
011015e0 CliConsoleApp!SomeFunc (void)
0:000> u 011015e0 011016ff
011015e0 6aff push 0FFFFFFFFh
011015e2 6815361001 push offset CliConsoleApp!CorExeMain+0xa5 (01103615)
011015e7 64a100000000 mov eax,dword ptr fs:[00000000h]
011015ed 50 push eax
011015ee 83ec10 sub esp,10h
011015f1 a118001101 mov eax,dword ptr [CliConsoleApp!__security_cookie (01110018)]
011015f6 33c4 xor eax,esp
011015f8 50 push eax
011015f9 8d442414 lea eax,[esp+14h]
011015fd 64a300000000 mov dword ptr fs:[00000000h],eax
01101603 a134401001 mov eax,dword ptr
[CliConsoleApp!_imp_?endlstdYAAAV?$basic_ostreamDU?$char_traitsDstd (01104034)]
01101608 8b0d70401001 mov ecx,dword ptr [CliConsoleApp!_imp_?coutstd (01104070)]
0110160e 50 push eax
0110160f 68c0451001 push offset CliConsoleApp!`string' (011045c0)
01101614 51 push ecx
01101615 e8a6010000 call
CliConsoleApp!std::operator<<<std::char_traits>char> > (011017c0)
0110161a 83c408 add esp,8
0110161d 8bc8 mov ecx,eax
0110161f ff1540401001 call dword ptr
[CliConsoleApp!_imp_??6?$basic_ostreamDU?$char_traitsDstdstdQAEAAV01P6AAAV01AAV01ZZ (01104040)]
01101625 8d542404 lea edx,[esp+4]
01101629 c744241c00000000 mov dword ptr [esp+1Ch],0
01101631 52 push edx
01101632 8d4c240c lea ecx,[esp+0Ch]
01101636 c7442408d8451001 mov dword ptr [esp+8],offset CliConsoleApp!`string' (011045d8)
0110163e ff15ac401001 call dword ptr [CliConsoleApp!_imp_??0exceptionstdQAEABQBDZ (011040ac)]
01101644 6888ea1001 push offset CliConsoleApp!_TI1?AVexceptionstd (0110ea88)
01101649 8d44240c lea eax,[esp+0Ch]
0110164d 50 push eax
0110164e e8171f0000 call CliConsoleApp!CxxThrowException (0110356a)
The FS register holds the address of the TIB, so to figure out where our exception handler is, we only need to find out where the FS register is being written into. That’s happening on line 14, and you can see that before that, the compiler emits code to push the address of a function (on line 6) and the previous exception registration record (on line 7,8). That’s the setup we were looking for, so the function address pushed must be our SEH exception handler. Let’s go ahead and disassemble that.
0:000> u 01103615
CliConsoleApp!CorExeMain+0xa5:
01103615 8b542408 mov edx,dword ptr [esp+8]
01103619 8d42f0 lea eax,[edx-10h]
0110361c 8b4aec mov ecx,dword ptr [edx-14h]
0110361f 33c8 xor ecx,eax
01103621 e81ee5ffff call CliConsoleApp!__security_check_cookie (01101b44)
01103626 b860eb1001 mov eax,offset CliConsoleApp!_TI1?AVexceptionstd+0xd8 (0110eb60)
0110362b e934ffffff jmp CliConsoleApp!_CxxFrameHandler3 (01103564)
Control jumps to a compiler generated function (_CxxFrameHandler3
), and following along the jumps takes us to MSVCR90!_CxxFrameHandler3
, the CRT exception handler. That function in turn calls another CRT function, which examines the parameters passed to it. One of the parameters is the exception record, and here’s how it looks.
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
ExceptionCode
contains the SEH exception code – there are predefined codes for access violations, C++ exceptions and CLR exceptions, among other things. The ExceptionFlags
is what I was talking about earlier, it tells the handler why it’s being called. The CRT function does different things based on what the ExceptionFlags
is, so let’s examine that part of the exception record.
0:000> dd 0012ebd4
0012ebd4 e06d7363 00000001 00000000 752f9617
e06d7363
is the exception code for C++ applications, and exception flags is 1
. The CRT function first verifies whether it’s a C++ exception by looking for that code. It then looks at the flag to figure out whether it should run the stack unwinding code. Apparently, 1
is the flag value when the OS makes the first pass over the exception registration chain, looking for a handler. We don’t have a catch
block in Somefunc
, so the function just returns without doing anything significant.
We’ll continue execution and wait for our handler to be called again the second time, hopefully asking it to unwind. Sure enough, control reaches the internal CRT function again, let’s see what the exception record contains this time.
0:000> dd 0012e5c8
0012e5c8 c0000027 00000002 00000000 68051870
Oops – it’s not a C++ exception anymore – the error code is c0000027
and not e06d7363
. So even though the handler is called to ask it to unwind, the CRT function doesn’t run unwinding logic because it’s not a C++ exception.
That explains why the destructor did not run – running the destructor code is part of the unwinding logic. OK, but who changed the exception code? Let’s look at the call stack.
0:000> kb
ChildEBP RetAddr Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
0012e51c 70aad82d 0012e5c8 0012ef84 0012e674 MSVCR90!_CxxExceptionFilter+0x707
0012e558 76f665f9 0012e5c8 0012ef84 0012e674 MSVCR90!_CxxFrameHandler3+0x26
0012e57c 76f665cb 0012e5c8 0012ef84 0012e674 ntdll!RtlRaiseStatus+0xb4
0012e944 68051870 0012f04c 6806c600 00000000 ntdll!RtlRaiseStatus+0x86
0012e968 680ccebd 0012f04c 6806c600 00000000 mscorwks+0x1870
0012ea84 680cd4f0 0012ebd4 0012f04c 0012ebf4 mscorwks!GetMetaDataInternalInterface+0x946a
0012eac4 680cd675 0012ebd4 0012f04c 0012eba8 mscorwks!GetMetaDataInternalInterface+0x9a9d
0012eae8 76f665f9 0012ebd4 0012f04c 0012ebf4 mscorwks!GetMetaDataInternalInterface+0x9c22
0012eb0c 76f665cb 0012ebd4 0012f04c 0012ebf4 ntdll!RtlRaiseStatus+0xb4
0012ebbc 76f66457 0012ebd4 0012ebf4 0012ebd4 ntdll!RtlRaiseStatus+0x86
0012ef28 70aadbf9 e06d7363 00000001 00000003 ntdll!KiUserExceptionDispatcher+0xf
0012ef60 01101653 0012ef78 0110ea88 f8a58fa0 MSVCR90!CxxThrowException+0x48
We see the CRT throwing the exception, but see who caught and triggered the second pass of the SEH handlers – mscorwks.dll, the core CLR engine. It apparently modified the SEH exception’s code when it was called as one of the handlers.
So this is what happened - the C++ code code raised an SEH exception with the error code for C++ exceptions (e06d7363
). When the OS walked the exception handler chain, it asked the C++ exception handler whether it would handle the exception, and the exception handler said no, because there is no catch block in our C++ code. The next handler in the chain is the one installed by the CLR. It says yes, it will handle the exception, and in the process, modifies the exception code from e06d7363
to c0000027
. When the OS calls the handlers again to ask them to unwind, the C++ handler doesn’t unwind because it doesn’t recognize it as a C++ exception from the error code. And that’s why the destructor did not run.
Why does the CLR exception handler modify the exception code? As this blog post says, it’s because managed exceptions also use SEH under the hood, and the CLR doesn’t want unknown exception codes to be passed to its handlers, for whatever reason. Except for SEH exceptions that it knows about, like access violations, it maps all other unmanaged exceptions to the same error code (c0000027
) and treats them as general SEH exceptions.
How do we fix it? Based on what we know, simply adding a catch (…) { throw; }
inside SomeFunc
should fix the problem – the C++ exception handler will now say yes when asked if it can handle the exception. It of course wouldn’t mangle the exception code, so when the same handler is called for unwinding, it will run the destructor properly. The CLR exception handler will be involved only when the exception is re-thrown, but our destructor would have already run by then.
The right fix though is to compile the library with the /EHa option, which tells the compiler to emit code to handle both C++ exceptions and other SEH exceptions. That way, the handler will run the stack unwind code when called during the second pass, C++ exception or not. In fact, the compiler doesn’t allow you to compile mixed mode code with /EHs – it would complain that the /clr and /EHs flags are incompatible. Unfortunately, neither the compiler nor the linker complain when linked against code compiled with /EHs – probably because they don’t know about that fact. There is some cost to compiling with /EHa though, your exception handlers would run in cases where they wouldn’t have run before, and I’d guess it also affects compiler optimizations to some extent.
We actually ran into this problem when using mixed mode code linked with Omni ORB, an open source native library for CORBA. It’s compiled with /EHs, and as you’d know by now, that caused some serious resource leak issues when there were exceptions involved. It took some serious debugging to narrow down the problem; missing C++ destructor calls don’t happen everyday, after all.