Windows 95 System Programming SECRENTS学习笔记(十)

类别:编程语言 点击:0 评论:0 推荐:

接下来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结构比较大些,也多了一些额外的成员。

    详细的结构说明,这里就不列出了,请参考原书。

本文地址:http://com.8s8s.com/it/it23675.htm