《Windows核心编程》读书笔记二十章 DLL高级技术

本文深入探讨了DLL的高级技术,包括显式载入和卸载DLL、DLL的入口函数、延迟载入DLL、函数转发器、已知DLL、重定向、模块基地址重定位和绑定等关键概念。详细介绍了DllMain的各个通知、如何避免死锁、延迟加载的实现机制及其优势,以及函数转发器的工作原理。此外,文章还提到了已知DLL的搜索路径和模块绑定对性能的影响。
摘要由CSDN通过智能技术生成

第二十章 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));


在能够使用 GetProcAddress所返回的指针来调用函数之前,还需要把它转换为与函数签名匹配的正确类型。


例如以下代码:

	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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值