windows:加载dll时的DLLMain介绍

29 篇文章 3 订阅

        

目录

DLLmain

DLL_PROCESS_ATTACH

DLL_THREAD_ATTACH

DLL_PROCESS_DETACH

DLL_THREAD_DETACH

总结


        在Windows操作系统中,当一个DLL(动态链接库)被加载时,系统会调用该DLL中的DllMain函数。DllMain是一个可选的入口点函数,可以用来执行一些初始化和清理工作。

DLLmain

  DllMain函数的原型如下:

BOOL APIENTRY DllMain(
    HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
);

其中参数解释如下:

  • hModule:DLL模块的句柄。
  • ul_reason_for_call:调用原因,可能的值包括DLL_PROCESS_ATTACHDLL_THREAD_ATTACHDLL_THREAD_DETACHDLL_PROCESS_DETACH
  • lpReserved:保留参数,在不同的调用原因下有不同的含义。

DllMain在以下几种情况下会被调用:

  1. DLL_PROCESS_ATTACH:当DLL被加载到一个进程的地址空间时调用。通常用于初始化与进程相关的数据或资源。
  2. DLL_THREAD_ATTACH:当一个新线程被创建,并且该线程将要使用该DLL时调用。一般用于初始化与线程相关的数据或资源。
  3. DLL_THREAD_DETACH:当一个线程退出并且该线程曾经使用过该DLL时调用。通常用于清理与线程相关的数据或资源。
  4. DLL_PROCESS_DETACH:当DLL从一个进程的地址空间卸载时调用。一般用于清理与进程相关的数据或资源。

#include <windows.h>

BOOL APIENTRY DllMain(
    HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
) {
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        // 初始化与进程相关的数据或资源
        // 例如:分配内存、打开文件等
        break;
    case DLL_THREAD_ATTACH:
        // 初始化与线程相关的数据或资源
        // 例如:分配线程本地存储(TLS)
        break;
    case DLL_THREAD_DETACH:
        // 清理与线程相关的数据或资源
        // 例如:释放线程本地存储(TLS)
        break;
    case DLL_PROCESS_DETACH:
        // 清理与进程相关的数据或资源
        // 例如:释放内存、关闭文件等
        break;
    }
    return TRUE; // 返回值为TRUE表示成功
}

DLL_PROCESS_ATTACH

        当一个DLL被加载到进程中时,系统会以DLL_PROCESS_ATTACH为参数调用DllMain。这通常是DLL初始化代码的地方。在这个阶段,通常会执行以下操作:

  • 初始化全局或静态变量:为全局变量或静态变量分配内存并进行初始化。
  • 分配资源:分配和初始化DLL所需的资源,例如内存、文件句柄、同步对象等。
  • 设置钩子:如果DLL需要设置全局钩子,这也是一个合适的时机。

调用时机:

  • 显式加载DLL:当使用 LoadLibraryLoadLibraryEx 函数显式加载DLL时。
  • 隐式加载DLL:当一个进程启动时,如果该进程依赖的某个DLL被隐式链接(在可执行文件的导入表中列出),那么在进程启动时会加载该DLL。
  • 继承加载DLL:如果一个进程创建了一个新进程,并且该新进程继承了父进程的句柄,并且父进程已经加载了某个DLL,那么在新进程中也会加载该DLL。

需要注意的是,在处理DLL_PROCESS_ATTACH时,不应该进行以下操作:

  • 不要调用LoadLibraryFreeLibrary,因为这可能导致死锁。
  • 避免在此阶段创建线程,因为线程库可能尚未完全初始化。
  • 尽量减少复杂的初始化逻辑,以避免长时间阻塞。

        在处理DLL_PROCESS_ATTACH时,通常需要进行DLL的初始化工作。这些工作可能包括分配内存、初始化全局变量、打开文件、设置钩子等。下面是一个详细的C++示例,展示了如何在DLL_PROCESS_ATTACH中进行各种初始化操作:

#include <windows.h>
#include <iostream>
#include <fstream>

std::ofstream logFile; // 全局文件句柄

// 初始化函数,打开日志文件
void Initialize() {
    logFile.open("C:\\path_to_log\\dll_log.txt", std::ios::out | std::ios::app);
    if (logFile.is_open()) {
        logFile << "DLL Loaded and Initialized" << std::endl;
    }
}

// 清理函数,关闭日志文件
void Cleanup() {
    if (logFile.is_open()) {
        logFile << "DLL Unloaded and Cleaned up" << std::endl;
        logFile.close();
    }
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        // 禁用DLL线程调用通知,以提高性能
        DisableThreadLibraryCalls(hModule);
        
        // 初始化全局变量或资源
        Initialize();

        // 其他初始化代码
        // 例如:创建全局同步对象
        // HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);

        break;

    case DLL_THREAD_ATTACH:
        // 线程初始化代码(如果需要)
        break;

    case DLL_THREAD_DETACH:
        // 线程清理代码(如果需要)
        break;

    case DLL_PROCESS_DETACH:
        // 清理全局变量或资源
        Cleanup();

        // 其他清理代码
        // 例如:释放全局同步对象
        // if (hMutex) {
        //     CloseHandle(hMutex);
        // }

        break;
    }
    return TRUE; // 返回TRUE表示成功
}

DLL_THREAD_ATTACH

        当一个线程被创建并且将使用该DLL时,系统会以DLL_THREAD_ATTACH为参数调用DllMain。在这个阶段,通常会执行以下操作:

  • 线程本地存储(TLS)初始化:为新线程分配和初始化线程本地存储(TLS)。
  • 线程特定的初始化:为新线程设置初始状态或分配资源。

        需要注意的是,默认情况下,DLL_THREAD_ATTACH 仅在DLL中使用 DllMain 函数处理线程初始化时被调用。如果在 DLL_PROCESS_ATTACH 中调用 DisableThreadLibraryCalls 函数来禁用线程库调用通知,则不会收到 DLL_THREAD_ATTACHDLL_THREAD_DETACH 通知。

    DLL_THREAD_ATTACH被调用:

#include <windows.h>
#include <iostream>
#include <process.h>

typedef void (*MyFunction)();

DWORD WINAPI ThreadFunc(LPVOID lpParam) {
    HMODULE hDll = LoadLibrary("MyDll.dll");
    if (hDll != NULL) {
        MyFunction func = (MyFunction)GetProcAddress(hDll, "MyFunction");
        if (func != NULL) {
            func(); // 调用DLL中的函数
        }
        FreeLibrary(hDll); // 卸载DLL
    } else {
        std::cout << "Failed to load DLL in thread" << std::endl;
    }
    return 0;
}

int main() {
    HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
    if (hThread != NULL) {
        WaitForSingleObject(hThread, INFINITE);
        CloseHandle(hThread);
    } else {
        std::cout << "Failed to create thread" << std::endl;
    }
    return 0;
}

        DLL代码 在DLL中处理 DLL_THREAD_ATTACH,进行线程初始化。

#include <windows.h>
#include <iostream>

extern "C" __declspec(dllexport) void MyFunction() {
    std::cout << "MyFunction called from DLL" << std::endl;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        std::cout << "DLL_PROCESS_ATTACH: DLL loaded" << std::endl;
        // 禁用DLL线程调用通知以提高性能(如果不需要处理线程初始化)
        // DisableThreadLibraryCalls(hModule);
        break;

    case DLL_THREAD_ATTACH:
        std::cout << "DLL_THREAD_ATTACH: Thread created" << std::endl;
        // 线程初始化代码
        break;

    case DLL_THREAD_DETACH:
        std::cout << "DLL_THREAD_DETACH: Thread exiting" << std::endl;
        // 线程清理代码
        break;

    case DLL_PROCESS_DETACH:
        std::cout << "DLL_PROCESS_DETACH: DLL unloaded" << std::endl;
        // 清理代码
        break;
    }
    return TRUE; // 返回TRUE表示成功
}

        在 DLL_THREAD_ATTACH 中,可以编写与线程初始化相关的代码。这些操作通常涉及初始化与线程相关的资源,例如线程本地存储(TLS)、线程特定的数据结构等。需要注意的是,DLL_THREAD_ATTACH 在每个线程创建时都会被调用,因此代码应尽量简洁高效,以免影响性能。

        以下是一个详细的C++示例,展示了在 DLL_THREAD_ATTACH 中进行线程初始化的具体操作:

#include <windows.h>
#include <iostream>

// 线程本地存储索引
DWORD g_dwTlsIndex;

// 线程特定数据结构
struct ThreadData {
    int threadId;
    // 其他线程相关的数据
};

// 线程初始化函数
void InitializeThread() {
    // 分配线程特定数据结构
    ThreadData* pThreadData = (ThreadData*)LocalAlloc(LPTR, sizeof(ThreadData));
    if (pThreadData != NULL) {
        pThreadData->threadId = GetCurrentThreadId();
        // 存储指针到TLS
        TlsSetValue(g_dwTlsIndex, pThreadData);
        std::cout << "Thread initialized: " << pThreadData->threadId << std::endl;
    } else {
        std::cout << "Failed to allocate thread local storage" << std::endl;
    }
}

// 线程清理函数
void CleanupThread() {
    // 从TLS中获取指针并释放
    ThreadData* pThreadData = (ThreadData*)TlsGetValue(g_dwTlsIndex);
    if (pThreadData != NULL) {
        std::cout << "Thread exiting: " << pThreadData->threadId << std::endl;
        LocalFree(pThreadData);
    }
}

extern "C" __declspec(dllexport) void MyFunction() {
    std::cout << "MyFunction called from DLL" << std::endl;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        std::cout << "DLL_PROCESS_ATTACH: DLL loaded" << std::endl;
        // 分配TLS索引
        g_dwTlsIndex = TlsAlloc();
        if (g_dwTlsIndex == TLS_OUT_OF_INDEXES) {
            return FALSE;
        }
        break;

    case DLL_THREAD_ATTACH:
        std::cout << "DLL_THREAD_ATTACH: Thread created" << std::endl;
        InitializeThread();
        break;

    case DLL_THREAD_DETACH:
        std::cout << "DLL_THREAD_DETACH: Thread exiting" << std::endl;
        CleanupThread();
        break;

    case DLL_PROCESS_DETACH:
        std::cout << "DLL_PROCESS_DETACH: DLL unloaded" << std::endl;
        // 释放TLS索引
        TlsFree(g_dwTlsIndex);
        break;
    }
    return TRUE; // 返回TRUE表示成功
}

DLL_PROCESS_DETACH

        当DLL从进程地址空间卸载时,系统会以DLL_PROCESS_DETACH为参数调用DllMain。这通常是DLL清理代码的地方。在这个阶段,通常会执行以下操作:

  • 释放全局或静态变量:释放全局变量或静态变量所占用的内存。
  • 释放资源:释放和清理DLL所占用的资源,例如内存、文件句柄、同步对象等。
  • 注销钩子:如果DLL设置了全局钩子,需要在此阶段注销。

需要注意的是,在处理DLL_PROCESS_DETACH时,不应该进行以下操作:

  • 避免长时间阻塞,因为进程正在关闭。
  • 不要依赖于其他DLL,因为它们可能已经被卸载。

DLL_PROCESS_DETACH 触发时机:

  • 进程退出时:当一个进程退出并且DLL正在使用时。
  • 显式卸载DLL:当使用 FreeLibrary 函数显式卸载DLL时。

        当一个DLL被卸载或进程退出时,系统会调用 DllMain 函数,并传递 DLL_PROCESS_DETACH 作为参数。这是进行清理工作的最佳时机,例如释放资源、关闭文件、注销钩子等。

可在 DLL_PROCESS_DETACH 中写的代码,在 DLL_PROCESS_DETACH 中,通常会执行以下操作:

  1. 释放分配的资源:例如内存、句柄、同步对象等。
  2. 关闭文件或日志:关闭在DLL运行期间打开的文件或日志。
  3. 注销钩子:如果在DLL中设置了全局钩子,则应在此时注销。
  4. 释放线程本地存储:如果在DLL中使用了线程本地存储(TLS),则应释放这些资源。
