Loading... # Windows核心编程-学习笔记11 ## DLL基础 Windows API提供的所有函数都包含在DLL中 * Kernel32.dll:包含用来管理内存、进程和线程的函数 * User32.dll:包含用来执行与用户界面相关的任务,如窗口创建、发送消息等函数 * GDI32.dll:包含用来绘图和显示文本的函数 * 其他:AdvAPI32.dll包含的函数与对象的安全性、注册表的操控和事件日志有关;ComDlg32.dll包含一些常用的对话框(如打开文件和保存文件);ComCtl32.dll支持所有常用的窗口控件 ### DLL和进程的地址空间 创建DLL必须为链接器指定/DLL开关,这样生成的文件映像才能被识别为DLL。在DLL函数能被调用之前,DLL的文件映像必须映射到调用进程的地址空间中 * 隐式加载时链接 * 显式运行时链接 一旦系统将DLL的文件映像映射到调用进程的地址空间,进程中的所有线程就可以调用该DLL中的函数。线程调用DLL中的一个函数时,该DLL函数会在线程栈中取得传给它的参数,并使用线程栈来存放它需要的局部变量。此外,由DLL中的函数创建的任何对象都为调用线程或调用进程所拥有,DLL绝对不会拥有任何对象。 ### 生成DLL 可执行模块:一个从DLL中导入函数和变量的模块 DLL模块:导出函数和变量供可执行文件使用的模块 如果一个可执行模块需要从一个DLL模块导入函数和变量,必须首先生成DLL模块,然后生成可执行模块。 **生成DLL**: 1. 必须创建一个头文件,在其中包含要从DLL导出的函数原型、结构以及符号 2. 创建C/C++源代码模块来实现DLL模块中的函数和变量 3. 生成DLL模块会造成编译器处理每个源代码模块并生成一个.obj模块,每个源文件模块对应一个.obj模块 4. 所有.obj模块都创建完毕后,链接器合并所有.obj模块的内容并生成单一的DLL映像文件。该映像文件包含DLL的所有二进制代码以及全局/静态数据变量 5. 如链接器检测到DLL的源文件至少导出了一个函数或变量,还会生成一个.lib文件(只是列出所有被导出的函数和变量符号名称) 一旦生成了DLL模块,就可以**生成可执行模块**: 1. 在所有引用了函数、变量、数据结构或符号的源代码模块中,必须包含由DLL开发人员创建的头文件 2. 创建C/C++源代码模块来实现想要包含在可执行模块中的函数和变量。当然,代码可以引用在DLL的头文件中定义的函数和变量 3. 生成可执行模块会造成编译器处理每个源代码模块并生成一个.obj模块 4. 所有.obj模块创建完毕后,链接器合并所有.obj模块的内容并生成一个可执行映像文件。该映像文件包含可执行文件的所有二进制代码以及全局变量/静态变量。可执行模块还包含一个**导入段**,其中列出了所有它需要的DLL模块的名称。此外,针对列出的每个DLL名称,该段还记录了可执行文件的二进制代码从中引用的函数和变量的符号名称。一旦DLL和可执行模块都生成完毕就可以执行了 5. 加载程序为新进程创建一个虚拟地址空间,并将可执行模块映射到新进程的地址空间。加载程序接着解析可执行模块的导入段,针对导入段列出的每个DLL名称,加载程序都会在用户的系统中定位该DLL模块,并将该DLL映射到进程的地址空间。注意DLL模块有可能从其他DLL模块导入函数和变量,因此加载程序需要解析每个模块的导入段,将所有要求的DLL都加载到进程的地址空间,所以进程的初始化可能非常耗时 #### 生成DLL模块 创建DLL实际是在创建一组可供某个可执行模块(或其他DLL)调用的函数。DLL可向其他模块导出变量、函数或C++类。 头文件(MyLib.h): ~~~c++ #ifdef MYLIBAPI #else // 表示所有函数变量都要导入 #define MYLIBAPI extern "C" __declspec(dllimport) #endif // 定义数据结构和符号、导出的变量、导出的函数原型 ~~~ 在DLL每个源代码文件(MyLibFile1.cpp)中必须包含上面的头文件 ~~~c++ #include<windows.h> #define MYLIBAPI extern "C" __declspec(dllexport) #include "MyLib.h" // 源代码 ~~~ `extern "C"`用来告诉编译器不要对变量名或函数名进行改编,保证C/C++等任何编程语言编写可执行模块都能访问 **导出的真正含义**: 当Microsoft的C/C++编译器看到`__declspec(dllexport)`修饰的变量、函数原型或C++类时,会在生成的.obj文件中嵌入一些额外的信息,链接DLL时,链接器会检测到这些信息并自动生成一个.lib文件(列出了DLL导出的符号),此外还会在生成的DLL文件中嵌入一个导出符号表(导出段),按字母顺序列出了导出的变量、函数和类的符号名。链接器还会保存相对虚拟地址(RVA),指出每个符号可在DLL模块的什么位置找到。 #### 生成可执行模块 很简单,导入DLL的符号 ~~~c++ #include "MyLib\MyLib.h" ~~~ #### 运行可执行模块 启动一个可执行模块时,操作系统加载程序会先为进程创建虚拟地址空间,接着将可执行模块映射到进程的地址空间。然后,加载程序会检查可执行模块的导入段,尝试定位所需的DLL并将其映射到进程的地址空间中。 由于导入段只包含DLL名称,不包含DLL路径,所以加载程序必须在用户的磁盘上搜索DLL,搜索顺序如下 1. 可执行映像文件所在目录 2. Windows系统目录,由GetSystemDirectory返回 3. 16位系统目录,即Windows目录中的System子目录 4. Windows目录,由GetWindowsDirectory返回 5. 进程的当前目录 6. PATH环境变量中列出的目录 5目录在2目录后原因是防止加载程序在应用程序的当前目录中找到并加载伪造的系统DLL ## DLL高级技术 ### DLL模块的显式加载和符号链接 隐式加载DLL就是上一节的做法,显式加载需要应用程序在运行期间自主决定调用某个DLL的某个函数 #### 显式加载DLL模块 调用函数可以是下面两个 ~~~c++ HMOUDLE LoadLibrary(PCTSTR pszDLLPathName); HMOUDLE LoadLibraryEx(PCTSTR pszDLLPathName, HANDLE hFile, DWORD dwFlags); ~~~ 这两个函数会采用上面搜索DLL的算法定位DLL文件映像,并尝试将该映像映射到调用进程的地址空间,返回值就是文件映像映射到的虚拟内存地址。 LoadLibraryEx有额外两个参数: * hFile:设为NULL,留待未来扩充使用 * dwFlags:可设为0或多种标志的组合 * DONT_RESOLVE_DLL_REFENCES:只需将DLL映射到调用进程的地址空间,不会对额外DLL自动加载进程的地址空间 * LOAD_LIBRARY_AS_DATAFILE:将DLL当做一个数据文件映射到进程地址空间,不分配页面保护属性(比如加载一个exe文件的资源) * LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE:DLL文件以独占访问模式打开,禁止其他任何应用程序在当前应用程序使用该DLL文件时对其进行修改 * LOAD_LIBRARY_AS_IMAGE_RESOURCE:与LOAD_LIBRARY_AS_DATAFILE相似,但使用这个时,系统加载DLL会使用RVA进行修复 * LOAD_WITH_ALTERED_SEARCH_PATH:改变LoadLibraryEx在定位指定DLL时所采用的搜索算法 * 如果pszDLLPathName不包含`\`字符,就用标准搜索路径来定位DLL * 如果pszDLLPathName包含`\`字符,取决于路径是否为完整路径 **完整路径**:直接尝试加载 **不是完整路径**:函数会将下列文件夹与pszDLLPathName连接起来 a. 进程的当前目录 b. Windows系统目录 c. 16位系统目录——Windows目录下的System子目录 d. Windows目录 e. PATH环境变量中列出的目录 如果出现`.`或`..`也会考虑进去拼接一个相对路径 * 如果不想用LOAD_WITH_ALTERED_SEARCH_PATH,可以调用SetDllDirectory,并将库文件夹作为参数传入(传递`""`表示将当前路径从搜索步骤中删除,传递NULL恢复默认算法)。这个函数告诉LoadLibrary和LoadLibraryEx在搜索时使用以下算法: a. 应用程序的当前目录 b. 通过SetDllDirectory设置的目录 c. Windows系统目录 d. 16位Windows系统目录 e. Windows目录 f. PATH环境变量中列出的目录 * LOAD_IGNORE_CODE_AUTHZ_LEVEL:用来关闭WinSafer提供的验证(对代码在执行过程中可以拥有的特权加以控制) #### 显式卸载DLL模块 ~~~c++ BOOL FreeLibrary(HMODULE hInstDll); VOID FreeLibraryAndExitThread(HMODULE hInstDll, DWORD dwExitCode); ~~~ 后者包含了FreeLibrary和ExitThread 每个DLL在进程中都有一个与之对应使用计数,LoadLibrary和LoadLibraryEx函数会递增该使用计数,而FreeLibrary和FreeLibraryAndExitThread会递减该使用计数。注意不同进程使用计数互不影响 线程可以调用GetModuleHandle来检测一个DLL是否已被映射到进程的地址空间,可以检查未映射再LoadLibrary ~~~c++ HMODULE GetModuleHandle(PCTSTR pszModuleName); ~~~ ~~~c++ HMODULE hInstDll = GetModuleHandle(TEXT("MyLib")); if (hInstDll == NULL) { hInstDll = LoadLibrary(TEXT("MyLib")); } ~~~ 如果向GetModuleHandle传递NULL,函数会返回应用程序的可执行文件的句柄 --- 如果只有DLL或exe的HINSTANCE/HMODULE,可使用GetModuleFileName函数获取DLL的完整路径: ~~~c++ DWORD GetModuleFileName(HMODULE hInstModule, PTSTR pszPathName, DWORD cchPath); ~~~ * hInstModule:DLL或exe的HMODULE;如果传递NULL在pszPathName中返回当前正在运行的应用程序的可执行文件的文件名 * pszPathName:一个缓冲区地址,函数会将文件映像的完整路径保存到这个缓冲区中 * cchPath:指定了缓冲区的大小 --- 一旦显式加载了一个DLL模块,线程必须调用GetProcAddress来得到想引用的符号的地址 ~~~c++ FARPROC GetProcAddress(HMODULE hInstDll, PCSTR pszSymbolName); ~~~ * hInstDll:指定了包含符号的那个DLL的句柄,是先前LoadLibrary(Ex)或GetModuleHandle返回的 * pszSymbolName:两种形式,第一种是以0为终止符的字符串(注意传递ANSI字符串,不要带TEXT);第二种是用序号来指定要获得地址的符号,但不常用 **如果DLL不包括指定符号,GetProcAddress返回NULL**,表示调用失败 ### DLL的入口点函数 DLL调用入口点函数主要是用来通知执行一些针对当前进程或线程的初始化和清理工作 ~~~c++ BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) { switch (fdwReason) { case DLL_PROCESS_ATTACH: // DLL映射到进程地址空间 break; case DLL_THREAD_ATTACH: // 创建了一个线程 break; case DLL_THREAD_DETACH: // 线程干净地退出 break; case DLL_PROCESS_DETACH: // DLL从进程地址空间撤销映射 break; } return (TRUE); // 仅用于DLL_PROCESS_ATTACH } ~~~ * hInstDll:类似_tWinMain的hInstExe参数,该值标识了一个虚拟内存地址,DLL文件映像被映射到进程地址空间中的这个位置。通常将该参数保存在一个全局变量中,以便在调用资源加载函数时使用。 * fdwReason:系统调用入口点函数的原因 * fImpLoad:DLL隐式加载时值非0,显式加载值为0 **注意**:避免在DllMain里调用**其他DLL导入的函数**(可能没初始化);避免调用**LoadLibrary(Ex)和FreeLibrary**(可能会产生循环依赖) #### DLL_PROCESS_ATTACH 系统**首次**将一个DLL映射到进程地址空间时会调用DllMain函数,并为fdwReason传递DLL_PROCESS_ATTACH,DllMain主要执行任何与进程相关的初始化操作。 在DllMain处理DLL_PROCESS_ATTACH通知时,DllMain返回值指出该DLL的初始化是否成功,其他fdwReason值时系统会忽略DllMain的返回值 **隐式加载DLL时程序运行-具体步骤**: 1. 系统创建新进程,分配进程地址空间并将exe文件映像以及所需DLL文件映像映射到进程的地址空间中 2. 系统创建进程的主线程,用这个线程来调用每个DLL的DllMain函数并传递DLL_PROCESS_ATTACH 3. 所有已映射的DLL都完成了对该通知的处理后,系统会先让进程的主线程开始执行可执行模块的c/c++运行库,然后执行可执行模块的入口点函数(\_tmain或\_tWinMain)。如果任何一个DLL的DllMain返回FALSE,系统会将所有文件映像从地址空间中清除,向用户显示一个消息框来指出进程无法启动,然后终止整个进程 **显式加载DLL时程序运行-具体步骤**: 1. 进程中的一个线程调用LoadLibrary(Ex)时,系统定位指定的DLL并将该DLL映射到进程地址空间 2. 系统调用LoadLibrary(Ex)的线程来调用DLL的DllMain函数,并传入DLL_PROCESS_ATTACH 3. 当DLL的DllMain完成了对通知的处理时,系统会让LoadLibrary(Ex)调用返回,线程和往常一样继续执行。若DllMain返回FALSE,系统自动从进程地址空间撤销对DLL文件映像的映射并让LoadLibrary(Ex)返回NULL #### DLL_PROCESS_DETACH 系统将一个DLL从进程地址空间中撤销映射时会调用DllMain函数,并为fdwReason传递DLL_PROCESS_DETACH,DllMain执行与进程相关的清理工作。 **按照撤销映射原因分类**: * 进程要终止:调用ExitProcess函数的线程将负责执行DllMain函数的代码。正常情况下,这个线程就是应用程序的主线程。入口点函数返回到C/C++运行库的启动代码后,启动代码会显式调用ExitProcess来终止进程 * 进程中的一个线程调用了FreeLibrary或FreeLibraryAndExitThread:发出调用的线程将负责DllMain函数中的代码。如果调用的是FreeLibrary,那么在DllMain处理完DLL_PROCESS_DETACH通知前,线程不会从该调用中返回的;如果调用TerminateProcess,系统不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数,即没有执行清理代码 #### DLL_THREAD_ATTACH 当进程创建一个线程的时候,系统会检查当前映射到该进程的地址空间中的所有DLL文件映像,并用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数,告诉DLL需要执行与线程相关的初始化。新建的线程负责执行所有DLL的DllMain函数代码。只有在所有DLL都完成了对该通知的处理后,系统才会让新线程开始执行它的线程函数。 系统将一个新的DLL映射到进程地址空间时,如果进程中已有多个线程在运行,系统不会让任何现有的线程用DLL_THREAD_ATTACH来调用该DLL的DllMain函数。只有创建新线程,而且DLL已被映射到进程地址空间,系统才会用DLL_THREAD_ATTACH来调用DLL的DllMain函数。 #### DLL_THREAD_DETACH 让线程终止的首选方式是让它的线程函数返回,系统会调用ExitThread来终止线程,然后系统会让这个将终止的线程用DLL_THREAD_DETACH来调用所有已映射的DLL的DllMain函数,进行线程相关的清理。 #### DLLMain和C/C++运行库 如果使用Microsoft的链接器并指定了/DLL开关,链接器会认为入口点函数的名称是\_DllMainCRTStartup。该函数包含在C/C++运行库文件中,并在链接DLL时静态链接到DLL文件映像中 系统将DLL的文件映像映射到进程地址空间时,实际调用的是\_DllMainCRTStartup函数,该函数先处理DLL_PROCESS_ATTACH通知,初始化C/C++运行库,并确保在\_DllMainCRTStartup收到DLL_PROCESS_ATTACH通知时,所有全局或静态C++对象都已构造完毕。在C/C++运行时的初始化完成后,函数才会调用DllMain函数。 链接DLL时,如果链接器无法在DLL的.obj文件中找到一个名为DllMain的函数,那么它会链接C/C++运行库的DllMain函数。如果不提供自己的DllMain,C/C++运行库会认为不关心DLL_THREAD_ATTACH和DLL_THREAD_DETACH的通知。所以为了提升创建线程和销毁线程的性能,C/C++运行库会在它提供的DllMain函数中调用DisableThreadLibraryCalls ### 延迟加载DLL 延迟加载DLL:隐式链接,系统一开始不会将该DLL加载,只有在代码试图引用DLL中包含的一个符号时,系统才会实际加载该DLL。主要在下列情况下非常有用: * 如果应用程序使用了多个DLL,它的初始化可能会比较慢,因为加载程序要将所有必需的DLL映射到进程地址空间。缓解该问题的一个办法是将DLL的加载过程延伸到进程的执行过程中 * 如果在代码中调用一个新函数,然后试图在一个不提供该函数的老版本OS中运行该应用程序,加载程序会报错并不允许应用程序运行 ### 已知的DLL 系统对OS提供的某些DLL进行了特殊处理,这些DLL被称为**已知的DLL**。在注册表中,有一个注册表项`HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs`,其中包含了多个DLL名称,每个数值名称的数值数据正好等于数值名称加上.dll扩展名(并非肯定如此)。当LoadLibrary或LoadLibraryEx被调用时,函数首先检查是否传入了一个包含扩展名的DLL名称,如果没有包含会用正常的搜索规则来搜索这个DLL。 如果指定了.dll扩展名,这两个函数会先将扩展名去掉,然后再KnownDLLs注册表项中搜索,看其中是否有与之相符的数值名称。如果没有数值名称与之相符,就使用正常的搜索规则。但如果找到了与之相符的数值名称,系统就会查看与数值名称对应的数值数据,并试图用该数据来加载DLL。系统会从`C:\Windows\System32`开始搜索DLL 最后修改:2026 年 01 月 13 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