Why are DLLs unloaded in the "wrong" order?


When a program starts or when a DLL is loaded, the loader builds a dependency tree of all the DLLs referenced by that program/DLL, that DLL's dependents, and so on. It then determines the correct order in which to initialize those DLLs so that no DLL is initialized until after all the DLLs upon which it is dependent have been initialized. (Of course, if you have a circular dependency, then this falls apart. And as you well know, calling the LoadLibrary function or the LoadLibraryEx function from inside a DLL's DLL_PROCESS_ATTACH notification also messes up these dependency computations.)

Similarly, when you unload a DLL or when the program terminates, the de-initialization occurs so that a DLL is de-initialized after all its dependents.

But when you load a DLL manually, crucial information is lost: Namely that the DLL that is calling LoadLibrary depends on the DLL being loaded. Consequently, if A.DLL manually loads B.DLL, then there is no guarantee that A.DLL will be unloaded before B.DLL. This means, for example, that code like the following is not reliable:

HSOMETHING g_hSomething;
typedef HSOMETHING (WINAPI* GETSOMETHING)(void);
typedef void (WINAPI* FREESOMETHING)(HSOMETHING);

GETSOMETHING GetSomething;
FREESOMETHING FreeSomething;

// Ignoring race conditions for expository purposes
void LoadB()
{
 HINSTANCE hinstB = LoadLibrary(TEXT("B.DLL"));
 if (hinstB) {
  GetSomething = (GETSOMETHING)
          GetProcAddress(hinstB, "GetSomething");
  FreeSomething = (FREESOMETHING)
          FreeProcAddress(hinstB, "FreeSomething");
 }
}

// Ignoring race conditions for expository purposes
HSOMETHING CacheSomethingFromB()
{
 if (!g_hSomething &&
     GetSomething && FreeSomething) {
  g_hSomething = GetSomething();
 }
 return g_hSomething;
}

BOOL CALLBACK DllMain(HINSTANCE hinst,
      DWORD dwReason, LPVOID lpReserved)
{
 switch (dwReason) {
 ...
 case DLL_PROCESS_DETACH:
  if (g_hSomething) {
   FreeSomething(g_hSomething); // oops
  }
  break;
 }
 return TRUE;
}

At the line marked "oops", there is no guarantee that B.DLL is still in memory because B.DLL does not appear in the dependency list of A.DLL, even though there is a runtime-generated dependency caused by the call to LoadLibrary.

Why can't the loader keep track of this dynamic dependency? In other words when A.DLL calls LoadLibrary(TEXT("B.DLL")), why can't the loader automatically say "Okay, now A.DLL depends on B.DLL"?

First of all, because as I've noted before, you can't trust the return address.

Second, even if you could trust the return address, you still can't trust the return address. Consider:

// A.DLL - same as before except for one line
void LoadB()
{
 HINSTANCE hinstB = MiddleFunction(TEXT("B.DLL"));
 if (hinstB) {
  GetSomething = (GETSOMETHING)
          GetProcAddress(hinstB, "GetSomething");
  FreeSomething = (FREESOMETHING)
          FreeProcAddress(hinstB, "FreeSomething");
 }
}

// MIDDLE.DLL
HINSTANCE MiddleFunction(LPCTSTR pszDll)
{
 return LoadLibrary(pszDll);
}

In this scenario, the load of B.DLL happens not directly from A.DLL, but rather through an intermediary (in this case, MiddleFunction). Even if you could trust the return address, the dependency would be assigned to MIDDLE.DLL instead of A.DLL.

