本章内容
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两小节介绍的技术非常重要,能显著提高整个系统的性能。
为了让线程调用DLL模块中的一个函数,必须将DLL文件映射到调用线程所在进程的地址空间中。有两种方式:
1)直接让应用程序源码引用DLL中所包含的符号,这样加载程序在应用程序运行的时候会隐式载入所需要的DLL(编译+应用程序启动时)
2)让应用程序在运行过程显式载入所需要的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
);
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所提供的验证值。其目的是为了在代码执行过程可以拥有特权加以控制。
进程不再需要DLL中的符号可以显示将DLL从进程地址空间卸载。
BOOL FreeLibrary(HMODULE hInstDLL);
传入一个LoadLibrary(Ex)返回的HMODULE值
还可以调用
WINBASEAPI
DECLSPEC_NORETURN
VOID
WINAPI
FreeLibraryAndExitThread(
_In_ HMODULE hLibModule,
_In_ DWORD dwExitCode
);
而该函数存在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
);
HMODULE hInstDll = GetModuleHandle(TEXT("MyLib"));
if (hInstDll == NULL) {
hInstDll = LoadLibrary(TEXT("MyLib"));
}
还可以获得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
);
第二个参数是一块缓存地址用于存放返回的路径
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;
}
将代码做一下修改。
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),因此又映射了一块地址空间。
三行代码的地址各不相同。
线程必须显示调用GetProcAddress来获得其引用符号的地址。
WINBASEAPI
FARPROC
WINAPI
GetProcAddress(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
例如显式的链接一个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);
}
每个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和LoadString)的时候使用它。
如果DLL是隐式载入的,那么最后一个参数fImpLoad的值不会零,如果DLL是显式载入的,那么fImpLoad的值为零。
fdwReason表示调用入口函数的原因。
DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH.
系统第一次将DLL映射到进程地址空间时,会调用DllMain函数,并在fdwReason传入DLL_PROCESS_ATTACH。如果后续线程再调用LoadLibrary来载入同一个DLL,系统只会增加引用计数器而不会以DLL_PROCESS_ATTACH来调用DllMain函数
DLL_PROCESS_ATTACH可以进行一些简要的初始化。例如调用HeapCreate创建所需要的堆并保存在全局变量中。
处理DLL_PROCESS_ATTACH时,DllMain函数的返回值表示该DLL是否初始化成功。成功是TRUE 失败是FALSE.
处理其他情况时系统将忽略DllMain的返回值
通常隐式加载DLL文件的线程来执行DllMain函数。例如一个exe载入一个DLL并映射到地址空间以后, 主线程会以DLL_PROCESS_ATTACH来调用DllMain。 等所有DLL都完成对改通知的处理,系统会让进程的主线程开始执行可执行模块C/C++的启动代码。然后执行入口函数(_tmain或_tWinMain)
如果其中的任何一个DllMain函数返回FALSE,也就是初始化失败,系统会把所有文件映像从地址空间清除并向用户显示一个消息框告知进程无法启动,然后终止整个进程。
显式加载DLL时,LoadLibrary会定位DLL文件并把DLL映射到进程地址空间,系统会用(调用LoadLibrary的)线程来调用DllMain函数,并传入DLL_PROCESS_ATTACH。
DllMain完成处理以后,系统会让LoadLibrary调用返回,线程就可以继续执行了。如果DllMain返回FALSE,也就是初始化失败。那么系统会自动从进程地址空间撤销对DLL文件映像的映射并让LoadLibrary返回NULL
当系统将一个DLL从进程地址空间撤销时,会调用DLL的DllMain函数,并在fdwReason参数中传入DLL_PROCESS_DETACH。 此时Dll应该执行一些清理操作。
例如调用HeapDestroy来销毁在处理DLL_PROCESS_ATTACH通知时候创建的堆。
另外如果在DLL_PROCESS_ATTACH处理时返回FALSE 那么将不会收到DLL_PROCESS_DETACH通知。如果撤销映射的原因是进程终止,那么ExitProcess的函数线程将负责执行DllMain的代码。(通常就是主线程)
在DllMain返回到启动代码以后,通常会显示调用ExitProcess来终止进程。
如果撤销映射的原因是因为一个线程调用了FreeLibrary或FreeLibraryAndExitThread发出调用的线程将执行DllMain中的代码。
注意DLL可能会阻碍进程的终止。当DllMain收到DLL_PROCESS_DETACH通知的时候,有可能会进入无限循环。只有当每个DLL都处理完DLL_PROCESS_DETACH以后,操作系统才会真正终止进程。
LoadLibrary时系统执行的步骤
FreeLibrary时系统执行的步骤
当进程创建一个线程的时候,系统会检查当前映射到进程地址空间中的所有DLL文件映像,并用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数。告知DLL需要执行线程相关的初始化代码。由新创建的线程来执行所有DLL的DllMain函数中的代码。只有当DLL都完成了对该通知的处理以后,系统才会让新线程开始执行线程函数。
如果系统在将一个DLL映射到进程地址空间的时候,进程中已经存在多个线程运行了,但是系统并不会让任何已有的线程以DLL_THREAD_ATTACH来调用该DLL。
只有在创建新线程的时候才会调用DllMain。
另外进程的主线程也不会以DLL_THREAD_ATTACH来调用DllMain。而是以DLL_PROCESS_ATTACH来调用DllMain
当线程函数返回以后,系统会调用ExitThread来终止线程。ExitThread仅仅是告知系统该线程想要终止,(但不是马上)。此时这个线程会以DLL_THREAD_DETACH来调用所有已映射DLL的DllMain函数。告知Dll进行线程相关的清理。(例如C++运行库会释放那些管理多线程应用程序的数据块)
DLL可能会妨碍线程的终止。例如当DllMain收到DLL_THREAD_DETACH通知的时候,可能会进入无限循环。只有当每个DLL都处理完DLL_THREAD_DETACH通知以后,操作系统才能真正终止线程。
如果在撤销映射一个DLL的时候还有任何线程在运行, 系统并不会让这些线程来以DLL_THREAD_DETACH来调用DllMain.
注意一个特殊的情况:
1)进程的一个子线程调用LoadLibrary来载入一个Dll ,这使得系统让该线程调用DLL_PROCESS_ATTACH来调用该DLL的DllMain(不会调用DLL_THREAD_ATTACH)
2)接着载入该DLL的线程退出了。这时候会以DLL_THREAD_DETACH来调用DllMain。
注意系统将该线程链接到DLL的时候,并不会发送DLL_THREAD_ATTACH,但是当该线程与DLL链接的时候却发送了DLL_THREAD_DETACH.这个原因在释放清理代码的时候要特效小心防止资源泄漏。
不过大部分时候调用LoadLibrary和FreeLibrary的都是同一个线程
系统会将DllMain函数的调用序列化。保证其线程安全。一次只有一个线程执行其中的代码。其他试图执行的线程会被挂起等待。
一个有缺陷的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; // Used only for DLL_PROCESS_ATTACH
}
有一个系统API
WINBASEAPI
BOOL
WINAPI
DisableThreadLibraryCalls(
_In_ HMODULE hLibModule
);
是否不向DLL发送通知就能解决问题呢。?
下面是修改后的方案
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.
// Prevent the system from calling DllMain
// When threads are created or destroyed.
DisableThreadLibraryCalls(hInstDll);
// 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; // Used only for DLL_PROCESS_ATTACH
}
在调用CreateThread的时候,系统首先会创建线程内核对象和线程栈。然后系统会在自己的内部调用WaitForSingleObject函数,并传入进程的互斥量对象句柄(Loader Lock)当新线程得到所有权以后,系统会让新线程调用DLL_THREAD_ATTACH来调用每个DLL的DllMain。只有这个时候,系统才会调用ReleaseMutex来放弃堆进程的互斥量对象的所有权。
因此不让新创建的线程调用DllMain并不能解决死锁问题。只能重新设计代码不住DllMain函数中调用WaitForSingleObject
如果使用VC++来构建DLL文件,在链接DLL的时候连接器会将DLL的入口函数地址嵌入到生成的DLL映像中。VC++的连接器默认会使用静态链接来嵌入_DllMainCTRSTartup函数(虽然他包含在C/C++运行库中,且不管最终链接CRT是静态还是动态,这个函数将以静态方式链接入DLL文件映像)。
后续系统加载DLL文件映像并映射到地址空间的时候实际上是调用_DllMainCRTStartup函数。而非用户自定义的DllMain函数。所有通知都先转发到__DllMainCRTStartup.(双下划线)
__DllMainCRTStartup(双下划线)会对DLL_PROCESS_ATTACH通知处理,初始化C/C++运行库构造全局或静态C++对象。接着将通知转发给DllMain
当Dll收到DLL_PROCESS_DETACH时候,首先是_DllMainCRTStartup转发给__DllMainCRTStartup(双下划线). 这一次函数调用DllMain。当DllMain返回以后__DllMainCRTStartup(双下划线)会调用DLL中所有全局或静态C++对象的析构函数。当收到DLL_THREAD_ATTACH或DLL_THREAD_DETACH的时候__DllMainCRTStartup(双下划线)不会进行任何处理。
_DllMainCRTStartup和__DllMainCRTStartup函数的代码实现 crtdll.c
BOOL WINAPI
_DllMainCRTStartup(
HANDLE hDllHandle,
DWORD dwReason,
LPVOID lpreserved
)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
/*
* The /GS security cookie must be initialized before any exception
* handling targetting the current image is registered. No function
* using exception handling can be called in the current image until
* after __security_init_cookie has been called.
*/
__security_init_cookie();
}
return __DllMainCRTStartup(hDllHandle, dwReason, lpreserved);
}
__declspec(noinline)
BOOL __cdecl
__DllMainCRTStartup(
HANDLE hDllHandle,
DWORD dwReason,
LPVOID lpreserved
)
{
BOOL retcode = TRUE;
__try {
__native_dllmain_reason = dwReason;
__try{
/*
* If this is a process detach notification, check that there has
* been a prior process attach notification.
*/
if ( (dwReason == DLL_PROCESS_DETACH) && (__proc_attached == 0) ) {
retcode = FALSE;
__leave;
}
if ( dwReason == DLL_PROCESS_ATTACH || dwReason == DLL_THREAD_ATTACH ) {
if ( _pRawDllMain )
retcode = (*_pRawDllMain)(hDllHandle, dwReason, lpreserved);
if ( retcode )
retcode = _CRT_INIT(hDllHandle, dwReason, lpreserved);
if ( !retcode )
__leave;
}
retcode = DllMain(hDllHandle, dwReason, lpreserved);
if ( (dwReason == DLL_PROCESS_ATTACH) && !retcode ) {
/*
* The user's DllMain routine returned failure. Unwind the init.
*/
DllMain(hDllHandle, DLL_PROCESS_DETACH, lpreserved);
_CRT_INIT(hDllHandle, DLL_PROCESS_DETACH, lpreserved);
if ( _pRawDllMain )
(*_pRawDllMain)(hDllHandle, DLL_PROCESS_DETACH, lpreserved);
}
if ( (dwReason == DLL_PROCESS_DETACH) ||
(dwReason == DLL_THREAD_DETACH) ) {
if ( _CRT_INIT(hDllHandle, dwReason, lpreserved) == FALSE ) {
retcode = FALSE ;
}
if ( retcode && _pRawDllMain ) {
retcode = (*_pRawDllMain)(hDllHandle, dwReason, lpreserved);
}
}
} __except ( __CppXcptFilter(GetExceptionCode(), GetExceptionInformation()) ) {
retcode = FALSE;
}
} __finally
{
__native_dllmain_reason = __NO_REASON;
}
return retcode ;
}
在DLL源码中实现DllMain函数不是必须的。如果没有自己的DllMain函数,C/C++运行库中有一个DllMain函数。大致如下
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {
if (fdwReason == DLL_PROCESS_ATTACH)
DisableThreadLibraryCalls(hInstDll);
return TRUE;
}
VC++支持延迟载入DLL,一个延迟载入的DLL是隐式链接的,系统一开始并不会载入DLL,只有当代码试图去访问一个DLL中的符号时,系统才会实际加载DLL。
应用场景:
1)如果一个应用程序使用了很多DLL,那么它的初始化可能会较慢,因为加载程序需要将所有的DLL映射到进程地址空间中。
2)要在代码中调用一个新函数,然后又试图在一个不提供该函数的老版本的操作系统中运行,那么加载程序会报告一个错误并不允许应用程序执行。
需要在代码中执行一些判断,如果程序运行在老版本操作系统,就不调用这个不存在的函数。
一个导出字段的DLL是无法延迟载入的(导出全局变量)
Kernel32.dll是无法延迟加载的,必须要载入该模块才能调用LoadLibrary和GerProcAddress
不应该在DllMain的入口函数调用延迟载入的函数,这可能导致程序崩溃。
参考一下文档"Constraints of Delay Loading DLLs"
https://msdn.microsoft.com/en-us/library/yx1x886y
以下是一个完整的示例。
1.创建一个DLL,再创建一个可执行文件。
在链接可执行文件的时候,必须增加两个连接器开关
/Lib:DelayImp.lib
/DelayLoad:MyDll.dll
接着在可执行文件的Project属性中配置Linker->Input
然后需要打开一下选项Project->Linker->Advanced
/Lib开关告知连接器将指定函数__delayLoadHelper2嵌入到可执行文件中。
第二个开关/DelayLoad告知连接器下列事项:
1)将MyDll.dll从可执行模块的导入段去除,这样进程初始化的时候,操作系统的加载程序不会隐式载入该DLL
2)在可执行模块中嵌入一个新的延迟载入段(Delay Import section, .didata)来表示要从MyDll.dll中导入哪些函数
3)通过让对延迟载入函数的调用跳转到__delayLoadHelper2函数,来完成对延迟载入函数的解析
应用程序执行的时候,对延迟载入函数的调用实际上会调用__delayLoadHelper2函数。这个函数会引用那个特殊的载入段,并先后调用LoadLibrary和GetProcAddress一旦得到了对应的延迟载入函数的地址,__delayLoadHelper2会修复对该函数的调用,今后将直接调用延迟载入函数的地址。
注意同一个DLL中的其他函数仍然必须在第一次被调用的时候修复。
可以多次指定/DelayLoad连接器开关-每个开关对应一个想要延迟载入的DLL.
如果在延迟载入DLL的时候无法找到DLL文件会抛出一个异常。如果在DLL中找不到试图调用的函数也会抛出一个异常。
VC++定义了两个软件异常码(exception code VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND) 和 VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND) 表示模块未找到和函数未找到。
异常过滤函数DelayLoadDllExceptionFilter会对两个异常码进行检查。如果抛出的异常不是其中任何一个,那么同任何过滤器一样,该过滤器会返回EXCEPTION_CONTINUE_SEARCH(不应该吞掉那些不知道该如何处理的异常)
如果抛出的异常是两者之一,__delayLoadHelper2函数会提供一个指向DelayLoadInfo结构(定义于DelayImp.h)的指针,包含了一些额外的信息。
typedef struct DelayLoadInfo {
DWORD cb; // size of structure
PCImgDelayDescr pidd; // raw form of data (everything is there)
FARPROC * ppfn; // points to address of function to load
LPCSTR szDll; // name of dll
DelayLoadProc dlp; // name or ordinal of procedure
HMODULE hmodCur; // the hInstance of the library we have loaded
FARPROC pfnCur; // the actual function that will be called
DWORD dwLastError;// error received (if an error notification)
} DelayLoadInfo, * PDelayLoadInfo;
在SEH过滤器内部,成员szDll指向试图载入的DLL的名字,试图查找的函数名字在dlp成员中。dlp的类型定义如下
typedef struct DelayLoadProc {
BOOL fImportByName;
union {
LPCSTR szProcName;
DWORD dwOrdinal;
};
} DelayLoadProc;
还可以查看dwLastError来知道是什么错误引发的异常。 pfnCur包含了想要查找的函数地址。在异常过滤器中该值始终为NULL。(因为__delayLoadHelper2无法找到该函数的地址才会抛出异常)
其他成员cb用于版本控制, pidd指向嵌入在模块中的延迟载入段,其中包含延迟载入DLL和延迟载入函数的列表。
ppfn是一个地址 若函数查找成功,则其地址会保存在这个成员。
cb和ppfn通常是__delayLoadHelper2函数内部使用,通常不需要理解。
MS还支持将一个延迟载入的DLL卸载。在构建可执行文件的时候指定一个额外的连接器开关(/Delay:unload)
必须修改源代码,在想要卸载DLL的地方调用__FUnloadDelayLoadedDLL2函数
ExternC
BOOL WINAPI
__FUnloadDelayLoadedDLL2(LPCSTR szDll);
调用__FUnloadDelayLoadedDLL2的时候,需要传入想要卸载的延迟载入的DLL的名字。该函数会引用文件的卸载段,将该DLL所有函数地址重置。然后__FUnloadDelayLoadedDLL2会调用FreeLibrary来卸载该DLL。
有几个需要注意的:
1)要确认不使用FreeLibrary来卸载DLL,否则函数地址不会被重置,再试图访问DLL中的函数会引发访问违规。
2)使用__FUnloadDelayLoadedDLL2的时候,传入DLL的名字不应该包含路径,且名字中字母大小写必须给和/DelayLoad连接器开关的DLL名字大小写完全相同,否则__FUnloadDelayLoadedDLL2会调用失败。
3)如果不打算卸载一个延迟载入的DLL,不必指定/Delay:unload连接器开关。这样还能减少exe文件的大小
4)在一个模块中调用__FUnloadDelayLoadedDLL2,但该模块中构建时并没有使用/Delay:unload开关,那么__FUnloadDelayLoadedDLL2什么也不会做并返回FALSE
延迟载入DLL的特性是,默认情况下,我们调用的函数会被绑定到进程地址空间中的一个内存地址上,这个地址是系统认为函数应该在的位置。由于创建可绑定的延迟载入DLL段会增加exe文件的大小,因此也可以使用/Delay:nobind开关来关闭绑定。
但是通常情况下绑定是需要的,大多数应用程序不应该使用此开关。
当__delayLoadHelper2执行的时候,可以调用挂钩函数(hook function)来接收__delayLoadHelper2的进度通知和错误通知。这些函数还能改变载入DLL以及得到函数虚拟地址的方式。
为了得到通知或覆盖默认行为,需要执行以下步骤:
1)编写一个挂钩函数(类似DelayLoadApp.cpp中的DliHook)
2)在DelayImp.lib静态链接库的内部,定义了两个全局变量__pfnDliNotifyHook2 和__pfnDliFailureHook2。 类型为PfnDliHook;
typedef FARPROC (WINAPI *PfnDliHook)(
unsigned dliNotify,
PDelayLoadInfo pdli
);
将两个变量设置为挂钩函数。这样
__delayLoadHelper2会使用回调函数来调用设置的挂钩函数。dliNotify表示函数被调用的原因。
DelayLoadApp示例程序
该程序会自己载入DelayLoadLib模块,操作系统的加载程序不会把该模块映射到进程地址空间中。示例程序调用IsModuleLoaded来通知模块是否已经被载入到进程地址空间中。
DelayLoadLib.h
/******************************************************************************
Module: DelayLoadLib.h
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#ifndef DELAYLOADLIBAPI
#define DELAYLOADLIBAPI extern "C" __declspec(dllimport)
#endif
///
DELAYLOADLIBAPI int fnLib();
DELAYLOADLIBAPI int fnLib2();
End of File //
/******************************************************************************
Module: DelayLoadLib.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h" /* See Appendix A. */
#include
#include
///
#define DELAYLOADLIBAPI extern "C" __declspec(dllexport)
#include "DelayLoadLib.h"
///
int fnLib() {
return(321);
}
///
int fnLib2() {
return(123);
}
End of File //
/******************************************************************************
Module: DelayLoadApp.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h"
#include
#include
#include
//
#include // For error handling & advanced features
#include "..\DelayLoadLib\DelayLoadLib.h"
//
// Statically link __delayLoadHelper2/__FUnloadDelayLoadedDLL2
#pragma comment(lib, "Delayimp.lib")
// Note: it is not possible to use #pragma comment(linker, "")
// for /DELAYLOAD and /DELAY
// The name of the Delay-Load module (only used by this sample app)
TCHAR g_szDelayLoadModuleName[] = TEXT("DelayLoadLib");
//
// Forward function prototype
LONG WINAPI DelayLoadDllExceptionFilter(PEXCEPTION_POINTERS pep);
//
void IsModuleLoaded(PCTSTR pszModuleName) {
HMODULE hmod = GetModuleHandle(pszModuleName);
char sz[100];
#ifdef UNICODE
StringCchPrintfA(sz, _countof(sz), "Module \"%S\" is %Sloaded.",
pszModuleName, (hmod == NULL) ? L"not " : L"");
#else
StringCchPrintfA(sz, _countof(sz), "Module \"%s\" is %sloaded.",
pszModuleName, (hmod == NULL) ? "not " : "");
#endif
chMB(sz);
}
//
int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) {
// Wrap all calls to delay-load DLL functions inside SEH
__try {
int x = 0;
// If you're in the debugger, try the new Debug.Modules menu item to
// see that the DLL is not loaded prior to executing the line below
IsModuleLoaded(g_szDelayLoadModuleName);
x = fnLib(); // Attempt to call delay-load function
// Use Debug.Modules to see that the DLL is now loaded
IsModuleLoaded(g_szDelayLoadModuleName);
x = fnLib2(); // Attempt to call delay-load function
// Unload the delay-loaded DLL
// NOTE: Name must exactly match /DelayLoad: (DllName)
PCSTR pszDll = "DelayLoadLib.dll";
__FUnloadDelayLoadedDLL2(pszDll);
// Use Debug.Modules to see that the DLL is now unloaded
IsModuleLoaded(g_szDelayLoadModuleName);
x = fnLib(); // Attempt to call delay-load function
// Use Debug>modules to see that the DLL is loaded again
IsModuleLoaded(g_szDelayLoadModuleName);
}
__except (DelayLoadDllExceptionFilter(GetExceptionInformation())) {
// Nothing to do in here, thread continues to run normally
}
// More code can go here...
return 0;
}
//
LONG WINAPI DelayLoadDllExceptionFilter(PEXCEPTION_POINTERS pep) {
// Assume we recognize this exception
LONG lDisposition = EXCEPTION_EXECUTE_HANDLER;
// If this is a Delay-load problem, ExceptionInformation[0] points
// to a DelayLoadInfo structure that has detailed error info
PDelayLoadInfo pdli =
PDelayLoadInfo(pep->ExceptionRecord->ExceptionInformation[0]);
// Create a buffer where we construct error messages
char sz[500] = { 0 };
switch (pep->ExceptionRecord->ExceptionCode) {
case VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND):
// the DLL module was not found at runtime
StringCchPrintfA(sz, _countof(sz), "Dll not found: %s", pdli->szDll);
break;
case VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND):
// The DLL module was found, but it doesn't contain the function
if (pdli->dlp.fImportByName) {
StringCchPrintfA(sz, _countof(sz), "Function %s was not found in %s",
pdli->dlp.szProcName, pdli->szDll);
}
else {
StringCchPrintfA(sz, _countof(sz), "Function ordinal %d was not found in %s",
pdli->dlp.dwOrdinal, pdli->szDll);
}
break;
default:
// We don't recognize this exception
lDisposition = EXCEPTION_CONTINUE_SEARCH;
break;
}
if (lDisposition == EXCEPTION_EXECUTE_HANDLER) {
// We recognized this error and constructed a message, show it
chMB(sz);
}
return lDisposition;
}
//
// Skeleton DliHook function does nothing interesting
FARPROC WINAPI DliHook(unsigned dliNotify, PDelayLoadInfo pdli) {
FARPROC fp = NULL; // Default return value
// NOTE: The members of the DelayLoadInfo structure pointed
// to by pdli shows the results of progress made so far.
switch (dliNotify) {
case dliStartProcessing:
// Called when __delayLoadHelper2 attempts to find a DLL/function
// Return 0 to have normal behavior or nonzero to override
// everything (you will still get dliNoteEndProcessing)
break;
case dliNotePreLoadLibrary:
// Called just before LoadLibrary
// Return NULL to have __delayLoadHelper2 call LoadLibrary
// or you can call LoadLibrary yourself and return the HMODULE
fp = (FARPROC)(HMODULE)NULL;
break;
case dliFailLoadLib:
// Called if LoadLibrary fails
// Again, you can call LoadLibrary yourself here and return an HMODULE
// If you return NULL, __delayLoadHelper2 raises the
// ERROR_MOD_NOT_FOUND exception
fp = (FARPROC)(HMODULE)NULL;
break;
case dliNotePreGetProcAddress:
// Called just before GetProcAddress
// Return NULL to have __delayLoadHelper2 call GetProcAddress,
// or you can call GetProcAddress yourself and return the address
fp = (FARPROC)NULL;
break;
case dliFailGetProc:
// Called if GetProcAddress fails
// You can call GetProcAddress yourself here and return an address
// If you return NULL, __delayLoadHelper2 raises the
// ERROR_PROC_NOT_FOUND exception
fp = (FARPROC)NULL;
break;
case dliNoteEndProcessing:
// A simple notification that __delayLoadHelper2 is done
// You can examine the members of the DelayLoadInfo structure
// pointed to by pdli and raise an exception if you desire
break;
}
return fp;
}
///
// Tell __delayLoadHelper2 to call my hook function
PfnDliHook __pfnDliNotifyHook2 = DliHook;
PfnDliHook __pfnDliFailureHook2 = DliHook;
End of File //
运行结果:
刚开始进程并未加载DelayLoadLib.dll
执行了符号中的函数以后,进程加载了DelayLoadLib.dll
执行了__FUnloadDelayLoadedDLL2以后,发现DelayLoadLib.dll又被卸载了
在编译完以后修改并删除dll中导出的fnlib2函数。运行DelayLoadApp会弹出提示:
展现了一个异常处理的作用。
函数转发器(function forwarder)是DLL输出段中的一个条目,用来将一个函数调用转发到另一个DLL中的另一个函数。
例使用DumpBin查看Kernel32.dll可以看到类似下面的输出
这个输出显示了4个被转发的函数。如果应用程序调用了CloseThreadpoolIo,CloseThreadpoolTimer,CloseThreadpoolWait或CloseThreadpoolWork,那么可执行文件会被动态转发的函数实际是在NTDLL.dll中。会将NTDLL.dll也一起载入。
比如可执行文件调用CloseThreadpoolIo的时候,实际上调用的是NTDLL.dll中的TpReleaseIoCompletion函数。实际上CloseThreadpoolIo函数在系统中根本不存在。
自己定义函数转发器。
// Function forwarder to functions in DllWork
#pragma comment(linker, "/export:SomeFunc=DllWork.SomeOtherFunc")
操作系统对某些DLL做了特殊处理,这些DLL被标记为已知DLL。在注册表中记录
所有的DLL存在一个值名。就是DLL的名称。比如在使用LoadLibrary加载dll的时候,如果不传入.dll名。可以传入值名。
已知的DLL会在注册表中指定的位置被优先搜索。然后才是搜索操作系统文件夹等后续规则。
系统还会在注册表项的DllDirectory值所表示的目录中搜索DLL。 在Vista中DllDirectory的默认值是%SystemRoot%\System32
例如一下语句就会使用这条规则来搜索:
书中举了一个例子。首先在注册表中的KnownDlls添加以下键值对。
Value Name: SomeLib
Value data : SomeOtherLib.dll
LoadLibrary(TEXT("SomeLib"));
系统会有正常的搜索规则来查找这个DLL
如果使用了这样的语句
LoadLibrary(TEXT("SomeLib.dll"));
系统会先去掉.dll扩展名,并在注册表中搜索是否为已知dll,找到了SomeLib的键值对。接着会在%SystemRoot%\System32中查找SomeOtherLib.dll如果系统在目录中找到了该文件,则会将其载入。如果未能找到该文件,会失败并返回NULL
早期的Windows操作系统由于受限于磁盘资源等内容会将所有动态库都存放于系统目录下,这就带来了一个隐患。任何应用程序的安装程序都可能试图往系统目录中增加或删除动态库。这可能会导致覆盖了老版本的动态库而影响系统和其他应用程序的正常运行。
在win2000以后MS增加了一个特性。强制操作系统加载程序首先从应用程序目录来加载模块。比如有一个AppName.exe的应用。在其目录下新建一个AppName.exe.local文件(内容无关紧要)那么LoadLibrary检查到这个文件以后,会优先在应用程序目录来加载对应模块。如果不存在对应的模块,那么搜索逻辑和以往一样。
不过为了安全性考虑,Vista中这项特性默认是关闭的。防止应用程序文件夹中载入伪造的系统DLL。需要修改注册表打开此特性。
HKLM\Software\Microsoft\WindowsNT\CurrentVersion\Image File Execution Options
增加一条DWORD DevOverrideEnable 讲其设置为1
每个dll模块和exe模块在链接以后会有一个首选基地址。默认exe是0x00400000 默认dll是0x10000000
可以使用DumpBin来查看例如:
Dumpbin的默认基地址是0x0040000
基地址有什么用呢?
查看一下代码:
int g_x;
void Func() {
g_x = 5;
}
编译器和连接器会生成类似代码:
MOV dword ptr ds:[0x00414540], 5
在连接器和编译器生成机器码的时候g_x变量的地址固定死了。也就是0x00414540。 只有当可执行文件被载入首选基地址0x00400000的时候,这个内存地址才是正确的。
DLL也同理。
可是如果一个模块的基地址被占用了。导致其无法被加载在默认的基地址上。加载程序会对其进行重定位。通常连接器在构建模块的时候会将重定位段(relocation section)嵌入到文件中。包含一个字节偏移量表。如果加载程序需要对模块进行重定位,会打开模块的重定位段并遍历所有条目。对每一个条目,加载程序会先找到包含机器指令的那个存储页面,然后取模块首选基地址与实际映射地址之间的差值,加到机器指令当前正在使用的内存地址上。
例如一个动态库的基地址为0x10000000, 其中一个全局变量地址为0x10014540. 如果动态库实际被加载到0x20000000.那么加载程序会先取地址差值0x20000000 - 0x10000000 = 0x10000000. 并将差值加到全局变量的地址上0x10014540。最后的修复地址为0x20014540.
当一个模块无法被载入到它的首选基地址时,有两个主要缺点:
1)加载程序必须遍历重定位段并修改模块中的大量代码。这样会损坏初始化时间
2) 加载程序写入到模块的代码页中时,系统的写时复制机制会强制这些页面以系统交换页为后备存储器。(减少了系统可用存储器的数量)
可以创建一个不包含重定位段的可执行文件或DLL模块。在构建模块时使用/FIXED开关就能达到这个目的。
但是若加载程序无法将无重定位段的模块加载到指定的基地址会直接导致进程终止。
对于资源DLL使用/FIXED开关。并在头文件中嵌入一些信息,表示该模块之所以不包含重定位信息是因为没有这个必要。
使用/SUBSYSTEM:WINDOWS, 5.0 或/SUBSYSTEM:CONSOLE, 5.0开关。而不要指定/FIXED开关。这样连接器检查到没有东西需要重定位,会自动将模块中的重定位段省略。并在PE文件头中关闭一个叫IMAGE_FILE_RELOCS_STRIPPED的标志。在加载程序载入该模块的时候,会发现该模块可以重定位(因为IMAGE_FILE_RELOCS_STRIPPED被关闭),但实际上它并不包含重定位段。
可以自己给模块设定构建时的基地址。
VS提供了一个工具Rebase可以修复一个exe和模块的基地址。
ImageHlp API 提供了一个ReBaseImage函数可以实现自己的重定位工具
BOOL
IMAGEAPI
ReBaseImage(
_In_ PCSTR CurrentImageName,
_In_ PCSTR SymbolPath,
_In_ BOOL fReBase, // TRUE if actually rebasing, false if only summing
_In_ BOOL fRebaseSysfileOk, // TRUE is system images s/b rebased
_In_ BOOL fGoingDown, // TRUE if the image s/b rebased below the given base
_In_ ULONG CheckImageSize, // Max size allowed (0 if don't care)
_Out_ ULONG *OldImageSize, // Returned from the header
_Out_ ULONG_PTR *OldImageBase, // Returned from the header
_Out_ ULONG *NewImageSize, // Image size rounded to next separation boundary
_Inout_ ULONG_PTR *NewImageBase, // (in) Desired new address.
// (out) Next address (actual if going down)
_In_ ULONG TimeStamp // new timestamp for image, if non-zero
);
1)它会模拟创建一个进程地址空间
2)他会打开被载入到这个地址空间中的所有模块,并得到每个模块的大小以及它们的首选基地址
3)会在模拟的地址空间中对模块重定位的过程进程模拟,是各模块之间没有交叠
4)对每个重定位过的模块,它会解析该模块的重定位段,并修改模块在磁盘文件中的代码
5)为了反映行的首选基地址,它会更新每个重定位过的模块的文件头。
Rebase是一个很不错的工具,作者强烈推荐。应该在自己构建过程的后期,所有应用程序模块都已经构建完成以后运行它。使用Rebase就不需要再执行手动设定模块的载入基地址,Rebase会覆盖该值。
第四章的ProcessInfo工具可以列出一个进程地址空间中的所有模块。在BaseAddr下面可以看到各模块被载入到的虚拟内存地址。BaseAddr列的右面是ImagAddr列。通常是空白的,表示模块被载入预期首选的基地址。如果看到一个模块显示在括号内,说明该模块并未被载入首选的基地址,括号中的地址是磁盘文件的首选基地址。
比如这个例子。
MSVCF90.dll被重定向了地址默认基地址是701A0000
采用模块绑定可以更快的初始化并使用更少的存储器。使用了该模块导入的所有符号的虚拟地址,来对该模块的导入段进行预处理。防止模块重定位(产生换页),或载入符号时的虚拟地址修复工作(修复RVA)。
VS提供了一个Bind.exe工具。也可以使用ImageHlp API提供的BindImageEx函数来实现相同的特性
BOOL
IMAGEAPI
BindImageEx(
_In_ DWORD Flags,
_In_ PCSTR ImageName,
_In_ PCSTR DllPath,
_In_ PCSTR SymbolPath,
_In_opt_ PIMAGEHLP_STATUS_ROUTINE StatusRoutine
);
ImageName // Pathname of file to be bound
DllPath // search path used for locating image files
SymbolPath // search path used to keep debug info accurate
StatusRoutine // callback function
最后一个参数是一个回调函数地址,BindImageEx会定期调用这个回调函数,就可以对绑定过程进行监控。以下是函数原型:
BOOL WINAPI StatusRoutine(
IMAGEHLP_STATUS_REASON Reason, // Module / procedure not found, etc.
PCSTR pszImageName, // Pathname of file being bound
PCSTR pszDllName, // Pathname of DLL
ULONG_PTR VA, // Computed virtual address
ULONG_PTR Parameter // Additional info depending on Reason
);
1)它会打开指定的映像文件的导入段
2)对导入段中列出的每个DLL,会查看dll文件的文件头,来确定该DLL的首选基地址
3)在DLL的导出段中查看每个符号
4)它会获取符号的RVA,并将它和首选基地址相加。它会将计算得到的地址,也就是导入符号预期的虚拟地址,写入到映像文件的导入段。
5)在映像文件中添加一些额外信息。包括文件被绑定到的各DLL模块的名称,以及模块的时间截。
之后使用DumpBin来查看Calc.exe的导入段。可以看到被绑定模块相关的导入信息。
整个Bind过程做了两个重要假设:
1)进程初始化,DLL实际上被载入到它们的首选基地址。通过Rebase工具可以保证
2)导出段中所有引用的符号位置没有发生变化。加载程序会通过每个DLL的时间截来验证这一点。例如上面的步骤5)
如果不满足以上任一条件,那么Bind做的绑定将被视为无效,加载程序就会像之前讨论的逻辑那样加载并修复符号地址。
但是有些系统依赖的DLL会随着操作系统的不同而不同,应该在什么时候来绑定呢?答案是在应用程序安装过程来绑定。
但是这存在一个缺点。如果操作系统更新了某些补丁导致系统库的符号的RVA发生了变化,那么绑定也可能失败。这时候用户也没有什么选择。
作者认为MS应该发布一个工具,这个工具可以在操作系统升级后自动对每个模块重新绑定。