Learn DLL
6. DLL 入口点函数
DLL入口点函数框架大致如下:
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// The DLL is being mapped into the process' address space.
break;
case DLL_THREAD_ATTACH:
// A thread is being created.
break;
case DLL_THREAD_DETACH:
// A thread is exiting cleanly.
break;
case DLL_PROCESS_DETACH:
// The DLL is being unmapped from the process' address space.
break;
}
return(TRUE); // Used only for DLL_PROCESS_ATTACH
}
其中,hInstDll为该DLL模块的实例句柄,标识其虚存地址;fImpLoad指示模块的加载方式,非0值表示隐式加载,0表示显式加载;fdwReason指示执行DllMain函数的原因,值为switch的4个case值之一。
注意:
DllMain仅仅是用来作基本的初始化工作,如设置线程存储、创建内核对象、打开文件等,应避免进行如下调用:
(1) 调用其它的DLL的函数
(2) 使用LoadLibrary(Ex)加载其它DLL
(3) 调用Shell、ODBC、COM、RPC、Socket等
在创建全局或static C++对象时也会面临同样的问题。
关于fdwReason的几个值:
(1)DLL_PROCESS_ATTACH
在DLL首次映射到进程的地址空间时,系统会调用DLL的DllMain函数,fdwReason设置为DLL_PROCESS_ATTACH。在处理DLL_PROCESS_ATTACH通知时,要完成进程相关的初始化工作,例如DLL含有一个函数,它使用自己的堆(在进程地址空间中创建),那么就可以调用HeapCreate完成Heap创建工作,创建完成后可以保存在一个全局变量中。
完成处理DLL_PROCESS_ATTACH通知后,要有一个返回值表明是否初始化成功;处理其它通知时系统会忽略返回值。
DllMain总是要由某个线程来执行的,在此顺便说一下进程的创建及启动过程。在新进程创建时,系统首先为进城分配地址空间,随后将exe文件映像及需要的DLL(隐式)映射进地址空间;接下来创建主线程,使用该主线程去调用每一个DllMain完成初始化,任何一个DllMain函数返回FALSE将导致进程终止;如果所有DllMain都返回TRUE,之后主线程会调用可执行模块的C/C++运行时启动代码,最后执行应用程序的入口点函数(_tmain或_tWinMain)。
如果是显式加载DLL,过程与前面类似,只是调用DllMain的线程是调用LoadLibrary(Ex)的线程,而不一定是主线程;另外在LoadLibrary(Ex)返回之后,线程继续顺序执行后面的代码。
注意:在此处可以调用DisableThreadLibraryCalls(hInstDll)禁止DllMain处理线程相关的通知。
(2)DLL_THREAD_ATTACH
在进程创建新线程时,该线程必须以DLL_THREAD_ATTACH通知调用所有已映射进进程地址空间的DLL的DllMain,完成之后才可以执行线程函数。
(3)DLL_THREAD_DETACH
线程结束的最好方式是在线程函数内返回,在这种情况下,系统并不是马上结束线程,而是要求线程以DLL_THREAD_DETACH通知执行所有已映射进进程地址空间的DLL的DllMain,完成必要的清理工作之后系统才会结束线程。
注意,调用TerminateThread 结束线程不会执行DllMain,有些清理工作不能完成,可能会造成数据丢失。DLL还可以防止线程结束,只要在此分支下安排一个无限循环就可以了,因为系统只有在所有DLLMain处理完DLL_THREAD_DETACH通知时才会结束线程。
另外,在DLL从进程空间卸载时,如果有任何线程仍在执行,系统不会为线程调用DllMain做线程相关的清理工作,这可能导致数据丢失。因此可以在DLL_PROCESS_DETACH中检测这一情况,以保证线程相关的清理工作得以执行。
(4) DLL_PROCESS_DETACH
在DLL从进程地址空间卸载时,处理DLL_PROCESS_DETACH通知,完成进程相关的清理工作。
DllMain函数的串行调用
要理解什么是串行调用,先看下面的例子:
有一个进程,它有两个线程A,B,还引用了一个DLL——SomeDLL.dll;现在A要创建一个新线程C,B要创建新线程D,DllMain代码如下:
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {
HANDLE hThread;
DWORD dwThreadId;
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// The DLL is being mapped into the process' address space.
// Create a thread to do some stuff.
hThread = CreateThread(NULL, 0, SomeFunction, NULL,
0, &dwThreadId);
// Suspend our thread until the new thread terminates.
WaitForSingleObject(hThread, INFINITE);
// We no longer need access to the new thread.
CloseHandle(hThread);
break;
case DLL_THREAD_ATTACH:
// A thread is being created.
break;
case DLL_THREAD_DETACH:
// A thread is exiting cleanly.
break;
case DLL_PROCESS_DETACH:
// The DLL is being unmapped from the process' address space.
break;
}
return(TRUE);
}
上面的代码将导致死锁,原因就是DllMain是串行调用的。只有当一个线程对DllMain的调用完成后,系统才允许别的线程进入该函数。而在例子的代码中,调用CreateThread之后线程挂起,等待新创建线程结束;而被创建的新线程在创建之前要先处理DllMain,但是它又必须等待创建自己的线程结束对DllMain的调用之后才能进入,从而新线程实际并没有创建完成,更不用提运行结束,这就形成了死锁条件——互相等待。
2. DLL 符号引用
此处主要说明显示加载DLL时的符号引用,注意红色代码。
// A simple program that uses LoadLibrary and
// GetProcAddress to access myPuts from Myputs.dll.
#include <windows.h>
#include <stdio.h>
typedef int (__cdecl *MYPROC)(LPWSTR); //定义要引用的函数指针类型
VOID main(VOID)
{
HINSTANCE hinstLib;
MYPROC ProcAdd; //定义函数指针,用于保存取得的DLL函数地址
BOOL fFreeResult, fRunTimeLinkSuccess = FALSE;
// Get a handle to the DLL module.
hinstLib = LoadLibrary(TEXT("MyPuts.dll"));
// If the handle is valid, try to get the function address.
if (hinstLib != NULL)
{
//取得要引用的DLL函数
ProcAdd = (MYPROC) GetProcAddress(hinstLib, "myPuts");
// If the function address is valid, call the function.
if (NULL != ProcAdd)
{
fRunTimeLinkSuccess = TRUE;
(ProcAdd) (L"Message sent to the DLL function/n"); //使用函数
}
// Free the DLL module.
fFreeResult = FreeLibrary(hinstLib);
}
// If unable to call the DLL function, use an alternative.
if (! fRunTimeLinkSuccess)
printf("Message printed from executable/n");
}