"What sort of crazy person would write a function like MiddleFunction?", you ask. This sort of intermediate function is common in helper/wrapper libraries or to provide additional lifetime management functionality (although it doesn't do it any more, though it used to).

Third, there is the case of the GetModuleHandle function.

void UseBIfAvailable()
{
 HINSTANCE hinstB = GetModuleHandle(TEXT("B"));
 if (hinstB) {
  DOSOMETHING DoSomething = (DOSOMETHING)
          GetProcAddress(hinstB, "DoSomething");
  if (DoSomething) {
   DoSomething();
  }
 }
}

Should this call to GetModuleHandle create a dependency?

Note also that there are dependencies among DLLs that go beyond just LoadLibrary. For example, if you pass a callback function pointer to another DLL, you have created a reverse dependency.

A final note is that this sort of implicit dependency, as hard as it is to see as written above, is even worse once you toss global destructors into the mix.

class SomethingHolder
{
public:
 SomethingHolder() : m_hSomething(NULL);
 ~SomethingHolder()
  { if (m_hSomething) FreeSomething(m_hSomething); }
 HSOMETHING m_hSomething;
};

SomethingHolder g_SomethingHolder;
...

The DLL dependency is now hidden inside the SomethingHolder class, and when A.DLL unloads, g_SomethingHolder's destructor will run and try to talk to B.DLL. Hilarity ensues.

Comments (15)
  1. Raymond’s pointed to this before, but I’ll do it again… His last example is the generalization of: http://blogs.msdn.com/larryosterman/archive/2004/04/22/118240.aspx

  2. Nate says:

    First of all, this code is sloppy because it does not keep the HINSTANCE returned by LoadLibrary() around; it would be more proper to keep it around and call FreeLibrary() when everything is all done.

    Second, would I be correct if I said that if this code did proper HINSTANCE lifetime management, that the only time the code would not work would be when the executable shuts down?

  3. Nate, if it’s your first time here, Raymond tends to omit anything from a sample that’s not critical to showing a point.

    Secondly, no, it’d still break even if you called FreeLibrary. Note that the "oops" point is inside DllMain’s process detach event. That means that it’s being called after the main program’s main() has returned, and the OS is unloading the DLLs from memory.

    The whole point of this article is that, for the reasons Raymond explained, the OS does not know that A.DLL depends on B.DLL — so it is free, if it desires, to unload B.DLL before it unloads A.DLL. (Remember, this is after the main program has exited!) If this happens, A.DLL will attempt to jump to a function in dead memory. The same problem would exist, as Raymond pointed out, if you called GetModuleHandle() to get a handle to a DLL already loaded by some other DLL, or if another DLL calls one of your functions to set a callback in their own code.

    Also, consider that DLL dependencies form a DAG and answer for yourself why a simple reference count would not solve the problem.

  4. Norman Diamond says:

    or to provide additional lifetime management

    > functionality (although it doesn’t do it any

    > more, though it used to).

    What did it used to do? (Obviously it doesn’t do it any more, giving a 404 error now.)

    Meanwhile, why not track dynamic dependencies in exactly the manner you’re describing? You describe them and then say they’re not what was wanted, well sure they might not be exactly what someone had in mind at design time, but they’re exactly what the code is doing, and the result still yields a logical dependency graph (except for the same exceptions as always, as you pointed out, when circular dependencies are created).

  5. "why not track dynamic dependencies in exactly the manner you’re describing?"

    ??? The whole point of the article was explaining why no such manner is valid. Or did I completely miss the point of your question? (What "manner" was I describing anyway?)

  6. In How To Do A Good Performance Investigation, Rico spells out step-by-step instructions on how…

  7. Norman Diamond says:

    Monday, May 23, 2005 8:14 PM by oldnewthing

    I thought you were anonymous when responding in your own blog? ^u^

    By the way since you’re reading your blog, please scroll down to what happens on May 19 and earlier. You really blue it in editing. ^_^

    > The whole point of the article was

    > explaining why no such manner is valid.

    My point was that even though no such manner is completely accurate, it would still be a useful thing to do.

    For example even though you can’t trust the return address, if module A does something to show its dependency on module B then you don’t have to trust the return address, you can accept module A’s action saying that module A wants to be unloaded before B gets unloaded.

    For example if A does something to depend on middle and middle does something to depend on B, then sure maybe the designer wanted A to depend on B, but it doesn’t hurt to accept the flow of execution making A depend on middle and middle depend on B.

    I think it doesn’t even hurt to interpret a call to GetModuleHandle as asking for a dependency. Sure a pathological case could create a circular dependency here where there wasn’t a circular dependency under the old rules. But in such a case I’ll bet the logical design had that circularity. (Of course we also need to check that no security problems are added. I can’t see any when it’s just a dependency graph.)

  8. Norman Diamond says:

    After reading Larry Osterman’s posting that he pointed to, and then the last "Hilarity" case here, I think I’ve figured out that another coding style suffers from the same kind of problem.

    Sorry for abusing a not exactly a .Net blog with .Net code.

    //Class-level declaration.

    /* Create a TraceSwitch to use in the entire application.*/

    static TraceSwitch* mySwitch = new TraceSwitch(S"General", S"Entire Application");

    Since the pointer is static, the TraceSwitch object doesn’t get deleted until after the main program exits, right?

    The sample code in this comment was copied from:

    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfSystemDiagnosticsTraceSwitchClassTopic.asp

  9. Dean Harding says:

    > Since the pointer is static, the TraceSwitch object doesn’t get deleted until after the main program exits, right?

    .NET doesn’t have deterministic finalization, so *any* finalizer can run after the main function has ended, whether it’s a global or not. In this case, though, the framework will ensure that all finalizers that are going to run have already run by the time it starts unloading the AppDomain.

  10. Is this similar to the behaviour in MSXML?

    It seems that MSXML does an internal LoadLibrary of WinHTTP, and does not release that.

  11. Justin Olbrantz says:

    Oh, this fun little feature. Learned about this one doing some debugging following a highy mysterious exit-time crash in one of my programs :P

  12. Universalis says:

    Norman Diamond – isn’t there a misconception behind all this?

    DLLs can’t call LoadLibrary or GetModuleHandle. [People, including Raymond, talk as if they did but that’s just sloppy language]. Only processes (.EXEs) can call things because only processes can execute code.

    So when you say "if module A does something", the problem is that module A can’t do anything because module A is a DLL.

    That’s where all the talk about return addresses came from, in a vain attempt to discover where the calling code was *located* as a way of inferring dependencies. But such attempts can’t succeed 100% (pathological test case: a wrapper DLL that exports a function called LoadLibray whose sole action is to call LoadLibrary) and in programming there are only two acceptable quality levels: 0% and 100%.

    The LoadLibraryAndEstablishDependency API is the only way round this problem… when it gets written!

  13. Todd says:

    STDAPI DllCanUnloadNow(void)

    {

    if (CanUnload())

    {

    if (g_hSomething)

    {

    FreeSomething(g_hSomething);

    g_hSomething = NULL;

    }

    return S_OK;

    }

    return S_FALSE;

    }

    BOOL CALLBACK DllMain(HINSTANCE hinst,

    DWORD dwReason, LPVOID lpReserved)

    {

    switch (dwReason) {



    case DLL_PROCESS_DETACH:

    if (g_hSomething) {

    //Do nothing. //Should have had a chance in DllCanUnloadNow. Anything we do now is suspect.

    }

    break;

    }

    return TRUE;

    }

    This is the way I have solved this in the past:

    If I am going to return S_OK from DllCanUnloadNow, then I do cleanup there.

    If I get to DllMain and I still have resources, then I dont touch them. I am basically getting kicked out of the process, and dont know the state of the other libraries in the process.

    Does this seem sound?

  14. Stefan Kuhr says:

    Raymond,

    what if A.DLL stored the HMODULE returned by LoadLibrary("B.DLL") in a TLS-Slot and called a function in B using this HMODULE extracted from the TLS-Slot during DLL_THREAD_DETACH, not DLL_PROCESS_DETACH in DllMain? Does this suffer from the same problem or is this safe? Or is there just a simple rule: Don’t call into DLLs that your DLL loaded dynamically from within DllMain with either DLL_THREAD_DETACH or DLL_PROCESS_DETACH invoked?



    Stefan

  15. Stefan Kuhr says:

    Hello all,

    I think what I tried to ask in my post sounds a bit convoluted so here is what I meant expressed in C (dunno if this compiles, though). My question is: Is my call into B.DLL via the function pointer obtained in DLL_THREAD_DETACH dangerous or safe? Am I allowed to do the FreeLibrary a few lines below this function call or is this dangerous as well?

    /// a global in A.DLL

    DWORD g_dwTlsSlot = TLS_OUT_OF_INDEXES;

    /// a function in A.DLL, not to be called from

    /// A.DLL’s DllMain

    void SomeAFunc()

    {

    if (TLS_OUT_OF_INDEXES != g_dwTlsSlot && !TlsGetValue(g_dwTlsSlot))

    TlsSetValue(g_dwTlsSlot, LoadLibrary("B.DLL"));

    }

    /// DllMain of A.DLL:

    BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)

    {

    switch(ul_reason_for_call)

    {

    case DLL_PROCESS_ATTACH:

    g_dwTlsSlot = TlsAlloc();

    case DLL_THREAD_ATTACH:

    break;

    case DLL_PROCESS_DETACH:

    case DLL_THREAD_DETACH:

    {

    if (TLS_OUT_OF_INDEXES != g_dwTlsSlot)

    {

    HMODULE hinstB = TlsGetValue(g_dwTlsSlot);

    GETSOMETHING GetSomething = NULL;

    if (hinstB)

    GetSomething = (GETSOMETHING) GetProcAddress(hinstB, "GetSomething");

    //// Ooops or no oops when calling GetSomething here?

    if (GetSomething)

    GetSomething();

    TlsSetValue(g_dwTlsSlot, NULL);

    FreeLibrary(hinstB);

    if (DLL_PROCESS_DETACH==ul_reason_for_call)

    {

    TlsFree(g_dwTlsSlot);

    g_dwTlsSlot = TLS_OUT_OF_INDEXES;

    }

    }

    }

    break;

    }

    return TRUE;

    }



    Stefan Kuhr

Comments are closed.

Skip to main content