When is a dynamic CRT not initialized when a user-provided DllMain is called?

Preamble: This question is specifically related to the behavior of a dynamic CRT used through /MD and only with it. He does not question the validity of any other recommendations. DllMain .


As we were told : (ref: Dynamic-Link Library Best Practices, MSDN, May 17, 2006)

You should not perform the following tasks from DllMain:

  • ...
  • Use the memory management function from Dynamic C Run-Time (CRT). If the CRT DLL is not initialized, calls to these functions may cause the process to crash.
  • ...

Others have already questioned this (as in the issue of the validity of the argument), and since we are gratefully receiving an answer there, we can clearly see one rather simple case where this can potentially cause problems :

You work on the assumption that the entry point for the DLL is always _DllMainCRTStartup. This is not the case; it is just the default linker. This may be all that the programmer wants it to be quickly and easily changed using the linker / ENTRYPOINT option. Microsoft cannot do anything to prevent this.

So, here are the elements of this question:

  • Is there a different situation when binding /MD and not supplying a custom /ENTRYPOINT where a dynamic CRT does not have to be fully initialized?

    • In particular, if all DLL loading is done only through "static dependencies", that is, there are no explicit LoadLibrary calls at all, just bind the time to the DLL dependencies.
  • Bonus: MS documents specifically call the "memory management function", but as far as I can tell, if the CRT is not initialized, potentially any CRT function should be unsafe. Why call memory management functions this way?

  • Number 3:

    Wrt. to user ENTRYPOINT : I don’t quite understand how this could be such an important scenario that needs to be included in the non-do-in-DllMain list without additional qualifications. IFF I provide a custom entry point, I am responsible for correctly initializing the CRT, or CRT will not work properly anywhere in my program, not just DllMain. Why call a DllMain part?

    This brings me back to Q.1, namely, if this is the only scenario when it is problematic for a dynamic CRT. Clarification or openness of why this would be more important for DllMain, for other parts of the DLL, or what I might skip here would be appreciated.


Bonus links:


Rationale: I feel I have to add this for context: I ask about it because we have a huge amount of code doing something using the global C ++ object constructors. Things that were actually broken have been verified over the years (e.g. parallel LoadLibrary , thread synchronization, etc.), but all the code is filled with std C ++ and CRT functions that have been running on Windows for many years. XP, 7 and Windows 10 without any known hiccups. Although I'm not one of those who cry, β€œbut it just works,” I have to make an engineering decision here about whether there is any short-term meaning, trying to β€œfix” it. Therefore, I would be grateful if the answers to the boxes could be left in their boxes.

+5
source share
1 answer

Is there another situation with /MD binding and not custom /ENTRYPOINT where a dynamic CRT should not be fully initialized?

first some notation:

  • X have static imports (depends on) Y and Z : X[ Y, Z]
  • X entry point: X_DllMain
  • X_DllMain call LoadLibrary(Y) : X<Y>

when we use /MD - we use crt in separate DLL (s). initialized in this context means that the access points (s) of the crt DLL (s) are already called. therefore, the question may be more general and understandable:

from X[Y] => Y_DllMain called before X_DllMain ?

generally not. because it can be cyclical when Y[X] or Y[Z[X]] .

the most famous example is user32[gdi32] , while gdi32[user32] or in win10 depends on gdi32[gdi32full[user32]] . so first you need to call user32_DllMain or gdi32_DllMain ? however, it is obvious that any crt dll (s) are independent of our custom dll. therefore, let it exclude the case of cyclic dependence.

when the boot loader module is X - it loads all its dependency modules (and its dependency is a recursive process), if it is no longer in memory, then build the loader assembly schedule and the entry point call modules start. obviously, if A[B] , the loader always tries to call B_DllMain before A_DllMain (except for the circular dependency, when the order of calls is undefined). but what modules will be in the call graph? are all X dependency modules? of course not. some of these modules may already be in memory (loaded) when we start loading X. so already entered entry points with DLL_PROCESS_ATTACH and should not be called a second time. this strategy is used in xp, vista, win7:

