Windows核心编程第五版学习笔记(三)
第十九章 DLL基础
1、Kernel32.dll 包含的函数用来管理内存、进程以及线程;
User32.dll包含的函数用来执行与用户界面相关的任务,如创建窗口和发送消息;
GDI32.dll包含的函数用来绘制图像和显示文字。
AdvAPI32.dll包含的函数与对象的安全性、注册表的操作以及事件日志有关;
ComDlg32.dll包含了一些常用的对话框(如打开文件和保存文件);
ComCtl32.dll支持所有常用的窗口控件。
2、一个地址空间是由一个可执行模块和多个dll模块构成的。这些模块中,有些会链接到C/C++运行库的静态版本,有些可能会链接到运行库的DLL版本,有些可能根本不需要C/C++运行库。
VOID ExeFunc() { PVOID pv = DLLFunc(); free(pv);}
PVOID DLLFunc() { return malloc(100);}
DLL中分配的内存块能够为EXE中的函数释放呢?也许。如果EXE和DLL都链接到C/C++运行库的DLL版本,那么代码将能够正常工作,但是,如果至少有一个模块链接到C/C++运行库的静态版本,free调用就会失败。
注意:当一个模块提供一个内存分配函数的时候,它必须同时提供另一个用来释放内存的函数。
3、只有当导出C++类的模块使用的编译器和导入C++类的模块使用的编译器由同一家厂商提供时 ,我们才可以导出C++类。
4、extern "C"标识符只有在编写C++代码时才应该使用这个修饰符,在编写C代码的时候不应该使用该修饰符,C++编译器通常会对函数名和变量名进行改编,这在连接时就会导致严重的问题。用该标识符用来告诉编译器不要对变量名和函数名进行改编,这样用C、C++或者任何编程语言编写的可执行模块都可以访问该变量或函数。
5、
MyLib.h
////////////////////////////////////////////////
#ifdef MYLIBAPI
#else
#define MYLIBAPI extern "C" _declspec(dllimport)
#endif
MYLIBAPI int g_nResult;
MYLIBAPI int Add(int nLeft, int nRight);
//////////////////////////////////////////////////
MyLib.cpp
/////////////////////////////////////////////////
#define MYLIBAPI extern "C" _declspec(dllexport)
#include "MyLib.h"
int g_nResult;
int Add(int nLeft, int nRight) { g_nResult = nLeft + nRight; return g_nResult; }
//////////////////////////////////////////////////
在编译DLL源文件的时候,MYLIBAPI在包含MyLib.h头文件之前被定义为_declspec(dllexport),如果编译器看到一个变量、函数或者类是用_declspec(dllexport)修饰的,那么它就知道应该在省城的dll模块中到处该变量、函数或者类。对于那些要被导出的变量和函数,我们必须在头文件中的变量和函数定义的前面加上MYLIBAPI。
可执行文件不应该在包含这个头文件之前定义MYLIBAPI,由于MYLIBAPI没有定义,因此头文件会将MYLIBAPI定义为_declspec(dllimport),这样编译器就知道该可执行文件源文件要从DLL模块中导入一些变量和函数。
6、为了用Microsoft的工具包来构建一个能与其他编译器厂商的工具包链接的DLL,我们必须告诉Microsoft编译器不要对到处的函数名进行改编,方法如下:
- 定义一个.def文件,并在该文件中包含以下
EXPORTS
MyFunc
这样导出的时候就是函数MyFunc,而不是改编后的函数名。
- #pragma comment(linker, "/export:MyFunc = _MyFunc8")
第一种方法好,第二种方法会到处MyFunc和_MyFunc8两个函数名。
第二十章 DLL高级技术
1、系统会在每个进程中为每个DLL维护一个使用计数,也就是说,如果进程A中的一个县城执行了下面的代码,然后进程B中的一个线程执行了同样的代码,那么MyLib.dll会被映射到两个进程的地址空间中,该DLL在进程A和进程B中的使用计数都是1.
HMODULE hInstDll = LoadLibrary(TEXT("MyLib.dll"));
2、一旦显示地载入 一个DLL模块,线程必须调用下面的函数来得到它想要引用的符号地址:FARPROC GetProcAddress(HMODULE hInstDll, PCSTR pszSymbolName);
第二个参数只接受ANSI的字符串。
FARPROC pfn = GetProcAddress(hInstDll, "SomeFuncInDll");
FARPROC pfn = GetProcAddress(hInstDll, MAKEINTRESOURCE(2));这种用法假定我们知道Dll的创建者给我们想要的符号名指定的序号是2.Microsoft强烈反对这种用法。这种用法传入的序号即使并没有任何函数与之相对应,GetProcAddress也可能会返回一个非NULL值。请务必小心。
3、函数名DllMain是区分大小写的。如果我们将入口点函数命名为DllMain之外的其他名称,那么虽然代码仍然能够编译和链接,但我们的入口点函数将永远不会被调用,DLL也永远不会进行初始化。
DLL使用DllMain函数对自己进行初始化,DllMain函数执行的时候同一个地址空间中的其他Dll可能还没有执行它们的DllMain。这意味着它们尚未初始化,因此我们应该避免调用那些从其他DLL中导入的函数,此外,我们应该避免在DllMain中个调用LoadLibrary(Ex)和FreeLibrary,因为这些函数可能产生循环依赖。
SDK说DllMain只应该执行简单的初始化,比如设置线程的局部存储区,创建内核对象,打开文件,等等,我们必须避免调用User、Shell、ODBC、COM、RPC以及套接字函数(或其他调用了这些函数的函数),因为包含这些函数的DLLkeneng尚未初始化,或者函数可能会在内部调用LoadLibrary(Ex),从而产生循环依赖。
另外值得注意的是,如果要创建全局或静态C++对象,会存在同样的问题,因为在DllMain函数被调用的同时,这些对象的构造函数和析构函数也会被调用。
4、系统第一次将一个DLL映射到进程的地址空间中时,会调用DllMain函数,并在fdwReason参数中传入DLL_PROCESS_ATTACH,只用当DLL的文件映像第一次被映射的时候,才会这样。之后一个线程调用LoadLibrary(Ex)来载入一个已经被映射到进程地址空间中的DLL,操作系统只不过是递增该Dll的使用计数,不会再次用DLL_PROCESS_ATTACH来调用DllMain函数。
如果进程终止时因为系统中的某个线程调用了TerminateProcess,系统便不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数,这意味着在进程终止之前,已映射到进程的地址空间中的任何DLL将没有机会执行任何清理嗲吗,这可能导致数据丢失,因此,除非万不得已,我们应该避免使用TerminateProcess函数。对于线程也是如此。
当进程创建一个线程的时候,系统会检查当前映射到进程的地址空间中的所有DLL文件映像,并用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数。当系统将一个新的DLL映射到进程的地址空间中时,如果进程中已经有多个线程在运行了,那么系统不会让任何已有的线程用DLL_THREAD_ATTACH来调用该DLL的DllMain函数,如果在创建新线程的时候Dll已经被映射到进程的地址空间中,那么只有在这种情况下系统才会用DLL_THREAD_ATTACH来调用DllMain函数。系统不会让进程的主线程用DLL_THREAD_ATTACH值来调用DllMain函数,在进程创建的时候被映射到进程地址空间中的任何DLL会收到DLL_PROCESS_ATTACH通知,但不会收到DLL_THREAD_ATTACH通知。
进程中的一个线程调用LoadLibrary来载入一个DLL,使得系统用DLL_PROCESS_ATTACH来调用该DLL的DllMain函数(不会得到DLL_THREAD_ATTACH的通知),接着,载入该DLL的线程退出,这使得系统再次调用DllMain函数,但这次通知是DLL_THREAD_DETACH,注意,虽然当系统将线程链接到该DLL时候不会发送DLL_THREAD_ATTACH,但是当系统将该线程与DLL解除连接的时候会向该DLL发送DLL_THREAD_DETACH通知。
5、DLL函数转发器。
//Function forwarders to functions in DllWork
#pragma comment(linker, "/export:SomeFunc = DllWork.SomeOtherFunc");
这个pragma告诉链接器,正在编译的DLL应该输出一个名为SomeFunc的函数,但实际实现SomeFunc的是另一个名为SomeOtherFunc的函数,该函数被包含在另一个名为DllWork.dll的模块中,我们必须为每个想要转发的函数单独创建一行pragma。
6、HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SessionManager
\KnownDlls这个注册表项中的DLL被成为KnownDLL。当LoadLibrary(Ex)调用时,函数会检查我们传入的DLL的名字是否包含了.dll扩展名,如果没有包含,那么函数会用正常的搜索规则来搜索这个DLL。如果指定了.dll扩展名,那么这两个函数会先将扩展名去掉,然后在KnownDLLs的注册表项中搜索,看其中是否有与之相符的值名,如果找到了,系统会查看与值名对应得数据,并试图用该数据来载入DLL,系统还会从这个注册表项的DllDirectory值所表示的目录中开始搜索DLL,如果在该目录中找不到该文件,那么LoadLibrary(Ex)失败并返回NULL。
7、Microsoft自Win2000开始新增了一项DLL重定向特性,这项特性强制操作系统那个的加载程序首先从应用程序的目录中载入模块,只有当加载程序无法找到要找的文件时,才会在其他的目录中搜索。为了实现这一点,我们将一个文件放到应用程序的目录中,内容无关紧要,文件名必须是AppName.local,例如我们有个SuperApp.exe的可执行文件,那么重定向文件的名称必须是SuperApp.exe.local。LoadLibrary(Ex)在内部做了修改,检查这个文件存在与否,如果存在,系统会载入这个目录中的模块,如果不存在,那么LoadLibrary的工作方式与以往相同,除了创建一个.local文件,我们还可以创建一个名为.local的文件夹,在这种情况下,我们可以将自己的DLL保存在这个文件夹中,让Windows能够轻易地找到它们。为了安全性的缘故,Vista中的这项特性默认是关闭的,因为它可能会是系统从应用程序的文件夹中载入伪造的系统DLL,而不是Windows的系统文件夹中的真正系统dll,为了打开这项特性,可以在HKLM\Software\Micorsoft\WindowsNT\CurrentVersion\Image File Execution Options注册表项中增加一个条目DWORD DevOverrideEnable,并将它的值设为1。
8、每个可执行文件和DLL模块都有一个首选基地址,它表示在将模块映射到进程的地址空间中时的最佳内存地址。exe文件的首选基地址是0x00400000,DLL的首选基地址是0x10000000;这样当一个exe用两个dll时,第一个dll加载到首选基地址0x10000000,第二个就必须重定位,而重定位的过程是痛苦的。当连接器构建我们的模块时,会将重定位段嵌入到生成的文件中,这个段包含一个字节偏移量的列表,每个字节偏移量表示一条机器指令所使用的一个内存地址,如果不能将模块加载到它的首选基地址,那么系统归咎会打开模块的重定位段并遍历其中所有的条目,对每一个条目,加载程序会先找到包含机器指令的那个存储页面,然后将模块的首选基地址与模块的实际映射地址之间的差值,加到机器指令当前正在使用的内存地址上。
当一个模块无法被在入到它的首选基地址的时候,存在以下两个主要缺点:
- 加载程序必须遍历重定位段并修改模块中的大量代码,这个过程不仅是性能的一个杀手也确实会损害应用程序的初始化时间。
- 当加载程序写入到模块的代码页面中时,系统的写时复制机制会强制这些页面以系统的也交换文件为后备存储器。
第二点真的很糟糕。意味着系统不能再抛弃模块的代码页面,并重新载入模块在磁盘上的文件映像,系统必须在需要的时候将内存页面换出到系统的页交换文件,并将页交换文件中的页面换入到内存。由于页交换文件是所有模块的代码页面的后备存储器,因此这会减少可供系统中所有进程使用的存储器的数量,这限制了用户的电子表格的大小、字处理文档的大小、CAD制图的大小、位图的大小。
首选基地址可以再vs中修改,但是必须从分配粒度的边界开始,在时至今日的所有平台中,系统的分配粒度都是64KB,但今后可能发生变化,但是我们要在一个地址空间中载入大量的模块这么做就不行了。Vs提供了一个名为Rebase.exe的小工具。如果在执行Rebase工具的时候传给它一组映像文件名,那么它会执行如下操作:
- 模拟创建一个进程地址空间
- 打开应该被在入到这个地址空间中的所有模块,并得到每个模块的大小以及他们的首选基地址。
- 它会在模块的地址空间中对模块重定位的过程进行模拟,使各模块之间灭有交叠。
- 对每个重地位过的模块,会解析该模块的重定位段,并修改模块在磁盘文件中的代码。
- 为了反映新的首选基地址,它会更新每个重定位过的模块的文件头。
9、加载程序将符号的虚拟地址写入到可执行文件模块的导入段中,这使得程序引用导入的符号时,实际上引用的是正确的内存地址。加载程序将导入符号的虚拟地址写入到.exe模块的导入段,那么会写入导入段的后备存储页面,由于这些页面具有写时复制属性,因此它们以页交换文件为后备存储器。所以我们就遇到了与基地址重定位相似的问题。系统必须将映像文件的一部分从内存换出到页交换文件,并从页交换文件换入到内存,而不能直接抛弃内存中的页面并在需要的时候再从文件的磁盘映像中重新读取,另外,加载程序必须解析所有导入符号的地址,这可能会耗费很长时间。
采用模块绑定技术,应用程序就可以更快地初始化并使用更少的存储器,对以个模块进行绑定,是用锅盖模块导入的所有符号的虚拟地址,来对该模块的导入段进行预处理。当然,为了减少初始化时间并且使用更少的存储器,我们必须在载入模块之前执行这一操作。
Vs提供了一个工具Bind.exe,如果在执行Bind工具的时候传给他一个映像文件名,它执行如下操作:
打开指定的映像文件导入段
对导入段中列出的每个dll,它会查看该dll文件的文件头,来确定该dll的首选基地址。
在dll的导出段中查看每个符号。
取得符号的RVA,并将它与模块的首选基地址相加,将计算得到的地址,也就是导入符号预期的虚拟地址,写入到映像文件的导入段中。
在映像文件的导入段中添加一些额外的信息,这些信息包括映像文件被当定到的各DLL模块的名称,以及各模块的时间戳。
第二十一章 线程局部存储区
1、C/C++运行库使用了TLS,C++运行库会为每个线程分配独立的字符串指针,专供_tcstok_s使用,同样享受这一特殊待遇的其他C/C++运行库函数包括asctime和gmtime。
2、应用程序通过调用4个函数来使用动态TLS。这些函数实际上最经常为DLL所使用
DWORD TlsAlloc();
BOOL TlsSetValue(DWORD dwTlsIndex, PVOID pvTlsValue);
PVOID TlsGetValue(DWORD dwTlsIndex);
BOOL TlsFree(DWORD dwTlsIndex);
当系统创建一个线程的时候,会分配TLS_MINIMUM_AVAILABLE个PVOID值,将他们都初始化为0,并与线程关联起来,每个线程都有自己的PVOID数组,数组中的每个PVOID可以保存任意值。如上图所示。
通常,如果DLL要使用TLS,那它会在DllMain函数处理DLL_PROCESS_ATTACH的时候调用TlsAlloc,在DllMain处理DLL_PROCESS_DETACH的时候调用TlsFree。而对TlsSetValue和TlsGetValue的调用则最有可能发生在DLL所提供的其他函数中。
3、静态TLS
_declspec(thread) DWORD gt_dwStartTime = 0; _declspec(thread)是Microsoft为VC++编译器新增加的一个修饰符,它告诉编译器应该在可执行文件或DLL文件中,把对应的变量放到它自己的段中。_declspec(thread)后面的变量必须被声明为全局变量或静态变量(可在函数内,也可在函数外)。当编译器对程序进行编译的时候,会将所有的TLS变量放到它们自己的段中,这个段名为.tls。连接器会将所有对象模块中的.tls段合并成一个大的.tls段,并将它保存到生成的可执行文件或DLL文件中。
第二十二章
1、Microsoft决定不允许SetWindowLongPtr对另一个进程创建的窗口的窗口过程进行修改。但我们仍然能从其他进程创建的窗口派生子类窗口,值不过要采用给另一种不同的方法,这个问题实际与派生子类窗口无关,而是与进程地址空间有关如果能够通过某种方式让我们的子类窗口的窗口过程进入到进程A的地址空间中,就能够轻易地调用SetWindowLongPtr,并把MySubclassProc在进程A中的地址传给它,我们称这项技术为DLL注入。
注:如果打算从同一个进程中的窗口派生子类窗口,那么应该利用SetWindowSubClass、GetWindowSubclass、RemoveWindowSubclass、DefSubclassProc。
第十三章
1、预定地址空间或者调拨物理存储器时,不能使用PAGE_WRITECOPY和PAGE_EXCUTE_WRITECOPY保护属性,这样做会导致VirtualAlloc失败,这两个属性是操作系统在影射.exe和.dll映像文件时用的。
2、当应用程序预定地址空间区域时,系统会确保区域的其实地址正好是分配粒度的整数倍,分配粒度会根据不同的cpu平台有所不同,但是在写作本书时,所有的CPU平台都是用相同的分配粒度大小为64KB,系统会把分配请求调整到64KB的整数倍。而且,系统会确保区域的大小正好是系统页面大小的整数倍,页面是一个内存单元,系统通过它来管理内存,与分配粒度相似,页面大小会根据不同的平台有所不同,x86和x64系统使用的页面大小是4KB,IA-64系统使用的页面大小为8KB。
虽然系统规定应用程序在预定地址空间区域时起始地址必须是分配粒度的正式被,但系统自己却不存在同样的限制,非常有可能出现的情况是系统为PEB(进程环境块)和TEB(线程环境块)预定的区域的起始地址并非是64KB的整数倍,但是这些区域必须是CPU页面大小的整数倍。
3、在Windows中,所有的.exe和.dll都载入到用户模式分区,每个进程都可能将这些DLL载入到这一分区内的不同地址,系统同时会把该进程可以访问的所有内存映射文件映射到这一分区。
4、当用户要求执行一个应用程序时,系统会打开该exe文件并计算应用程序的代码和数据的大小,然后系统会预定一块地址空间,并注明该区域相关联的物理存储器就是exe文件本身,系统并没有从也交换文件中分配空间,而是将exe文件的时机内容用作程序预定的地址空间区域,遮掩更依赖,不但载入程序非常快,而且页交换文件也可以保持一个合理的大小。
5、对于写时复制保护属性保护的页面,由操作系统来给共享的存储页指定写时复制属性。当系统把exe或dll映射到一个地址空间的时候,系统会计算有多少页面是可写的,然后系统从也交换文件中分配存储空间来容纳这些科协的页面,除非应用程序真的写入可写页面,否则不会用到页交换文件中的存储器。
浙公网安备 33010602011771号