DllMain and the Loader Lock — The Real Reason You Are Told to "Do Nothing in DLL Initialization"

· Updated: · · Windows, DLL, Windows Development, C++, Bug Investigation, Multithreading, Win32 API

Revision history (1 updates, last updated Sep 8, 2026)

A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.

Retranslated as a full translation of the current Japanese original. The previous English version was an abridgement that dropped subsections, tables, diagrams, and paragraphs; all of them have been restored to match the Japanese article, and the knowledge map section has been added where the Japanese article has one. The technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.22170901)
First published
Cite this article(DOI: 10.5281/zenodo.22170900)

This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.

Go Komura (2026). DllMain and the Loader Lock — The Real Reason You Are Told to "Do Nothing in DLL Initialization". KomuraSoft LLC. https://doi.org/10.5281/zenodo.22170900 https://comcomponent.com/en/blog/dllmain-loader-lock/

DOI (latest version)
10.5281/zenodo.22170900
DOI (this version)
10.5281/zenodo.22652526

“When we load our own DLL, LoadLibrary never returns.” “It hangs only on certain PCs, or only when the service starts.” When you see failures like these, one of the first places to check is the DLL’s initialization code: DllMain and everything it calls.

DllMain is not like an application’s ordinary initialization function. It is a function the OS calls while holding the loader lock, so what it may do is tightly restricted. Microsoft’s position that “the ideal DllMain is an empty stub” comes from the fact that this restriction affects not only your own DLL but the other DLLs and threads in the process.1

This article is for developers who write DLLs, plug-ins, and C++/CLI wrappers on Windows. It works through the topic in this order: why it stops, then where to move initialization, then how to shut down, then how to investigate a hang.

1. The Bottom Line First — Keep DllMain Small and Change When the Work Runs

Rather than looking for a safe way to write something inside DllMain, the basic approach is to reduce the work that runs there. Decide what stays in the following order.1

Decision Basic policy Where to read more
When to initialize Do statically whatever can be decided at compile time; defer the rest to first use after the load completes Section 5
What to call from DllMain Avoid LoadLibrary / FreeLibrary, synchronization with other threads, and the use of User, Shell, COM, and the like. Check indirect calls too Section 3
What to do at shutdown Separate the case where only the DLL is unloaded from the case where the whole process exits Section 6

What is easy to miss is that the same restrictions apply to the constructors and destructors of global and static C++ objects. In C++/CLI, you also eliminate every path that executes MSIL under the loader lock. Whether the DllMain body itself is empty is not enough to judge by.23

If you are investigating a hang right now, start at Section 7; if you are reviewing a design, read Sections 2 through 6 in order, and you can match each prohibition with its remedy.

2. The Mechanism — DllMain Is Called Inside the Loader Lock

2.1 It Is Called Not Only at Load Time but Also When Threads Start and Exit

DllMain is the entry point the OS loader calls when a DLL enters or leaves a process or a thread. There are four notifications.2

Notification When
DLL_PROCESS_ATTACH When the DLL is loaded into the process
DLL_THREAD_ATTACH When a new thread starts in the process
DLL_THREAD_DETACH When a thread exits normally
DLL_PROCESS_DETACH When the DLL is unloaded, or when the process exits

A thread-start notification goes not only to the DLL that created the thread but to every DLL loaded in the process. DllMain is not “code that runs once when my DLL is loaded”. In a process that creates threads frequently, it runs on every notification.2

If you do not need the notifications, one option is to call DisableThreadLibraryCalls inside DLL_PROCESS_ATTACH. It cannot be used in a DLL linked with the static CRT, however, and static TLS imposes a condition of its own, so Section 5.3 treats that decision separately.4

2.2 Being Called With the Lock Held Is Where Every Restriction Starts

To keep DLL loads, unloads, and notifications consistent, the OS loader serializes them with a single loader lock per process. The important point is that it acquires this lock before calling DllMain and keeps holding it while DllMain runs.1

During that time, any other thread in the same process that tries to load a DLL or to proceed through a thread start or exit notification waits for the lock to be released. It is not a place where you may insert a long wait just because it suits your own DLL.