when we load X :

  • load or find in memory all its dependency modules
  • new call entry points are loaded (only after X ).
  • if A[B] - call B_DllMain before A_DllMain
Example

: loaded X[Y[W[Z]], Z]

 //++begin load X Z_DllMain W_DllMain Y_DllMain X_DllMain // --end load X 

but this scenario does not take into account the following case - some module may already be in memory, but its entry point has not yet been called. how can this happen? this can happen if some entry point in the module calls LoadLibrary .

example - loaded X[Y<W[ Z]>, Z]

 //++begin load X Y_DllMain //++begin load W W_DllMain //--end load W Z_DllMain X_DllMain // --end load X 

therefore, W_DllMain will be called before Z_DllMain , despite W[Z] . precisely because it is not recommended to call LoadLibrary from the entry point of the DLL.


but from the best recommendations of the Dynamic-Link library

This can lead to a deadlock or failure.

the words about the impasse are not true - of course, any impasse cannot be basically. Where? as? we already hold the loader lock inside the DLL entry point, and this lock can be obtained recursively. accident really can be (before victory8).

or other false :

Call ExitThread . Exiting a thread when a DLL is disconnected can cause the loader to lock, which will be received again, leading to a deadlock or crash.

  • can cause a reload of the bootloader lock - it cannot , but always
  • causes a dead end - false - we already hold this lock
  • accident - there will be no accident, one more false

but in reality it will be - the output of the stream without blocking the free bootloader. he became busy forever. as a result, the creation or exit of new threads, any new loading or unloading of a DLL, or simply calling ExitProcess - freezes when trying to get a bootloader lock. so there really will be a dead end, but not during Call ExitThread - the latter.

and, of course, an interesting note - the windows themselves call LoadLibrary from DllMain - user32.dll always calls LoadLibrary for imm32.dll from its entry point (still true on win10)


but starting with win8 (or win8.1), the bootloader has become smarter in handle dependency modules. now changing 2

2. entry points for incoming calls of newly loaded (after X) modules or if the module has not yet been initialized.

therefore, in modern windows (8+) to load X[Y<W[Z]>, Z]

 //++begin load X Y_DllMain //++begin load W Z_DllMain W_DllMain //--end load W X_DllMain // -- end load X 

Z initialization will be transferred to the load <graph. >. as a result, everything will be right now.

to verify this, we can build the following solution: test.exe[ kernel32, D1< D2[kernel32, msvcrt] >, msvcrt ]

  • D2 import from kernel32 and msvcrt and export SomeFunc
  • Import D1 only from kernel32 and call LoadLibraryW(L"D2") from its entry point, and then call D2.SomeFunc
  • import test.exe from kernel32 , D1 and msvcrt

(in this order, it is critically important - D1 must be up to msvcrt in the import, for this you need to install D1 up to msvcrt in the linker command line)

as a result of D1 , the entry point will be called before msvcrt . this is normal - D1 does not depend on msvcrt but when D1 booted D2 from its entry point, it became interesting

for D2.dll ( /NODEFAULTLIB kernel32.lib msvcrt.lib )

 #include <Windows.h> extern "C" { __declspec(dllimport) int __cdecl sprintf(PSTR buf, PCSTR format, ...); } BOOLEAN WINAPI MyEp( HMODULE , DWORD ul_reason_for_call, PVOID ) { if (ul_reason_for_call == DLL_PROCESS_ATTACH) { OutputDebugStringA("D2.DllMain\n"); } return TRUE; } INT_PTR WINAPI SomeFunc() { __pragma(message(__FUNCDNAME__)) char buf[32]; // this is only for link to msvcrt.dll sprintf(buf, "D2.SomeFunc\n"); OutputDebugStringA(buf); return 0; } #ifdef _WIN64 #define FuncName " ?SomeFunc@ @YA_JXZ" #else #define FuncName " ?SomeFunc@ @YGHXZ" #endif __pragma(comment(linker, "/export:" FuncName ",@1,NONAME,PRIVATE")) 

