Date: | May 23, 2005 / year-entry #126 |
Tags: | other |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20050523-05/?p=35573 |
Comments: | 15 |
Summary: | 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... |
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 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
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
Why can't the loader keep track of this dynamic dependency?
In other words
when 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
"What sort of crazy person would write a function like
Third, there is the case of
the void UseBIfAvailable() { HINSTANCE hinstB = GetModuleHandle(TEXT("B")); if (hinstB) { DOSOMETHING DoSomething = (DOSOMETHING) GetProcAddress(hinstB, "DoSomething"); if (DoSomething) { DoSomething(); } } }
Should this call to
Note also that there are dependencies among DLLs
that go beyond just 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
|
Comments (15)
Comments are closed. |
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
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?
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.
"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?)
In How To Do A Good Performance Investigation, Rico spells out step-by-step instructions on how…
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
> 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.
Is this similar to the behaviour in MSXML?
It seems that MSXML does an internal LoadLibrary of WinHTTP, and does not release that.
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
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!
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?
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
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