Why work in DllMain affects DLL notifications across the processThe loader acquires the process-wide loader lock before calling DllMain, and DLL loads and thread notifications on other threads wait for it to be released, so work in DllMain affects other DLLs and threads tooLoader acquires the shared lockDllMain runsDllMain returnsLoader lock is releasedDLL load or notification on another threadWaits for the same lock to be releasedWaiting work can proceed

Figure 1: The shared lock is held while DllMain runs, so a wait there stops the progress of other DLLs and threads.

The prohibitions that follow make sense once you ask “does this work need, directly or indirectly, the loader lock or the initialization of another DLL?”

3. Why It Stops — Understanding the Prohibitions Through Four Paths

3.1 Calling Something That Loads Another DLL

Avoid calling LoadLibrary / FreeLibrary from DllMain. LoadLibrary creates circular dependencies in the load order and leads to a DLL being used before its initialization code has run. On the shutdown side, there is likewise a risk of using a DLL that has already been processed or released.2

Not having written LoadLibrary yourself does not make you safe. Some User, Shell, and COM functions load other system components internally, and they can touch a component that is not yet initialized or already released and cause an access violation.2

The baseline of what can safely be called is the functions in Kernel32.dll that do not load other DLLs, since Kernel32.dll is guaranteed to be loaded by the time DllMain runs. For example, you can create synchronization objects such as critical sections and mutexes, and you can use TLS. The official documentation states explicitly, however, that no exhaustive list of safe functions exists. Do not reason “it is in Kernel32, so anything goes” or “I can create a synchronization object, so I may wait for other threads”.2

3.2 Waiting Inside DllMain for Another Thread to Exit

The classic deadlock happens when the DLL is unloaded: DllMain asks a worker thread to stop and then waits for it to exit.

Even after the worker finishes its own work, it has to pass through the DLL_THREAD_DETACH notification on thread exit. That notification needs the loader lock, which the waiting DllMain is holding. The result is that DllMain waits for the worker, and the worker waits for DllMain to return.5

Why waiting for a thread in DllMain deadlocksDllMain, holding the loader lock, waits for a worker thread to exit, but the exiting worker thread waits for the loader lock to be released for its DLL_THREAD_DETACH notification, so the two wait on each other and deadlockWorker threadDllMainLoader (holding the lock)Worker threadDllMainLoader (holding the lock)Exit notification needs the loader lockDllMain waits holding the lock, W waits for the lockNotify DLL_PROCESS_DETACHRequest exit and wait for completionFinish work and head for thread exit

Figure 2: Even after the worker finishes its work, it cannot get through the thread exit notification, so the exit wait inside DllMain never ends.

This is not a case of “it stops if you are unlucky”; the mutual wait holds structurally. The cleanup needed at unload is treated in Section 6, separately from the work at process exit.

3.3 Your Own Lock and the Loader Lock Are Acquired in Opposite Orders

It is not only thread start and exit; APIs such as GetModuleHandle also need the loader lock internally. When the following two paths overlap, the lock acquisition order is inverted.6

  • On the DllMain side, code that already holds the loader lock tries to take private lock G.
  • On the worker side, code that already holds private lock G calls an API and tries to take the loader lock.
Order inversion between the loader lock and a private lockDllMain goes for a private lock while holding the loader lock, and a worker thread goes for the loader lock, for GetModuleHandle and the like, while holding the private lock, so the acquisition order is inverted and they deadlockDllMain: holding the loader lockGoes for private lock GWorker: holding private lock GGoes for the loader lockDeadlock from inverted acquisition orderRequired internally by GetModuleHandle and others

Figure 3: When one side takes the loader lock and then G, and the other takes G and then the loader lock, each waits for the other to release.

The official guidance asks you to treat the loader lock as the top of the application’s lock hierarchy, that is, the lock acquired first. By the time you are inside DllMain, that lock is already held. Check not only the name of the function you call but also which locks you are holding when you call it.6

