If dynamic DLL dependencies were tracked, they’d be all backwards


Whenever the issue of DLL dependencies arises, I can count on somebody arguing that these dynamic dependencies should be tracked, even if doing so cannot be proven to be reliable. Even if one could walk the call stack reliably, you would still get it wrong.

The example I gave originally was the common helper library, where A.DLL loads B.DLL via an intermediate function in MIDDLE.DLL. You want the dependency to be that A.DLL depends on B.DLL, but instead the dependency gets assigned to MIDDLE.DLL.

"But so what? Instead of a direct dependency from A.DLL to B.DLL, we just have two dependencies, one from A.DLL to MIDDLE.DLL, and another from MIDDLE.DLL to B.DLL. It all comes out to the same thing in the end."

Actually, it doesn't. It comes out much worse.

After all, MIDDLE.DLL is your common helper library. All of the DLLs in your project depend on it. Therefore, the dependency diagram in reality looks like this:

A.DLL B.DLL
MIDDLE.DLL

A.DLL depends on B.DLL, and both DLLs depend on MIDDLE.DLL. That common DLL really should be called BOTTOM.DLL since everybody depends on it.

Now you can see why the dependency chain A.DLL → MIDDLE.DLL → B.DLL is horribly wrong. Under the incorrect dependency chain, the DLLs would be uninitialized in the order A.DLL, MIDDLE.DLL, B.DLL, even though B.DLL depends on MIDDLE.DLL. That's because your "invented" dependency introduces a cycle in the dependency chain, and a bogus one at that. Once you have cycles in the dependency chain, everything falls apart. You took something that might have worked into something that explodes upon impact.

This situation appears much more often than you think. In fact it happens all the time. Because in real life, the loader is implemented in the internal library NTDLL.DLL, and KERNEL32.DLL is just a wrapper function around the real DLL loader. In other words, if your A.DLL calls LoadLibrary("B.DLL"), you are already using a middle DLL; its name is KERNEL32.DLL. If this "dynamic dependency generation" were followed, then KERNEL32.DLL would be listed as dependent on everything. When it came time to uninitialize, KERNEL32.DLL would uninitialized before all dynamically-loaded DLLs, because it was the one who loaded them, and then all the dynamically-loaded DLLs would find themselves in an interesting world where KERNEL32.DLL no longer existed.

Besides, the original problem arises when A.DLL calls a function in B.DLL during its DLL_PROCESS_DETACH handler, going against the rule that you shouldn't call anything outside your DLL from your DllMain function (except perhaps a little bit of KERNEL32 but even then, it's still not the best idea). It's one thing to make accommodations so that existing bad programs continue to run, but it's another to build an entire infrastructure built on unreliable heuristics in order to encourage people to do something they shouldn't be doing in the first place, and whose guesses end up taking a working situation and breaking it.

You can't even write programs to take advantage of this new behavior because walking the stack is itself unreliable. You recompile your program with different optimizations, and all of a sudden the stack walking stops working because you enabled tail call elimination. If somebody told you, "Hey, we added this feature that isn't reliable," I suspect your reaction would not be "Awesome, let me start depending on it!"

