GetProcessHeap
使用一个Win32 Heap函数,首先你得有一个heap handle。大部分程序都使用KERNEL32在程序产生时给与的一个默认堆(default heap)。你可以调用GetProcessHeap获得其Heap handle。这个函数很简单,它取出KERNEL32的一个全局变量,指向当前进程的process database。其中存放有进程的默认堆的句柄(handle)。
HeapAlloc和IHeapAlloc
HeapAlloc,如其名称所示,你可以利用它从某个heap中分配一块内存。它其实只是做参数检验的工作,真实分配内存是由IHeapAlloc和HPAlloc完成。HeapAlloc检查hHeap所代表的Heap的大小是否足够容纳一个Heap表头。虽然它也可以检查其它成员如signature和checksum,但很奇怪的是它忽略那些成员。如果hHeap通过测试,HeapAlloc就调用IHeapAlloc。
IHeapAlloc其实只是HPAlloc的外包函数。后者才是真正的HeapAlloc“主体”。在调用HPAlloc之前,IHeapAlloc对dwFlags(调用者传入的标志)做了一些处理。唯一可能幸存的是HEAP_ZERO_MEMORY和HEAP_GENERATE_EXCEPTIONS两个标志。前者的值(如果幸存的话)比其原值左移三位。
HPAlloc
这才是HeapAlloc的主体。它首先检查,要求的区块大小是否太大。太大意味0x0FFFFF98(接近256MB)。接下来HPAlloc调用hpTakeSem,这使得heap表头中的critical section被取得。从此,进程中就没有起他线程可以调用HPAlloc,直到HPAlloc结束为止。在调试版中,hpTakeSem也会随机检验heap是否被摧毁。hpTakeSem还可以遍历整个heap,检查checksum以及signature。你可以利用HeapSetFlags切换这些能力。HeapSetFlags加入Windows 95的时间太晚,以至于我没有办法把它放到本书中来讲。
HPAlloc接下来去出区块大小参数,调整使它接近4的倍数(也要考虑arena的大小)。最小值是0x18。在减去arena的大小之后,留给使用者的只剩8个位。知道区块大小之后,HPAlloc就能决定搜寻四个自由链表中的那一个。找到正确的链表之后,HPAlloc遍历整个链表(使用自由区块的prev指针)以找出第一个够大的区块。
这个时候,让我们假设HPAlloc找到了一个够大的区块。于是它调用hpCarve(稍候我将介绍)。hpCarve函数检查一个区块,看看它是否刚好够大,或是可以分裂为两块。如果需要分裂,hpCarve处理所有的工作,包括产生新的arena、初始化prev和next等等。分裂出来的其中一块刚好够大,满足HPAlloc的需求。另一块被放到自由链表之中。
在hpCarve返回之后,HPAlloc将新的区块的arena成员初始化。除了“取得HPAlloc调用者的EIP”,以及“计算前三个成员的checksum”之外,其余只是一些简单的设定动作。最后,HPAlloc释放heap的临界区对象,返回一个指针,指向arena之后的第一个字节。
如果HPAlloc没有发现一个自由区块的话,并且heap允许自动增长(产生堆时,dwMaximumSize被指定为0),HPAlloc则需要产生一个新的sunheap。先前我说过,一个subheap是另一个独立空间,内含一些heap区块。KERNEL32负责跟踪所有的subheaps,作法是把它们统统维护在一个链表之中。如果需要产生subheap,KERNEL32会决定其初始大小(通常是4KB),并调用VMM报留一些pages。接下来HPAlloc调用HPInit将新的subheap的表头初始化。初始化之后,HPAlloc把它安插到subheaps所组成的链表中。最后,HPAlloc跳到“搜寻自由链表”的起始处。我想,这一次应该可以找到足够大小的区块了。
在Windows 95中,Win32堆中的内存最初都处于reserved,并为committed。如果我们有1MB堆并且在还不需要这1MB内存时,并不会提交实际的内存页。当程序触及被reserved而尚未被committed的page时,会引发page fault。因此,堆函数必须确定所有使用中的区块涉及到的pages都作了commit动作。在Windows 95中,系统并不是用结构化异常来做commit动作。
HeapSize和IHeapSize
HeapSize获得一个指针,指向先前分配的一块内存,并返回其大小(不含arena)。
HeapFree和IHeapFree
HeapFree仅负责参数检验。真实的动作发生在IHeapFree和x_HeapFree中,除了释放指定的堆内存,该函数还会检查是否有必要进行自由区块的合并。
HeapReAlloc和IHeapReAlloc
HeapReAlloc对原已存在的一个Win32堆重新配置其大小。HeapReAlloc只负责参数检验,其动作和HeapFree所做的一样。
在Windows 95中,IHeapReAlloc有一点诡异,它会重新按排dwFlags参数,能通过的标志如下:
HEAP_GENERATGE_EXCEPTIONS
HEAP_NO_SERIALIZE
HEAP_ZERO_MEMORY
HEAP_REALLOC_IN_PLACE_ONLY
在进行重新分配时,HeapReAlloc有四种情况需要考虑:
l 新区块比原区块小
l 新区块和原区块大小相差不多
l 新区块比原区块大。而堆中的下一个区块是自由的,并且可以和原区块合并其来,形成满足要求的新区块。
l 新区块比原区块大。而堆中的下一个区块并不自由,或者虽然它是自由的,但和原区块合并后还是无法满足要求。
HeapCreate
HeapCreate是所有Win32堆函数的根源。每个Win32程序在开始之前都有一个默认的堆。此外,程序还可以调用HeapCreate产生另外的堆。除了被应用程序使用之外,KERNEL32也调用HeapCreate在全局共享内存中产生堆,并用这些堆来对系统数据结构(诸如线程或进程相关资料)
产生一个Win32堆的过程可分为两个部分。第一部分是保留内存并将其连接到进程的堆链表上,另一部分是初始化堆表头。
HeapDestroy和IHeapDestroy
与前面介绍的函数类似,HeapDestroy只是做参数检验工作,实际的摧毁Win32堆的动作是由IHeapDestroy完成的。摧毁Win32堆并不像释放堆还给操作系统那么简单。有两件事情使它比较复杂。第一,所有未以HEAP_NO_SERIALIZE属性产生出来的堆,都持有一个临界区对象。IHeapDestroy检查要摧毁的堆是否拥有此对象,并适当的释放之。
另一个复杂原因是堆链表。如果IHeapDestroy只是单单释放堆所占用的页,这个堆链表就会被破坏掉。IHeapDestroy必须遍历整个链表,并更新之。
在链表被更新之后,IHeapDestroy调用VMM的_PageFree,释放堆所拥有的页。仅调用一次可能不足以释放所有的页。为什么?如果堆使用者做了许多的配置,或是一个非常大的配置,HeapAlloc可能产生出额外的subheaps并追加到subheap链表上。因此,IHeapDestroy必须保证释放main heap以及任何一个subheap。
最后请注意一点,程序退出之前,系统不会自动调用HeapDestroy。我推测如果进程的地址空间消失了,所有的堆内存也就被释放了吧。
HeapValidate
这是一个Windows NT函数。它扫描一个Win32堆,检查其一致性。其实我看不出有什么理由它不应该出现在Windows 95 API中。
HeapCompact
这也是个Windows NT函数。它会尝试合并自由区块并将Win32堆中未使用的页统统decommit掉。Windows 95已经在日常维护中完成了这些事情,所以它没有存在的必要。
GetProcessHeaps
该函数仅存在于Windows NT系列操作系统中,该函数返回一个由堆句柄组成的数组。
HeapLock
这也是一个Windows NT函数,获得Win32堆的临界区对象。
HeapUnLock
也是Windows NT函数,释放Win32堆的临界区对象。
HeapWalk
也是一个Windows NT函数,遍历一个Win32堆的所有区块。
笔记:
在Windows XP和Windows Server 2003系列中,增加了下面两个新的堆函数:
HeapSetInformation
HeapQueryInformation
Win32 Virtual函数 |
Win32堆函数 |
VirtualAlloc |
HeapCreate |
VirtualAllocEx |
HeapSize |
VirtualFree |
HeapAlloc |
VirtualFreeEx |
HeapReAlloc |
VirtualLock |
HeapLock |
VirtualUnlock |
HeapUnLock |
VirtualProtect |
HeapFree |
VirtualProtectEx |
HeapDestroy |
VirtualQuery |
HeapValidate |
VirtualQueryEx |
HeapCompact |
|
HeapSetInformation |
|
HeapQueryInformation |
|
GetProcessHeap |
|
GetProcessHeaps |
Win32的Local Heap函数
Win32的Local Heap和Global Heap函数都是Win16遗留下来的。在Windows 95中其实已无需要。Win16的Local Heap之所以产生是为了让EXE或DLL可以不需要改变selector就找到其Heap内容。Global Heap之所以存在则是因为没有办法在不处理selector的情况下分配大块的内存。Windows 95中的Win32程序没有这两个限制,所以Win32 API可以免除这两组函数。
然而我们知道,Win32 API因为向下的兼容性作了某些妥协。有太多的Win16程序使用Global Heap和Local Heap函数。如果把它们从Win32 API中去除,会使得程序的移植工作走不出实验室,因此微软决定保留它们。
绝大部分而言,Windows 95的Local Heap和Global Heap函数是完全一致的。也就是说,GlobalAlloc和LocalAlloc虽然同为输出函数,但使用相同的KERNEL32.DLL地址。GlobalFree和LocalFree也是相同的情况。
Windows 95中的Global Heap函数几乎没有功能,大部分时候,它们只是直接跳进其对应的Local Heap函数。要不就是像GlobalAlloc那样,根本就和对应的Local Heap函数共享同一个进入点。大部分函数都会接受一个HGLOBAL参数。GlobalAlloc或LocalAlloc分配的内存都是从HPAlloc而来。
笔记:
关于Local Heap和Global Heap函数,这里就不详细的讲了。因为在Win32程序中,这两组函数都已不被推荐使用。其存在的意义仅仅是为了提供向下的兼容性。
下面列出在Windows 2000/XP中还存在的函数列表:
Win32 的 Local Heap函数 |
Win32 的 Global Heap 函数 |
LocalAlloc |
GlobalAlloc |
LocalDiscard |
GlobalDiscard |
LocalFlags |
GlobalFlags |
LocalFree |
GlobalFree |
LocalHandle |
GlobalHandle |
LocalLock |
GlobalLock |
LocalReAlloc |
GlobalReAlloc |
LocalSize |
GlobalSize |
LocalUnlock |
GlobalUnlock |
可以看出,一些Windows 95支持的Local Heap和Global Heap函数并没有出现在上面的列表中,原因很简单,没有存在的必要了。可以肯定地讲,在基于Windows NT内核的操作系统中,这些函数的实现细节肯定会和Windows 95有所不同。
杂项函数:
WriteProcessMemory和ReadProcessMemroy
这是两个被核准用来读写另一个进程的内存数据的函数。为了使用他们,你必须先获得另一个进程的句柄(handle)。然而Win32 API不提供什么方便的做法,让你轻易达到这一目的。这两个函数是Win32调试器的关键函数。调试器是属于那种“必须读写其他进程内存数据”的另类软件。J
这两个函数的底层十分类似。因此我决定只展示其中一个的虚拟代码。唯一明显的差异在于WriteProcessMemory调用VWIN32的0x002A0017 Service,而ReadProcessMemory调用VWIN32的0x002A0016 Service。
WriteProcessMemory首先做同步控制。它先确定它没有持有Win16Mutex和Krn32Mutex,然后进入“必须完成”状态—意思是:不能在执行过程中被切换出来。然后确定源地址(source address)位于应用程序私有的arena中(也就是VMM文件中说的4MB-2GB之间)。
接下来WriteProcessMemory取得指针,指向源进程(source process),再从其身上获取线程链表。为了某些理由,“实际内存复制动作”的VWIN32 Service,希望获得目标进程(target process)之当前线程的ring0 stack地址。一旦所有东西都到手了,它就请求Krn32Mutex并调用VWIN32 Server。在VWIN32完成其memory context之神奇魔法后,WriteProcessMemory释放Krn32Mutex,并调用LeaveMustComplete以退出“必须完成”状态。如果这个程序中有某些事情出了错,就调用SetLastError,让调用者知道错在哪儿了。
GlobalMemoryStauts
GlobalMemoryStatus可以很方便的观察内存的状态。这个函数填充MEMORYSTATUS结构,像是有多少pages已经映射到实际的RAM、交换文件(swap file)的大小等等。这个函数很类似于Win16的MemManInfo。
GlobalMemoryStatus其实只是个参数检验层,它只确定一件事:传给函数的指针指向一块足以存放MEMORYSTATUS结构的内存。别管文件上怎么说,你不需要在调用GlobalMemoryStatus之前先初始化MEMORYSTATUS结构的dwLength成员。
笔记:
自Windows 2000开始,微软新增加了一个GlobalMemoryStatusEx函数,该函数使用MEMORYSTATUSEX结构,不同于GlobalMemoryStatus函数的是,在调用之前,你需要初始化MEMORYSTATUSEX结构的dwLength程序。如:
MEMORYSTATUSEX statex;
statex.dwLength = sizeof (statex);
GetThreadSelectorEntry
当我看到GetThreadSelectorEntry,我很震惊它也出现在Win32 API队伍中。这个函数并没有对线程作任何动作,事实上,hThread参数只是为了检验,却从来没有用到。GetThreadSelectorEntry给你一个只读机会,处理System VM的Local Descriptor Tables(LDTs)。这是特定的descriptor table,用来记录Win32程序的flat程序代码区和数据区。Win16程序也是通过它取得它们的代码区和数据区,以及GlobalAlloc句柄。这个函数对于任何探索系统的工具软件,都是非常有用的一个利器。
需要注意的是,这个函数只能用于采用x86 CPU的系统中。这里提到的System VM指的就是x86虚拟机环境。
如果你传给GetThreadSelectorEntry一个合法的selector,就可以取回一个8bits的结构,格式与LDT descriptor相同。由于Win32程序有一个flat指针,可以到达任何地方,所以可利用此函数将16:16地址转换为一个flat32地址。于是Win32程序可以读取该处的内容,也可以将数据写入该处。你甚至可以将Win16的GetSelectorBase和GetSelectorLimit函数构建成属于你自己的Win32版。
GetThreadSelectorEntry函数中主要的趣味在LDT Alias和LDT Ptr这两个变量身上。这是KERNEL32.DLL的两个全局变量。LDT Ptr内含System VM的LDT的线性地址,LDTAlias则是一个selector值,拥有对selector table的内存的读写权力。
C/C++编译器提供的malloc和new
许多时候,程序员会忽略操作系统提供的内存管理函数,使用C Runtime Library(特别是malloc和free函数)来做内存管理。C++呢?就我所知,所有PC上的C++编译器提供的new运算符都被直接对应到malloc,delete运算符则被对应到free。问题在于,这些函数如何使用底层的操作系统的能力呢?
在这一章中,我已经告诉你,堆函数(如HeapAlloc和HeapFree)是多么接近malloc和free。这是否意味着malloc和free只是HeapAlloc和HeapFree的另一个包装吗?至少到Viusal C++ 4.0为止,答案是否定的。唯一的例外的是CRTDLL.DLL---微软的C Runtime Library。在CRTDLL.DLL之中,malloc和new都只是简单的调用HeapAlloc,free和delete则调用HeapFree。CRTDLL.DLL使用于许多标准的Windows NT和Windows 95的EXEs和DLLs身上。这是一个伟大的想法,让微软不必为了“不同的EXE和DLL身上有不同的C Runtime Library”而伤脑筋。
不幸的是,C编译器厂商不捧场,他们没有让每个人都使用CRTDLL.DLL。因此,每个EXE之内还是可能各有一个C Runtime Library。这种情况短时间内不会改变,所以我们最好知道这些runtime library的底层在做些什么。
我并不打算细究malloc和free。取而代之的是,我将给你足够的信息,让你足以进行自我判断,该如何设计你自己的内存管理体系。
到目前为止,我已经能够确定。Borland和Microsoft以类似的方法实做其runtime library的Heaps。事实上,除了大小之外,整个形势并没有和Windows 3.1时代有太多改变。每个EXE和DLL都有它自己的Heap。程序如果使用三个DLLs,就会有四个独立的Heaps(EXE有一个,DLLs各有一个)。DLL所分配的内存将来自DLL的Heap。这和Win32的HeapAlloc不同,后者一定是从Exe Heap中分配内存(假设你总是将进程的默认堆丢给它)。
C编译器的RTLs(Runtime Library)并不使用操作系统的高级Heap函数如HeapAlloc,它们使用自己的数据结构和内存管理器。于是,要把malloc所获得的内存和HeapAlloc所获得的内存混合起来使用是很困难的。
只要挖掘C/C++ RTL足够深,我们就可以了解如何将malloc对应到底层的操作系统函数。我潜伏到Borland C++ 4.5 RTL原始代码中,那是件困难的工作,我很高兴你不必再做一次。下面这段call stack显示,malloc如何在Windows 95上实现的:
malloc (HEAP.C)
_getmem (GETMEM.C)
_virt_reserve ( VIRTMEM.C)
VirtualAlloc( NULL, size, MEM_RESERVE, PAGE_NOACCESS )
阿哈,C RTL利用VirtualAlloc从操作系统分配大量内存,那正是HeapAlloc的行为。被分配的内存最初处于reserved状态,然后再以_virt_reserve将他们“commit”---在它们被使用之前一刻。_virt_reserve其实只是VirtualCommit的另一个外包函数。这个过程听起来熟悉吗?是的,这正是Windows 95的做法。如果你需要复习,请回头看看hpCarve和hpCommit这两个函数。
RTL并不会在你每次调用malloc时就调用VirtualAlloc。它们需要设定并维护内部结构并持续追踪什么区块被分配了,什么不是….。同时也保持一个自由链表,以便能够快速进行分配。
当你使用RTL提供的堆时,一个潜在的问题是其生命期。当一个DLL被剔出内存并收到DLL_PROCESS_DETACH消息时,Runtime Library会调用VirtualFree,将堆占用的内存释放掉。如果另一个DLL有个指针指向此块内存中的某个区块,指针会突然间失效。如果该DLL稍后也被剔出内存,并在其DLL_PROCESS_DETACH处理过程中使用了这些指针,程序就会完蛋了,而且很不容易发现这种错误。这段心得是从有痛苦经验的人那儿学来的。
那么,回答我最初的问题,malloc基本上是Windows 95的HeapAlloc函数的编译器版本。但至少有两个关键差异。第一,每个EXE和DLL都有它们自己的堆,由RTL提供,而HeapAlloc所分配的内存一概来自系统所设定的进程默认堆。在某种操作程序下,使用RTL的堆可能会造成问题。这并不是说你应该避免使用malloc或new,只是要告诉你它们是什么,以及让你了解其中的潜在性问题。
笔记:
这两个函数,都属于Win32 Debugging Funciton,下面列出MSDN 2002.4中的这些函数:
Win32 Debugging Function |
ContinueDebugEvent |
DebugActiveProcess |
DebugActiveProcessStop |
DebugBreak |
DebugBreakProcess |
DebugSetProcessKillOnExit |
FatalExit |
FlushInstructionCache |
GetThreadContext |
GetThreadSelectorEntry |
IsDebuggerPresent |
OutputDebugString |
ReadProcessMemory |
SetThreadContext |
WaitForDebugEvent |
WriteProcessMemory |
-----------------第五章完-----------------