3.4 Even Just Creating a Thread Leaves Start-Wait and Lifetime Problems

CreateThread inside DllMain is not recommended either. The new thread cannot begin running its thread function until the DLL_THREAD_ATTACH notification has been processed. Because the current DllMain holds the loader lock, waiting inside that DllMain for the thread to start or finish deadlocks.5

Not waiting does not solve everything either. If the DLL is unloaded after DllMain returns but before the thread you created has started running, the thread’s start address points at code that has already been released, and a crash remains possible.5

Two problems that remain when a thread is created in DllMainA thread created in DllMain waits for the loader lock for its start notification, so if DllMain waits for it to start or finish they deadlock, and even if DllMain returns without waiting, the code's lifetime ends and it crashes if the DLL is unloaded before the thread starts runningYesNoThread created inside DllMainNew thread waits for the start notificationWait for start or completion inside DllMain?Mutual wait while holding the lockReturn from DllMainDLL unloaded before the thread starts runningCode at the start address is gone

Figure 4: Not waiting inside DllMain and protecting the lifetime of the DLL the created thread uses are two separate requirements.

4. Two Places That Are Dangerous Even When DllMain Is Empty

4.1 Dynamic Initialization of Global and Static Objects

In a DLL linked with the CRT (the C/C++ runtime), the constructors and destructors of global and static C++ objects run through the CRT’s entry point. They are effectively part of DllMain and are subject to the same restrictions.2

Putting configuration-file loading, logger startup, COM initialization, or thread startup into a constructor only hides where the call is made; the moment it runs is still inside the loader lock. If that complex work includes loading another DLL or synchronizing with threads, it is exactly as dangerous as writing it in the DllMain body.

How global object initialization becomes a landmineLoading the DLL acquires the loader lock and the constructors of global objects run through the CRT, so a LoadLibrary, thread synchronization, or COM initialization inside them is an execution of what DllMain prohibitsDLL load (loader lock acquired)CRT entry pointGlobal object constructorLoadLibrary-equivalent workStart a thread and wait for it to finishUse of COM or User32All of these are DllMain prohibitions

Figure 5: Review not only the DllMain body but also the initialization and termination of static objects called from the CRT.

Treat compile-time constant initialization, for example anything that can be constexpr, separately from complex run-time initialization. Defer dynamic initialization that involves function calls, and arrange for the first access to happen outside DllMain as well.

4.2 MSIL Executed in a C++/CLI Callee

Wrapping a native DLL in C++/CLI is covered in Calling Native DLLs from C#: C++/CLI Wrapper vs P/Invoke. In this configuration, watch for paths that execute MSIL (managed code) under the loader lock. If executing the MSIL requires CLR initialization or the load of another assembly, a deadlock is possible.3

The compiler emits warning C4747 for code in which DllMain directly tries to execute MSIL. But it cannot detect indirect execution through a function in another module. The absence of the warning alone is no basis for calling the code safe. Dynamic initializers of static objects are also in scope.3

Whether MSIL execution under the loader lock can be detectedCode in which DllMain executes MSIL directly can be detected by the compiler with warning C4747, but indirect execution through a function in another module cannot, so it has to be prevented by reviewing the call tree and compiling natively throughoutCall from DllMainExecutes MSIL directlyExecutes via another moduleDetected by warning C4747Compiler cannot detect itPrevent by review and #pragma unmanaged

Figure 6: In addition to the direct path that C4747 detects, review calls that go through another module.

The remedy is to compile DllMain and every function reachable from it as native code with #pragma unmanaged, or to have no DllMain at all. Even with the latter, do not overlook indirect paths such as static initializers.3

5. Designing Initialization — Make It Static, Defer It, Keep Only the Minimum

5.1 Before Leaving Something in DllMain, Ask Whether Its Timing Can Change

The official baseline is to complete whatever initialization you can at compile time and defer the rest as far as possible. Only work that must be detected early as a load failure stays, as an exception and at a minimum.1

For instance, there can be a requirement to make the DLL load itself fail because a configuration file it depends on is corrupt. Even then, narrow it to “attempt the required work and fail immediately” rather than running other initialization first and failing afterward. This is not an exception that permits complex initialization at load time.1