Comments (15)
  1. Alexandre Grigoriev says:

    I don’t get your point about KERNEL32.DLL. It doesn’t need to uninitialize anything in PROCESS_DETACH. You can’t unload it. When it’s time for it to go, the process is on the way down.

    [You’re getting too caught up in implementation details and missing the point of the article. -Raymond]
  2. Tom says:

    Editorial note:  The sentence fragment ‘even if doing so is cannot be proven to be reliable.’ has a superfluous ‘is’ and should be ‘even if doing so cannot be proven to be reliable.’.

    [Fixed. Thanks. -Raymond]
  3. Mark (The other Mark) says:

    "If somebody told you, "Hey, we added this feature that isn’t reliable," I suspect your reaction would not be "Awesome, let me start depending on it!""

    My reaction might not be, but I suspect many people would say exactly that. Check out Raymond Chen’s blog, especially the parts about compatibility shims, or people depending on undocumented implementation details… :-)

    Note: The Smiley indicates this is intended to be a "funny" comment, as many entries in your blog point out that yes, people are silly enough to depend on unreliable behavior.

  4. Someone You Know says:

    "…all the dynamically-loaded DLLs would find themselves in an interesting world where KERNEL32.DLL no longer existed."

    This reminds me of that supposedly ancient curse that goes: "May you live in interesting times."

  5. Peter Bindels says:

    This is exactly why there is no definite implementation of a singleton in C++ (or any other language for that matter) – you have the same kind of fake dependencies, where the order of destruction is wrong no matter how you construct them.

  6. porter says:

    > shouldn’t call anything outside your DLL from your DllMain function (except perhaps a little bit of KERNEL32 but even then, it’s still not the best idea).

    What about initializing my DLL’s global critical section?

  7. Anonymous Coward says:

    What about initializing my DLL’s global critical section?

    It is safe to call other functions in Kernel32.dll, because this DLL is guaranteed to be loaded in the process address space when the entry-point function is called. It is common for the entry-point function to create synchronization objects such as critical sections and mutexes, and use TLS. Do not call the registry functions, because they are located in Advapi32.dll. If you are dynamically linking with the C run-time library, do not call malloc; instead, call HeapAlloc.

    -the Platform SDK

  8. Doug says:

    What I find amusing, having been down the Dllmain path many times, from the MSDN reading developer perspective, is the number of rules that are either not written or described in a cryptic manner.

    It wouldn’t have been so bad, except that Visual Basic liked to unload DLLs at times other than process death, meaning you HAD to make sure your threads were stopped.  No matter what they were doing.  Getting that to work correctly is not a simple task.

  9. Karellen says:

    “A.DLL loads B.DLL […] You want the dependency to be that A.DLL depends on B.DLL”

    And that’s where things have gotten backwards.

    If RANDOM.DLL depends on CORE.DLL (e.g. kernel32.dll), then CORE.DLL must be loaded and intialised *before* RANDOM.DLL, and CORE.DLL must be around until after RANDOM.DLL has been released/unloaded.

    If RANDOM.DLL dynamically loads MODULE.DLL, then it follows that RANDOM.DLL must have been loaded, initialised and running before MODULE.DLL. It is therefore inherently erroneous to say that RANDOM.DLL “depends on” MODULE.DLL. If anything, you’d want to introduce a fake dependency saying that the dynamically loaded MODULE.DLL depends on RANDOM.DLL.

    That way, if RANDOM.DLL starts to go away, you’d want to unload MODULE.DLL *first*, releasing your resources in the opposite order to that which they were acquired. However, that cannot work in the general case because it’s possible in the semantics of that application for RANDOM.DLL to have “transferred ownership/cleanup responsibility” of MODULE.DLL to CORE.DLL, and CORE.DLL might still be using MODULE.DLL.

    [And if we followed your rules, then all delay-loads would have backwards dependencies. FANCY.DLL uses WININET.DLL only in certain scenarios, so instead of declaring a load-time dependency, it loads WININET.DLL on demand. According to your logic, this means that WININET.DLL depends on FANCY.DLL and therefore WININET.DLL should be shut down before FANCY.DLL. When FANCY.DLL calls InternetCloseHandle() in its DLL_PROCESS_DETACH, it ends up calling into a DLL that has already been uninitialized. -Raymond]
  10. John says:

    >When FANCY.DLL calls InternetCloseHandle() in its DLL_PROCESS_DETACH, it ends up calling into a DLL that has already been uninitialized.

    Good!  Serves the bastards right for breaking the DllMain contract.  But I guess Microsoft wouldn’t know anything about that.  Do you guys enjoy being slaves to design/compatibility decisions made 25+ years ago?  I bet there is an alternate dimension where Windows 7 only allows 8.3 filenames.

    [You missed the underlying counterfactual, which was “If Windows tracked DLL dependencies better, then we wouldn’t need these crazy rules about DLL_PROCESS_DETACH.” Your argument is therefore circular: “Changing the rules by tracking dependencies better is a bad idea because that would make it possible to change the rules!” -Raymond]
  11. porter says:

    > It wouldn’t have been so bad, except that Visual Basic liked to unload DLLs at times other than process death, meaning you HAD to make sure your threads were stopped.

    32bit OS/2 required all threads to terminate before a process ended.

  12. John says:

    I understand that, but my comment wasn’t specifically aimed at this topic; it was more about (breaking) backward compatibility in general.

  13. @John – yes, compatibility is a bitch.  But throwing away compatibility hurts customers (which hurts business).  So you just acknowledge it and deal with it.

    It’s hardly unique to Microsoft, though, or to the software industry.  Look around and you’ll see all kinds of decisions based on backward compatibility.  E.g., "cigarette lighter" jacks in (American) cars becoming the standard form factor for power outlets.

  14. bahbar says:

    @Aaron,

    Actually, those jacks are probably one of the best followed de facto standards throughout the world. Try and plug a US standard power plug in Europe, and then try your car-jack plug. Guess which one works…

  15. @bahbar – I didn’t want to assume that what’s true in the US is true around the world, but I’m not surprised that this one is.  The point is, though, that if you were designing a standard form factor for a power outlet, would your first idea be something that you could easily stick large fingers or thumbs into?  Oh – and the English language itself.  It’s a total mess, but it’s what we’ve got.

Comments are closed.

Skip to main content