LDR_DATA_TABLE_ENTRY

The LDR_DATA_TABLE_ENTRY structure is NTDLL’s record of how a DLL is loaded into a process.

Access

Each process has its own list of loaded modules. In some sense, it has three lists since although there is only the one LDR_DATA_TABLE_ENTRY structure for each module, each is linked in three different orders. The way to find the list is well known, including to malware. The Ldr member of the process’s PEB points to the process’s PEB_LDR_DATA which contains the list heads as InLoadOrderModuleList, InMemoryOrderModuleList and InInitializationOrderModuleList. Less well known—or less well respected in real-world practice, even by programmers who aren’t writing malware—is that the links in these lists are not safe to follow while modules might be loaded and unloaded. That this can’t happen at the time can be hard enough to ensure even for the current process.

Documentation Status

In an ideal world, the LDR_DATA_TABLE_ENTRY might be opaque outside NTDLL. But various high-level modules supplied with Windows over the years have used at least one member of the LDR_DATA_TABLE_ENTRY, which eventually had to be disclosed. A new header, named WINTERNL.H, for previously internal APIs was added to the Software Development Kit (SDK) apparently in 2002, and remains to this day. Starting with the SDK for Windows 7, WINTERNL.H presents a modified LDR_DATA_TABLE_ENTRY that has just the InMemoryOrderLinks, DllBase, FullDllName, CheckSum and TimeDateStamp members, plus padding that gets these members to the same offsets as in the true structure. It seems unlikely that Microsoft will change the LDR_DATA_TABLE_ENTRY in any way that moves these members.

Layout

Indeed, given that LDR_DATA_TABLE_ENTRY at least started as an undocumented structure for NTDLL’s internal use, it is surprisingly stable across Windows versions. Until a significant reworking for Windows 8, the structure grew only by extension and many of the original members—which happen to be the most useful in practice—keep their same positions through the whole history. The following table shows the changing sizes:

Version Size (x86) Size (x64)
3.10 to 3.51 0x44  
4.0  to 5.0 0x48  
5.1 before Windows XP SP2 0x4C  
5.1 from Windows XP SP2 to 5.2 0x50 0x98
6.0 0x68 0xC8
6.1 0x78 0xE0
6.2 0x98 0x0110
6.3 to 1511 0xA0 0x0118
1607 and higher 0xA8 0x0120

These sizes, and the offsets, types and names in the tables that follow, are from Microsoft’s symbol files for the kernel starting with Windows 2000 SP3 and for NTDLL starting with Windows XP. Symbol files for earlier versions do not contain type information for the LDR_DATA_TABLE_ENTRY, but inspection confirms that all but one member that was in use by then had near enough the same usage as far back as Windows NT 3.10.

Original

Offset (x86) Offset (x64) Definition Versions
0x00 0x00
LIST_ENTRY InLoadOrderLinks;
3.10 and higher
0x08 0x10
LIST_ENTRY InMemoryOrderLinks;
3.10 and higher
0x10 0x20
LIST_ENTRY InInitializationOrderLinks;
3.10 to 6.1
union {
    LIST_ENTRY InInitializationOrderLinks;
    LIST_ENTRY InProgressLinks;
};
6.2 and higher
0x18 0x30
PVOID DllBase;
3.10 and higher
0x1C 0x38
PVOID EntryPoint;
3.10 and higher
0x20 0x40
ULONG SizeOfImage;
3.10 and higher
0x24 0x48
UNICODE_STRING FullDllName;
3.10 and higher
0x2C 0x58
UNICODE_STRING BaseDllName;
3.10 and higher
0x34 0x68
ULONG Flags;
3.10 to 6.1
union {
    UCHAR FlagGroup [4];
    ULONG Flags;
    struct {
        /*  bit fields, see below  */
    };  
};
6.2 and higher
0x38 0x6C
USHORT LoadCount;
3.10 to 6.1
USHORT ObsoleteLoadCount;
6.2 and higher
0x3A 0x6E
USHORT TlsIndex;
all
0x3C 0x70
union {
    LIST_ENTRY HashLinks;
    struct {
        PVOID SectionPointer;
        ULONG CheckSum;
    };
};
3.10 to 6.1
LIST_ENTRY HashLinks;
6.2 and higher