Design guidelines for DLL initializationFirst consider whether initialization can be static at compile time; if not, default to deferring it to first use, and leave in DllMain only the minimum that must be detected early as a load failureYesNoNoYesDecidable at compile time?Make it static initializationMust the failure be detected at load?Defer to first use (the default)Do only the minimum in DllMainGuard with INIT_ONCE or a function-local static

Figure 7: Consider static and deferred initialization first, and leave in DllMain only the minimum that needs early detection.

5.2 Deferred Initialization Has to Be Designed Down to “Who Calls It First”

For mutual exclusion on first use, you can use one-time initialization with INIT_ONCE or a C++ function-local static (a magic static). The idea is to move complex global objects to a pointer created on first access or to a function-local static.

Deferring alone, however, does not get you out of the loader lock. The restriction is lifted only when the first access comes from “an ordinary API called after the DLL load has completed”. In that case, it can be designed as ordinary initialization code that may use nearly the entire Windows API.1

Conversely, if that first access happens from DllMain or a static initializer, the initialization still ends up running under the loader lock. Beyond extracting an initialization function, confirm who calls it first, and when.

When deferred initialization escapes the loader lockIf the first access of deferred initialization comes from an ordinary API after the load completes, it can initialize outside the loader lock, but if it comes from DllMain or a static initializer, it runs under the same restrictionsOrdinary API after the load completesDllMain or a static initializerWhere does the first access come from?Initialize outside the loader lockInitialize under the same restrictionsGuard with INIT_ONCE or a function-local staticMove the moment of first access too

Figure 8: INIT_ONCE and function-local statics take care of mutual exclusion for the initialization; they do not guarantee it is called outside the loader lock.

5.3 Decide on DisableThreadLibraryCalls by Three Conditions

In a DLL that does not depend on thread notifications, calling DisableThreadLibraryCalls in DLL_PROCESS_ATTACH stops the DLL_THREAD_ATTACH / DLL_THREAD_DETACH notifications. In a process that creates threads frequently, this reduces notification overhead.4

Before applying it, check the static CRT, static TLS, and whether anything uses the notifications. A DLL linked with the static CRT must not call it, because the CRT itself needs the thread notifications. When static TLS via thread_local or __declspec(thread) is in effect, the call itself fails and returns FALSE.4

Deciding whether to call DisableThreadLibraryCallsA DLL linked with the static CRT must not call it, and with static TLS in effect the call itself fails so it is not called, but a DLL that is neither and does not use thread notifications can call it in DLL_PROCESS_ATTACH, checking the return value, to cut notification costYesNoYesNoNoYesLinked with the static CRT?Must not call itUses static TLS?Call fails anyway (FALSE)Needs thread notifications?Call it in ATTACH (check the return value)Do not call it; handle the notifications

Figure 9: Distinguish the static CRT, where it must not be used, from static TLS, where it fails, and check the return value even when notifications are unneeded.

It is an optimization to consider for a typical DLL that uses the dynamically linked CRT and meets these conditions. DisableThreadLibraryCalls exists to reduce notifications; it is not a means of doing complex initialization in DllMain.

6. Shutdown — Separate DLL Unload From Process Exit

6.1 The Same DLL_PROCESS_DETACH, but What Remains Afterward Differs

DLL_PROCESS_DETACH is delivered both when only the DLL is unloaded and when the whole process exits. The notification has the same name, but the premises for cleanup differ.5

On an unload via FreeLibrary, the process keeps running afterward. So threads must be stopped, and open handles, allocated resources, state that needs persisting, and so on must be cleaned up properly. As Section 3.2 showed, however, waiting inside DllMain for a thread’s natural exit for that purpose deadlocks.5

Better still, avoid a design in which a DLL that can be unloaded owns threads at all; moving thread ownership to the EXE side is the safest option. For an existing design in which the DLL owns threads, consider the following stop protocol together with its constraints, never one without the other.

