The Big Picture: Who Brings the DLLs In?

When you start a program, Windows creates a process (a running program with its own private memory, see Chapter 9). That process is almost empty at first. Before the program's own code can run, something must bring in all the DLLs it needs and connect them.

That "something" is the loader. It is a part of ntdll.dll, the lowest user-mode DLL. Because ntdll.dll is mapped into every process first, it is always ready to do this job. The loader's main function is called LdrLoadDll (Ldr stands for "loader").

3.1 Load-Time Linking: Listed in the Import Table

What it is: In load-time linking (also called implicit linking), the program's EXE has an import table: a list of "I need these functions from these DLLs". Windows reads that list before the program starts and gets everything ready.

Think of a stage crew setting up before a play. The actors (your code) only walk on stage when every prop is in place.

The load sequence, step by step:

 1. You double-click program.exe
        │
 2. Windows creates the process, maps the EXE and ntdll.dll
        │
 3. The loader reads program.exe's IMPORT TABLE
        │   "needs kernel32.dll, user32.dll, mylib.dll ..."
        │
 4. For each DLL on the list:
        │   a) find the file            (search order, Section 4)
        │   b) map it into the process  (at its preferred address,
        │                                or elsewhere if taken)
        │   c) read ITS import table and repeat for ITS DLLs
        │
 5. Fill in the IAT: write each function's real address
        │   into the program's Import Address Table
        │
 6. Run each DLL's startup code (DllMain, "process attach")
        │
 7. Jump to the program's own first instruction

What each step means:

a) Mapping. The loader does not read the whole DLL into RAM and copy it. It asks Windows to map the file: connect part of the process's address space to the file on disk, so pages come in when needed. Code pages of the same DLL in other processes share the same physical RAM (Section 1.2).

b) Preferred base address and relocation. Every DLL has a preferred base address: where it would like to sit in memory. If something else already uses that spot (or if Windows picks a random one), the DLL is placed elsewhere. Then the loader relocates it: it adjusts every stored address inside the DLL by the difference. A relocation table in the file lists what to adjust.

On modern Windows, ASLR (Address Space Layout Randomization) deliberately picks different, random addresses for DLLs. This makes it harder for attackers to guess where code lives. (Windows typically picks one random address per DLL per boot, so all processes still share its pages.)

c) The IAT (Import Address Table). This is a table in the program with one empty slot for every function it imports. The loader fills each slot with the function's real address in memory. When the program calls CreateFileW, it jumps through its slot.

 program.exe                           kernel32.dll
┌──────────────────────┐             ┌────────────────────────┐
│ call [IAT slot 7]    │             │ ...                    │
│                      │             │ CreateFileW:  code ... │
│ IAT                  │             │ ...                    │
│  slot 7: 0x7FF8...  ─┼────────────>│                        │
└──────────────────────┘             └────────────────────────┘
   slot filled by the loader at load time

d) DllMain. Most DLLs have an optional function named DllMain. The loader calls it at key moments with a reason code:

ReasonWhen it is called
DLL_PROCESS_ATTACHThe DLL is loaded into the process for the first time
DLL_THREAD_ATTACHA new thread starts in the process
DLL_THREAD_DETACHA thread ends
DLL_PROCESS_DETACHThe DLL is being unloaded or the process is ending

Use DllMain for small setup only. The loader holds a lock (a "one at a time" gate) while it runs, so doing heavy work or loading other DLLs inside DllMain can cause freezes. This is a classic programming warning.

If anything fails: If a DLL is missing or a needed function is not found, the loader stops and the program never starts. You see an error box (Section 6.2).

3.2 Run-Time Linking: LoadLibrary and GetProcAddress

What it is: In run-time linking (also called explicit linking), the program does not list the DLL in its import table. Instead, the program's own code asks for the DLL while running, using two Windows functions:

FunctionWhat it does
LoadLibrary"Please load this DLL." Returns a handle (a ticket number that stands for the loaded DLL), or fails
GetProcAddress"Give me the address of the function with this name (or number) in that DLL"
FreeLibrary"I'm done with this DLL." Decreases its use count

The three work like borrowing a tool from a neighbour:

 1. LoadLibrary("plugin.dll")        -> "May I borrow your toolbox?"   (get a handle)
 2. GetProcAddress(handle, "Draw")   -> "Where is the hammer?"         (get an address)
 3. call that address                -> use the hammer
 4. FreeLibrary(handle)              -> "Thanks, here it is back."

Why use run-time linking?

  • Plug-ins: the program does not know in advance which DLLs will exist (a music editor loading effect plug-ins).
  • Optional features: use a newer function only if the DLL has it, and fall back otherwise. This lets one EXE run on both old and new Windows.
  • Faster start-up: load a heavy DLL only when the user needs it.
  • Graceful failure: if the DLL is missing, the program can show a message instead of failing to start.

Delay-load (the hedge between the two). With delay-load, the linker writes special helper code. The first time the program calls a function from that DLL, the helper calls LoadLibrary and GetProcAddress for you, then remembers the address. The programmer writes ordinary code but gets run-time behaviour.

3.3 Reference Counting and Unloading

What it is: Many programs and parts of programs may ask for the same DLL. Windows keeps a reference count for every loaded DLL in a process: a number that goes up by one each time something asks for it and down by one each time something releases it.

 LoadLibrary("a.dll")    count = 1   (DLL is loaded)
 LoadLibrary("a.dll")    count = 2   (no second copy; just the count)
 FreeLibrary(handle)     count = 1   (still in use)
 FreeLibrary(handle)     count = 0   (now it can be unloaded;
                                      DllMain gets DLL_PROCESS_DETACH)

The count is per process. One program unloading a DLL does not affect another program using the same file.

Libraries loaded at start-up through the import table stay loaded until the process ends. Only DLLs loaded with LoadLibrary are normally freed early.

Already loaded? Same name, same DLL. If a process asks for a name that is already loaded, Windows reuses the one it has. It does not load a second copy. That is also why the load order matters for security (Section 4).

Windows-specific tip: To see which DLLs a process has loaded right now, run tasklist /m /fi "imagename eq notepad.exe". You will do this in the Practical Lab.

Summary

TopicRemember this
LoaderPart of ntdll.dll (LdrLoadDll); finds, maps and connects DLLs
Load-time linkingImport table read before the program starts; loader fills the IAT
Preferred base / ASLRDLL wants a certain address; ASLR randomises it; loader relocates if needed
DllMainStartup/shutdown hook with reasons such as DLL_PROCESS_ATTACH; keep it small
Run-time linkingProgram calls LoadLibrary and GetProcAddress itself
Delay-loadConnect on first call
Reference countEach load adds one, each FreeLibrary removes one; unloads at zero