Reactos 第 5 章 进程与线程 — 5.12 进程挂靠

第 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:公司倒闭(进程退出),所有员工打包离开(全局清理)。

本节内容概览

  1. 5.12.0 框架图:进程挂靠的完整流程总览;
  2. 5.12.1 DLL入口函数DllMain:DllMain签名、四种调用原因;
  3. 5.12.2 进程启动时的挂靠流程:LdrInitializeProcess、DLL_PROCESS_ATTACH顺序;
  4. 5.12.3 线程创建时的挂靠流程:LdrInitializeThread、DLL_THREAD_ATTACH顺序;
  5. 5.12.4 DLL加载时的挂靠流程:LoadLibrary、动态加载时的挂靠;
  6. 5.12.5 卸载与清理流程:DLL_PROCESS_DETACH、DLL_THREAD_DETACH;
  7. 5.12.6 关键数据结构:LDR_DATA_TABLE_ENTRY、PEB_LDR_DATA;
  8. 5.12.7 线程通知禁用机制:DisableThreadLibraryCalls;
  9. 5.12.8 设计哲学与常见问题:设计原理、调试技巧;
  10. 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;
}

回滚流程的关键点

  1. 逆序卸载:确保依赖关系的正确性;
  2. 传递终止标志lpvReserved = (PVOID)1 表示进程终止;
  3. 部分清理:只回滚到失败的 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() → 等待锁...

结果:所有操作串行化

性能优化策略

  1. 批量加载:在进程启动时一次性加载所有需要的 DLL;
  2. 延迟加载 :使用 /DELAYLOAD 延迟加载非关键 DLL;
  3. 禁用线程通知 :使用 DisableThreadLibraryCalls 减少锁竞争;
  4. 避免在热点路径调用:不在高频代码中调用 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 进程挂靠的设计原则

  1. 有序性:初始化按加载顺序,卸载按逆序;
  2. 原子性:加载器锁确保操作的原子性;
  3. 可扩展性:支持动态加载和卸载;
  4. 可靠性:初始化失败时能正确清理。

5.12.8.2 常见问题调试

问题 现象 调试方法
DLL_PROCESS_ATTACH 返回 FALSE 进程启动失败 检查依赖项、日志
死锁 进程挂起 使用调试器检查加载器锁
TLS 访问失败 空指针异常 检查 TLS 初始化顺序
DLL 未加载 导入函数解析失败 检查 PATH、依赖项

5.12.8.3 性能优化建议

  1. 使用 DisableThreadLibraryCalls:减少线程通知开销;
  2. 合并 DLL:减少模块数量;
  3. 延迟加载 :使用 /DELAYLOAD 延迟加载非关键 DLL;
  4. 避免循环依赖:优化 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参数?

AlpvReserved 用于区分正常卸载和进程终止。当进程终止时,系统会快速清理,此时某些操作(如网络连接)可能无法完成。DLL 可以通过检查这个参数来决定做多少清理工作。


总结

进程挂靠是 Windows DLL 机制的核心,理解它对于编写健壮的 DLL 和调试加载问题至关重要。本章介绍了:

  1. DllMain 的四种调用原因:PROCESS_ATTACH、THREAD_ATTACH、THREAD_DETACH、PROCESS_DETACH;
  2. LdrInitializeProcess:进程启动时的初始化流程;
  3. LdrInitializeThread:线程创建时的初始化流程;
  4. 动态加载:LoadLibrary 的实现和挂靠机制;
  5. 关键数据结构:LDR_DATA_TABLE_ENTRY、PEB_LDR_DATA;
  6. 性能优化:DisableThreadLibraryCalls 的作用。

核心要点回顾

  1. 进程挂靠是 DLL 生命周期管理的核心机制;
  2. DllMain 在加载器锁保护下执行,需要注意线程安全;
  3. 初始化顺序与依赖关系密切相关;
  4. 卸载顺序与加载顺序相反;
  5. 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
相关推荐
谢娘蓝桥2 小时前
windows 开启openssh
windows
设计师小聂!2 小时前
Windows 系统 Docker 安装与配置指南
windows·docker·容器
骑士雄师2 小时前
16.1深入讲解 LangGraph 的静态配置 configurable
windows·microsoft
我命由我123452 小时前
Windows 操作系统 - Windows 查看防火墙是否开启、Windows 查看防火墙放行端口
java·运维·开发语言·windows·java-ee·操作系统·运维开发
Byte Wizard2 小时前
C语言编译与链接
c语言
winlife_2 小时前
全程用 AI 做一款商业级手游 · EP10 道具系统:让三个按钮真正改变棋盘
windows·算法·unity·ai编程·游戏开发·mcp·玩法系统
社交怪人3 小时前
【判断整除】信息学奥赛一本通C语言解法(题号1046)
c语言
小二·3 小时前
Prompt Engineering 实战
网络·windows·prompt
fastjson_3 小时前
使用 ventoy 安装WinToGo
windows