6.2 The Officially Documented Stop Protocol When the DLL Owns a Worker

Microsoft’s best practices document a stop procedure for unload that waits not for “the thread’s natural exit” but for “a signal that it has reached a consistent state”.5

  1. The DllMain side signals the worker to stop, using an event.
  2. The worker winds down its current work to a consistent state, signals completion, and enters an infinite wait.
  3. The DllMain side confirms the consistent state and ends the thread with TerminateThread.

It looks rough, but it was documented under the constraint that waiting for a natural exit makes the exit notification collide with the loader lock. The premise is that the work of reaching the consistent state also obeys the same restrictions as DllMain. If that work enters a load of another DLL or a wait for the loader lock, a deadlock with the side waiting for the signal is unavoidable.5

Waiting at unload for consistency completion rather than natural exitDllMain signals the worker to stop, the worker reaches a consistent state while obeying the same restrictions as DllMain, signals back, and enters an infinite wait, and DllMain confirms the consistent state before terminating the threadWorker threadDllMain (at unload)Worker threadDllMain (at unload)What is awaited is the consistency signal, not natural exitSignal stop with an eventReach a consistent state under the same restrictionsSignal consistency reachedEnter an infinite waitConfirm consistency, then TerminateThread

Figure 10: Even with this protocol, the work that establishes the consistent state must not wait for the loader lock.

This does not mean that any running thread may simply be cut off. First consider whether the thread can be owned outside the DLL.

6.3 At Process Exit, the Ideal Is to Return Without Doing Anything

By the time DLL_PROCESS_DETACH is delivered at process exit, the other threads have exited or been forcibly terminated, and the consistency of the address space cannot be relied on. The state of dependent DLLs and runtimes cannot be trusted either, so cleanup such as freeing memory becomes dangerous rather than helpful. The official guidance also says that the ideal handler in this case is empty.5

Write out data that needs persisting in the application’s own shutdown code. Do not use DLL_PROCESS_DETACH as the last place where anything can be tidied up.

Cleanup premises differ between DLL unload and process exitWhen only the DLL is unloaded, the process keeps running so resources must be cleaned up, but avoid waiting for natural exit inside DllMain; at process exit, do not rely on the consistency of other threads and resources, basically return without doing anything, and write out state to save in the application's shutdown code beforehandDLL unload onlyProcess exitReason for DLL_PROCESS_DETACH?Process keeps running afterwardClean up remaining resources properlyDo not wait for natural exit inside DllMainState of other threads and resources is uncertainBasically return without doing anythingSave beforehand in the application's shutdown code

Figure 11: Decide not just “is cleanup needed” but whether the process keeps running and whether the resources used for cleanup can be trusted.

7. Investigating a Hang — Find the Side Waiting for the Loader Lock and the Side Holding It

7.1 Confirm the Pair of Mutually Waiting Threads in a Dump

Capture a dump at the moment of the freeze and examine each thread’s stack. The typical fingerprint is a pair: a thread waiting for a lock inside a loader function in ntdll.dll whose name starts with Ldr, and a thread waiting for something else inside DllMain or a static initializer (dynamic initializer).

A thread stopped in the middle of a LoadLibrary call is another typical participant. Once the relationship between the side waiting for the lock and the side holding it while waiting for something else connects, you can almost certainly conclude that it is a loader-lock deadlock.

Confirming a loader-lock mutual wait from a hang dumpIn each thread's stack look for a lock wait in Ldr functions and a wait inside DllMain or a static initializer, confirm that the two are waiting on each other, and if the typical pair does not appear, investigate other kinds of hang as wellYesNot visibleCapture a dump during the hangExamine each thread's stackLock wait in Ldr functionsWait inside DllMain or a static initializerDoes the mutual-wait relationship connect?Loader-lock deadlockInvestigate other kinds of hang too

Figure 12: Do not decide from a single stack; look for the pair of the side waiting for the loader lock and the side holding it while waiting.

Conditions such as “occasionally at startup”, “only on a particular machine”, or “only when run as a service” are clues as well. The mutual wait forms when a DLL load coincides with a thread start or exit, so environment differences and execution timing change how it surfaces.