A practical reason to know of this structure is for the debugging exercise of finding why a DLL did not get unloaded when expected or did get unloaded but by surprise. Both are questions of DLL reference counting. Before Windows 8, the LoadCount member of this structure is the reference count. The LDR_DATA_TABLE_ENTRY for the DLL in question is most easily found when the DLL has just loaded. A program’s loading and unloading of the DLL can then be tracked easily by setting a write-memory breakpoint on the LoadCount member. At each break to the debugger, look at what the count has changed to and look at a stack dump to see who made the change.

Appended for Windows NT 4.0

Offset (x86) Offset (x64) Definition Versions
0x44 0x80
union {
    ULONG TimeDateStamp;
    PVOID LoadedImports;
};
4.0 to 6.1
ULONG TimeDateStamp;
6.2 and higher

Appended for Windows XP

Offset (x86) Offset (x64) Definition Versions
0x48 0x88
PVOID EntryPointActivationContext;
5.1 and higher
0x4C 0x90
PVOID PatchInformation;
5.1 from Windows XP SP2 to 6.2
PVOID Spare;
6.3 only
PVOID Lock;
10.0 and higher

Appended for Windows Vista

Insertion of the LDR_DATA_TABLE_ENTRY into three more lists for Windows Vista soon enough got undone when Windows 8 greatly reworked the tracking of DLLs as they get loaded and unloaded. These members’ positions have an entirely different use in Windows 8 and higher.

Offset (x86) Offset (x64) Definition Versions
0x50 (6.0 to 6.1) 0x98 (6.0 to 6.1)
LIST_ENTRY ForwarderLinks;
6.0 to 6.1
0x58 (6.0 to 6.1) 0xA8 (6.0 to 6.1)
LIST_ENTRY ServiceTagLinks;
6.0 to 6.1
0x60 (6.0 to 6.1) 0xB8 (6.0 to 6.1)
LIST_ENTRY StaticLinks;
6.0 to 6.1

Redone for Windows 8

Offset (x86) Offset (x64) Definition Versions
0x50 0x98
LDR_DDAG_NODE *DdagNode;
6.2 and higher
0x54 0xA0
LIST_ENTRY NodeModuleLink;
6.2 and higher
0x5C 0xB0
LDRP_DLL_SNAP_CONTEXT *SnapContext;
6.2 to 6.3
LDRP_LOAD_CONTEXT *LoadContext;
10.0 and higher
0x60 0xB8
PVOID ParentDllBase;
6.2 and higher
0x64 0xC0
PVOID SwitchBackContext;
6.2 and higher
0x68 0xC8
RTL_BALANCED_NODE BaseAddressIndexNode;
6.2 and higher
0x74 0xE0
RTL_BALANCED_NODE MappingInfoIndexNode;
6.2 and higher

Appended for Windows 7

One addition for Windows 7 also got caught up in the reorganisation for Windows 8. Others are retained but shifted.

Offset (x86) Offset (x64) Definition Versions
0x68 (6.1) 0xC8 (6.1)
PVOID ContextInformation;
6.1 only
0x6C (6.1);
0x80
0xD0 (6.1);
0xF8
ULONG_PTR OriginalBase;
6.1 and higher
0x70 (6.1);
0x88
0xD8 (6.1);
0x0100
LARGE_INTEGER LoadTime;
6.1 and higher

Appended for Windows 8

Offset (x86) Offset (x64) Definition Versions
0x90 0x0108
ULONG BaseNameHashValue;
6.2 and higher
0x94 0x010C
LDR_DLL_LOAD_REASON LoadReason;
6.2 and higher

If only for now, it seems the LDR_DLL_LOAD_REASON isn’t held elsewhere and may as well be enumerated here:

Appended for Windows 8.1

Offset (x86) Offset (x64) Definition Versions
0x98 0x0110
ULONG ImplicitPathOptions;
6.3 and higher

Appended for Windows 10

When Windows 8 extended the LoadCount from its old 16 bits, it defined a ReferenceCount, distinct from the LoadCount, but placed it in the LDR_DDAG_NODE with the new LoadCount. Windows 10 moves it here.

Offset (x86) Offset (x64) Definition Versions
0x9C 0x0114
ULONG ReferenceCount;
10.0 and higher
0xA0 0x0118
ULONG DependentLoadFlags;
1607 and higher
0xA4 0x011C
UCHAR SigningLevel;
1703 and higher

Flags

Starting with version 6.2, what had just been a ULONG for Flags is elaborated formally as bit fields.

Mask Definition Versions
0x00000001
ULONG PackagedBinary : 1;
6.2 and higher
0x00000002
ULONG MarkedForRemoval : 1;
6.2 and higher
0x00000004
ULONG ImageDll : 1;
6.2 and higher
0x00000008
ULONG LoadNotificationsSent : 1;
6.2 and higher
0x00000010
ULONG TelemetryEntryProcessed : 1;
6.2 and higher
0x00000020
ULONG ProcessStaticImport : 1;
6.2 and higher
0x00000040
ULONG InLegacyLists : 1;
6.2 and higher
0x00000080
ULONG InIndexes : 1;
6.2 and higher
0x00000100
ULONG ShimDll : 1;
6.2 and higher
0x00000200
ULONG InExceptionTable : 1;
6.2 and higher
 
ULONG ReservedFlags1 : 2;
6.2 and higher
0x00001000
ULONG LoadInProgress : 1;
6.2 and higher
0x00002000
ULONG ReservedFlags2 : 1;
6.2 to 6.3
ULONG LoadConfigProcessed : 1;
10.0 and higher
0x00004000
ULONG EntryProcessed : 1;
6.2 and higher
0x00008000
ULONG ProtectDelayLoad : 1;
10.0 and higher
 
ULONG ReservedFlags3 : 3;
6.2 to 6.3
ULONG ReservedFlags3 : 2;
10.0 and higher
0x00040000
ULONG DontCallForThreads : 1;
6.2 and higher
0x00080000
ULONG ProcessAttachCalled : 1;
6.2 and higher
0x00100000
ULONG ProcessAttachFailed : 1;
6.2 and higher
0x00200000
ULONG CorDeferredValidate : 1;
6.2 and higher
0x00400000
ULONG CorImage : 1;
6.2 and higher
0x00800000
ULONG DontRelocate : 1;
6.2 and higher
0x01000000
ULONG CorILOnly : 1;
6.2 and higher
0x02000000
ULONG ChpeImage : 1;
1803 and higher
 
ULONG ReservedFlags5 : 3;
6.2 to 1709
ULONG ReservedFlags5 : 2;
1803 and higher
0x10000000
ULONG Redirected : 1;
6.2 and higher
 
ULONG ReservedFlags6 : 2;
6.2 and higher
0x80000000
ULONG CompatDatabaseProcessed : 1;
6.2 and higher

In earlier versions, the Flags bits are presumably defined by macros. Names and values for some are known from the !dlls command as implemented in debugger extensions (KDEXTX86.DLL in versions 3.51 and 4.0, but EXTS.DLL for Windows XP and higher):

Mask Symbolic Name Versions
0x00000002 LDRP_STATIC_LINK 3.51 to 6.1
LDRP_MARKED_FOR_REMOVAL 6.2 and higher
0x00000004 LDRP_IMAGE_DLL 3.51 and higher
0x00000008 LDRP_SHIMENG_ENTRY_PROCESSED 5.1 to 6.1
LDRP_LOAD_NOTIFICATIONS_SENT 6.2 and higher
0x00000010 LDRP_TELEMETRY_ENTRY_PROCESSED 5.1 and higher
0x00001000 LDRP_LOAD_IN_PROGRESS 3.51 and higher
0x00002000 LDRP_UNLOAD_IN_PROGRESS 3.51 to 6.1
0x00004000 LDRP_ENTRY_PROCESSED 3.51 and higher
0x00008000 LDRP_ENTRY_INSERTED 3.51 to 4.0
0x00010000 LDRP_CURRENT_LOAD 3.51 to 4.0
0x00020000 LDRP_FAILED_BUILTIN_LOAD 3.51 to 4.0
0x00040000 LDRP_DONT_CALL_FOR_THREADS 3.51 and higher
0x00080000 LDRP_PROCESS_ATTACH_CALLED 3.51 and higher
0x00100000 LDRP_DEBUG_SYMBOLS_LOADED 3.51 to 4.0
0x00400000 LDRP_COR_IMAGE 5.1 and higher
0x00800000 LDRP_COR_OWNS_UNMAP 5.1 to 6.1
LDRP_DONT_RELOCATE 6.2 and higher
0x01000000 LDRP_COR_IL_ONLY 5.1 and higher
0x10000000 LDRP_REDIRECTED 5.1 and higher