windows核心编程---DLL高级技术Ex

-显示地载入DLL模块

HMODULE WINAPI LoadLibrary(
  _In_ LPCTSTR lpFileName
);

HMODULE WINAPI LoadLibraryEx(
  _In_       LPCTSTR lpFileName,
  // 为将来扩展预备,NULL。
  _Reserved_ HANDLE  hFile,
  // 0
  // DONT_RESOLVE_DLL_REFERENCES 正常下,一个DLL被映射到地址空间时,系统调用DLL的DllMain。且会载入,此DLL关联的DLL。此标志设置时,不调用DllMain,不载入关联DLL。
  // LOAD_LIBRARY_AS_DATAFILE 将DLL作为数据文件映射。对用此标志载入的DLL调用GetProcAddress时,返回值为NULL。调用LoadLibraryEx来载入.exe时,必须指定此标志。
  // LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE LOAD_LIBRARY_AS_DATAFILE+以独占方式打开此DLL
  // LOAD_LIBRARY_AS_IMAGE_RESOURCE 系统载入DLL时,会对相对虚拟地址进行修复。
  // LOAD_WITH_ALTERED_SEARCH_PATH 改变对DLL定位时的搜索算法。
  // lpFileName,不含\字符,使用标准搜索路径。
  // lpFileName,含\字符,
  // 如传入参数为全路径或网络共享路径(如C:\Apps|Libraries\MyLibrary.dll或\\Server\Share\MyLibrary.dll),则,直接载入该DLL。如文件不存在,返回NULL。
  // 其它情况,将下列文件夹与lpFileName连接。
  // a.进程当前目录
  // b.windows系统目录(System32)
  // c.16位windows系统目录(System)
  // d.windows目录
  // e.PATH环境变量的目录
  // 如lpFileName中出现"."或"..",搜索过程的每一步骤都会将它们考虑在内来构建一个相对路径。
  // 如将("..\\MyLibrary.dll")作为参数传入,则,LoadLibraryEx在下列位置搜索:
  // a的上一级目录
  // b的上一级目录
  // c的上一级目录
  // d的上一级目录
  // e的上一级目录
  // 如果构建应用时,希望从一个zhong'suo'z
  _In_       DWORD   dwFlags
);

以上两函数可以用来将一个DLL映射到进程的地址空间。
会在用户的系统中对DLL的文件映像进行定位,并试图将文件映像映射到调用进程地址空间。

返回的HMODULE等价于HINSTANCE,表示文件映像被映射到的虚拟内存地址。无法将DLL映射到进程的地址空间,函数会返回NULL。

如果希望从众所周知的文件夹动态载入DLL,应调用SetDllDirectory,传入文件夹。
此时搜索时:
a.进程的当前目录
b.SetDllDirectory指定的。
c.windows系统目录
d.16位windows系统目录
e.windows目录
f.PATH环境变量中列出的目录

GetDllDirectory。
SetDllDirectory会恢复使用默认算法

-显式地卸载DLL模块

// 将DLL从进程的地址空间中卸载
BOOL WINAPI FreeLibrary(
  _In_ HMODULE hModule
);

-DLL使用计数
每个DLL在进程中有一个使用计数。LoadLibrary(Ex)增加,FreeLibrary减1。进程内多次载入同一DLL,后续载入时仅仅改变该DLL使用计数。
FreeLibrary递减使用计数,使用计数变为0,从进程地址空间撤销对DLL文件映像的映射。

系统会在每个进程中为每个DLL维护一个使用计数。
进程A和B分别执行LoadLibrary载入同一DLL,此DLL会被映射到两个进程的地址空间,在进程A,进程B的使用计数均为1。若进程B执行FreeLibrary,该DLL在进程B使用计数为0,从进程B地址空间撤销对该DLL的映射。但对进程A无映像,进程A
中该DLL使用计数仍为1。

// 返回载入到进程的模块句柄
HMODULE WINAPI GetModuleHandle(
// 模块名
  _In_opt_ LPCTSTR lpModuleName
);

// 由模块句柄得到模块全路径
DWORD WINAPI GetModuleFileName(
  _In_opt_ HMODULE hModule,
  _Out_    LPTSTR  lpFilename,
  _In_     DWORD   nSize
);

