dllMain函数的作用

1 DLLMain()函数的功能

 

Windows在加载DLL的时候,需要一个入口函数,就如同控制台或DOS程序需要main函数、Win32程序需要WinMain函数一样。根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用工程中引用DllMain函数,DllMain是自动被调用的。

注:一些例子中,DLL并没有提供DllMain函数,应用工程也能成功引用DLL,这是因为Windows在找不到DllMain的时候,系统会从其它运行库中引入一个不做任何操作的缺省DllMain函数版本,并不意味着DLL可以放弃DllMain函数。

2 使用示例

 我们来看一个DllMain函数的例子

BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
 switch (ul_reason_for_call)
 {
  case DLL_PROCESS_ATTACH:
   printf("\nprocess attach of dll");
   break;
  case DLL_THREAD_ATTACH:
   printf("\nthread attach of dll");
   break;
  case DLL_THREAD_DETACH:
   printf("\nthread detach of dll");
   break;
  case DLL_PROCESS_DETACH:
   printf("\nprocess detach of dll");
   break;
 }
 return TRUE;
}

 

3  参数含义

DllMain的函数头定义为:
BOOL APIENTRY DllMain( HANDLE hModule, WORD ul_reason_for_call, LPVOID lpReserved )
其中,APIENTRY被定义为__stdcall,它意味着这个函数以标准Pascal的方式进行调用,也就是WINAPI方式;
函数参数hModule是进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识(句柄代表了DLL模块在进程虚拟空间中的起始地址, 只有在特定的进程内部有效 ) 。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用。
参数ul_reason_for_call指明了被调用的原因。 共有4种,即PROCESS_ATTACH、PROCESS_DETACH、THREAD_ATTACH和THREAD_DETACH,以switch语句列出。
lpReserved 表示一个保留参数,目前已经很少使用
 
4 调用方式
 

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

1) DLL被加载

    一个程序要调用Dll里的函数,首先要先把DLL文件映射到进程的地址空间。要把一个DLL文件映射到进程的地址空间,有两种方法:静态链接和动态链接的LoadLibrary或者LoadLibraryEx。

    当一个DLL文件被映射到进程的地址空间时,系统调用该DLL的DllMain函数,传递的fdwReason参数为DLL_PROCESS_ATTACH。这种调用只会发生在第一次映射时。如果同一个进程后来为已经映射进来的DLL再次调用LoadLibrary或者LoadLibraryEx,操作系统不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数,而是增加DLL的使用次数。不同进程用LoadLibrary同一个DLL时,每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用DLL的DllMain函数。

2) DLL被卸载

 当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的fdwReason值是DLL_PROCESS_DETACH。当 DLL处理该值时,它应该执行进程相关的清理工作。 DLL被从进程的地址空间解除映射的情况有两种:
   ① F reeLibrary()被调用。(有几个 LoadLibrary,就要有几个FreeLibrary)
   ②  进程结束。 在进程结束前还没有解除DLL的映射,进程结束后会解除DLL映射。(如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。)
       注意:当用DLL_PROCESS_ATTACH调用DLL的DllMain函数时,如果返回FALSE,说明没有初始化成功,系统仍会用DLL_PROCESS_DETACH调用DLL的DllMain函数。因此, 必须确保没有清理那些没有成功初始化的东西。

 

3) 单个线程启动

 

当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。 新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许进程开始执行它的线程函数。
注意跟DLL_PROCESS_ATTACH的区别,我们在前面说过,第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,只增加使用次数,而不用DLL_PROCESS_ATTACH调用DllMain。DLL_THREAD_ATTACH不同,进程中的每次建立线程,都会用值DLL_THREAD_ATTACH调用DllMain函数,哪怕是线程中建立线程也一样。

 

4) 单个线程终止

 

如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。
注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread(),系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。

 


5 注意事项

 

__stdcall约定

为了使 VC++编写的DLL能被其他语言编写的程序调用,  函数的调用方式应显式声明为__stdcall方式,因为在 C/C++中,缺省的调用方式为__cdecl。
__stdcall方式与__cdecl调用方式的区别在于会将函数名处理成不同的符号。具体如下:
__stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;
__cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。

Windows编程中常见的几种函数类型声明宏都是与__stdcall和__cdecl有关的(节选自windef.h):

#define CALLBACK __stdcall //这就是传说中的回调函数
#define WINAPI __stdcall //这就是传说中的WINAPI
#define WINAPIV __cdecl
#define APIENTRY WINAPI //DllMain的入口就在这里
#define APIPRIVATE __stdcall
#define PASCAL __stdcall

 

在lib.h中,应这样声明add函数:

int __stdcall add(int x, int y);


在应用工程中函数指针类型应定义为:

typedef int(__stdcall *lpAddFun)(int, int);

 

 


  执行下列代码:

hDll = LoadLibrary("text.dll");
if (hDll != NULL)
{
 addFun = (lpAddFun)GetProcAddress(hDll, MAKEINTRESOURCE(1));
 //MAKEINTRESOURCE直接使用导出文件中的序号
 if (addFun != NULL)
 {
  int result = addFun(2, 3);
  printf("\ncall add in dll:%d", result);
 }
 FreeLibrary(hDll);
}


我们看到输出顺序为:

process attach of dll
call add in dll:5
process detach of dll


这一输出顺序验证了DllMain被调用的时机。

  代码中的GetProcAddress ( hDll, MAKEINTRESOURCE ( 1 ) )值得留意,它直接通过.def文件中为add函数指定的顺序号访问add函数,具体体现在MAKEINTRESOURCE ( 1 ),MAKEINTRESOURCE是一个通过序号获取函数名的宏,定义为(节选自winuser.h):

#define MAKEINTRESOURCEA(i) (LPSTR)((DWORD)((WORD)(i)))
#define MAKEINTRESOURCEW(i) (LPWSTR)((DWORD)((WORD)(i)))
#ifdef UNICODE
#define MAKEINTRESOURCE MAKEINTRESOURCEW
#else
#define MAKEINTRESOURCE MAKEINTRESOURCEA

 
 
 
 

DLL入口点函数DllMain .

每个DLL都可以有一个入口点函数DllMain,系统会在不同的时刻调用此函数。以下是DllMain的一般形式:

BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,  // handle to DLL module
    DWORD fdwReason,     // reason for calling function
    LPVOID lpReserved )  // reserved
{
    // Perform actions based on the reason for calling.
    switch( fdwReason ) 
    { 
        case DLL_PROCESS_ATTACH:
         // Initialize once for each new process.
         // Return FALSE to fail DLL load.
            break;

        case DLL_THREAD_ATTACH:
         // Do thread-specific initialization.
            break;

        case DLL_THREAD_DETACH:
         // Do thread-specific cleanup.
            break;

        case DLL_PROCESS_DETACH:
         // Perform any necessary cleanup.
            break;
    }
    return TRUE;  // Successful DLL_PROCESS_ATTACH.
}
以上代码摘自MSDN,几乎所有的DllMain都以这种形式呈现。
先来看一下这个函数传递进来的参数:
1、 HINSTANCE hinstDLL
这个参数是该DLL实例的句柄,也就是此DLL映射到进程地址空间后,在该进程地址空间中的位置。
2、 DWORD fdwReason
此参数标示了调用DllMain函数的原因。有四种值,就是函数中case后的取值。各个取值的含义,稍后论述。
3、 LPVOID lpReserved 
保留。
现在我们来讨论一下fdwReason的四种取值,这些取值,也直接反映了操作系统会在何种情况下调用DllMain。
1、DLL_PROCESS_ATTACH
当系统第一次将一个DLL映射到进程地址空间中时,会调用DllMain,并为fdwReason传入DLL_PROCESS_ATTACH。
注意,只有在第一次映射的时候,才会这样。如之后,另一线程再次显式加载此DLL,则操作系统只是增加该DLL的使用计数,而不会再次使用DLL_PROCESS_ATTACH来调用DllMain。
对DLL_PROCESS_ATTACH的处理,代表了DLL的初始化。
DllMain的返回值,也是针对DLL_PROCESS_ATTACH消息的。对于其余的三种取值,不起作用。
对于隐式加载,如DllMain返回FALSE,则程序会启动失败。对于显式加载,则会使LoadLibrary返回NULL。
2、DLL_PROCESS_DETACH
当系统将一个DLL从进程地址空间中撤销映射时,则会向DllMain传入DLL_PROCESS_DETACH。我们应当在此处放置一些清理代码。
当使用FreeLibrary时,如该线程的使用计数为0时,操作系统才会使用DLL_PROCESS_DETACH来调用DllMain。如使用计数大于0,则只是单纯的减少该DLL的计数。
3、DLL_THREAD_ATTACH
当进程创建一个线程,则系统会检查当前已映射到该进程空间中的所有DLL映像,并用DLL_THREAD_ATTACH来调用每个DLL的DllMain。
只有当所有DLL都完成了对DLL_THREAD_ATTACH的处理后,新线程才会执行它的线程函数。
另外,主线程不可能用DLL_THREAD_ATTACH来调用DllMain,因为主线程必然是在进程初始化的时候,用DLL_PROCESS_ATTACH调用DllMain的。
4、DLL_THREAD_DETACH
线程若要终止,会调用ExitThread,但是此函数不会立即终止线程,而是会利用DLL_THREAD_DETACH来调用当前进程地址空间中的所有DLL镜像的DllMain.
当每个DLL的DllMain都处理完后,系统才会真正的结束线程。
最后看一下DllMain的序列化调用
举个例子:
进程中有两个线程,A与B。进程的地址空间中,映射了一个名为SomeDll.dll的DLL。两个线程都准备通过CreateThread来创建另两个线程,C和D。
当线程A调用CreateThread来创建线程C的时候,系统会用DLL_THREAD_ATTACH来调用SomeDll.dll的DllMain,当线程C执行其中代码的时候,线程B调用CreateThread来创建线程D。
这时,系统同样会用DLL_THREAD_ATTACH来调用SomeDll.dll的DllMain,这次是让线程D来执行其中的代码。
但是此时,系统会对DllMain执行序列化,它会将线程D挂起,直至线程C执行完DllMain中的代码返回为止。
当C线程执行完DllMain中的代码并返回时,可以继续执行C的线程函数。此时,系统会唤醒线程D,让D执行DllMain中的代码。当返回后,线程D开始执行线程函数。
posted @   QuincyYi  阅读(610)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律

喜欢请打赏

扫描二维码打赏

了解更多

点击右上角即可分享
微信分享提示