7.2 Prevent It With Application Verifier and Callee Review

Running tests with Application Verifier enabled detects the typical DllMain mistakes at run time.1 In C++/CLI, do not ignore warning C4747, and check the indirect paths the warning cannot detect from the perspective of Section 4.2.

In review, look for functions that call LoadLibrary indirectly among the work reachable from DllMain. COM initialization, some CRT features, and delay-load imports are things to check. In particular, it is easy to overlook that the first call to a delay-loaded import function becomes a LoadLibrary internally. Even if the function name lives outside DllMain, as long as the caller is DllMain, the restriction remains.

8. Summary — Look Not at Function Names but at “When, and Under Which Lock, It Runs”

The DllMain restrictions can all be understood from one point: it is called while holding the process-wide loader lock. Not only your own DllMain but also the CRT’s static initialization and termination and C++/CLI’s indirect calls fall within the same scope.

Start a review by asking whether initialization can be made static and whether the rest can be deferred until after the load completes. Check all the way down to the first access of the deferred work, and if notifications are unnecessary, consider DisableThreadLibraryCalls after checking the static CRT and static TLS conditions.

On the shutdown side, separate a DLL-only unload from a whole-process exit. If the DLL owns a worker, follow the protocol of signal, consistency check, and termination, and where possible move thread ownership to the EXE side. Bring DllMain at process exit close to empty, and put data saving in the application’s own shutdown code.

The order for reviewing DllMain and what it callsExamine the scope including not only DllMain but static initialization and indirect calls, move initialization to static or after-load, design stopping and cleanup according to the shutdown reason, then confirm with Application Verifier and dumpsDllMain, static initialization, and calleesMove initialization to static or after-loadAlso check the first access of deferred workDesign cleanup per shutdown reasonConfirm with Verifier, warnings, and dumps

Figure 13: Tracking not only what initialization does but when it runs and its lifetime at shutdown lets you treat the prohibitions as a single design policy.

The question to ask at a borderline case is “is this work acceptable to run while holding the loader lock?” Not cramming questionable initialization into DllMain, and changing when it runs instead, is the starting point of a safe DLL design.

KomuraSoft LLC handles root-cause investigation (dump analysis) of hangs and deadlocks at startup or DLL load time, design reviews around DllMain and static initialization, and reworking C++/CLI wrappers and plug-in DLLs toward a safe initialization design. You are welcome to consult us even at the hard-to-reproduce stage of “startup freezes only in a particular environment”.

References

  1. Microsoft Learn, Dynamic-Link Library Best Practices. On DllMain being called while the loader lock is held, so that the functions it can call are severely restricted; the ideal DllMain being an empty stub, with initialization deferred as far as possible; the recommendation of compile-time static initialization; doing only the minimum for failures that must be detected early; and detecting typical DllMain mistakes with Application Verifier. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  2. Microsoft Learn, DllMain entry point. On performing only simple initialization and termination in the entry point; why LoadLibrary / FreeLibrary must not be called (circular load order and use of a DLL before initialization or after termination); Kernel32.dll being guaranteed loaded, so that it can be called in the range that does not load other DLLs; no exhaustive list of safe functions existing; User, Shell, and COM functions causing access violations; DLL notifications being serialized, so that communication with other threads or processes causes deadlocks; and the same restrictions applying to the constructors and destructors of static objects when linked with the CRT. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  3. Microsoft Learn, Initialization of Mixed Assemblies. On not executing MSIL under the loader lock; not compiling DllMain and its call tree to MSIL and dealing with that via #pragma unmanaged; warning C4747 being emitted when DllMain tries to execute MSIL directly, while indirect execution through another module cannot be detected; and dynamic initializers of static objects being able to cause the same problem. ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). On disabling DLL_THREAD_ATTACH / DLL_THREAD_DETACH notifications to reduce overhead at thread creation and destruction; not calling it from a DLL linked with the static CRT; and the optimization not being performed when static TLS (thread_local or __declspec(thread)) is in effect. ↩ ↩2 ↩3

  5. Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. On the structure that deadlocks when DllMain waits for a thread to exit (the DLL_THREAD_DETACH notification on thread exit needs the loader lock); the protocol for stopping threads at unload (signal with an event, confirm a consistent state, then terminate); DLL_PROCESS_DETACH at process exit, where the other threads have already been forcibly terminated, address-space consistency is not guaranteed, and the ideal handler is empty; and creating a thread in DllMain leaving notifications pending with initialization incomplete and causing problems. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  6. Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. On defining a lock hierarchy and always acquiring in the same order; the loader acquiring the loader lock before calling DllMain, so that the loader lock should sit at the top of the lock hierarchy; keeping the acquisition order between APIs that take the loader lock indirectly, such as GetModuleFileName, and private locks; and a concrete example of a deadlock caused by lock-order inversion. ↩ ↩2

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Frequently Asked Questions