-其它
当我们用
LOAD_LIBRARY_AS_DATAFILE,
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE ,
LOAD_LIBRARY_AS_IMAGE_RESOURCE
标志调用LoadLibraryEx时,系统会先检测该DLL是否已被LoadLibrary或LoadLibraryEx(但没使用这些标志)载入过。如果已被载入过,那么函数返回DLL已被映射到的地址。如果尚未载入,windows会将该DLL载入到地址空间中一个可用的地址,但并不认为它是一个完全载入的DLL。这时如用此模块句柄调用GetModuleFileName,返回0。这是一中让我们知道一个DLL模块不含动态函数的方法,无法通过GetProcAddress来得到函数地址并调用函数。
对同一个DLL,前后分别用上述3个标志载入,DLL会被载入3次,映射到三个地址空间部分。

-显示地链接到导出符号

FARPROC WINAPI GetProcAddress(
  _In_ HMODULE hModule,
 // 符号名字符串
  _In_ LPCSTR  lpProcName
);

// 用法举例:
void DynamicDumpModule(HMODULE hModule);
typedef void (CALLBACK *PFN_DUMPMODULE)(HMODULE hModule);

PFN_DUMPMODULE pfnDumpModule = (PFN_DUMPMODULE)GetProcAddress(hDll, "DumpModule");
if(pfnDumpModule != NULL)
{
    pfnDumpModule(hDll);
}

-DLL的入口点函数

// HINSTANCE hInstDll 表示DLL文件映像被映射到进程地址空间中的虚拟地址
BOOL WINAPI DllMain(HINSTANCE hInstDll,
// 调用入口点函数的原因
DWORD fdwReason,
// DLL隐式载入 不为0
// DLL显示载入 为0
PVOID fImpLoad)
{
switch(fdwReason)
{
// 第一次将一个DLL映射到进程的地址空间时。之后再载入此DLL时,仅仅递增其使用计数。
// 处理时,成功,返回TRUE。失败,返回FALSE。其它通知消息,系统会忽略返回值。
case DLL_PROCESS_ATTACH:
    break;
case DLL_THREAD_ATTACH:
    break;
case DLL_THREAD_DETACH:
    break;
case DLL_PROCESS_DETACH:
    break;
}
return TRUE;
}

1.DLL_PROCESS_ATTACH
创建新进程时,
系统分配地址空间,并将EXE和DLL映射到进程地址空间。
创建进程主线程,用此线程调用每个DLL的DllMain,传入DLL_PROCESS_ATTACH。
所有已映射的DLL完成对通知处理后,主线程开始执行可执行模块的C/C++运行时启动代码。
执行可执行模块的入口点函数。
任何一个DLL的DllMain返回FALSE,系统会把所有的文件映像从地址空间清除,终止进程。

显示载入DLL时,
调用LoadLibrary(Ex)时,系统对指定的DLL定位,将该DLL映射到进程的地址空间。
系统用调用LoadLibrary(Ex)的线程来调用DLL的DllMain,传入DLL_PROCESS_ATTACH。
DllMain处理完毕,LoadLibrary(Ex)返回。
如DllMain返回FALSE,则,系统会自动从进程的地址空间撤销对DLL文件映像的映射,让LoadLibrary(Ex)返回NULL。

2.DLL_PROCESS_DETACH
将一个DLL从进程的地址空间撤销映射时,会调用DLL的DllMain,传入DLL_PROCESS_DETACH。
DLL_PROCESS_ATTACH返回FALSE,导致的撤销映射,不会调用DLL_PROCESS_DETACH。

撤销映射的原因是进程终止,则,调用ExitProcess的线程负责执行DllMain的代码。
撤销映射的原因是进程中的一个线程调用了FreeLibrary或FreeLibraryAndExitThread,则,发出调用的线程将执行DllMain函数的代码。
TerminateProcess导致的进程终止,不会调用DllMain。

3.DLL_THREAD_ATTACH
进程创建线程时,检查映射到进程地址空间的所有DLL文件映像,用DLL_THREAD_ATTACH调用每个DLL的DllMain。
新创建线程负责执行所有DLL的DllMain,所有DllMain处理完通知DLL_THREAD_ATTCH,线程继续执行。

主线程创建时,不需要。

4.DLL_THREAD_DETACH
ExitThread让线程终止,让即将终止的线程用DLL_THREAD_DETACH调用所有已映射DLL的DllMain,再终止。
要终止的线程,用DLL_THREAD_DETACH来调用所有已映射DLL的DllMain。
C/C++运行库这时会释放那些用来管理多线程应用程序的数据块。

TerminateThread导致的线程终止,不会调用DllMain。

-DllMain的序列化调用
例:
进程有线程A,线程B。
进程地址空间映射了SomeDLL.dll。
线程A和B都准备用CreateThread创建线程C,线程D。
序列化,就是多线程同时调用DllMain时,线程1执行完毕,才会让线程2执行。
线程1执行中,线程2如需要执行DllMain,则,挂起等待。

系统创建进程时,会同时创建一个锁。
进程中的线程调用映射到进程地址空间的DLL的DllMain时,用这个锁来同步各个线程。
程序调用CreateThread时,先创建线程内核对象和线程栈。
系统再内部调用WaitForSingleObject。
新线程得到互斥量所有权后,系统让新线程用DLL_THREAD_ATTACH调用每个DLL的DllMain。
这时,系统让线程放弃对进程互斥量所有权。
故,为避免死锁,不要在DllMain调用创建线程函数。

-DllMain和C/C++运行库
链接DLL时,链接器会将DLL的入口点函数的地址嵌入到生成的DLL文件映像中。
可用链接器的/ENTRY指定入口点函数地址。

如用Microsoft链接器并指定/DLL,链接器认为入口点函数函数名为:_DllMainCRTStartup。这个函数包含在C/C++运行库中,链接DLL时,会被静态地链接到DLL的文件映像。即使用C/C++运行库的DLL版本,对这个函数的链接仍然是静态的。

系统将DLL的文件映像映射到进程的地址空间时,实际调用的是_DllMainCRTStartup,而不是DllMain。
将所有通知转发到__DllMainCRTStartup前,
为支持/GS开关提供的安全性特性,_DllMainCRTStartup会对DLL_PROCESS_ATTCH处理。
__DllMainCRTStartup会初始化C/C++运行库,确保_DllMainCRTStartup收到DLL_PROCESS_ATTACH时,
所有全局或静态的C++对象都已构造完毕。
C/C++运行时,初始化完成后,__DllMainCRTStartup会调用我们的DllMain。

DLL收到DLL_PROCESS_DETACH通知时,系统再次调用__DllMainCRTStartup。
这次,该函数会调用我们的DllMain。
DllMain返回时,__DllMainCRTStartup会调用DLL中所有全局或静态C++对象的析构函数。

收到DLL_THREAD_ATTACH或DLL_THREAD_DETACH通知时,__DllMainCRTStartup不会做任何特俗处理。

如DLL没有提供DllMain,则,会使用C/C++运行库提供的。

// 大致如下:
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad)
{
if(fdwReason == DLL_PROCESS_ATTACH)
{
    DisableThreadLibraryCalls(hInstDll);
}
return TRUE;
}

链接DLL时,如链接器无法在DLL的.obj文件中找到一个名为DllMain的函数,则,它会链接到C/C++运行库的DllMain。

-延迟载入DLL
一个延迟载入的DLL是隐式链接的,一开始不将该DLL载入。只在代码试图引用DLL包含的一个符号时,才载入。

1.延迟载入DLL局限
一个导出了字段(全局变量)的DLL无法延迟载入。
Kernel32.dll模块无法延迟载入。
不应在DllMain中调用一个延迟载入的函数。

2.延迟载入
创建一个DLL。
创建一个可执行文件。

链接可执行文件时,须修改一些链接器开关。
项目属性中设置:
2.1./Lib:DelayImp.lib
将指定的函数__delayLoadHelper2嵌入我们的可执行文件。

#pragma comment(lib, "Delayimp.lib")

2..2./DelayLoad:MyDll.dll
将MyDll.dll从可执行模块导入段去除。进程初始化时,系统加载程序就不会隐式载入该DLL。
在可执行模块中嵌入新的延迟载入段,表示从MyDll.dll中导入那些函数。
让对延迟载入函数的调用转到__delayLoadHelper2,完成对延迟载入函数的解析。

