作者:shenzi
链接:http://blog.csdn.net/shenzi
图1显示了应用程序如何显示地载入一个DLL并与DLL的符号进行链接:
图1:DLL创建过程以及应用程序显式链接到DLL的过程
构建DLL 1)头文件,其中包含待导出函数的原型、结构和符号的声明 2)C/C++源文件,其中包含待导出函数的实现和变量 3)编译器为每个C/C++源文件生成.obj文件 4)连接器将每个.obj模块合并,从而生成DLL 5)如果至少导出了一个函数/变量,那么链接器会同时生成.lib文件 注意:在显示链接的时候,没有用到这个.lib文件 |
构建EXE 6)头文件,其中包含待导出函数的原型、结构和符号的声明 7)C/C++源文件,其中包含待导出函数的实现和变量 8)编译器为每个C/C++源文件生成.obj文件 9)链接器将每个.obj模块合并,从而生成.exe 注意:由于没有直接引用该DLL导出的符号,因此这里不需要它的.lib文件。生成的.exe文件中不包含导入表 |
显示地载入DLL模块
在任何时候,进程中的一个线程可以调用下面两个函数来将一个DLL映射到进程的地址空间中:
HMODULE LoadLibrary(PCTSTR pszDLLPathName);
HMODULE LoadLibraryEx(
PCTSTR pszDLLPathName,
HANDLE hFile,
DWORD dwFlags);
这两个函数会在用户的系统中对DLL的文件映像进行定位,并试图将该文件映像映射到调用进程的地址空间中。两个函数返回的HMODULE表示文件映像被映
射到的虚拟内存地址。DllMain入口点所接收的HINSTANCE参数也同样是文件映像被映射到的虚拟内存地址。
显示地卸载DLL模块
当进程不再需要引用DLL中的符号时,我们应该调用下面的函数来显示地将DLL从进程的地址空间中卸载:
VOID FreeLibraryAndExitThread(
HMODULE hInstDll,
DWORD dwExitCode);
线程可以通过调用GetModuleHandle函数来检测一个DLL是否已经被映射到了进程的地址空间中:
HMODULE GetModuleHandle(PCTSTR pszModuleName);
如果传NULL给
GetModuleHandle,那么函数会返回应用程序的可执行文件的句柄。
显示地链接到导出符号
一旦显示地载入了一个DLL模块,线程必须通过调用下面的函数来得到它想要引用的符号的地址:
FARPROC GetProcAddress(
HMODULE hInstDll,//指定包含符号的DLL句柄,通过先前调用LoadLibrary(Ex)或GetModuleHandle返回
PCSTR pszSymbolName);//指定想要返回的符号名或序号
注意:参数
pszSymbolName
在函数原型中的类型为PCSTR,而不是PCTSTR。这意味着GetProcAddress函数只能接受ANSI字符串——我们从来不会传Unicode字符串给这个函数,这是因为编译器/链接器始终都是将符号的名称以ANSI字符串的形式保存在DLL的导出段中的。
2.DLL的入口点函数
一个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实例的句柄,这个值表示一个虚拟内存地址,DLL的文件映像就被映射到进程地址空间中的这个位置。如果DLL是隐式载入
的,那么最后一个参数fImpLoad的值将不为零,如果DLL是显式载入的,那么fImpLoad的值将为零。参数fdwReason表示系统调用入口
点函数的原因。这个参数可能是下列4个值之一:DLL_PROCESS_ATTACH
, DLL_PROCESS_DETACH
, DLL_THREAD_ATTACH
, 或 DLL_THREAD_DETACH
。
DLL_PROCESS_ATTACH:
当系统第一次将一个DLL映射到进程的地址空间中时,会调用DllMain函数,并在fdwReason参数中传入DLL_PROCESS_ATTACH。
DLL_PROCESS_DETACH:
当系统将一个DLL从进程的地址空间中撤销映射时,会调用DLL的DllMain函数,并在fdwReason参数中传入
DLL_PROCESS_DETACH
。
图2显式了县城调用LoadLibrary时系统执行的步骤;图3显式了当线程调用FreeLibrary时系统执行的步骤:
图2:线程调用LoadLibrary是系统执行的步骤
图3:线程调用FreeLibrary时系统执行的步骤
当进程创建一个线程的时候,系统会检查当前映射到该进程的地址空间中的所有DLL文件映射,并用
DLL_THREAD_ATTACH来调用每个DLL的DllMain函数。
DLL_THREAD_DETACH:
让线程终止的首选方式是它的线程函数返回。这会使得系统调用ExitThread来终止线程。ExitThread告诉系统改线程想要终止,但系统不会立即终止该线程,而会让这个即将终止的线程用
DLL_THREAD_DETACH来调用所有已映射DLL的DllMain函数。
DllMain的序列化调用:
系统会将对DLL的DllMain函数的调用序列化。
DllMain和C/C++运行库:
在
编写一个DLL得时候,可能需要C/C++运行库在启动方面给予我们一些帮助。举个例子,假设我们正在构建的DLL包含一个全局变量,这个全局变量是一个
C++类的实例。在我们能够在DllMain函数中安全地使用该全局变量之前,必须保证它的构造函数已经被调用过。这就是C/C++运行库的DLL启动代
码的工作。
在默认情况下,如果用Microsoft链接器并制定了/DLL开关,那么链接器会认为入口点函数名是_DllMainCRTStartup
。这个函数包含在C/C++运行库中,在链接DLL的时候回被静态地链接到DLL的文件映像。(即便用的是C/C++运行库的DLL版本,对这个函数的链接仍会是静态的)。在C/C++运行时的初始化完成之后,_DllMainCRTStartup
函数会调用我们的DllMain函数。
3.延迟载入DLL
一个延迟载入的DLL是隐式链接的,系统一开始不会将该DLL载入,只有当我们的代码视图区引用DLL中包含的一个符号时,系统才会实际载入该DLL。
4.函数转发器
函数转发器(function forwarder)是DLL输出段中的一个条目,用来将一个函数调用转发到另一个DLL中的另一个函数。
我们可以在自己的DLL中模块中使用函数转发器。最简单的方法是使用pragma指示符,如下所示:
#pragma comment(linker, "/export:SomeFunc=DllWork.SomeOtherFunc")
5.已知的DLL
系统对操作系统提供的某些DLL进行了特殊的处理,这些DLL被称为已知的DLL
(known DLL)。在注册表有一个注册表项:
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session Manager/KnownDLLs
举例说明,假设我们在KnownDLLs注册表项中添加了下列值:
Value name: SomeLib
Value data: SomeOtherLib.dll
系统会用正常的搜索规则来对这个DLL进行定位
LoadLibrary(TEXT("SomeLib"));
//载入的是
SomeOtherLib.dll
LoadLibrary(TEXT("SomeLib.dll"));
//载入的依然是
SomeOtherLib.dll
,而不是
SomeLib
LoadLibrary
或LoadLibraryEx
被调用的时候,函数首先会检查我们传入的DLL地名字是否包含.dll扩展名。如果没有包含,那么函数会用正常的搜索规则来搜索这个DLL。如果指定
了.dll扩展名,那么这两个函数会先将扩展名去掉,然后再KnownDLLs注册表项中搜索,看其中是否有与之相符的值名。如果没有值名与之相符,那么
函数会使用正常的搜索规则。但是,如果找到了与之相符的值名,那么系统会查看与值名相对应的数据,并试图用该数据来载入DLL。
6.DLL重定向
7.模块的基地址重定位
Rebase.exe
如果在执行Rebase工具的时候传给它一组映像文件名,那么它会执行下列操作:
- 它会模拟创建一个进程地址空间
- 它会打开应该被载入到这个地址空间中的所有模块,并得到每个模块的大小以及它们的首选基地址
- 它会在模拟的地址空间中对模块重定位的过程进行模拟,使各模块之间没有交叠
- 对每个重定位过的模块,它会解析该模块的重定位段,并修改模块在磁盘文件中的代码
- 为了反映新的首选基地址,它会更新每个重定位过的模块的头文件
8.模块的绑定