for D1.dll ( /NODEFAULTLIB kernel32.lib )

 #include <Windows.h> #pragma warning(disable : 4706) BOOLEAN WINAPI MyEp( HMODULE hmod, DWORD ul_reason_for_call, PVOID ) { if (ul_reason_for_call == DLL_PROCESS_ATTACH) { OutputDebugStringA("D1.DllMain\n"); if (hmod = LoadLibraryW(L"D2")) { if (FARPROC fp = GetProcAddress(hmod, (PCSTR)1)) { fp(); } } } return TRUE; } INT_PTR WINAPI SomeFunc() { __pragma(message(__FUNCDNAME__)) OutputDebugStringA("D1.SomeFunc\n"); return 0; } #ifdef _WIN64 #define FuncName " ?SomeFunc@ @YA_JXZ" #else #define FuncName " ?SomeFunc@ @YGHXZ" #endif __pragma(comment(linker, "/export:" FuncName ",@1,NONAME")) 

for exe ( /NODEFAULTLIB kernel32.lib D1.lib msvcrt.lib )

 #include <Windows.h> extern "C" { __declspec(dllimport) int __cdecl sprintf(PSTR buf, PCSTR format, ...); } __declspec(dllimport) INT_PTR WINAPI SomeFunc(); void ep() { char buf[32]; // this is only for link to msvcrt.dll sprintf(buf, "exe entry\n"); OutputDebugStringA(buf); ExitProcess((UINT)SomeFunc()); } 

for xp:

 LDR: D1.dll loaded - Calling init routine D1.DllMain Load: D2.dll LDR: D2.dll loaded - Calling init routine D2.DllMain D2.SomeFunc LDR: msvcrt.dll loaded - Calling init routine exe entry D1.SomeFunc 

for win7:

 LdrpRunInitializeRoutines - INFO: Calling init routine for DLL "D1.dll" D1.DllMain Load: D2.dll LdrpRunInitializeRoutines - INFO: Calling init routine for DLL "D2.DLL" D2.DllMain D2.SomeFunc LdrpRunInitializeRoutines - "msvcrt.dll" exe entry D1.SomeFunc 

in both cases the call flow is the same - D2.DllMain , called the point before msvcrt, despite D2[msvcrt]

but on win8.1 and win10, the call flow is different:

 LdrpInitializeNode - INFO: Calling init routine for DLL "D1.dll" D1.DllMain LdrpInitializeNode - INFO: Calling init routine for DLL "msvcrt.dll" LdrpInitializeNode - INFO: Calling init routine for DLL "D2.DLL" D2.DllMain D2.SomeFunc exe entry D1.SomeFunc 

D2 entry point, named after msvcrt .

So what is a conclusion?

if when loading the module X[Y] and Y was not initialized in memory, Y_DllMain will be called before X_DllMain . or in other words - if no one calls LoadLibrary(X) (or LoadLibrary(Z[X]) ) from the entry point into the DLL. therefore, if your DLL is loaded in the "usual" way (not by calling LoadLibrary from DllMain or is not entered from the driver at any dll loading event) - you can be sure that the crt entry point has already been called (crt is initialized)

more - if you run on win8.1 + - and X[Y] loads - Y_DllMain will always be called before X_DllMain .


Now about custom /ENTRYPOINT in your dll.

even if you use crt in separate DLLs - some small crt code will be statically linked to your DllMainCRTStartup module - which calls your DllMain function (this is not an entry point) by name. therefore, in the case of dynamic crt - we really have 2 parts of crt - the main part in separate DLLs and will be initialized to . The entry point to the DLL is called (if not the special case that I describe above and win7, vista, xp). and a small static part (code inside your module). when this static part will be called is completely up to you. this part of DllMainCRTStartup does some internal initialization, initializes global objects in your code ( initterm ), and calls DllMain after it returns (on dll detach) call destructors for global variables.

if you install a custom entry point into the DLL - at the moment crt in separate DLLs is already initialized, but your static crt no (as and global objects). from this user entry point you will need to call DllMainCRTStartup

+1
source

Source: https://habr.com/ru/post/1274758/


All Articles