Common questions about the topic of this article.

Can you really do nothing at all in DllMain?
"Do nothing" is not an exaggeration; it is the official design policy, and Microsoft itself says the ideal DllMain is a near-empty stub. What is safe is limited to the functions in Kernel32.dll, which is guaranteed to be loaded by the time DllMain runs, that do not load other DLLs. For example, you can create critical sections and mutexes and use TLS. Conversely, LoadLibrary/FreeLibrary, synchronization with other threads, and calls into User32, Shell, COM, and the like are forbidden because they cause deadlocks and access violations. For initialization you are unsure about, the right answer is not to do it in DllMain but to defer it until the first time it is used.
Do the constructors of C++ global variables (static objects) also fall under the DllMain restrictions?
They do. When the DLL is linked with the CRT (the C++ runtime), the constructors and destructors of global and static objects run through the entry point the CRT provides, effectively as part of DllMain. That means calling LoadLibrary in a constructor, starting another thread and waiting for it to finish, initializing COM, and so on all carry the same danger as doing them in DllMain. For global objects with complex initialization, shift the execution timing outside DllMain: keep a pointer and create the object on first access, or use a function-local static.
Should I call DisableThreadLibraryCalls?
It is useful under conditions. If the DLL does not need DLL_THREAD_ATTACH/DETACH notifications, calling DisableThreadLibraryCalls in DLL_PROCESS_ATTACH stops the notification on every thread creation and destruction and reduces overhead in a process that creates threads frequently. There are two exceptions, however. Do not call it in a DLL linked with the static CRT (the static CRT needs the thread notifications). And when static TLS via thread_local or __declspec(thread) is in effect, the call itself fails and returns FALSE, so make a habit of checking the return value. Use it in a typical DLL with the dynamically linked CRT, after confirming that nothing depends on the thread notifications.
Why does a C++/CLI (mixed managed) DLL hang at startup?
The typical cause is an attempt to execute MSIL (managed code) while the loader lock is held. In a C++/CLI mixed assembly, if DllMain, the functions it calls, or the dynamic initializers of global variables are compiled to MSIL, CLR initialization or the load of another assembly becomes necessary under the loader lock, and that can deadlock. The compiler emits warning C4747 when DllMain tries to execute MSIL directly, but it cannot detect indirect execution through another module. The remedy is to compile DllMain and its call tree as native code with #pragma unmanaged, or not to have a DllMain at all.
May I clean up resources in DLL_PROCESS_DETACH?
The answer differs between "process exit" and "unload via FreeLibrary". In DLL_PROCESS_DETACH at process exit, the other threads have already been forcibly terminated and there is no guarantee that the address space is consistent, so cleanup such as freeing memory is actually dangerous, and the official guidance is that "the ideal handler is empty". Write out data that needs persisting in the application's own shutdown code, and in the handler basically do nothing and return. On an unload via FreeLibrary, by contrast, the process keeps running, so complete cleanup, such as stopping threads and closing handles, is required. Waiting for a thread to exit inside DllMain deadlocks, however, so you have to follow the protocol the official documentation describes: signal, wait until a consistent state, and do the final work outside DllMain.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog