第 5 章 进程与线程 --- 5.12 进程挂靠
本节深入剖析 Windows/ReactOS 中进程挂靠(Process Attachment)机制的完整实现。
概述
进程挂靠(Process Attachment)是 Windows 操作系统中 DLL 加载和初始化的核心机制。当一个 DLL 被加载到进程地址空间时,系统会通过一系列回调机制通知 DLL 执行初始化和清理操作。
进程挂靠的本质是什么?
进程挂靠是一种生命周期管理机制 ,确保 DLL 在进程和线程的各个生命周期阶段都能执行必要的初始化和清理工作。它通过
DllMain函数实现回调,让 DLL 能够响应进程加载、线程创建、进程卸载和线程退出等事件。
想象一个公司场景:
- DLL_PROCESS_ATTACH:新员工(DLL)入职,需要进行入职培训(初始化);
- DLL_THREAD_ATTACH:部门来了新同事(线程),需要进行团队介绍(线程初始化);
- DLL_THREAD_DETACH:同事离职,需要办理离职手续(清理);
- DLL_PROCESS_DETACH:公司倒闭(进程退出),所有员工打包离开(全局清理)。
本节内容概览
- 5.12.0 框架图:进程挂靠的完整流程总览;
- 5.12.1 DLL入口函数DllMain:DllMain签名、四种调用原因;
- 5.12.2 进程启动时的挂靠流程:LdrInitializeProcess、DLL_PROCESS_ATTACH顺序;
- 5.12.3 线程创建时的挂靠流程:LdrInitializeThread、DLL_THREAD_ATTACH顺序;
- 5.12.4 DLL加载时的挂靠流程:LoadLibrary、动态加载时的挂靠;
- 5.12.5 卸载与清理流程:DLL_PROCESS_DETACH、DLL_THREAD_DETACH;
- 5.12.6 关键数据结构:LDR_DATA_TABLE_ENTRY、PEB_LDR_DATA;
- 5.12.7 线程通知禁用机制:DisableThreadLibraryCalls;
- 5.12.8 设计哲学与常见问题:设计原理、调试技巧;
- 5.12.9 为什么会这样------10个设计哲学问答:深入理解挂靠设计决策。
学习目标
读完本节后,读者应当能够:
- 理解进程挂靠的四个阶段及其触发条件;
- 掌握 DllMain 函数的工作机制;
- 分析 LdrInitializeProcess 和 LdrInitializeThread 的实现;
- 理解 DLL 加载顺序和依赖解析机制;
- 解释 DisableThreadLibraryCalls 的作用;
- 识别常见的 DLL 初始化问题。
涉及的内核子系统
| 子系统 | 职责 |
|---|---|
| ntdll | 加载器核心(LdrInitializeProcess、LdrInitializeThread) |
| kernel32 | 用户态 DLL 加载 API(LoadLibrary、FreeLibrary) |
| PEB/LDR | 进程/线程环境块管理 |
5.12.0 框架图
┌──────────────────────────────────────────────────────────────────────────────┐
│ Windows 进程挂靠完整流程 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ 进程启动阶段 │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 内核创建进程 │ │
│ │ 2. 加载 ntdll.dll │ │
│ │ 3. 调用 LdrInitializeProcess │ │
│ │ ├─► 解析导入表 │ │
│ │ ├─► 加载依赖 DLL │ │
│ │ ├─► 按顺序调用 DLL_PROCESS_ATTACH │ │
│ │ └─► 初始化主线程 │ │
│ │ └─► 调用 LdrInitializeThread │ │
│ │ └─► 按顺序调用 DLL_THREAD_ATTACH │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 运行时阶段 │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 动态加载 DLL(LoadLibrary) │ │
│ │ ├─► 解析依赖 │ │
│ │ ├─► 调用新 DLL 的 DLL_PROCESS_ATTACH │ │
│ │ └─► 对所有现有线程调用 DLL_THREAD_ATTACH │ │
│ │ │ │
│ │ 创建新线程(CreateThread) │ │
│ │ ├─► 初始化线程环境 │ │
│ │ └─► 调用 LdrInitializeThread │ │
│ │ └─► 对所有已加载 DLL 调用 DLL_THREAD_ATTACH │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 进程退出阶段 │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 线程退出 │ │
│ │ ├─► 调用 LdrShutdownThread │ │
│ │ └─► 按逆序调用 DLL_THREAD_DETACH │ │
│ │ │ │
│ │ 进程退出 │ │
│ │ ├─► 调用 LdrShutdownProcess │ │
│ │ └─► 按逆序调用 DLL_PROCESS_DETACH │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
5.12.1 DLL入口函数DllMain
5.12.1.1 DllMain函数签名
c
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // DLL模块句柄(基地址)
DWORD fdwReason, // 调用原因
LPVOID lpvReserved // 保留参数
);
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
hinstDLL |
HINSTANCE |
DLL 的模块句柄,等于 DLL 的基地址 |
fdwReason |
DWORD |
调用原因,决定执行哪种操作 |
lpvReserved |
LPVOID |
保留参数,用于区分正常加载和进程卸载 |
5.12.1.2 fdwReason参数的四种取值
| 取值 | 触发时机 | lpvReserved | 含义 |
|---|---|---|---|
DLL_PROCESS_ATTACH |
DLL 被加载到进程时 | NULL(正常)或非 NULL(静态加载) |
进程级初始化 |
DLL_THREAD_ATTACH |
新线程创建时 | 始终为 NULL |
线程级初始化 |
DLL_THREAD_DETACH |
线程正常退出时 | 始终为 NULL |
线程级清理 |
DLL_PROCESS_DETACH |
DLL 被卸载时 | NULL(正常)或非 NULL(进程终止) |
进程级清理 |
5.12.1.3 返回值处理
DllMain 的返回值只对 DLL_PROCESS_ATTACH 有意义:
c
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
// 返回 FALSE 会导致进程启动失败
if (!InitializeMyDll())
return FALSE;
break;
case DLL_THREAD_ATTACH:
// 返回值被忽略
InitializeThreadData();
break;
case DLL_THREAD_DETACH:
// 返回值被忽略
CleanupThreadData();
break;
case DLL_PROCESS_DETACH:
// 返回值被忽略
CleanupMyDll();
break;
}
return TRUE;
}
5.12.1.4 完整示例代码
c
#include <windows.h>
// 进程级全局数据
CRITICAL_SECTION g_cs;
DWORD g_tlsIndex;
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
{
// 初始化临界区
InitializeCriticalSection(&g_cs);
// 分配 TLS 槽
g_tlsIndex = TlsAlloc();
if (g_tlsIndex == TLS_OUT_OF_INDEXES)
return FALSE;
// 禁用线程通知以提高性能(可选)
DisableThreadLibraryCalls(hinstDLL);
break;
}
case DLL_THREAD_ATTACH:
{
// 每个新线程初始化 TLS 数据
PVOID threadData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 1024);
TlsSetValue(g_tlsIndex, threadData);
break;
}
case DLL_THREAD_DETACH:
{
// 清理线程的 TLS 数据
PVOID threadData = TlsGetValue(g_tlsIndex);
if (threadData != NULL)
HeapFree(GetProcessHeap(), 0, threadData);
break;
}
case DLL_PROCESS_DETACH:
{
// 释放 TLS 槽
TlsFree(g_tlsIndex);
// 删除临界区
DeleteCriticalSection(&g_cs);
break;
}
}
return TRUE;
}
5.12.1.5 DllMain的限制
DllMain 中不能调用某些 API,否则可能导致死锁或未定义行为:
| 禁止调用的 API | 原因 |
|---|---|
LoadLibrary / FreeLibrary |
可能导致死锁(加载器锁已被持有) |
CreateThread |
可能触发递归的 DLL_THREAD_ATTACH |
CoInitialize / CoUninitialize |
COM 初始化可能调用 LoadLibrary |
GetStringTypeW 等区域设置函数 |
可能触发延迟加载 |
5.12.1.6 DllMain中的线程安全问题
在 DllMain 中需要特别注意线程安全:
c
// 错误示例:在 DLL_THREAD_ATTACH 中使用未保护的全局变量
int g_globalCounter = 0; // 全局变量,未加锁
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_THREAD_ATTACH:
// 竞态条件!多个线程可能同时执行这里
g_globalCounter++; // 不是原子操作
break;
}
return TRUE;
}
正确做法:
c
// 正确示例:使用 TLS 或锁保护
CRITICAL_SECTION g_cs;
DWORD g_tlsIndex;
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
InitializeCriticalSection(&g_cs);
g_tlsIndex = TlsAlloc();
break;
case DLL_THREAD_ATTACH:
// 使用 TLS,每个线程有独立的计数器
PDWORD counter = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(DWORD));
*counter = 1;
TlsSetValue(g_tlsIndex, counter);
break;
case DLL_THREAD_DETACH:
PDWORD counter = TlsGetValue(g_tlsIndex);
if (counter) HeapFree(GetProcessHeap(), 0, counter);
break;
case DLL_PROCESS_DETACH:
TlsFree(g_tlsIndex);
DeleteCriticalSection(&g_cs);
break;
}
return TRUE;
}
5.12.1.7 DllMain中可以安全调用的API
| API类别 | 示例 | 说明 |
|---|---|---|
| 内存分配 | HeapAlloc, HeapFree, LocalAlloc, LocalFree |
可以安全调用 |
| 线程同步 | InitializeCriticalSection, DeleteCriticalSection |
可以安全调用 |
| 字符串操作 | lstrcpy, lstrlen, wcslen |
可以安全调用 |
| TLS操作 | TlsAlloc, TlsFree, TlsGetValue, TlsSetValue |
可以安全调用 |
| 注册表访问 | RegOpenKey, RegQueryValue, RegCloseKey |
可以安全调用(但不推荐) |
5.12.1.8 实际项目中的DllMain模式
模式一:最小化DllMain
c
// 将初始化逻辑移到单独的函数
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
if (fdwReason == DLL_PROCESS_ATTACH)
{
// 只做最基本的初始化
DisableThreadLibraryCalls(hinstDLL);
}
return TRUE;
}
// 由用户显式调用的初始化函数
BOOL InitializeMyLibrary()
{
// 在这里做复杂的初始化
// 可以安全调用 LoadLibrary 等函数
}
模式二:延迟初始化
c
static LONG g_initialized = 0;
void EnsureInitialized()
{
if (InterlockedCompareExchange(&g_initialized, 1, 0) == 0)
{
// 第一次调用时执行初始化
InitializeCriticalSection(&g_cs);
// ... 其他初始化 ...
}
}
// 导出函数
void __stdcall MyFunction()
{
EnsureInitialized();
// 使用初始化的资源
}
5.12.2 进程启动时的挂靠流程
5.12.2.1 LdrInitializeProcess执行流程
c
NTSTATUS
NTAPI
LdrInitializeProcess(IN PCONTEXT Context,
IN PVOID SystemArgument1)
{
// 1. 初始化堆管理器
RtlInitializeHeapManager();
// 2. 初始化 PEB
InitializePeb();
// 3. 初始化加载器锁
RtlInitializeCriticalSection(&LdrpLoaderLock);
// 4. 设置已知DLL路径
LdrpInitializeKnownDlls();
// 5. 初始化哈希表
LdrpInitializeHashTable();
// 6. 初始化TLS位图
LdrpInitializeTlsBitmaps();
// 7. 加载依赖的DLL
LdrpProcessRelocations(Context);
// 8. 执行初始化例程(DLL_PROCESS_ATTACH)
LdrpRunInitializeRoutines(Context);
return STATUS_SUCCESS;
}
源码位置:dll/ntdll/ldr/ldrinit.c#L1777(file:///d:/reactos/dll/ntdll/ldr/ldrinit.c#L1777)
5.12.2.2 DLL加载顺序
进程启动时,DLL 按以下顺序加载:
1. ntdll.dll(内核模式加载)
↓
2. kernel32.dll(通过导入表)
↓
3. kernel32 的依赖(如 advapi32.dll)
↓
4. 用户程序的其他依赖
↓
5. 按导入表顺序递归加载
5.12.2.3 DLL_PROCESS_ATTACH调用顺序
c
NTSTATUS
NTAPI
LdrpRunInitializeRoutines(IN PCONTEXT Context OPTIONAL)
{
PLIST_ENTRY ListHead, NextEntry;
PLDR_DATA_TABLE_ENTRY LdrEntry;
// 获取模块列表头
ListHead = &Peb->Ldr->InMemoryOrderModuleList;
// 遍历模块列表(按加载顺序)
NextEntry = ListHead->Flink;
while (NextEntry != ListHead)
{
LdrEntry = CONTAINING_RECORD(NextEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
// 检查是否需要调用初始化例程
if (!(LdrEntry->Flags & LDR_ENTRY_DONT_CALL_FOR_THREADS))
{
// 调用 DllMain(hModule, DLL_PROCESS_ATTACH, NULL)
if (LdrEntry->EntryPoint)
{
if (!LdrpCallInitRoutine(LdrEntry->EntryPoint,
LdrEntry->DllBase,
DLL_PROCESS_ATTACH,
NULL))
{
// 初始化失败
return STATUS_PROCESS_INITIALIZATION_FAILED;
}
}
}
NextEntry = NextEntry->Flink;
}
return STATUS_SUCCESS;
}
5.12.2.4 初始化失败的处理
如果某个 DLL 的 DLL_PROCESS_ATTACH 返回 FALSE,系统会执行回滚操作:
c
// 简化的回滚逻辑
NTSTATUS LdrpRollbackInitialization(PLDR_DATA_TABLE_ENTRY FailedEntry)
{
PLDR_DATA_TABLE_ENTRY LdrEntry;
PLIST_ENTRY NextEntry;
// 获取模块列表
NextEntry = Peb->Ldr->InInitializationOrderModuleList.Blink;
// 按逆序卸载已初始化的 DLL
while (NextEntry != &Peb->Ldr->InInitializationOrderModuleList)
{
LdrEntry = CONTAINING_RECORD(NextEntry, LDR_DATA_TABLE_ENTRY,
InInitializationOrderLinks);
// 调用 DLL_PROCESS_DETACH
if (LdrEntry->EntryPoint)
{
LdrpCallInitRoutine(LdrEntry->EntryPoint,
LdrEntry->DllBase,
DLL_PROCESS_DETACH,
(PVOID)1); // 非 NULL 表示进程终止
}
// 卸载 DLL
LdrpUnmapDll(LdrEntry->DllBase);
NextEntry = NextEntry->Blink;
// 如果到达失败的 DLL,停止回滚
if (LdrEntry == FailedEntry)
break;
}
return STATUS_PROCESS_INITIALIZATION_FAILED;
}
回滚流程的关键点:
- 逆序卸载:确保依赖关系的正确性;
- 传递终止标志 :
lpvReserved = (PVOID)1表示进程终止; - 部分清理:只回滚到失败的 DLL,之前的系统 DLL(如 ntdll)保持加载。
5.12.2.5 真实启动序列分析
以一个典型的 Win32 应用为例:
启动序列时间线
┌─────────────────────────────────────────────────────────────────┐
│ Time │ 事件 │
├──────┼─────────────────────────────────────────────────────────┤
│ T0 │ 内核创建进程,映射可执行文件 │
│ T1 │ 加载 ntdll.dll │
│ T2 │ 调用 LdrInitializeProcess │
│ T3 │ 解析应用程序导入表 │
│ T4 │ 加载 kernel32.dll │
│ T5 │ kernel32.dll 的 DLL_PROCESS_ATTACH │
│ T6 │ 加载 kernel32 的依赖(advapi32, user32, gdi32 等) │
│ T7 │ 各依赖 DLL 的 DLL_PROCESS_ATTACH │
│ T8 │ 应用程序主模块的 DLL_PROCESS_ATTACH(如果是 DLL) │
│ T9 │ 调用 LdrInitializeThread(主线程) │
│ T10 │ 各 DLL 的 DLL_THREAD_ATTACH │
│ T11 │ 跳转到应用程序入口点(WinMain/main) │
└──────┴─────────────────────────────────────────────────────────┘
5.12.2.6 加载器锁的详细分析
LdrpLoaderLock 是进程挂靠机制中的核心同步原语:
c
// 加载器锁的初始化
RTL_CRITICAL_SECTION LdrpLoaderLock = {
&LdrpLoaderLockDebug, // DebugInfo
-1, // LockCount (初始为 -1,表示未锁定)
0, // RecursionCount
0, // OwningThread
0, // LockSemaphore
0 // SpinCount (多核系统可设置)
};
加载器锁保护的操作:
| 操作类型 | 具体操作 | 保护原因 |
|---|---|---|
| DLL 加载 | LdrLoadDll, LdrpMapDll |
修改模块列表 |
| DLL 卸载 | LdrUnloadDll, LdrpUnmapDll |
修改模块列表 |
| 初始化调用 | LdrpRunInitializeRoutines |
遍历模块列表 |
| 线程初始化 | LdrInitializeThread |
遍历模块列表 |
| TLS 操作 | TlsAlloc, TlsFree |
修改 TLS 位图 |
| 依赖解析 | LdrpResolveImports |
修改导入表 |
加载器锁的递归特性:
c
// 加载器锁允许递归获取
// 这在 DLL_PROCESS_ATTACH 中加载其他 DLL 时很重要
void ExampleRecursiveLoad()
{
// 第一次获取锁
RtlEnterCriticalSection(&LdrpLoaderLock);
// 在锁内调用 LoadLibrary
// LoadLibrary 内部会再次调用 RtlEnterCriticalSection
// 由于是递归锁,这是允许的
HMODULE hModule = LoadLibrary(L"mydll.dll");
RtlLeaveCriticalSection(&LdrpLoaderLock);
}
5.12.2.7 加载器锁的性能影响
加载器锁是一个进程级的全局锁,会影响多线程应用的性能:
加载器锁竞争场景
线程 A: LoadLibrary("A.dll") → 获取锁 → 等待...
线程 B: LoadLibrary("B.dll") → 等待锁...
线程 C: CreateThread() → 等待锁...
结果:所有操作串行化
性能优化策略:
- 批量加载:在进程启动时一次性加载所有需要的 DLL;
- 延迟加载 :使用
/DELAYLOAD延迟加载非关键 DLL; - 禁用线程通知 :使用
DisableThreadLibraryCalls减少锁竞争; - 避免在热点路径调用:不在高频代码中调用 LoadLibrary。
5.12.3 线程创建时的挂靠流程
5.12.3.1 LdrInitializeThread执行流程
c
VOID
NTAPI
LdrInitializeThread(IN PCONTEXT Context)
{
PPEB Peb = NtCurrentPeb();
PLDR_DATA_TABLE_ENTRY LdrEntry;
PLIST_ENTRY NextEntry, ListHead;
// 获取加载器锁
RtlEnterCriticalSection(&LdrpLoaderLock);
// 获取模块列表头
ListHead = &Peb->Ldr->InMemoryOrderModuleList;
// 遍历所有已加载的模块
NextEntry = ListHead->Flink;
while (NextEntry != ListHead)
{
LdrEntry = CONTAINING_RECORD(NextEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
// 检查是否需要调用线程初始化例程
if (!(LdrEntry->Flags & LDR_ENTRY_DONT_CALL_FOR_THREADS))
{
// 调用 DllMain(hModule, DLL_THREAD_ATTACH, NULL)
if (LdrEntry->EntryPoint)
{
LdrpCallInitRoutine(LdrEntry->EntryPoint,
LdrEntry->DllBase,
DLL_THREAD_ATTACH,
NULL);
}
}
NextEntry = NextEntry->Flink;
}
// 释放加载器锁
RtlLeaveCriticalSection(&LdrpLoaderLock);
}
源码位置:dll/ntdll/ldr/ldrinit.c#L509(file:///d:/reactos/dll/ntdll/ldr/ldrinit.c#L509)
5.12.3.2 主线程与子线程的区别
| 特性 | 主线程 | 子线程 |
|---|---|---|
| 初始化时机 | 进程初始化后期 | 线程创建时 |
| 调用顺序 | 在 DLL_PROCESS_ATTACH 之后 | 在新线程上下文中 |
| TLS 状态 | 可能已部分初始化 | 需要完整初始化 |
| 加载器状态 | 正在初始化 | 已完全初始化 |
5.12.3.3 TLS初始化与线程挂靠的关系
线程挂靠过程中,TLS 初始化是一个重要环节:
线程挂靠与TLS的关系
┌─────────────────────────────────────────────────────────────────┐
│ 1. 创建线程堆栈 │
│ 2. 初始化 TEB(包含 TlsSlots[64]) │
│ 3. 调用 LdrInitializeThread │
│ ├─► 遍历所有已加载 DLL │
│ ├─► 对每个 DLL 调用 DLL_THREAD_ATTACH │
│ └─► DLL 内部调用 TlsSetValue 初始化线程本地数据 │
│ 4. 线程开始执行用户代码 │
└─────────────────────────────────────────────────────────────────┘
5.12.3.4 性能考虑
对于创建大量线程的应用,DLL_THREAD_ATTACH 的调用开销可能成为瓶颈:
c
// 假设有 100 个 DLL,创建 1000 个线程
// 总调用次数 = 100 * 1000 = 100,000 次
这正是 DisableThreadLibraryCalls 存在的原因。
5.12.4 DLL加载时的挂靠流程
5.12.4.1 LoadLibrary实现流程
c
HINSTANCE
WINAPI
LoadLibraryExA(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
{
NTSTATUS Status;
UNICODE_STRING DllName;
PVOID DllBase;
// 1. 将 ANSI 路径转换为 Unicode
RtlAnsiStringToUnicodeString(&DllName, lpLibFileName, TRUE);
// 2. 调用 NTDLL 的 LdrLoadDll
Status = LdrLoadDll(NULL, 0, &DllName, &DllBase);
// 3. 处理错误
if (!NT_SUCCESS(Status))
{
BaseSetLastNTError(Status);
RtlFreeUnicodeString(&DllName);
return NULL;
}
RtlFreeUnicodeString(&DllName);
return (HINSTANCE)DllBase;
}
源码位置:dll/win32/kernel32/client/loader.c#L150(file:///d:/reactos/dll/win32/kernel32/client/loader.c#L150)
5.12.4.2 LdrLoadDll内部流程
c
NTSTATUS
NTAPI
LdrLoadDll(IN PWCHAR PathToFile OPTIONAL,
IN ULONG Flags,
IN PUNICODE_STRING ModuleFileName,
OUT PVOID *BaseAddress)
{
// 1. 获取加载器锁
RtlEnterCriticalSection(&LdrpLoaderLock);
// 2. 检查是否已加载
Status = LdrpFindLoadedDll(ModuleFileName, &ExistingEntry);
if (NT_SUCCESS(Status))
{
// 已加载,增加引用计数
InterlockedIncrement(&ExistingEntry->ReferenceCount);
*BaseAddress = ExistingEntry->DllBase;
RtlLeaveCriticalSection(&LdrpLoaderLock);
return STATUS_SUCCESS;
}
// 3. 解析 DLL 路径
LdrpResolveDllName(ModuleFileName, &ResolvedPath);
// 4. 映射 DLL 文件到内存
LdrpMapDll(ResolvedPath, &DllBase);
// 5. 解析导入表,递归加载依赖
LdrpResolveImports(DllBase);
// 6. 执行重定位
LdrPerformRelocations(DllBase);
// 7. 调用 DLL_PROCESS_ATTACH
if (DllEntryPoint)
{
if (!LdrpCallInitRoutine(DllEntryPoint, DllBase,
DLL_PROCESS_ATTACH, NULL))
{
// 初始化失败,卸载已加载的 DLL
LdrpUnloadDll(DllBase);
RtlLeaveCriticalSection(&LdrpLoaderLock);
return STATUS_PROCESS_INITIALIZATION_FAILED;
}
}
// 8. 对所有现有线程调用 DLL_THREAD_ATTACH
LdrpCallThreadInitRoutines(DllBase);
// 9. 添加到模块列表
LdrpAddModuleToList(DllBase);
// 10. 释放加载器锁
RtlLeaveCriticalSection(&LdrpLoaderLock);
*BaseAddress = DllBase;
return STATUS_SUCCESS;
}
5.12.4.3 已加载线程的DLL_THREAD_ATTACH通知
当动态加载 DLL 时,需要通知所有已存在的线程:
c
VOID
NTAPI
LdrpCallThreadInitRoutines(IN PVOID DllBase)
{
PLDR_DATA_TABLE_ENTRY LdrEntry;
PTEB Teb;
// 找到新加载的 DLL 条目
LdrEntry = LdrpFindEntryForAddress(DllBase);
if (!LdrEntry) return;
// 获取当前进程的线程列表
// ...(简化,实际实现更复杂)
// 对每个线程调用 DLL_THREAD_ATTACH
// 注意:这需要特殊处理,不能中断正在执行的线程
}
5.12.4.4 依赖链的递归加载
DLL 的依赖链按广度优先顺序加载:
依赖链加载示例
应用程序.exe 依赖:A.dll, B.dll
A.dll 依赖:C.dll
B.dll 依赖:C.dll, D.dll
加载顺序:
1. A.dll
2. C.dll(A 的依赖)
3. B.dll
4. D.dll(B 的依赖,C 已加载)
DLL_PROCESS_ATTACH 顺序:
1. C.dll
2. A.dll
3. D.dll
4. B.dll
5.12.5 卸载与清理流程
5.12.5.1 DLL_PROCESS_DETACH触发条件
| 触发条件 | lpvReserved | 说明 |
|---|---|---|
FreeLibrary 引用计数归0 |
NULL |
正常卸载 |
| 进程终止 | 非 NULL |
进程退出时的清理 |
5.12.5.2 DLL_THREAD_DETACH触发条件
| 触发条件 | 说明 |
|---|---|
| 线程正常退出 | 通过 ExitThread 或线程函数返回 |
| 进程终止 | 所有线程强制终止时 |
5.12.5.3 卸载顺序与加载顺序的关系
卸载顺序与加载顺序相反:
c
NTSTATUS
NTAPI
LdrpUnloadDll(IN PVOID DllBase)
{
PLDR_DATA_TABLE_ENTRY LdrEntry;
// 1. 获取加载器锁
RtlEnterCriticalSection(&LdrpLoaderLock);
// 2. 找到 DLL 条目
LdrEntry = LdrpFindEntryForAddress(DllBase);
if (!LdrEntry)
{
RtlLeaveCriticalSection(&LdrpLoaderLock);
return STATUS_NOT_FOUND;
}
// 3. 减少引用计数
if (InterlockedDecrement(&LdrEntry->ReferenceCount) > 0)
{
// 还有其他引用,不卸载
RtlLeaveCriticalSection(&LdrpLoaderLock);
return STATUS_SUCCESS;
}
// 4. 调用 DLL_PROCESS_DETACH
if (LdrEntry->EntryPoint)
{
LdrpCallInitRoutine(LdrEntry->EntryPoint,
LdrEntry->DllBase,
DLL_PROCESS_DETACH,
NULL);
}
// 5. 从模块列表移除
LdrpRemoveModuleFromList(LdrEntry);
// 6. 卸载 DLL(释放内存映射)
LdrpUnmapDll(DllBase);
// 7. 释放加载器锁
RtlLeaveCriticalSection(&LdrpLoaderLock);
return STATUS_SUCCESS;
}
5.12.5.4 资源清理的最佳实践
c
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_DETACH:
{
// 检查是否是进程终止(快速退出)
if (lpvReserved != NULL)
{
// 进程正在终止,不要做耗时操作
// 只需释放关键资源
break;
}
// 正常卸载,可以做完整清理
CleanupAllResources();
break;
}
}
return TRUE;
}
5.12.6 关键数据结构
5.12.6.1 LDR_DATA_TABLE_ENTRY结构
c
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase; // DLL 基地址
PVOID EntryPoint; // DllMain 地址
ULONG SizeOfImage; // 镜像大小
UNICODE_STRING FullDllName; // 完整路径
UNICODE_STRING BaseDllName; // 基础名称
ULONG Flags; // 标志位
USHORT LoadCount; // 加载计数
USHORT TlsIndex; // TLS 索引
LIST_ENTRY HashLinks;
PVOID SectionPointer;
ULONG CheckSum;
ULONG TimeDateStamp;
PVOID LoadedImports;
PVOID EntryPointActivationContext;
PVOID PatchInformation;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
源码位置:sdk/include/ndk/ldrtypes.h(file:///d:/reactos/sdk/include/ndk/ldrtypes.h)
关键字段说明:
| 字段 | 说明 |
|---|---|
DllBase |
DLL 加载到内存的基地址 |
EntryPoint |
DllMain 函数地址 |
Flags |
控制标志(如 LDR_ENTRY_DONT_CALL_FOR_THREADS) |
LoadCount |
引用计数 |
InLoadOrderLinks |
按加载顺序链接 |
InMemoryOrderLinks |
按内存顺序链接 |
InInitializationOrderLinks |
按初始化顺序链接 |
5.12.6.2 PEB_LDR_DATA结构
c
typedef struct _PEB_LDR_DATA {
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID EntryInProgress;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
源码位置:sdk/include/ndk/ldrtypes.h(file:///d:/reactos/sdk/include/ndk/ldrtypes.h)
5.12.6.3 DLL链表管理机制
PEB 维护三个 DLL 链表:
PEB 中的 DLL 链表
┌─────────────────────────────────────────────────────────────────┐
│ PEB.Ldr │
│ ├─► InLoadOrderModuleList → 按加载顺序 │
│ │ ntdll.dll → kernel32.dll → user32.dll → ... │
│ │ │
│ ├─► InMemoryOrderModuleList → 按内存地址顺序 │
│ │ (按 DllBase 排序) │
│ │ │
│ └─► InInitializationOrderModuleList → 按初始化顺序 │
│ ntdll.dll → kernel32.dll → advapi32.dll → ... │
└─────────────────────────────────────────────────────────────────┘
5.12.6.4 加载器锁的作用
LdrpLoaderLock 是一个关键的同步原语:
c
RTL_CRITICAL_SECTION LdrpLoaderLock = {
&LdrpLoaderLockDebug,
-1, // LockCount
0, // RecursionCount
0, // OwningThread
0, // LockSemaphore
0 // SpinCount
};
保护的操作:
- DLL 加载/卸载
- 模块列表修改
- TLS 位图操作
- 导入表解析
5.12.7 线程通知禁用机制
5.12.7.1 DisableThreadLibraryCalls的作用
c
/*
* @implemented
*/
BOOL
WINAPI
DisableThreadLibraryCalls(IN HMODULE hLibModule)
{
NTSTATUS Status;
// 调用 NTDLL 的内部函数
Status = LdrDisableThreadCalloutsForDll((PVOID)hLibModule);
if (!NT_SUCCESS(Status))
{
BaseSetLastNTError(Status);
return FALSE;
}
return TRUE;
}
源码位置:dll/win32/kernel32/client/loader.c#L85(file:///d:/reactos/dll/win32/kernel32/client/loader.c#L85)
5.12.7.2 内部实现
c
NTSTATUS
NTAPI
LdrDisableThreadCalloutsForDll(IN PVOID DllBase)
{
PLDR_DATA_TABLE_ENTRY LdrEntry;
// 找到 DLL 条目
LdrEntry = LdrpFindEntryForAddress(DllBase);
if (!LdrEntry)
return STATUS_NOT_FOUND;
// 设置标志位
LdrEntry->Flags |= LDR_ENTRY_DONT_CALL_FOR_THREADS;
return STATUS_SUCCESS;
}
5.12.7.3 适用场景分析
| 场景 | 是否应该禁用 | 原因 |
|---|---|---|
| 高性能服务器 | 是 | 创建大量线程,减少开销 |
| GUI 应用 | 视情况 | 通常线程数较少 |
| 使用 TLS 的 DLL | 否 | 需要线程通知来初始化 TLS |
| 仅使用进程级资源 | 是 | 不需要线程级初始化 |
5.12.7.4 性能优化效果
性能对比(创建1000个线程,100个DLL)
禁用前:
- DLL_THREAD_ATTACH 调用:100,000 次
- 线程创建时间:较长
禁用后(假设有50个DLL禁用):
- DLL_THREAD_ATTACH 调用:50,000 次
- 线程创建时间:减少约 50%
5.12.8 设计哲学与常见问题
5.12.8.1 进程挂靠的设计原则
- 有序性:初始化按加载顺序,卸载按逆序;
- 原子性:加载器锁确保操作的原子性;
- 可扩展性:支持动态加载和卸载;
- 可靠性:初始化失败时能正确清理。
5.12.8.2 常见问题调试
| 问题 | 现象 | 调试方法 |
|---|---|---|
| DLL_PROCESS_ATTACH 返回 FALSE | 进程启动失败 | 检查依赖项、日志 |
| 死锁 | 进程挂起 | 使用调试器检查加载器锁 |
| TLS 访问失败 | 空指针异常 | 检查 TLS 初始化顺序 |
| DLL 未加载 | 导入函数解析失败 | 检查 PATH、依赖项 |
5.12.8.3 性能优化建议
- 使用 DisableThreadLibraryCalls:减少线程通知开销;
- 合并 DLL:减少模块数量;
- 延迟加载 :使用
/DELAYLOAD延迟加载非关键 DLL; - 避免循环依赖:优化 DLL 依赖结构。
5.12.9 为什么会这样------10个设计哲学问答
Q1:为什么需要DllMain?
A :DllMain 提供了一种统一的初始化和清理机制。没有 DllMain,每个 DLL 需要自己实现初始化逻辑,调用时机难以统一,容易出错。DllMain 让系统能够在正确的时机调用 DLL 的初始化代码。
Q2:为什么有四种调用原因?
A :这是为了支持进程级和线程级的分离:
DLL_PROCESS_ATTACH/DETACH:处理进程级资源(全局变量、共享内存);DLL_THREAD_ATTACH/DETACH:处理线程级资源(TLS、线程本地状态)。
这种分离让 DLL 能够正确管理不同生命周期的资源。
Q3:为什么DLL_PROCESS_ATTACH按加载顺序调用?
A :因为 DLL 之间可能存在依赖关系。如果 A.dll 依赖 B.dll 的导出函数,那么 B.dll 必须先初始化,否则 A.dll 的初始化会失败。按加载顺序调用确保了依赖的 DLL 先完成初始化。
Q4:为什么DLL_THREAD_ATTACH是必要的?
A :因为每个线程可能需要线程本地数据。当新线程创建时,已加载的 DLL 需要为这个新线程初始化它们的 TLS 数据。没有 DLL_THREAD_ATTACH,线程可能会访问未初始化的数据。
Q5:为什么需要加载器锁?
A :加载器锁确保了线程安全。DLL 加载涉及修改共享数据结构(模块列表、TLS 位图等),如果多个线程同时加载 DLL,可能导致数据损坏。加载器锁序列化了这些操作。
Q6:为什么卸载顺序与加载顺序相反?
A :这是为了维护依赖关系的正确性。如果 A.dll 依赖 B.dll,那么 A.dll 必须先卸载,然后才能卸载 B.dll。逆序卸载确保了被依赖的 DLL 最后被卸载。
Q7:为什么DisableThreadLibraryCalls存在?
A :这是一种性能优化。对于创建大量线程的应用,每次创建线程都要调用所有 DLL 的 DLL_THREAD_ATTACH,开销很大。DisableThreadLibraryCalls 允许 DLL 选择退出线程通知,提高性能。
Q8:为什么DllMain不能做耗时操作?
A :因为 DllMain 在加载器锁的保护下执行。如果 DllMain 做耗时操作,会阻塞其他线程的 DLL 加载操作,可能导致整个进程挂起。
Q9:为什么DllMain不能调用某些API?
A :因为某些 API 可能会递归调用加载器。例如,LoadLibrary 会尝试获取加载器锁,但此时锁已经被持有,导致死锁。
Q10:为什么需要lpvReserved参数?
A :lpvReserved 用于区分正常卸载和进程终止。当进程终止时,系统会快速清理,此时某些操作(如网络连接)可能无法完成。DLL 可以通过检查这个参数来决定做多少清理工作。
总结
进程挂靠是 Windows DLL 机制的核心,理解它对于编写健壮的 DLL 和调试加载问题至关重要。本章介绍了:
- DllMain 的四种调用原因:PROCESS_ATTACH、THREAD_ATTACH、THREAD_DETACH、PROCESS_DETACH;
- LdrInitializeProcess:进程启动时的初始化流程;
- LdrInitializeThread:线程创建时的初始化流程;
- 动态加载:LoadLibrary 的实现和挂靠机制;
- 关键数据结构:LDR_DATA_TABLE_ENTRY、PEB_LDR_DATA;
- 性能优化:DisableThreadLibraryCalls 的作用。
核心要点回顾:
- 进程挂靠是 DLL 生命周期管理的核心机制;
- DllMain 在加载器锁保护下执行,需要注意线程安全;
- 初始化顺序与依赖关系密切相关;
- 卸载顺序与加载顺序相反;
- DisableThreadLibraryCalls 可以显著提高多线程应用的性能。
本章代码索引
| 文件 | 内容 |
|---|---|
| dll/ntdll/ldr/ldrinit.c(file:///d:/reactos/dll/ntdll/ldr/ldrinit.c) | LdrInitializeProcess、LdrInitializeThread |
| dll/ntdll/ldr/ldrapi.c(file:///d:/reactos/dll/ntdll/ldr/ldrapi.c) | LdrLoadDll、LdrDisableThreadCalloutsForDll |
| dll/win32/kernel32/client/loader.c(file:///d:/reactos/dll/win32/kernel32/client/loader.c) | LoadLibrary、FreeLibrary、DisableThreadLibraryCalls |
| sdk/include/ndk/ldrtypes.h(file:///d:/reactos/sdk/include/ndk/ldrtypes.h) | LDR_DATA_TABLE_ENTRY、PEB_LDR_DATA |