第二十章 DLL高级技术
本章内容
20.1 DLL模块的显式载入和符号链接
20.2 DLL的入口点函数
20.3 延迟载入DLL
20.4 函数转发器
20.5 已知的DLL
20.6 DLL重定向
20.7 模块的基础地址重定位
20.8 模块的绑定
作者认为 20.7 和20.8两小节介绍的技术非常重要,能显著提高整个系统的性能。
20.1 DLL模块的显示载入和符号链接
为了让线程调用DLL模块中的一个函数,必须将DLL文件映射到调用线程所在进程的地址空间中。有两种方式:
1)直接让应用程序源码引用DLL中所包含的符号,这样加载程序在应用程序运行的时候会隐式载入所需要的DLL(编译+应用程序启动时)
2)让应用程序在运行过程显式载入所需要的DLL并显式与想要的输出符号链接。(运行时)
20.1.1 显示地载入DLL模块
任何时候进程的一个线程可以调用以下函数来将一个DLL映射到进程的地址空间中。
WINBASEAPI
_Ret_maybenull_
HMODULE
WINAPI
LoadLibraryW(
_In_ LPCWSTR lpLibFileName
);
WINBASEAPI
_Ret_maybenull_
HMODULE
WINAPI
LoadLibraryExW(
_In_ LPCWSTR lpLibFileName,
_Reserved_ HANDLE hFile,
_In_ DWORD dwFlags
);
这两个函数首先会用19章介绍的搜索算法找到DLL文件,创建文件映像,并把其文件映像映射到进程地址空间中。
HMODULE表示返回映射成功的虚拟地址。(等价HINSTANCE)
LoadLibraryEx有两个额外的参数:hFile和dwFlags
hFile 扩充保留,设置为NULL
dwFlags可以是一下标志: DONT_RESOLVE_DLL_REFERENCES, LOAD_LIBRARY_AS_DATAFILE,LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE
LOAD_LIBRARY_AS_IMAGE_RESOURCE, LOAD_WITH_ALTERED_SEARCH_PATH以及LOAD_IGNORE_CODE_AUTHZ_LEVEL.
1. DONT_RESOLVE_DLL_REFERENCE标志
只需要将DLL映射到调用进程的地址空间而不调用DLL自身的DllMain函数
同时若目标DLL存在导入段(需要加载其他DLL)也不会将额外的DLL自动载入到进程地址空间中。
因此若此时调用任何该DLL的函数将面临风险。 所以通常情况应该避免使用此标志
2. LOAD_LIBRARY_AS_DATAFILE标志
表示将DLL作为数据文件映射到进程地址空间。和DONT_RESOLVE_DLL_REFERENCE标志类似。但是前者会给DLL中的不同段指定不同的保护属性。
如果需要的是资源DLL,可以用这种方式加载。利用返回的HMODULE来载入系统资源。
通常载入一个exe会启动新进程如果仅需要使用一个exe的资源,也可以用这种方式作为数据文件载入。
3. LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE
和上面的标志2类似。唯一不同时DLL文件以独占方式打开,禁止其他进程对其修改。
4. LOAD_LIBRARY_AS_IMAGE_RESOURCE
系统载入DLL的时候,会对虚拟地址进行修复。(将RVA修复成地址空间的地址)
5. LOAD_WITH_ALERTED_SEARCH_PATH标志
改变LoadLibraryEx对dll的搜索算法。使用pszDLLPathName来搜索文件。
1)如果pszDllPathName不包含\字符,使用19章的标准算法搜索。
2)如果pszDllPathName包含\字符。
a.如果是绝对路径。会直接加载该dll,将不再对dll进行搜索
b.否则会在以下文件夹中拼接搜索。 例如当前进程目录, windows系统目录, 16位系统目录, windows目录, PATH环境列出的目录。
相对路径还支持"."或".."
3)如果不希望使用LOAD_WITH_ALTERED_SEARCH_PATH来调用LoadLibraryEx,或者不希望改变当前应用程序的目录。
可以设定一个dll的加载路径。SetDllDirectory 接着LoadLibrary在搜索时使用以下算法。
a. 进程当前目录
b. SetDllDirectory所设置的目录
c. Windows系统目录
d. 16位Windows系统目录
e. Windows目录
f. PATH环境变量的目录
如果使用SetDllDirectory(TEXT(""));表示将当前目录从搜索步骤中删除。 传入NULL会恢复默认算法。
GetDllDirectory可以返回这个特定的目录的当前值。
6. LOAD_IGNORE_CODE_AUTHZ_LEVEL标志
关闭WinSafer所提供的验证值。其目的是为了在代码执行过程可以拥有特权加以控制。
20.1.2 显示地卸载DLL模块
进程不再需要DLL中的符号可以显示将DLL从进程地址空间卸载。
BOOL FreeLibrary(HMODULE hInstDLL);
传入一个LoadLibrary(Ex)返回的HMODULE值
还可以调用
WINBASEAPI
DECLSPEC_NORETURN
VOID
WINAPI
FreeLibraryAndExitThread(
_In_ HMODULE hLibModule,
_In_ DWORD dwExitCode
);
适用于某个DLL中存在创建线程的代码,需要将其卸载并退出线程。如果dll中直接写这样的代码。因为在FreeLibrary已经将DLL从地址空间中卸载,后面的ExitThread代码不再存在。
而该函数存在Kernel32.dll中,系统内核动态库一般在整个进程执行过程中都会一直存在。这样目标dll被卸载以后,也能安全的退出目标dll所创建的线程。
每个DLL在进程中有一个使用计数,LoadLibrary会增加1, FreeLibrary会递减1.
系统发现使用计数器为0的DLL映像,会将其完全卸载。
而且这个使用计数器是每个进程独立的。
线程可以调用GetModuleHandle函数来检测一个DLL是否已经被映射到进程的地址空间中。
WINBASEAPI
_When_(lpModuleName == NULL, _Ret_notnull_)
_When_(lpModuleName != NULL, _Ret_maybenull_)
HMODULE
WINAPI
GetModuleHandleW(
_In_opt_ LPCWSTR lpModuleName
);
例如一下代码判断某DLL是否已经被加载,若没有才加其载入。
HMODULE hInstDll = GetModuleHandle(TEXT("MyLib"));
if (hInstDll == NULL) {
hInstDll = LoadLibrary(TEXT("MyLib"));
}
如果传递NULL给GetModuleHandle会返回引用程序可执行文件的句柄。
还可以获得DLL的全路径。
WINBASEAPI
_Success_(return != 0)
_Ret_range_(1, nSize)
DWORD
WINAPI
GetModuleFileNameW(
_In_opt_ HMODULE hModule,
_Out_writes_to_(nSize, ((return < nSize) ? (return + 1) : nSize)) LPWSTR lpFilename,
_In_ DWORD nSize
);
第一个是DLL或exe的HMODULE
第二个参数是一块缓存地址用于存放返回的路径
nSize指定缓存的大小
如果传NULL给hModule会返回当前可执行文件的文件的完整路径。 参考第四章
LoadLibraryEx加载的DLL有时候返回的HMODULE和Library不同。不应该将其返回的HMODULE混用。只有当LoadLibraryEx不使用任何flags时才和LoadLibrary等价。
参考以下例子
int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
HMODULE hDll1 = LoadLibrary(TEXT("MyLib.dll"));
HMODULE hDll2 = LoadLibraryEx(TEXT("MyLib.dll"), NULL,
LOAD_LIBRARY_AS_IMAGE_RESOURCE);
HMODULE hDll3 = LoadLibraryEx(TEXT("MyLib.dll"), NULL,
LOAD_LIBRARY_AS_DATAFILE);
printf("Module1: %p\n", hDll1);
printf("Module1: %p\n", hDll2);
printf("Module1: %p\n", hDll3);
return 0;
}
3个模块加载的地址都相同。
将代码做一下修改。
int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
HMODULE hDll1 = LoadLibraryEx(TEXT("MyLib.dll"), NULL,
LOAD_LIBRARY_AS_DATAFILE);
HMODULE hDll2 = LoadLibraryEx(TEXT("MyLib.dll"), NULL,
LOAD_LIBRARY_AS_IMAGE_RESOURCE);
HMODULE hDll3 = LoadLibrary(TEXT("MyLib.dll"));
printf("Module1: %p\n", hDll1);
printf("Module1: %p\n", hDll2);
printf("Module1: %p\n", hDll3);
return 0;
}
运行结果
载入了3个地址。
因为第一行以数据文件的方式先载入DLL该地址空间不可载入函数(因为代码不可执行)
第二行以映像方式载入DLL,因为第一行载入的DLL是数据方式(不会修复RVA)。因此LoadLibraryEx重新映射了一块地址空间并加载DLL且修复RVA
第三行以正常方式加载DLL(并且会加载所有导入段和映射所有使用的符号且修复RVA),因此又映射了一块地址空间。
三行代码的地址各不相同。
20.1.3 显式连接到导出符号
线程必须显示调用GetProcAddress来获得其引用符号的地址。
WINBASEAPI
FARPROC
WINAPI
GetProcAddress(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
该函数不支持Unicode。默认是ANSI版本。
例如显式的链接一个dll中的导出符号
FARPROC pfn = GetProcAddress(hInstDll, "SomeFuncInDll");
用序号来指定想要的那个符号的地址:
FARPROC pfn = GetProcAddress(hInstDll, MAKEINTRESOURCE(2));
例如以下代码:
typedef void(CALLBACK *PFN_DUMPMODULE)(HMODULE hModule);
PFN_DUMPMODULE pfnDumpModule = (PFN_DUMPMODULE)GetProcAddress(hDll, "DumpModule");
if (pfnDumpModule != NULL) {
pfnDumpModule(hDll);
}
20.2 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实例句柄。(与_tWinMain的hInstExe参数类似)
这个值其实就是一个虚拟地址,DLL文件映像被映射到进程地址空间的这个位置。 通常可以将其保存在全局变量中,可以在调用资源载入函数(DialogBox和L