第20章 DLL高级技术(1)
20.1 DLL模块的显式载入和符号链接
20.1.1 显式载入DLL模块
(1)构建DLL时,如果至少导出一个函数/变量,那么链接器会同时生成一个.lib文件,但这个文件只是在隐式链接DLL时使用(显示链接时并没有用到这文件)
(2)显式载入DLL的函数:LoadLibrary(Ex)
参数 |
含义 |
pCTSTR pszDllPathName |
LoadLibrary只有这个参数。函数会根据第19章介绍的搜索算法在用户的计算机中对DLL文件进行定位,并映射到进程的地址空间。 |
HANDLE hFile |
该参数为将来扩充所保留的,这里必须为NULL |
DWORD dwFlags |
可为0或下列标志的组合 ①DONT_RESOLVE_DLL_REFERENCES:只将该DLL映射到进程地址空间,但不调用DllMain函数及不检查该DLL导入段中的其他额外DLL,这也意味着不自动载入额外的DLL。(一般应避免使用该标志,因为代码所依赖的DLL可能尚未被载入!) ②LOAD_LIBRARY_AS_DATAFILE:将DLL作为数据文件映射到进程。(一般用在一个DLL只包含资源而没有函数时或想用一个EXE文件中包含的资源时可用这个标志,而且载入EXE时必须使用这个标志) ③LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE:与②标志相似,唯一不同的是以独占方式来打开这个DLL文件,以防止其他程序对其修改。 ④LOAD_LIBRARY_AS_IMAGE_RESOURCE:与②标志相似,但不同之处在于当系统载入DLL的时候,会对相对虚拟地址(RVA)进行修复。这样RVA就可以直接使用,而不必再根据DLL载入的内存地址来转换了。(当需要对DLL进行遍历其PE段时,这个标志特别有用) ⑤LOAD_IGNORE_CODE_AUTHZ_LEVEL:用来关闭UAC对代码在执行过程中可以拥有的特权加以控制。 ⑥LOAD_WITH_ALTERED_SEARCH_PATH:用来改变LoadLibrary对DLL文件进行定位所使用的搜索算法。 A、如果pszDllPathName不包含“\”字符,会使用标准搜索路径算法 B、如果pszDllPathName包含“\”会因全路径(网络共享路径)或相对路径而有所不同。(见课本P557) C、可以调用SetDllDirectory改变搜索算法,搜索指定的目录路径。(具体顺序为:EXE所在目录→SetDlldirectory设置的文件夹→Windows系统目录→16位Windows系统目录→Windows目录→PATH列出的目录。 |
返回值 |
HMODULE类型,等价于HINSTANCE,表示DLL被映射到的虚拟内存地址。当返回NULL,表示映射失败,可进一步调用GetLastError |
(3)混用LoadLibrary和LoadLibraryEx加载同一个DLL可能带来的问题
【情况1】:不会出现问题,此时hDll1=hDll2=hDll3
HMODULE hDll1 = LoadLibrary(TEXT("MyLibrary.dll")); HMODULE hDll2 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_IMAGE_RESOURCE); HMODULE hDll3 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_DATAFILE);
【情况2】:将上面的调用顺序改变一下,则hDll1≠hDll2≠hDll3,说明DLL被多次映射到进程的地址空间。
HMODULE hDll1 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_DATAFILE); HMODULE hDll2 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_IMAGE_RESOURCE); HMODULE hDll3 = LoadLibrary(TEXT("MyLibrary.dll"));
【分析原因】当LoadLibraryEx时(使用LOAD_LIBRARY_AS_DATAFILE, LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE, or LOAD_LIBRARY_AS_IMAGE_RESOURCE标志),系统会检测该DLL是否被LoadLibrary(或未使用上述标志LoadLibraryEx)进来,如果己经被载入过,那么函数会返回空间中DLL原先被映射的地址。如果DLL未被载入,那么DLL会将这个DLL载入,但会认为是个未完全载入的DLL,如果这时再载入时会被多次的映射到进程的地址空间,从而产生不同的地址。
20.1.2 显式卸载DLL模块
(1)BOOL FreeLibrary(HMODULE hInstDll);
(2)VOID FreeLibraryAndExitThread(hInstDll,dwExitCode);
①函数的内部实现(在Kernel32.dll中):
VOID FreeLibraryAndExitThread(HMODULE hInstDll,DWORD dwExitCode){ FreeLibrary(hInstDll); ExitThread(dwExitCode);//调用该行指令在kernel32.dll
}
②为什么需要FreeLibraryAndExitThread函数?
A、假设在A这个DLL里创建一个线程,当该线程完成一些工作后,先调用FreeLibrary再调ExitThread来撤销对DLL的映射并终止线程,这里会出现一个严重的问题。因为FreeLibrary会立即从进程的地址空间撤销对DLL的映射。当FreeLibrary返回时,线程会试图调用ExitThread,而这行代码本来在DLL里的,这个DLL己经不存在了,这时线程会试图执行不存在的代码,将引发访问违规,并导致整个进程被终止。
B、但如果调用FreeLibraryAndExitThread,由于函数内部会调用FreeLibrary和ExitThread,而这两个函数是在Kernel32.dll的内部调用(而不是A这个DLL)。所以当撤销了A这个DLL后,这个线程可以继续执行ExitThread,只不过当ExitThread时线程不再返回A这个DLL里了,所以也不会出错。
(3)DLL的使用计数问题
①当LoadLibrary(Ex)时使用计数递增,第一个Load时使用计数为1.如果同一个进程的一个线程再调用LoadLibrary时,系统不会再次进行映射而是将使用计数递增。
②FreeLibrary或FreeLibraryAndExitThread使计数递减,但计数递减到0时,系统会这个DLL从进程的地址空间中撤销映射。
③系统为每个进程的每个DLL维护一个使用计数。如进程A和B都加载了MyLib.dll,那么这个DLL会被映射进两个进程的地址空间,但该DLL在进程A和B中的使用计数都是1。如果后来进程B的一个线程再次LoadLibrary这个DLL,则进程B中这个DLL的使用计数为2,但进程A中仍为1。
(4)检测DLL的两个函数
①检测DLL是否被映射:HMODULE GetModuleHandle(PCTSTR pszModuleName);
A、返回NULL时表示未被映射。
B、如果传NULL参数时,会返回应用程序的EXE文件的句柄。
②获得DLl/EXE的全路径名:GetModuleFileName(hInstModule,pszPathName,cchPath);其中的hInstModule为DLL或EXE的句柄。
20.1.3 显示链接到导出符号——获得函数地址:GetProcAddress
参数 |
描述 |
HMODULE hInstDll |
DLL的句柄,即先前调用LoadLibrary(Ex)或GetModuleHandle时的返回值。 |
PCSTR pszSymbolName |
函数的名称或序号,注意这里的类型是PCSTR,而不是PCTSTR,说明这个函数只接受ANSI的字符串。 |
返回值 |
FARPROC,要获得的函数的地址,须转为该函数原型的指针。一般用typedef来声明要获取的函数原型的指针 |
20.2 DLL的入口点函数
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) { switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// DLL第1次被映射到进程的地址空间时
break; case DLL_THREAD_ATTACH:
// 创建一个线程时
break; case DLL_THREAD_DETACH:
// 线程终止时
break; case DLL_PROCESS_DETACH:
// 进程撤销一个DLL映射时
break;
} return(TRUE); // 该返回值只在 DLL_PROCESS_ATTACH通知时有用,用来表示DLL的初始化是否成功,如果return FALSE表示加载DLL失败,如果系统会终止整个进程或撤销对该DLL的映射。其他通知时,系统将忽略这个返回值。
}
20.2.1 DLL_PROCESS_ATTACH通知
(1)只有当DLL第1次被映射到进程地址空间时,才会发送该通知。如果以后一个线程再调用LoadLibrary(Ex)来载入这个DLL,则只会递增该DLL的使用计数,但不会再发该通知。
(2)在该通知里,一般用来执行与进程相关的初始化,如创建DLL中一般函数要使用的堆。
(3)当DllMain处理DLL_PROCESS_ATTACH通知时,返回值用来表示DLL的初始化是否成功。如果return FALSE表示加载DLL失败,系统会终止整个进程(这种情况发生在刚创建进程时)或撤销对该DLL的映射(这种情况发生在显式调用LoadLibrary(Ex)时)。
(4)这个通知由进程中的某个线程来调用的。如果是刚创建新的进程时,则由主线程调用。如果某个线程调用LoadLibrary第1次显式载入这个DLL时,则由这个线程来调用执行这个通知,然后线程继续正常执行其他任务,如果return FALSE表示初始化失败,系统会撤销对DLL的映射并让LoadLibrary(Ex)返回NULL。
20.2.2 DLL_PROCESS_DETACH通知
(1)当系统将一个DLL从进程的地址空间中撤销映射时,发送该通知。(注意:在处理DLL_PROCESS_ATTACH时如果的返回FALSE时,那么就不会收到DLL_PROCESS_DETACH通知。)
(2)如果在处理DLL_PROCESS_ATTACH时返回FALSE,则DllMain就不会收到DLL_PROCESS_DETACH通知。
(3)如果是因调用ExitProcess而导致撤销DLL映射,则调用ExitProcess函数的线程将负责执行DllMain函数的代码。
(4)如果是因线程调用了FreeLibrary(或FreeLibraryAndExitThread)而撤销Dll映射,该线程将执行DllMain函数中的代码。该线程会直到处理完DLL_PROCESS_DETACH完才从FreeLibrary中返回。因此,如果该通知时死循环,则阻碍线程的终止,只有当每个DLL都处理完该通知后,操作系统才会真正地终止进程。
(5)如果某个线程调用TerminateProcess来终止进程,则系统不会发送DLL_PORCESS_DETACH通知。这意味着Dll没有机会执行一些清理代码的操作。因此,不到万不得己,应避免使用TerminateProcess函数。
20.2.3 DLL_THREAD_ATTACH通知
(1)当进程创建一个线程时,系统会向当前映射到该进程地址空间中的所有DLL发送DLL_THREAD_ATTACH通知。告诉这些DLL执行一些与线程相关的初始化。新创建的线程负责执行所有DLL中DllMain函数中相关的代码。只有当所有DLL完成了对该通知的处理后,新线程才会开始执行它的线程函数。
(2)当一个新的DLL映射到进程地址空间时,进程中己经有的线程不会是不会收到DLL_THREAD_ATTACH通知的。(即只有在创建新线程时,己经被映射到进程地址空间中的DLL才会收到这个通知)
(3)因创建进程时,何任被映射到进程地址空间中的DLL都会收到DLL_PROCESS_ATTACH通知,并由主线程负责执行,这里就可以执行一些相关的初始化工作,所以系统不会让主线程用DLL_THREAD_ATTACH来调用DllMain函数(即主线程只接收DLL_PROCESS_ATTACH通知,而不接收DLL_THREAD_ATTACH通知)。
20.2.4 DLL_THREAD_DETACH通知
(1)当线程函数返回后,系统会调用ExitThread来终止线程,但在终止前,这个线程会用DLL_THREAD_DETACH去调用所有己映射DLL的DllMain函数。告诉DLL执行与线程相关的清理操作(如C/C++运行库在这里可释放多线程应用程序的数据块)。
(2)如果该通知里有死循环,将妨碍线程的终止。只有当每个DLL都处理完DLL_THREAD_DETACH通知后,操作系统才会真正的终止线程。
(3)如果某个线程调用了TerminateThread来终止线程,那会系统将不会发送DLL_THREAD_DETACH通知给线程。这意味着DLL没有机会执行任何清理操作。
(4)如果在撤销一个DLL映射时,还有其他线程(正在运行),系统不会发送DLL_THREAD_DETACH给这些线程。(即这些线程不会用DLL_THREAD_DETACH来调用这个DLL的DllMain)
【注意】上面的规则可能会出现一个情况:当进程中的一个线程调用LoadLibrary来载入一个DLL时,系统会用DLL_PROCESS_ATTACH来调用该DLL的DllMain(但该线程不会得到DLL_THREAD_ATTACH通知)。接着,这个载入DLL的线程退出,这时该线程会收到DLL_THREAD_DETACH通知。由于这个原因,当进行与线程相关的清理里必须极为小心。一般调用LoadLibrary与调用FreeLibrary的线程应该是同一个线程。
20.2.5 DllMain的序列化调用
(1)系统会将对DLL的DllMain函数的调用序列化。在创建进程的时候,会同时创建一个锁(关键段,不同进程不会共享这个锁,这是关键段的特点!)
(2)当进程中的线程调用己映射的DLL的DllMain时,会用这个锁来同步各个线程(即不同线程不能同时执行DllMain函数中的代码,这种访问会被串行化)。
(3)禁止系统向某个DLL发送DLL_THREAD_ATTACH和DLL_THREAD_DETACH的方法
BOOL DisableThreadLibraryCall(HMOUDLE hInstDll);//可写在DLL_PROCESS_ATTACH通知中
【DllMain的错误调用】演示DllMain序列化调用导致的死锁问题
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason,PVOID fImpload){
HANDLE hThread;
DWORD dwThreadId;
switch (fdwReason)
{
case DLL_PROCESS_ATTACH: //DLL被映射到进程地址空间 //创建线程
hThread = CreateThread(NULL, , SomeFunction, NULL, , &dwThreadId); //将我们的线程(即调用LoadLibrary函数的线程,设为A)挂起,直到新的线程结束
WaitForSingleObject(hThread, INFINITE);//等待新线程结束
//注意
//1、当新线程(设为B)被创建时,系统会让B线程用DLL_THREAD_ATTACH去调用所有己经映射
//到进程的Dll(包括本Dll的DllMain),当执行本DllMain时,由于系统会用关键段去同步各个
//线程对DllMain调用(即DllMain调用的序列化),这时新线程B会被挂起去等待A线程执行完
//DllMain,但此时的A线程由于调用了WaitForSingleObject而挂起去等待B线程执行完毕,
//这就形成了“死锁”。
//2、即使在该DLL里调用DisableThreadLibraryCall来禁止线程执行这个DLL里的
//DLL_THREAD_ATTACH也会发生这种死锁。因为在调用CreateThread函数时,这个函数内部
//会调用WaitForSingleObject,并传入进程的这个关键段,只有当新线程拥有这个关键段时
//才会用DLL_THREAD_ATTACH来调用每个DLL里的DllMain函数。所以在CreateThread新线程时
//这个新线程会因得不到关键段(因为被A线程拥有)而挂起。但A线程执行到WaitFor*的那行
//代码里,会挂起自己,所以仍然会造成死锁。 //不再需要新的线程了
CloseHandle(hThread);
break; case DLL_THREAD_ATTACH: //线程被创建时
break; case DLL_THREAD_DETACH: //线程退出时
break; case DLL_PROCESS_DETACH://撤销DLL映射时
break;
} return TRUE;
}
20.2.6 DllMain和C/C++运行库
(1)用VC++编译器来构建DLL时,链接器会将DllMain函数调用在__DllMainCRTStartup函数里(也就是说,__DllMainCRTStarup才是Dll真正的入口函数!)
(2)在入口函数里会初始化C/C++运行库,并初始化所有全局或静态C++对象,保证收到在DLL_PROCESS_ATTACH通知之前,所有的全局的C++类实例的构造函数己经被调用。
(3)当Dll收到DLL_PROCESS_DETACH通知时,系统会再次调用这个入口函数,并进一步调用DllMain,当DllMain执行完后,__DllMainCRTStartup会调用Dll中所有全局或静态C++对象的析构函数。
(4)当接收DLL_THREAD_ATTACH或DLL_THREAD_DETACH时,__DllMainCRTStartup不会做任何的特殊处理。
(5)Dll源文件中的DllMain函数并不是必需的。如果在链接DLL时,链接器无法找到Dll源文件中(准确讲,应该是.obj里)的DllMain函数时,它会链接到C/C++运行库提供的DllMain函数。但C/C++运行库里的DllMain默认会处理成让该Dll不接收DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知(可以从C/C++运行库提供的DllMain函数的实现中看到这一点,代码如下)
BOOL WINAPI DllMain(HINSTANCE hInstDll,DWORD fdwReason,PVOID fImpload){ if(fdwReason == DLL_PROCESS_ATTACH)
DisableThreadLibraryCalls(hInstDll); return (TRUE);
}
第20章 DLL高级技术(1)的更多相关文章
- 第20章 DLL高级技术(3)
20.4 函数转发器 (1)函数转发器原理(下图是利用Dependency Walker打开Kernel32.dll得到) ①图中CloseThreadpool*等4个函数转发到NTDLL中相应的函数 ...
- 第20章 DLL高级技术(2)
20.3 延迟载入DLL 20.3.1延迟载入的目的 (1)如果应用程序使用了多个DLL,那么它的初始化可能比慢,因为加载程序要将所有必需的DLL映射到进程的地址空间.→利用延迟加载可将载入过程延伸到 ...
- 《Windows核心编程系列》二十谈谈DLL高级技术
本篇文章将介绍DLL显式链接的过程和模块基地址重定位及模块绑定的技术. 第一种将DLL映射到进程地址空间的方式是直接在源代码中引用DLL中所包含的函数或是变量,DLL在程序运行后由加载程序隐式的载入, ...
- 第7章—SpringMVC高级技术—不用web.xml,而使用java类配置SpringMVC
不用web.xml,而使用java类配置SpringMVC DispatcherServlet是Spring MVC的核心,按照传统方式, 需要把它配置到web.xml中. 我个人比较不喜欢XML配置 ...
- 第7章—SpringMVC高级技术—处理异常
处理异常 处理异常 不管发生什么事情,不管是好的还是坏的,Servlet请求的输出都是一个Servlet响应.如果在请求处理的时候,出现了异常,那它的输出依然会是Servlet响应.异常必须要以某种方 ...
- 第7章—SpringMVC高级技术—处理multipart形式的数据
处理multipart形式的数据 MultipartResolver 用于处理文件上传,当收到请求时 DispatcherServlet 的 checkMultipart() 方法会调用 Multip ...
- 第07章-Spring MVC 的高级技术
Spring MVC 的高级技术 1. Spring MVC配置的替代方案 1.1 自定义DispatcherServlet配置 AbstractAnnotationConfigDispatcherS ...
- 【ASP.NET Identity系列教程(三)】Identity高级技术
注:本文是[ASP.NET Identity系列教程]的第三篇.本系列教程详细.完整.深入地介绍了微软的ASP.NET Identity技术,描述了如何运用ASP.NET Identity实现应用程序 ...
- ASP.NET Identity系列教程-4【Identity高级技术】
https://www.cnblogs.com/r01cn/p/5194257.html 15 ASP.NET Identity高级技术 In this chapter, I finish my de ...
随机推荐
- smartstore-net
记录一下,抽空下载源码了研究下
- Orchard中文版源码下载
本版本基于Orchard1.7.2修改: 新增Bootstrap主题 新增中文语言包 增加了对Sqlite.Orchard数据库的支持 优化工程,减少临时符号生成,增加工程效率 和一些BUG的修正 默 ...
- [ASP.NET MVC] Model Binding With NameValueCollectionValueProvider
[ASP.NET MVC] Model Binding With NameValueCollectionValueProvider 范例下载 范例程序代码:点此下载 问题情景 一般Web网站,都是以H ...
- JSDoc那些事
几天工作上需要文档化一些Javascript东西,所以在找一些JS文档化工具,以下分析几种工具. 1.JSDoc-toolkit 一开始还想用这个工具,但后来在解析生成文档时候,出现了很严重的错误,还 ...
- Vue条件渲染
gitHub地址:https://github.com/lily1010/vue_learn/tree/master/lesson08 一 v-if显示单个元素 注意else只能跟在v-if或者v-s ...
- 配置windows失败,还原更新,请勿关机
同事叫我帮忙弄一下电脑,开机,出现"配置Windows Update失败,还原更改,请勿关闭计算机",我从来不更新Windows Update,更新都为成功,第一次遇到失败了,不知 ...
- sharepoint 顺序工作流创建
顺序工作流提供了一系列有组织的步骤,一般情况下,步骤是逐一执行的. 1.新建 > 项目,选择 SharePoint解决方案 > 空项目: 2.部署为场解决方案 3.添加 > 新项,选 ...
- Sharepoint学习笔记—习题系列--70-573习题解析 -(Q35-Q39)
Question 35You have a custom Web Part that is deployed as a sandboxed solution.You need to ensure th ...
- ListView属性整理
stackFromBottom属性,这只该属性之后你做好的列表就会显示你列表的最下面,值为true和false android:stackFromBottom="true" 第 ...
- iOS 在制作framework时候对aggregate的配置
# Sets the target folders and the final framework product.# 如果工程名称和Framework的Target名称不一样的话,要自定义FMKNA ...