How Windows Actually Loads a DLL
What ntdll is really doing when you call LoadLibrary. PEB walkers, LDR data tables, TLS callbacks, and why DllMain has rules that exist for a reason.
LoadLibrary Is Not a Simple Function
Most developers treat LoadLibrary as a black box: you give it a path and get back a handle. What actually happens is a surprisingly complex sequence of operations in ntdll.dll that involves the loader lock, the process environment block, LDR data tables, dependency resolution, and TLS initialisation, all before your DllMain ever runs.
Understanding this sequence matters for several reasons. It explains the rules around what you're not allowed to do in DllMain. It explains why certain injection techniques work and others don't. It explains why reflective loaders go to such lengths to replicate this process manually. And it's genuinely interesting as a piece of OS engineering: the loader has to handle cycles in the dependency graph, forwarded exports, delayed imports, and manifest-based activation contexts, all while holding a lock that prevents reentrancy.
Walking the PEB
Every process has a Process Environment Block accessible via the GS register (at offset 0x60 on x64). The PEB contains a pointer to the PEB_LDR_DATA structure, which maintains three doubly-linked lists of loaded modules: InLoadOrderModuleList, InMemoryOrderModuleList, and InInitializationOrderModuleList. Each entry is an LDR_DATA_TABLE_ENTRY containing the module base, size, full path, and base name.
Before searching disk, the loader walks InLoadOrderModuleList to check if the DLL is already loaded. If the base name matches an existing entry, it increments the reference count and returns immediately. This is why loading the same DLL twice doesn't load it twice. It's also why injection by writing to disk and calling LoadLibrary is detectable: forensics tools walk these same lists and compare loaded modules against disk images. A reflective loader that maps a DLL without registering LDR entries is invisible to this check, which is both the point and the detection surface.
LDR Data Tables and the Load Order
Once the loader decides a DLL needs to be mapped, it creates a new LDR_DATA_TABLE_ENTRY, maps the image into the process address space using NtMapViewOfSection, applies base relocations if the image loaded at a different address than its preferred ImageBase, and walks the import directory to resolve dependencies recursively. Each dependency gets its own LDR entry and goes through the same process.
The InInitializationOrderModuleList is the key one for DllMain. Modules are added to this list only after their dependencies have been initialised. So when your DLL's DllMain runs, you're guaranteed that every DLL you import from has already had its DllMain called. This ordering is what makes it safe to call functions from your dependencies inside DllMain, and it's why you can't safely call LoadLibrary inside DllMain: doing so would attempt to acquire the loader lock, which is already held by the thread running your DllMain.
TLS Callbacks Fire Before DllMain
Thread Local Storage callbacks are a detail most writeups skip. If your PE has a TLS directory (rare in normal code but common in packed binaries as an anti-debugging technique), the callbacks in the TLS callback array fire before DllMain. For executables, they fire before the entry point. This runs in the context of the loader while the loader lock is held.
Packers and protectors use TLS callbacks to run anti-debug checks before any user-visible code runs. If you're debugging a packed binary and you're not catching TLS callbacks, the packer has already run its checks before you see anything in the entry point. In x64dbg you can break on TLS callbacks explicitly. Missing them is a common reason for anti-debug routines to trigger unexpectedly when they worked fine stepping through normally.
What You Can and Can't Do in DllMain
The restrictions on DllMain exist because it runs while the loader lock is held. The loader lock is a critical section in ntdll. If you call anything inside DllMain that tries to acquire the loader lock again, you deadlock. What acquires the loader lock? LoadLibrary, GetModuleHandle, FreeLibrary, and several other functions. The rule: don't call any of those inside DllMain.
What's safe: memory allocation, creating events and mutexes, calling functions in DLLs you import from (since they're already initialised). What's not safe: creating threads that call LoadLibrary (the new thread might try to acquire the lock while you hold it), calling functions that trigger DLL loading on first call, or anything involving COM initialisation. These rules trip up people writing injected DLLs constantly. The standard pattern is to do minimal setup in DllMain, signal an event, and have a worker thread handle the real initialisation outside the loader lock.
The Loader Lock in Practice
The loader lock is LdrpLoaderLock in ntdll, accessible as an undocumented export on some Windows versions. Holding it prevents any other thread from loading or unloading DLLs. This is a global lock for the process, which means a DllMain that takes a long time degrades responsiveness for every other thread that might want to load a library.
In practice, the most common place I've seen this bite is injected DLL code that does network I/O or file I/O in DllMain before starting a separate thread. The I/O blocks, the loader lock is held the entire time, and any thread in the process that calls LoadLibrary (including system code doing lazy DLL loading) deadlocks until the I/O completes. The fix is always the same: get out of DllMain as fast as possible, do your real work elsewhere.