和可执行文件一样,动态链接库也有自己的入口地址,如果系统或者当前进程的某个线程调用LoadLibrary函数加载或者使用FreeLibrary卸载该动态链接库的时候,会自动使用3个特定的堆栈参数跳转到该地址来运行。入口函数是为了完成动态链接库代码的初始化和善后工作,比如卸载后的资源释放。
这三个参数具有特殊的含义。
BOOL APIENTRY DllMain(
HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
第一个参数是实例句柄,这个所谓的句柄实际上就是该加载进程在内存中的景象地址,注意进程句柄和动态链接库的模块句柄是不一样的。比如,如果用户想使用hModule去加载该动态链接库中的一个位图资源,那将永远得不到结果。动态链接库的模块句柄依附于加载进程,脱离进程独立于系统空间中的动态链接库是不存在的,也没有意义。动态链接库总是要加载到进程中的某个地址,该地址就是动态链接库的模块句柄,这个句柄不同于进程实例句柄,它是相对于进程入口地址的一个偏移量,只有通过模块句柄才能寻址到该动态链接库中嵌套的各种窗口或者其他资源。
ul_reason_for_call 参数表示该动态链接库是在什么条件下被加载的,即加载的原因。当用户显式采用LoadLibrary(Ex)函数加载一个动态链接库或者由进程本身隐式加载该库时,该入口函数就会被调用,入口参数 ul_reason_for_call 这时就等于DLL_PROCESS_ATTACH,入口函数的返回值就是LoadLibrary函数的返回值,它们都是通过EAX寄存器来传递的。根据LoadLibrary(Ex)函数的说明,这个函数如果返回一个NULL值,表示加载失败。实际上在DllMain 函数中,只要返回FALSE,LoadLibrary(Ex)函数就会返回一个NULL值。如果动态链接库需要排他调用,可以在入口点函数中判断加载进程的文件名,或者履行其他合法性检查;如果不希望被某个进程调用,可以直接在DllMain函数中返回FALSE。DLL_PROCESS_ATTACH分支往往用于实现系统初始化,比如建立数据库链接,创建钩子函数,分配系统资源,保存入口进程实例句柄等。
同样,当进程不再使用该动态链接库,比如调用ExitProcess函数或者显式调用FreeLibrary函数时,系统又会使用DLL_PROCESS_DETACH参数执行相关分支。用于释放系统资源、断开数据库链接、卸载钩子函数、关闭文件、释放内存等。
注意:DLL_PROCESS_DETACH分支的执行是有条件的,进程的意外终止,并不会执行DLL_PROCESS_DETACH分支语句,这样分支中的关闭资源、断开链接、关闭文件等善后语句就无法执行,这将会造成一些数据的潜在丢失。因此除非万不得已,不要轻易使用TerminateProcess函数终止进程执行。
如果线程创建时DLL完成到进程空间的映射,这样系统会使用DLL_THREAD_ATTACH进入入口点。而系统执行ExitThread函数时会执行DLL_THREAD_DETACH分支。同样的规则适用于线程,用户不要轻易使用TerminateThread函数,这也会导致一些不可预知的内存泄漏或者资源没有释放和数据丢失。
如果用户不在乎DLL_THREAD_DETACH和DLL_THREAD_ATTACH通知,又希望提高创建和撤销线程的性能,可以在收到DLL_THREAD_ATTACH通知时,调用DisableThreadLibraryCalls函数。
注意:关于lpReserved参数----在静态(隐式)加载和调用LoadLibray函数实现动态(显式)加载动态库时,这两种情况是不一样的,动态加载时这个值为0。如果用户希望自己编写的的动态链接库只能被动态加载或者只能被静态加载,可以通过判断这个参数来实现。
代码示例:
dllmain.cpp(生成mydll.dll):
// dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "stdafx.h" #include<stdio.h> #include<Psapi.h> #include<Windows.h> typedef void(*pFnPtr)(char*); BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { char szName[MAX_PATH]; if (lpReserved != 0) { //只允许动态加载,静态加载将提示错误并退出 MessageBox(NULL, "只允许动态加载", "Sorry", MB_OK); return FALSE; } GetModuleBaseName(GetCurrentProcess(),NULL ,szName,MAX_PATH);//获取当前主进程的base name switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: //故意将调用进程的名字改为test.exe,该动态链接库将无法加载。 if (strcmp(szName, "test.exe") == 0) return FALSE; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } extern "C" { //主调进程将调用该函数,该函数再调用主调进程的ExeFn函数 _declspec(dllexport) int fnImportingDLL() { MessageBox(NULL, "Dll Function called!", "mydll", MB_OK); pFnPtr fn = (pFnPtr)::GetProcAddress(GetModuleHandle(NULL), "ExeFn"); if (fn) fn("梦回吹角连营"); else { MessageBox(NULL, "It Did not work:", "From DLL", MB_OK); return -1; } } }
main.cpp(生成console.exe):
#include<iostream> #include<Windows.h> #define _DYNAMIC_ #ifndef _DYNAMIC_ #pragma comment(lib,"mydll.lib") //静态加载 extern "C" _declspec(dllexport) int fnImportingDLL(); #endif typedef int(*pfnImportingDLL)(); using namespace std; int main(int argc, char* argv[]) { #ifdef _DYNAMIC_ //动态加载 pfnImportingDLL fnImportingDLL = NULL; HMODULE hModule=::LoadLibrary("mydll.dll"); if (hModule == NULL) { cout << "无法加载mydll.dll" << endl; return -1; } fnImportingDLL =(pfnImportingDLL) GetProcAddress(hModule, "fnImportingDLL"); if (fnImportingDLL == NULL) { cout << "找不到fnImportingDLL函数" << endl; return -2; } #endif cout << "I‘m going...\n"; fnImportingDLL(); cout << "Game Over\n"; #ifdef _DYNAMIC_ ::FreeLibrary(hModule); #endif return 0; } extern "C" { //该函数将有动态链接库中的函数来调用 _declspec(dllexport) void ExeFn(char* lpszMessage) { MessageBox(NULL, lpszMessage, "From Exe", MB_OK); } }
将生成的 console.exe文件和mydll.dll文件放在同一个文件夹下
注释掉 _DYNAMIC_ 宏将静态加载mydll.dll,这样会弹出对话框提示只能动态加载,因为在DllMain中检查了lpReserved的值并做了判断。
将console.exe的名字改为test.exe,然后在cmd中运行test.exe,将显示无法加载mydll.dll。