Click here to Skip to main content
15,886,199 members
Articles / Programming Languages / C++

When a C++ Destructor Did Not Run – Part 2

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
5 Dec 2009CPOL6 min read 19.2K   4   3
Why did it not run?

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

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

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Atmel R&D India Pvt. Ltd.
India India
I'm a 27 yrs old developer working with Atmel R&D India Pvt. Ltd., Chennai. I'm currently working in C# and C++, but I've done some Java programming as well. I was a Microsoft MVP in Visual C# from 2007 to 2009.

You can read My Blog here. I've also done some open source software - please visit my website to know more.

Comments and Discussions

 
GeneralThe reason why I say it is a bug Pin
Rama Krishna Vavilala7-Dec-09 2:37
Rama Krishna Vavilala7-Dec-09 2:37 
GeneralRe: The reason why I say it is a bug Pin
S. Senthil Kumar7-Dec-09 17:17
S. Senthil Kumar7-Dec-09 17:17 
GeneralNice Pin
N a v a n e e t h6-Dec-09 21:47
N a v a n e e t h6-Dec-09 21:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.