// 清理函数,关闭日志文件和释放互斥锁
void Cleanup() {
    if (logFile.is_open()) {
        logFile << "DLL Unloaded and Cleaned up" << std::endl;
        logFile.close();
    }
    if (hMutex != NULL) {
        CloseHandle(hMutex);
    }
    // 释放TLS索引
    if (g_dwTlsIndex != TLS_OUT_OF_INDEXES) {
        TlsFree(g_dwTlsIndex);
    }
}


    case DLL_PROCESS_DETACH:
        std::cout << "DLL_PROCESS_DETACH: DLL unloaded" << std::endl;
        Cleanup();
        break;

DLL_THREAD_DETACH

        当一个线程退出时,如果该线程曾经使用过该DLL,系统会以DLL_THREAD_DETACH为参数调用DllMain。在这个阶段,通常会执行以下操作:

  • 释放TLS:释放与线程相关的线程本地存储(TLS)。
  • 清理线程资源:释放或关闭与线程相关的资源,例如句柄、内存等。

 DLL_THREAD_DETACH 触发时机:

        线程退出时:当一个线程退出并且DLL正在使用时。

        当一个线程退出时,系统会调用 DllMain 函数,并传递 DLL_THREAD_DETACH 作为参数。这是进行线程清理工作的最佳时机,例如释放与线程相关的资源、关闭文件、释放线程本地存储(TLS)等。

#include <windows.h>
#include <iostream>

typedef void (*MyFunction)();

DWORD WINAPI ThreadFunc(LPVOID lpParam) {
    HMODULE hDll = LoadLibrary("MyDll.dll");
    if (hDll != NULL) {
        MyFunction func = (MyFunction)GetProcAddress(hDll, "MyFunction");
        if (func != NULL) {
            func(); // 调用DLL中的函数
        }
        FreeLibrary(hDll); // 卸载DLL
    } else {
        std::cout << "Failed to load DLL in thread" << std::endl;
    }
    return 0;
}

int main() {
    HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
    if (hThread != NULL) {
        WaitForSingleObject(hThread, INFINITE);
        CloseHandle(hThread);
    } else {
        std::cout << "Failed to create thread" << std::endl;
    }
    return 0;
}

        可在里面写代码:

// 线程清理函数
void CleanupThread() {
    // 从TLS中获取指针并释放
    ThreadData* pThreadData = (ThreadData*)TlsGetValue(g_dwTlsIndex);
    if (pThreadData != NULL) {
        std::cout << "Thread exiting: " << pThreadData->threadId << std::endl;
        LocalFree(pThreadData);
    }
}

    case DLL_THREAD_DETACH:
        std::cout << "DLL_THREAD_DETACH: Thread exiting" << std::endl;
        CleanupThread();
        break;

总结

        在实际开发中,通常情况下,系统自带的初始化和清理机制(如全局变量初始化和析构、自动内存管理等)已经足够。然而,有些特定场景下,需要开发者自己在 DllMain 中处理 DLL_THREAD_ATTACHDLL_THREAD_DETACHDLL_PROCESS_ATTACHDLL_PROCESS_DETACH。以下是一些可能的情况:

线程本地存储(TLS)

        当需要在每个线程中维护独立的数据(如线程本地存储),必须手动进行线程初始化和清理。例如,多线程日志记录、线程特定的配置等。

        示例:每个线程都有独立的日志文件句柄或配置对象。

资源管理

        当需要在DLL加载时分配全局资源(如文件句柄、网络连接)并在卸载时释放这些资源时,必须手动进行初始化和清理。

示例:打开全局日志文件、初始化网络连接、加载大规模数据结构等。

兼容性和移植性

        一些跨平台的库或应用需要在不同操作系统上有一致的行为,可能需要在DLL加载和卸载时进行特定的初始化和清理工作,以确保跨平台的一致性。

示例:在Windows和Linux上进行不同的初始化,但保持接口一致。

性能优化

        在某些性能敏感的应用中,需要手动管理资源以提高性能,例如在DLL加载时进行复杂初始化,而不是在每次调用时进行。

示例:预加载和缓存大量数据以减少运行时延迟。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas_Lbw

欣赏我就赏赐给我吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值