应用运行时,对延迟载入函数调用会调用__delayLoadHelper2,这个函数会引用特殊的延迟载入段,并调用LoadLibrary和GetProcAddress,得到对应的延迟载入函数地址后,__delayLoadHelper2会修复对该函数的调用。
同一个DLL中的其它函数仍须在第一次被调用时修复。

延迟载入函数被调用时,加载程序找不到DLL,会抛出异常。找到,但里面没有引用函数,也抛出异常。
软件异常码:
没找到DLL
VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND)
没找到引用函数
VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND)

// __delayLoadHelper2分配和初始化
typedef struct DelayLoadInfo
{
// 版本控制
DWORD cb;
// 嵌入在模块中的延迟载入段。
// 包含延迟载入DLL和函数列表
PCImgDelayDescr pidd;
// 查找成功时,保存函数地址
FARPROC* ppfn;
// DLL名字
LPCSTR szDll;
// 要找的函数信息
DelayLoadProc dlp;
// DLL被载入处虚拟内存地址
HMODULE hmodCur;
// 异常处理中此值应为NULL,因为此时,找不到引用的函数,故函数地址会是NULL
FARPROC pfnCur;
// 引发异常的错误
DWORD dwLastError;
}
DelayLoadInfo;

可执行模块属性中Linker里设置 延迟载入DLL。

-卸载一个延迟载入的DLL
须在构建可执行文件时,指定一个额外的链接器开关。(/Delay:unload)
须修改源代码,在想卸载DLL的地方调用__FUnloadDelayLoadedDLL2。

BOOL __FUnloadDelayLoadedDLL2(PCSTR szDll);

链接器开关/Delay:unload告诉链接器在文件中嵌入另一个段。
这个段包含必要的信息,来重置我们已经调用过的函数。
程序调用这些函数时,再次调用__delayLoadHelper2。
调用__FUnloadDelayLoadedDLL2时,传入要卸载的延迟载入DLL名字。
函数会引用文件的卸载段,将DLL所有函数地址重置,再调用FreeLibrary卸载该DLL。

调用__FUnloadDelayLoadedDLL2时,传入的DLL名字不应包含路径,名字中字母的大小写须和传给/DelayLoad链接器开关的DLL名字相同。
在一个模块中调用__FUnloadDelayLoadedDLL2,但模块构建时没用/Delay:unload,函数会失败并返回FALSE。

-得到通知&覆盖默认行为
1.编写挂钩函数。
2.将全局变量__pfnDliNotifyHook2或__pfnDliFailureHook2设为挂钩函数地址。
一个报告通知,一个报告失败。
PfnDliHook __pfnDliNotifyHook2 = DliHook;
PfnDliHook __pfnDliFailureHook2 = DliHook;

-函数转发器
函数转发器是DLL输出段中的一个条目,将一个函数调用转发到另一个DLL中的另一个函数。

C:\Windows\System32>DumpBin -Exports Kernel32.dll
...
CloseThreadpoolIo (forwarded to NTDLL.TpReleaseIoCompletion)
...

在自己的DLL模块中使用函数转发器

#pragma comment(linker, "/export:SomeFunc=DllWork.SomeOtherFunc")

-已知的DLL
系统在载入已知的DLL时总是在同一个目录中查找。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
注册表项:
Name Type Data
例:
user23 REG_SZ user32

LoadLibrary(Ex)被调用时,函数首先检查传入的DLL的名字是否包含了.dll扩展名。
如没包含,用正常搜索规则,搜索。
如包含,去掉扩展名,在KnownDLLs注册表项中搜索。搜索失败,用正常搜索规则搜索。
如找到匹配的项(名称匹配),试图载入数据内容。
试图载入数据对应DLL时,
先从注册表项DllDirectory执行目录搜索。默认为%SystemRoot%\System32。
如在此目录找不到,LoadLibrary(Ex)失败并返回NULL。

-DLL重定向
建议:
将应用的文件放到自己的目录,不修改windows系统目录的东西。
DLL重定向,强制系统加载程序先从应用目录载入模块,无法找到时,才在其它目录查找。

做法:
1.将一个名字为:AppName.exe.local的文件放到应用目录。
除了创建一个.local文件,还可创建一个名为.local的文件夹。此时,可将自己的DLL保存在这个文件夹中。

