接下来VirtualAlloc处理来自fdwAllocationType参数的各种标志值。首先,它看看是否有未公开的0x80000000标志,那意味要分配2GB以上的内存。VirtualAlloc忽略MEM_TOP_DOWN标志。然后它再测试是否你只传入了MEM_COMMIT或MEM_RESERVED标志。任何其他的标志都会引发调试版的一个警告信息。最后,函数代码调用mmPAGEToPC,那是一个辅助函数(下一节描述),把fdwProtect参数标志转换为VMM的_PageReserve所使用的标志。
这时候,函数兵分两路。如果不在乎哪一段内存要保留,就执行其中一路。如果调用者指定了某特定范围的内存,那么就执行另一路。不管是哪一路,如果内存要被保留,VirtualAlloc就调用Win32 Service 00010000,那是VMM’s _PageReserve函数的一个外包函数。保留这段内存之后,如果调用者还有指定MEM_COMMIT标志的话,VirtualAlloc就调用Win32 Service 00010001,那是VMM’s _PageCommit函数的一个外包函数。如果调用者指定了一个特定地址范围,VirtualAlloc会检查它是不是在0xC0000000(VxD领域的起始处)之下。
整个函数代码中,VirtualAlloc不断周到的检查返回至_PageReserve和_PageCommit。如果任何事情失败,它就会产生一个调试诊断信息,然后从某个唯一出口退出。该出口处会释放先前保留的内存。
VirtualAlloc函数的虚拟代码
// Parameters
// LPVOID lpvAddress
// DWORD cbSize
// DWORD fdwAllocationType
// DWORD fdwProtect
// Locals:
// DWORD address, startPage
// DWORD sizeInPages
// DWORD pcFlags // Return from mmPAGEToPC
// BOOL fReserve
if ( cbSize > 0x7FC00000 ) // 2GB – 4MB
{
_DebugOut(“VirtualAlloc: dwSize too big\n\r”, SLE_WARNING
+ FstopOnRing3MemoryError);
InternalSetLastError( ERROR_NOT_ENOUGH_MEMORY );
return 0;
}
address = lpvAddress;
// Calculate how many pages will be spanned by this memory request
sizeInPage = lpvAddress & 0x00000FFF;
sizeInPage += cbSize;
sizeInPage += 0x00000FFF;
sizeInPage = sizeInPage >> 12;
startPage = PR_PRIVATE; // 0x80000400 from VMM.INC This value can be either
// an actual page number or a PR_equate.
if ( fdwAllocationType & 0x80000000 ) // Undocumented shared mem flag
{
startPage = PR_SHARED; // 0x80000000 in VMM.INC
fdwAllocationType &= ~0x80000000; // Don’t need this flag anymore
}
fdwAllocationType &= ~MEM_TOP_DOWN; // Ignore the MEM_TOP_DOWN flag
// You can specify MEM_COMMIT and/or MEM_RESERVE, but no other flags
// (the Undocumented one above not with standing).
if ( (fdwAllocationType != MEM_COMMIT)
&& (fdwAllocationType != MEM_RESERVE)
&& (fdwAllocationType != (MEM_RESERVE | MEM_COMMIT)) )
{
_DebugOut( “VirtualAlloc: bad flAllocationType\n\r”,
SLE_WARNING + FstopOnRing3MemoryError );
InternalSetLastError( ERROR_INVALID_PARAMETER );
return 0;
}
// Convert the fdwProtect flags into the PC_flag values used by
// VMM.VXD Pseudocode follows this function
pcFlags = mmPAGEToPC(fdwProtect);
if ( pcFlags == -1 ) // Something Error
return 0;
if ( lpvAddress == 0 ) // Don’t care where the memory is allocated.
{
// Reserve the memory block. startPage should be either
// PR_PRIVATE or PR_SHARED
lpvAddress = VxDCall( _PageReserve, startPage, sizeInPages, pcFlags );
if ( lpvAddress == -1 )
{
_DebugOut( “VirtualAlloc: reserver failed\n”,
SLE_WARNING + FStopOnRing3MemoryError );
InternalSetLastError( ERROR_NOT_ENOUGH_MEMORY );
return 0;
}
// If caller is just reserving, we’re finished.
if ( !(fdwAllocationType & MEM_COMMIT) )
return lpvAddress;
// Caller has specified MEM_COMMIT
if ( VxDCall(_PageCommit, lpvAddress>>12, sizeInPages, 1, 0, pcFlags) )
return lpvAddress; // Success!
// Oops. Something went wrong. Tell the user, then fall through
// to the code to free the pages.
_DebugOut( “Virtualloc: commit failed\n”,
SLE_WARNING + FStopOnRing3MemoryError );
InternalSetLastError( ERROR_NOT_ENOUGH_MEMORY );
}
else
{
if ( address > 0xBFFFFFFF )
{
_DebuOut( “VirtualAlloc: bad base address\n”,
SLE_WARNING + FStopOnRing3MemoryError );
InternalSetLastError( ERROR_INVALID_ADDRESS );
return 0;
}
fReserve = fdwAllocationType & MEM_RESERVE;
if ( fReserve )
{
// Call VMM _PageReserve to allocate the memory. Note that
// the caller-specified lpvAddress is rounded down to the
// nearest 4KB page. Note that it’s not down to 64KB like
// the doc says. However, _PageReserve still rounds it down.
lpvAddress = VxDCall(_PageReserve, address>>12, sizeInPages, pcFlags);
if ( lpvAddress == -1)
{
_DebugOut( “VirtualAlloc: reserve failed\n”,
SLE_WANING + FStopOnRing3MemroyError );
InternalSetLastError( ERROR_NOT_ENOUGH_MEMORY );
return 0;
}
// Hmmm….. It turns out that KERNEL32 will complain if you
// didn’t specify an address aligned on a 64KB boundary!
if ( lpvAddress != (address & 0xFFFF0000) )
{
_DebugOout(“VirtualAlloc: reserve in wrong place\n”,
SLE_ERROR);
}
}
if ( !(fdwAllocationType & MEM_COMMIT) )
{
return lpvAddress;
}
lpvAddress &= 0xFFFFF000;
if ( VxDCall(_PageCommit, lpvAddress >> 12, sizeInPages, 1, 0, pcFlags) )
return lpvAddress;
else
{
_DebugOut( “VirtualAlloc: commit failed\n”,
SLE_WARNING + FStopOnRing3MemoryError );
InternalSetLastError( ERROR_NOT_ENOUGH_MEMORY);
if ( !fReserve )
return 0;
}
}
// Unreserve the memory allocated earlier.
VxDCall( _PageFree, lpvAddress & 0xFFFF0000, 0);
return_0:
lpvAddress = 0;
return_lpvAddress:
return lpvAddress;
mmPAGEToPC
这个函数被VirtualAlloc、VirtualAllocEx、VirtualProtect等函数使用。它把定义在WINUSER.H中的PAGE_XXX标志(如PAGE_READONLY)转换为对应的PC_XXX标志。PC_XXX标志(Page Commit)标志定义与VMM.INC,被VMM的_PageCommit函数使用。
Windows 95所使用的标志之中,有一个用来表示此page是受保护的page。当程序企图对受保护的page做写入动作时,会引发一个page fault,于是操作系统必须提交(Commit)额外的内存到stack的底部,以允许stack向下成长。然而,很明显你不能够以VirtualAlloc请求一个受保护的page,因为mmPAGEToPC把PAGE_GUARD标志给去掉了。这个函数也忽略了PAGE_NOCACHE标志。mmPAGEToPC的主要内容就是简单的对应各种各样的PAGE_XXX标志。除了PAGE_NOACCESS之外,被转换的标志都含入了PC_USER位,意思是此page可以被ring3代码(User Level)存取。如果page应该是可以写入的,PC_WRITEABLE也会被放到返回的标志内。换一个说法,除了PAGE_NOACCESS之外,所有的PAGE_XXX标志都会被映射为PC_USER或PC_USER | PC_WRITEABLE。
VirtualFree
VirtualFree执行VirtualAlloc相反的功能。它可以把pages的状态从committed改变为reserved,或从committed改变为free,或从reserved改变为free。函数的第一部分首先检查传入的是否是合法的地址以及合理的大小。地址必须在3GB以下,大小必须小于2GB-4MB(即应用程序私有地址空间的大小)。
你可以制定MEM_RELEASE或MEM_DECOMMIT标志,但不能两者同时指定。MEM_RELEASE使得VirtualFree调用VMM’s_PageFree函数,将整个范围内的所有pages都“decommit”(如果需要的话)并“unreserve”。这种情况下,你必须指定大小为0,使得VirtualFree释放原先以VirtualAlloc分配的整块空间。如果你传入MEM_DECOMMIT进入,会使得VirtualFree调用VMM’s_PageDecommit,将某个范围内的pages“decommit”掉。
VirtualQueryEx
这或许是Windows 95中最俏皮讨好的函数了。它针对某个地址,提供内存类型方面的丰富信息。例如,给与进程地址空间中的任意地址,VirtualQueryEx可以告诉你哪一个EXE或DLL拥有此块内存。该函数可为任何一个指定的进程显示一张内存布局图。
VirtualQueryEx最初并不在Windows 95的Win32函数名单中,这对于开发系统层面工具软件(如调试器等)的厂商而言,真是令人震惊。幸运的是,Windows 95小组从善如流,最终还是纳入了VirtualQueryEx。
VirtualQueryEx将某个地址的信息填入MEMORY_BASIC_INFORMATION结构中。这个结构看起来像这样:
PVOID BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
SIZE_T RegionSize;
DWORD State;
DWORD Protect;
DWORD Type;
这些成员都在Win32文件中有所描述。这里我必须对其中一个成员特别加以说明。AllocationBase听起来或许不怎么样,然而它却是最重要的一个成员。技术上来说,它代表最初以VirtualAlloc分配而来的内存块的基地址。更重要的是,当VirtualQueryEx的lpvAddress参数落在一个EXE或DLL模块中时,AllocationBase就成为此EXE或DLL的基地址。也就是说,AllocationBase和EXE或DLL的HINSTANCE/HMODULE是一样的。NT SDK所附的PWALK程序就是利用这一点来遍历一个进程的地址空间,并且为各个区域贴上起拥有者(EXE或DLL)的名称。调试器则可以使用这个功能算出哪一个EXE或DLL关联到某个有问题地址。
VirtualQueryEx基本上只是调用VWIN32.VXD的第40h号Win32 Service(也就是VxDCall 002A0040)。这个Service内部调用VMM的_PageQuery函数。DDK文件中说_PageQuery函数需要一个参数,指向MEMORY_BASIC_INFORMATION结构。
或许是为了避免因为一个不适当的线程切换动作而返回不协调的值(在MEMORY_BASIC_INFORMATION结构中),所以VirtualQueryEx一开始就先取得Krn32Mutex,并在离开时释放之。它是以未公开的KERNEL32_EnterSysLevel和_LeaveSysLevel函数完成这些工作的。
第43h号VWIN32 Service,也是填写MEMORY_BASIC_INFORMATION结构,就比简单的_PageQuery外包函数更多一些。目前我还不能够确切地说它到底做了什么事情,然而很显然这个函数必须知道查询对象(进程)的当前线程的ring0 stack的地址。因此,在调用VWIN32 Service之前,VirtualQueryEx使用hProcess参数取得一个指针,指向进程的结构PDB。从那个地方,VirtualQueryEx取得当前线程的Thread Database,交给VWIN32 Service。有趣的是,在仔细观察第43h号VWIN32 Service数次之后,老实说,我未曾发现这个程序代码除了调用_PageQuery之外还作了些什么。
VirtualQuery和IVirtualQuery
VirtualQuery只不过是VirtualQueryEx的一个特例。VirtualQuery获取当前进程中某个地址的信息,而VirtualQueryEx可以取得任何进程的地址信息。
VirtualQuery的虚拟代码似乎没有什么值得说的,它只是检验参数的合法性。它看看一个被指针所指的缓冲区是否大到足够容纳MEMORY_BASIC_INFORMATION结构。假设答案是肯定的,VirtualQuery就跳到IVirtualQuery去。VirtualQuery先做参数检验,再跳到一个内部函数中去,这是System DLLs的许多函数的典型做法。例如:VirtualProtect也是如此。
不比某些在调试版中只做执行纪录用的函数,IVirtualQuery其实只是调用VirtualQueryEx,并以当前进程的handle作为第一参数。请注意,在Windows 95之中,IvritualQuery调用VirtualQueryEx。这和Win32s不同,后者的VirtualQueryEx调用VirtualQuery。其间的关键差异在于Win32s中每个进程共享相同的地址空间,所以VirtualQuery应该等同于VirtualQueryEx。
VirtualProtectEx
VirtualProtectEx可以改变一个committed page或一系列pages的存取保护状态。它可以处理任何进程,只要你有进程的handle。VirtualProtectEx和VirtualAlloc之间的关键差异在于前者假设你已经“committed”,你“正打算改变其状态”的那些pages,而VirtualAlloc允许你分配、提交(commit)然后指定处理某一个page或某一些pages。
VirtualProtectEx的虚拟代码十分直接了当。就像我所说过的其他virtual函数一样,它先以一些错误检验代码揭开序幕。函数中的检验代码要修改的地址范围是否小于2GB-4MB,起始地址是否小于0xC0000000。VirtualProtectEx的中心是调用VWIN32 Service 0x3F。这个Service最终调用VMM’s _PageModifyPermission。就像在VirtualQueryEx中一样,这个VWIN32 Call为了某些理由,期望获得一个指针,指向指定进程中的当前线程的ring0 stack。有一些代码用来决定这个ring0 stack是否即是我们在VirtualQueryEx中获得的。VirtualProtectEx也和VirtualQueryEx一样,在VWIN32 Call执行期间,取得并持有Krn32Mutex。
VWIN32 Service 0x3F返回被修改的pages的先前状态(如果调用成功的话)。然而,这个状态是以VMM的PC_XXX标志记录,而不是调用者所期望的PAGE_XXX标志。VirtualProtectEx因此做一次快速转换。最后,如果调用者有指定一个指针用来存放旧的page属性,函数代码就把那些PAGE_XXX标志拷贝进去。
VirtualProtect和IVirtualProtect
VirtualProtect是VirtualProtectEx的简化版本。只针对调用进程才有效。VirtualProtect事实上只做参数检验,真正的代码在IVirtualProtect之中。唯一在VirtualProtect中进行的检验工作是决定pfdwOldProtect指针是一个合法的DWORD或是0。
IVirtualProtect是VirtualProtectEx的外包函数。它所使用的hProcess是一个虚拟handle,用以表示当前的进程(0x7FFFFFFF)。
VirtualLock和VirtualUnlock
这两个函数并不存在于Windows 95中。在支持它们的Win32平台(如Windows NT)上,它们允许进程“PageLock”一个范围的pages。系统保证那些pages总是能够映射到实际的RAM。对于那种承担不起page fault的代价者(例如,对于时间非常吹毛求疵的设备驱动程序),帮助很大。
在Windows 95之中,VirtualLock和VirtualUnlock都跳到CommonUnimpStub代码中。那是一小段代码,所有为实现的Win32 APIs都会跳到那儿。CommonUnimpStub的影响是双重的。第一,在调试版中,KERNEL32会在终端上显示一个调试信息,像这样:
**** Unimplemented Win32 API : VirtualLock
第二个影响是清楚stack中适当的参数。在VirtualLock/VirtualUnlock一例中,清除的是8个二进制位。由于CommonUnimpStub所处理的APIs的参数个数并不全都相同,所以需要清楚地stack大小必须先让CommonUnimpStub知道。还可以通过CL寄存器的返回而得知。CL寄存器中存放的是经过编码的值,不是直接的二进制位的个数。
Win32的Heap函数
微软终于在Win32操作系统中放入了一些高级的Heap管理函数。DOS内存管理机制针对一个Heap建立一块空间,往往太久又太慢。Win16 GlobalAlloc的最小分配限额为20h个二进制位,并且受限于8192个selectors。Win16的LocalHeap比较适用于小量配置,但它最大又只能是64KB。此外,这些函数都没有内存泄漏追踪机制(leak tracking)或内存溢出(memroy overrun)的能力。
Win32 Heap函数优秀多了。在Windows 95中,每个区域只需额外消耗4bit,而理论上你可以产生一个高达2GB-4MB大小的Heap。此外,Windows 95的Win32 Heap为各种大小的区块维护了四个独立的自由链表,为的是避免内存碎片过多。另一个优点仅在调试版中才起作用,那是,每个被分配的区块都加上了额外的数据,使你能够轻易找出内存的泄漏、溢出情况,并知道是谁分配了这块内存。稍后我还会介绍如何使用这些额外的调试信息。不幸的是,唯一能够让内存溢出生效的是,使用一个晦涩的,只Windows 95才有的函数:HeapSetFlags。在我下笔的时候,微软的任何文件都没有提到过这个函数,但是我得到消息,说它将会被提供出来。
除了这些极佳的函数外,Windows 95也允许进程拥有一个以上的Heap。这使你得以方便的将你的内存分配以Heap区分不同的类型。这常常是避免内存碎片的一个好策略。由于Windows 95支持一个以上的Heap,所以你必须提供一个Heap Handle给任何Win32 Heap函数,用来表示你打算操作的对象。Heap Handle其实就是Heap的起始地址(线性地址)。
Windows 95 Heaps的另一个好性质是,它们可以成长。这种情况下,KERNEL32分配额外的内存,并将其与Heap产生关联。我称此额外内存为SubHeaps。图5-7显示了一个复杂的Heap。
Win32 Heap函数包括HeapAlloc、HeapFree、HeapReAlloc等等。或许你会以为,对于需要实现malloc、realloc、free、new、delete函数的编译器厂商而言,这些基本的Heap函数将是必然的选择。但,真是情况并非如此。Boland和Microsoft都回避将Win32 Heap函数用于其Runtime Library中。Win32 SDK Runtime Library(CRTDLL.DLL)是一个例外,它的malloc和free函数分别使用HeapAlloc和HeapFree。注意,NT和95所使用的CRTDLL.DLL是不同的版本。
更正启示:
本书即将付印之前,我发现Visual C++ 4.0使用Win32 Heap函数来完成其C/C++ Runtime Heap。
在Windows 95的Win32 Heap服务更上层,你会发现GlobalAlloc和LocalAlloc。它们都是以HeapAlloc家族函数完成的。LocalAlloc并不只是HeapAlloc的另一个包装,因为有些Win16程序员针对LocalAlloc分配而来的内存玩了一些难缠的把戏(译注:所谓的sub-allocation),Win32版的LocalAlloc必须考虑到向后兼容。我将在后续章节中详细讨论这个主题。在Win32 Heap函数的下层,使用的是VMM提供的有关于内存管理的Win32 VxD Services。然而,我并没有看到这些函数中有任何东西没办法在Virtual函数(稍早我讨论过的)实现出来。因为这一点,我相信Win32 Heap函数其实是Win32 Virtual函数的上层。有趣的是,VMM的_HeapXXX函数(提供Heap机制给VxD)所使用的Heap结构,与KERNEL32用于ring3进程的Heap结构相同。
Win32的Heap Header和Heap arenas
一个Windows 95 Heap的所有零组件都是从VMM_PageReserve所分配的内存中得来的。这块内存被分为两块。前面是一个Heap表头。这个表头(稍候我们会详细的看个清楚)内含一些用以管理Heap的信息,像是自由链表、Heap大小、Heap生成标志等等。表头之下便是Heap区块。每一个Heap区块一开始一个所谓的arena结构,内含该区块的信息。区块的头紧跟着前一个区块的尾部。所有的区块一直延伸到Heap空间的尾部,但并不是其中的每个Page都一定得映射到实际的RAM。图5-8展示了典型的Heap布局。
记住,每个Heap区块,不管是自由或是使用中,都以一个arena结构开始。其格式在Windows 95的调试版和零售版中并不相同。如果Heap区块是自由的,arena中还会出现一些额外的结构成员。这于是导致了arena布局的四种变化:retail free、retail in-use、debug free、debug in-use。
每个Heap Arena一开始都有一个DWORD,内含Heap的大小。这个大小包括arena自己使用的空间。然而你可以简单地取出第一个DWORD并以它当作区块的大小。这为什么?因为在这第一个DWORD中有一些位被用于和区块大小无关的项目。这个DWORD的较高位总是0xA0,其意义不甚清楚,我猜想它是一个“bit pattern”,用来告诉KERNEL32说这个arena是否被覆盖。其他与区块大小无关的位还有,因为所有的Heap区块的大小总是4的倍数,所以最低两个位总是用不到,可以当作某种标志。这两个位的实际意义如下:
第一位:如果设立,则表示此区块是自由的。0表示此区块已被分配。
第二位:如果设立,则表示此区块的前一个区块为自由区块。这个位只有在已被分配的区块中才会设立。若此区块是自由的,它就可以和前一个自由区块联结在一起。如果该位未被设立,则表示前一个区块不是自由的,所以不需要和本区块联结。
把所有位纳入考虑,很容易就可以算出区块的大小。这要把第一个DWORD和数值0x5FFFFFFC做AND运算即可,这个运算把DWORD中所有非用于指示区块大小的位都屏蔽掉。早先的一种作法是把第一个arena DWORD和数值~0xA0000003做AND运算。为计算出有多少内存还可被调用者使用,只要把区块大小减去arena大小即可。
Windows 95零售版(Retail Version)的in-use区块
这种区块的arena的类型是最简的:
DWORD size // OR’ed with 0xA0000000 or 0xA0000002
Windows 95零售版(Retail Version)的free区块
这个区块的arena类型与前一中相同,但是增加了prev和next成员
DWORD size // OR’ed with 0xA0000001
DWORD prev // Point to the previous heap arena
DWORD next // Point to the next heap arena
Windows 95调试版(Debug Version)的in-use区块
和零售版类似,但增加了些成员:
DWORD size
DWORD allocating EIP // The EIP value that called HeapAlloc/HeapReAlloc
DWORD thread number // The thread number (not ID) that allocated the block.
WORD signature // 0x4842 == “BH”
DWORD checksum // A checksum of the previous three DWORDs
这些成员用以跟踪内存溢出以及Heap被破坏等情况。“allocating EIP”成员存储了一个程序地址,区块就是在该处被分配的。这可以用来定点测试(where a block of code that somehow wsan’t free was allocated)。Thread number成员的作用类似,不过它是用来辨别那一个线程分配了这块空间。请注意,Thread number和Thread ID并不相同,或者是GetCurrentThreadId返回的值。Thread number是一个索引,指向当前的线程链表。你可以利用SoftICE/W的“THREAD”命令看到这个索引值。对于in-use区块儿烟,signature成员应该总是0x4842。如果不是,这个arena可能是被破坏了。
Arena的最后一个成员提供更有力的Heap破坏防范。这个成员内含三个DWORDs的校验总和。其算法在稍候说明ChecksumHeapBlock时有所描述。虽然这个成员永远有在维护,但是KERNEL32调试版却从来不会自动验证它,你必须推一下,它才动一下。这一性质,成为“paranoid heap corruption checking”,靠HeapSetFlags函数而决定作用与否。
Windows 95调试版(Debug Version)的free区块
这些区块的arena是零售版free arena和调试版in-use arena的混合。它也有prev和next成员,也有thread number、signature、checksum等成员。Signature从0x4842改为0x4846。Checksum的算法也有轻微改变。由于比调试版in-use arena多处一个DWORD,所以KERNEL32检验这个arena时,它使用前四个(而非前三个)DWORDs。
DWORD size
DWORD prev
DWORD thread number
DWORD signature
DWORD next
DWORD checksum
Windows 95的Heap Header(表头结构)
每个Heap的开始处是一个Heap表头结构。所谓Heap handle,例如,你以GetProcessHeap所获取的,只不过是个指针,指向Heap的表头结构而已。HeapCreate函数的主要工作,除了保留内存给Heap使用之外,就是将表头结构初始化。Windows 95的零售版和调试版的Heap表头结构大小是会变化的(但其格式变化不大)。紧跟在表头之后的就是第一个Heap区块的arena。
Windows 95零售版(Retail Version)的Heap表头
00h WORD dwSize
保留给此Heap的内存总量。每个进程都有一个默认Heap,大小是1MB+4KB。
04h DWORD nextBlock
如果调用HeapCreate时将dwMaximum参数指定为0,那么此Heap产生之后,大小还可以扩充。这种情况下,如果调用者要求的区块对此Heap而言太大的话,KERNEL32就会保留其它内存,并设定subheaps。Subheaps也使用Heap arena,但不使用整个表头结构。为了追踪这些SubHeaps,KERNEL32以链表存放它们。链表头就记录在主表头结构所在位置之中。指向一个Subheap的指针则放在每个subheap的04偏移位置。一旦heap被摧毁,KERNEL32会遍历链表中的每个subheaps,将它们的pages释放给系统。
08h FREE_LIST_HEADER_RETAIL freeListArray[4]
为了尽量减少内存碎片,并加速对自由区块的搜寻,每个Heap表头都维护有四个自由链表。分别管理32(0x20)bit以下、128(0x80)bit以下、512(0x200)bit以下、0xFFFFFFFF bit以下的自由区块。KERNEL32会针对最适当的链表展开搜寻动作。如果你需要一块24(0x18)大小的空间,KERNEL32搜寻第一个链表。如果你需要一块256(0x100)大小的空间,KERNEL32则搜寻第三个链表。
这四个自由链表是以四个简单结构所组成的数组来表示,该结构如下:
l DWORD maxBlockSize 这是链表中最大的区块。可以是0x20、0x80、0x200、0xFFFFFFFF。
l Free arena 这个arena和零售版的free arena类似,但是其size成员是0。prev成员指向第一个free arena。由于size是0,所以搜寻过程中从来不会把此arena列入考虑。
48h PVOID nextHeap
在Windows 95零售版的heap中,此成员是一个指针,指向一个以HeapCreate创建初来的heap。请注意,下一个heap并不是下一个subheaps(记录在04偏移处)而是一个万万整整如假包换的heap。除非你调用CreateHeap,否则此成员为0。
4Ch HCRITICAL_SECTION hCriticalSection
这个成员内含cirtical section handle。那是heap函数用来同步控制用的。请注意此成员存放的并不是CRITICAL_SECTION本身,而是一个指针,指向KERNEL32之中与cirtical section有关的一个内部结构。
50h CRITICAL_SECTION criticalSection
此成员内含CRITICAL_SECTION本身(定义于WINBASE.H)。当程序代码需要同步控制时,KERNEL32就把一个指向此成员的指针交给EnterCriticalSection。此结构的各成员的初始化动作是在进程调用InitializeCriticalSection时完成的。如果你不需要同步控制(例如你只有一个线程),你可以传入HEAP_NO_SERIALIZE标志给HeapCreate、HeapAlloc等函数。
68h DWORD unknown1[2]
此成员意义未明
70h BYTE flags
此成员内含标志值,可以交给HeapCreate:
HEAP_NO_SERIALIZE
HEAP_GROWABLE
HEAP_GENERATE_EXEPTIONS
HEAP_ZERO_MEMORY
HEAP_REALLOC_IN_PLACE_ONLY
HEAP_TALL_CHECKING_ENABLED
HEAP_FREE_CHECKING_ENABLED
HEAP_DISABLE_COALESCE_ON_FREE
Windows 95技术文件中只解释了其中的两个项目:HEAP_NO_SERIALIZE和HEAP_GENERATE_EXCEPTIONS
71h BTYE unknow2
此成员意义未明,可能是保留给扩充的HEAP_XXX标志使用。
72h WORD signature
在合法的Windows 95 Heap中,此成员应该是0x4948(“HI”)
Windows 95调试版(Debug Version)的Heap表头
调试版的Heap表头十分类似零售版的Heap表头。然而,内嵌在其中的free arena结构比较大些,也多了一些额外的成员。
详细的结构说明,这里就不列出了,请参考原书。