When a C++ destructor did not run – Part 2

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.

   1: C::C()


   2: { 


   3:     cout << "Constructed" << endl; 


   4: }


   5:  


   6: C::~C()


   7: { 


   8:     cout << "Destructed" << endl; 


   9: }


  10:  


  11: void SomeFunc()


  12: {


  13:     C c;


  14:     throw std::exception("Gone");


  15: }

And this is the code consuming the library.

   1: int main(array<System::String ^> ^args)


   2: {


   3:     try


   4:     {


   5:         SomeFunc();


   6:     }


   7:     catch(Exception ^e)


   8:     {


   9:         Console::WriteLine(e->ToString());


  10:     }


  11:     return 0;


  12: }

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

 

   1: 0:000> x *!SomeFunc


   2: 011015e0 CliConsoleApp!SomeFunc (void)


   3:  


   4: 0:000> u 011015e0 011016ff


   5: 011015e0 6aff            push    0FFFFFFFFh


   6: 011015e2 6815361001      push    offset CliConsoleApp!CorExeMain+0xa5 (01103615)


   7: 011015e7 64a100000000    mov     eax,dword ptr fs:[00000000h]


   8: 011015ed 50              push    eax


   9: 011015ee 83ec10          sub     esp,10h


  10: 011015f1 a118001101      mov     eax,dword ptr [CliConsoleApp!__security_cookie (01110018)]


  11: 011015f6 33c4            xor     eax,esp


  12: 011015f8 50              push    eax


  13: 011015f9 8d442414        lea     eax,[esp+14h]


  14: 011015fd 64a300000000    mov     dword ptr fs:[00000000h],eax


  15: 01101603 a134401001      mov     eax,dword ptr [CliConsoleApp!_imp_?endlstdYAAAV?$basic_ostreamDU?$char_traitsDstd (01104034)]


  16: 01101608 8b0d70401001    mov     ecx,dword ptr [CliConsoleApp!_imp_?coutstd (01104070)]


  17: 0110160e 50              push    eax


  18: 0110160f 68c0451001      push    offset CliConsoleApp!`string' (011045c0)


  19: 01101614 51              push    ecx


  20: 01101615 e8a6010000      call    CliConsoleApp!std::operator<<<std::char_traits<char> > (011017c0)


  21: 0110161a 83c408          add     esp,8


  22: 0110161d 8bc8            mov     ecx,eax


  23: 0110161f ff1540401001    call    dword ptr [CliConsoleApp!_imp_??6?$basic_ostreamDU?$char_traitsDstdstdQAEAAV01P6AAAV01AAV01ZZ (01104040)]


  24: 01101625 8d542404        lea     edx,[esp+4]


  25: 01101629 c744241c00000000 mov     dword ptr [esp+1Ch],0


  26: 01101631 52              push    edx


  27: 01101632 8d4c240c        lea     ecx,[esp+0Ch]


  28: 01101636 c7442408d8451001 mov     dword ptr [esp+8],offset CliConsoleApp!`string' (011045d8)


  29: 0110163e ff15ac401001    call    dword ptr [CliConsoleApp!_imp_??0exceptionstdQAEABQBDZ (011040ac)]


  30: 01101644 6888ea1001      push    offset CliConsoleApp!_TI1?AVexceptionstd (0110ea88)


  31: 01101649 8d44240c        lea     eax,[esp+0Ch]


  32: 0110164d 50              push    eax


  33: 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 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.

   1: 0:000> u 01103615


   2: CliConsoleApp!CorExeMain+0xa5:


   3: 01103615 8b542408        mov     edx,dword ptr [esp+8]


   4: 01103619 8d42f0          lea     eax,[edx-10h]


   5: 0110361c 8b4aec          mov     ecx,dword ptr [edx-14h]


   6: 0110361f 33c8            xor     ecx,eax


   7: 01103621 e81ee5ffff      call    CliConsoleApp!__security_check_cookie (01101b44)


   8: 01103626 b860eb1001      mov     eax,offset CliConsoleApp!_TI1?AVexceptionstd+0xd8 (0110eb60)


   9: 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.

   1: typedef struct _EXCEPTION_RECORD {


   2:  DWORD ExceptionCode;


   3:  DWORD ExceptionFlags;


   4:  struct _EXCEPTION_RECORD *ExceptionRecord;


   5:  PVOID ExceptionAddress;


   6:  DWORD NumberParameters;


   7:  DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];


   8:  }  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.
 
   1: 0:000> dd 0012ebd4 


   2: 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.

   1: 0:000> dd 0012e5c8


   2: 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.

   1: 0:000> kb


   2: ChildEBP RetAddr  Args to Child              


   3: WARNING: Stack unwind information not available. Following frames may be wrong.


   4: 0012e51c 70aad82d 0012e5c8 0012ef84 0012e674 MSVCR90!_CxxExceptionFilter+0x707


   5: 0012e558 76f665f9 0012e5c8 0012ef84 0012e674 MSVCR90!_CxxFrameHandler3+0x26


   6: 0012e57c 76f665cb 0012e5c8 0012ef84 0012e674 ntdll!RtlRaiseStatus+0xb4


   7: 0012e944 68051870 0012f04c 6806c600 00000000 ntdll!RtlRaiseStatus+0x86


   8: 0012e968 680ccebd 0012f04c 6806c600 00000000 mscorwks+0x1870


   9: 0012ea84 680cd4f0 0012ebd4 0012f04c 0012ebf4 mscorwks!GetMetaDataInternalInterface+0x946a


  10: 0012eac4 680cd675 0012ebd4 0012f04c 0012eba8 mscorwks!GetMetaDataInternalInterface+0x9a9d


  11: 0012eae8 76f665f9 0012ebd4 0012f04c 0012ebf4 mscorwks!GetMetaDataInternalInterface+0x9c22


  12: 0012eb0c 76f665cb 0012ebd4 0012f04c 0012ebf4 ntdll!RtlRaiseStatus+0xb4


  13: 0012ebbc 76f66457 0012ebd4 0012ebf4 0012ebd4 ntdll!RtlRaiseStatus+0x86


  14: 0012ef28 70aadbf9 e06d7363 00000001 00000003 ntdll!KiUserExceptionDispatcher+0xf


  15: 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.

3 thoughts on “When a C++ destructor did not run – Part 2”

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>