// 要打开此特性
HKLM\Software\Microsoft\WindowsNT\CurrentVersion\Image File Execution Options\
加一项:
DWORD DevOverrideEnable

-模块的基地址重定位
每个可执行文件和DLL模块有一个首选基地址。
构建可执行模块时,默认下,链接器将模块基地址设为0x00400000。对DLL,默认的首选基地址为0x10000000。

如果一个可执行文件或DLL模块,不能被映射到其首选基地址,对其进行重定位耗时。
编译和链接生成DLL或EXE时,机器码中地址都按首选基地址进行了确定。

链接器在构建我们的模块时,会将重定位段嵌入到生成的文件中。
这个段包含一个字节偏移量的列表,每个字节偏移量表示一条机器指令所使用的一个内存地址。
加载程序将模块载入到首选基地址时,系统不会访问模块的重定位段。
加载程序无法将模块载入首选基地址时,系统会打开模块的重定位段,遍历其中条目,对每个条目,加载程序找到包含机器指令的那个存储页面,将模块首选基地址和模块实际映射地址的差值,加到机器指令当前使用的内存地址上。
此时,
加载程序,遍历重定位段,并修改模块大量代码,性能损耗。
修改模块代码页时,写时复制会强制这些页面以系统页交换文件为后备存储器。

构建模块时,用/FIXED使模块不包含重定位段。此时模块无法被重定位。
链接器允许我们在创建模块时,在文件头嵌入一些信息,来表示模块之所以不包含重定位信息,是因为没必要(如资源DLL)。此时,即使无法被重定位也不会报错终止。

创建一个不包含重定位信息的映像,用/SUBSYSTEM:WINDOWS, 5.0或/SUBSYSTEM:CONSOLE, 5.0。

-载入大量模块时,确定所有模块基地址的方法
1.Rebase
执行Rebase工具时,传给它一组映像文件名,它会:
a.模拟创建一个进程地址空间
b.打开应被载入到这个地址空间的所有模块,得到每个模块大小和首选基地址
c.在模拟的地址空间,对模块重定位进行模拟,使各模块没有交叠。
d.对重定位过的模块,解析该模块的重定位段,修改模块在磁盘文件中的代码
e.更新每个重定位过模块的模块头

等应用所有模块已经构建完成后,运行rebase。
运行后,可以忽略project properitys对话框的基地址,rebase会覆盖它。

不需要对随操作系统一起发布的模块重定位。

2.

BOOL ReBaseImage(
  _In_    PCSTR     CurrentImageName,
  _In_    PCSTR     SymbolPath,
  _In_    BOOL      fReBase,
  _In_    BOOL      fRebaseSysfileOk,
  _In_    BOOL      fGoingDown,
  _In_    ULONG     CheckImageSize,
  _Out_   ULONG     *OldImageSize,
  _Out_   ULONG_PTR *OldImageBase,
  _Out_   ULONG     *NewImageSize,
  _Inout_ ULONG_PTR *NewImageBase,
  _In_    ULONG     TimeStamp
);

-模块的绑定
用法:
1.Bind.exe
在执行Bind工具时,传给它一个映像文件名,它会执行下列操作:
打开映像文件导入段
对导入段的每个DLL,查看该DLL的文件头,确定该DLL的首选基地址
在DLL的导出段查看每个符号
取得符号的RVA,将它与模块的首选基地址相加,将得到的地址写入映像文件的导入段。
在映像文件的导入段添加一些额外的信息,包括映像文件被绑定到的各DLL模块的名称,各模块的时间戳。

正确的假设前提:
DLL会被载入其首选基地址。
DLL导出段所引用符号的相对位置没变化。(通过查看时间戳判断)

2.

BOOL BindImageEx(
  _In_ DWORD                    Flags,
  _In_ PSTR                     ImageName,
  _In_ PSTR                     DllPath,
  _In_ PSTR                     SymbolPath,
  _In_ PIMAGEHLP_STATUS_ROUTINE StatusRoutine
);

BOOL StatusRoutine(
  _In_ IMAGEHLP_STATUS_REASON Reason,
  _In_ PSTR                   ImageName,
  _In_ PSTR                   DllName,
  _In_ ULONG_PTR              Va,
  _In_ ULONG_PTR              Parameter
);

// DumpBin查看模块信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

raindayinrain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值