Why is the debugger telling me I crashed because my DLL was unloaded, when I see it loaded right here happily executing code?

A customer was puzzled by what appeared to be contradictory information coming from the debugger.

We have Windows Error Reporting failures that tell us that we are executing code in our DLL which has been unloaded. Here's a sample stack:

Child-SP          RetAddr           Call Site
00000037`7995e8b0 00007ffb`fe64b08e ntdll!RtlDispatchException+0x197
00000037`7995ef80 000007f6`e5d5390c ntdll!KiUserExceptionDispatch+0x2e
00000037`7995f5b8 00007ffb`fc977640 <Unloaded_contoso.dll>+0x3390c
00000037`7995f5c0 00007ffb`fc978296 RPCRT4!NDRSRundownContextHandle+0x18
00000037`7995f610 00007ffb`fc9780ed RPCRT4!DestroyContextHandlesForGuard+0xea
00000037`7995f650 00007ffb`fc9b5ff4 RPCRT4!ASSOCIATION_HANDLE::~ASSOCIATION_HANDLE+0x39
00000037`7995f680 00007ffb`fc9b5f7c RPCRT4!LRPC_SASSOCIATION::`scalar deleting destructor'+0x14
00000037`7995f6b0 00007ffb`fc978b25 RPCRT4!LRPC_SCALL_BROKEN_FLOW::FreeObject+0x14
00000037`7995f6e0 00007ffb`fc982e44 RPCRT4!LRPC_SASSOCIATION::MessageReceivedWithClosePending+0x6d
00000037`7995f730 00007ffb`fc9825be RPCRT4!LRPC_ADDRESS::ProcessIO+0x794
00000037`7995f870 00007ffb`fe5ead64 RPCRT4!LrpcIoComplete+0xae
00000037`7995f910 00007ffb`fe5e928a ntdll!TppAlpcpExecuteCallback+0x204
00000037`7995f980 00007ffb`fc350ce5 ntdll!TppWorkerThread+0x70a
00000037`7995fd00 00007ffb`fe60f009 KERNEL32!BaseThreadInitThunk+0xd
00000037`7995fd30 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

But if we ask the debugger what modules are loaded, our DLL is right there, loaded as happy as can be:

0:000> lm
start             end                 module name
000007f6`e6000000 000007f6`e6050000   contoso    (deferred)

In fact, we can view other threads in the process, and they are happily running code in our DLL. What's going on here?

All the information you need to solve this problem is given right there in the problem report. You just have to put the pieces together.

Let's take a closer look at that <Unloaded_contoso.dll>+0x3390c entry. The address that the symbol refers to is the return address from the previous frame: 000007f6`e5d5390c. Subtract 0x3390c from that, and you get 000007f6`e5d20000, which is the base address of the unloaded module.

On the other hand, the lm command says that the currently-loaded copy of contoso.dll is loaded at 000007f6`e6000000. This is a different address.

What happened here is that contoso.dll was loaded into memory at 000007f6`e5d20000, and then it ran for a while. The DLL was then unloaded from memory, and later loaded back into memory. When it returned, it was loaded at a different address 000007f6`e6000000. For some reason (improper cleanup when unloading the first copy, most likely), there was still a function pointer pointing into the old unloaded copy, and when NDRS­Rundown­Context­Handle tries to call into that function pointer, it calls into an unloaded DLL, and you crash.

When faced with something that seems impossible, you need to look more closely for clues that suggest how your implicit assumptions may be incorrect. In this case, the assumption was that there was only one copy of contoso.dll.

Comments (8)
  1. Joshua says:

    Well OK then. Bleeetch.

    Only two models ever avoided this problem — static linking and a.out shared libs (all shared libs had to be registered at build time to set non-overlapping load areas — no points for guessing what that entails for distributed binary libraries).

  2. Henke37 says:

    There is also the option of marking the DLL as non relocatable. Which is fine if you hate address space randomization and can manually pick a non conflicting location

  3. Jon says:

    Not relocating the DLL would prevent this exact crash but it is masking the bug, not fixing it.

    The callback can still occur when after it unloads but before it reloads.

    And even if it has reloaded, and the callback calls the correct function,  the internal state of the DLL has reset and the the callback could be passing in a handle, say, that this instance didn't create.

    [All excellent points. -Raymond]
  4. George says:

    I've seen this problem with Explorer. Particularly in Windows 8.1 it will unload my shell extension even if I return FALSE from DllCanUnloadNow. Next time it loads it on a different address and all such registered callbacks don't work. The fix is to call GetModuleHandleEx bump up the DLL ref count.

    [CoUninitalize will ask a DLL if it is okay to unload now, but the answer is a foregone conclusion. -Raymond]
  5. Joshua says:

    [CoUninitalize will ask a DLL if it is okay to unload now, but the answer is a foregone conclusion. -Raymond]

    Given that explorer can't do much w/o COM I find it surprising that it would ever call CoUninitialize.

    [Every STA thread uninitializes COM before it exits. -Raymond]
  6. Myria says:

    How can we diagnose a crash in our product that occurs on a separate thread spawned by WinInet.dll?  The thread crashes because a deep function unloads a Microsoft DLL that is used higher up in the call stack, like this example.  It only happens on Windows 7; XP, Vista, 8, and 8.1 have never shown in crash reports.

    An unrelated company's product also triggers this crash somehow in their own product; we found that in an Internet search.  Either it's a Windows bug, or this other company's product is making the same mistake we are.

    [You could set a breakpoint when the DLL unloads to see if that gives you a clue. I'm not going to answer follow-up questions because I do not do one-on-one consulting. -Raymond]
  7. Joshua says:

    It's a known bug in W7. Pin the DLL being unloaded with LoadLibrary to fix.

  8. Crescens2k says:


    The rule of thumb is a call to CoUninitialize for every successful CoInitialize(Ex). Since you have to initialize COM on every thread that you want to use it on inside a process*, then it is not surprising to call these functions more than once. Even with MTA threads, it is still good practice to call these two functions.

    (*Terms and conditions apply, please see small print)

Comments are closed.