Windows 95 System Programming SECRENTS学习笔记---第五章(4)

GetProcessHeap

    使用一个Win32 Heap函数,首先你得有一个heap handle。大部分程序都使用KERNEL32在程序产生时给与的一个默认堆(default heap)。你可以调用GetProcessHeap获得其Heap handle。这个函数很简单,它取出KERNEL32的一个全局变量,指向当前进程的process database。其中存放有进程的默认堆的句柄(handle)。

 

HeapAllocIHeapAlloc

    HeapAlloc,如其名称所示,你可以利用它从某个heap中分配一块内存。它其实只是做参数检验的工作,真实分配内存是由IHeapAllocHPAlloc完成。HeapAlloc检查hHeap所代表的Heap的大小是否足够容纳一个Heap表头。虽然它也可以检查其它成员如signaturechecksum,但很奇怪的是它忽略那些成员。如果hHeap通过测试,HeapAlloc就调用IHeapAlloc

 

    IHeapAlloc其实只是HPAlloc的外包函数。后者才是真正的HeapAlloc“主体”。在调用HPAlloc之前,IHeapAllocdwFlags(调用者传入的标志)做了一些处理。唯一可能幸存的是HEAP_ZERO_MEMORYHEAP_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、初始化prevnext等等。分裂出来的其中一块刚好够大,满足HPAlloc的需求。另一块被放到自由链表之中。

 

    hpCarve返回之后,HPAlloc将新的区块的arena成员初始化。除了“取得HPAlloc调用者的EIP”,以及“计算前三个成员的checksum”之外,其余只是一些简单的设定动作。最后,HPAlloc释放heap的临界区对象,返回一个指针,指向arena之后的第一个字节。

 

如果HPAlloc没有发现一个自由区块的话,并且heap允许自动增长(产生堆时,dwMaximumSize被指定为0),HPAlloc则需要产生一个新的sunheap。先前我说过,一个subheap是另一个独立空间,内含一些heap区块。KERNEL32负责跟踪所有的subheaps,作法是把它们统统维护在一个链表之中。如果需要产生subheapKERNEL32会决定其初始大小(通常是4KB),并调用VMM报留一些pages。接下来HPAlloc调用HPInit将新的subheap的表头初始化。初始化之后,HPAlloc把它安插到subheaps所组成的链表中。最后,HPAlloc跳到“搜寻自由链表”的起始处。我想,这一次应该可以找到足够大小的区块了。

 

Windows 95中,Win32堆中的内存最初都处于reserved,并为committed。如果我们有1MB堆并且在还不需要这1MB内存时,并不会提交实际的内存页。当程序触及被reserved而尚未被committedpage时,会引发page fault。因此,堆函数必须确定所有使用中的区块涉及到的pages都作了commit动作。在Windows 95中,系统并不是用结构化异常来做commit动作。

 

HeapSizeIHeapSize

    HeapSize获得一个指针,指向先前分配的一块内存,并返回其大小(不含arena)。

 

HeapFreeIHeapFree

    HeapFree仅负责参数检验。真实的动作发生在IHeapFreex_HeapFree中,除了释放指定的堆内存,该函数还会检查是否有必要进行自由区块的合并。

 

HeapReAllocIHeapReAlloc

    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堆的过程可分为两个部分。第一部分是保留内存并将其连接到进程的堆链表上,另一部分是初始化堆表头。

 

HeapDestroyIHeapDestroy

    与前面介绍的函数类似,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 XPWindows 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函数

Win32Local HeapGlobal Heap函数都是Win16遗留下来的。在Windows 95中其实已无需要。Win16Local Heap之所以产生是为了让EXEDLL可以不需要改变selector就找到其Heap内容。Global Heap之所以存在则是因为没有办法在不处理selector的情况下分配大块的内存。Windows 95中的Win32程序没有这两个限制,所以Win32 API可以免除这两组函数。

 

然而我们知道,Win32 API因为向下的兼容性作了某些妥协。有太多的Win16程序使用Global HeapLocal Heap函数。如果把它们从Win32 API中去除,会使得程序的移植工作走不出实验室,因此微软决定保留它们。

 

绝大部分而言,Windows 95Local HeapGlobal Heap函数是完全一致的。也就是说,GlobalAllocLocalAlloc虽然同为输出函数,但使用相同的KERNEL32.DLL地址。GlobalFreeLocalFree也是相同的情况。

 

Windows 95中的Global Heap函数几乎没有功能,大部分时候,它们只是直接跳进其对应的Local Heap函数。要不就是像GlobalAlloc那样,根本就和对应的Local Heap函数共享同一个进入点。大部分函数都会接受一个HGLOBAL参数。GlobalAllocLocalAlloc分配的内存都是从HPAlloc而来。

 

笔记:

关于Local HeapGlobal 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 HeapGlobal Heap函数并没有出现在上面的列表中,原因很简单,没有存在的必要了。可以肯定地讲,在基于Windows NT内核的操作系统中,这些函数的实现细节肯定会和Windows 95有所不同。

 

 

 

 

 

杂项函数:

WriteProcessMemoryReadProcessMemroy

这是两个被核准用来读写另一个进程的内存数据的函数。为了使用他们,你必须先获得另一个进程的句柄(handle)。然而Win32 API不提供什么方便的做法,让你轻易达到这一目的。这两个函数是Win32调试器的关键函数。调试器是属于那种“必须读写其他进程内存数据”的另类软件。J

 

这两个函数的底层十分类似。因此我决定只展示其中一个的虚拟代码。唯一明显的差异在于WriteProcessMemory调用VWIN320x002A0017 Service,而ReadProcessMemory调用VWIN320x002A0016 Service

 

WriteProcessMemory首先做同步控制。它先确定它没有持有Win16MutexKrn32Mutex,然后进入“必须完成”状态意思是:不能在执行过程中被切换出来。然后确定源地址(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)的大小等等。这个函数很类似于Win16MemManInfo

 

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 VMLocal Descriptor TablesLDTs)。这是特定的descriptor table,用来记录Win32程序的flat程序代码区和数据区。Win16程序也是通过它取得它们的代码区和数据区,以及GlobalAlloc句柄。这个函数对于任何探索系统的工具软件,都是非常有用的一个利器。

 

需要注意的是,这个函数只能用于采用x86 CPU的系统中。这里提到的System VM指的就是x86虚拟机环境。

 

如果你传给GetThreadSelectorEntry一个合法的selector,就可以取回一个8bits的结构,格式与LDT descriptor相同。由于Win32程序有一个flat指针,可以到达任何地方,所以可利用此函数将16:16地址转换为一个flat32地址。于是Win32程序可以读取该处的内容,也可以将数据写入该处。你甚至可以将Win16GetSelectorBaseGetSelectorLimit函数构建成属于你自己的Win32版。

 

GetThreadSelectorEntry函数中主要的趣味在LDT AliasLDT Ptr这两个变量身上。这是KERNEL32.DLL的两个全局变量。LDT Ptr内含System VMLDT的线性地址,LDTAlias则是一个selector值,拥有对selector table的内存的读写权力。

 

 

C/C++编译器提供的mallocnew

许多时候,程序员会忽略操作系统提供的内存管理函数,使用C Runtime Library(特别是mallocfree函数)来做内存管理。C++呢?就我所知,所有PC上的C++编译器提供的new运算符都被直接对应到mallocdelete运算符则被对应到free。问题在于,这些函数如何使用底层的操作系统的能力呢?

 

在这一章中,我已经告诉你,堆函数(如HeapAllocHeapFree)是多么接近mallocfree。这是否意味着mallocfree只是HeapAllocHeapFree的另一个包装吗?至少到Viusal C++ 4.0为止,答案是否定的。唯一的例外的是CRTDLL.DLL---微软的C Runtime Library。在CRTDLL.DLL之中,mallocnew都只是简单的调用HeapAllocfreedelete则调用HeapFreeCRTDLL.DLL使用于许多标准的Windows NTWindows 95EXEsDLLs身上。这是一个伟大的想法,让微软不必为了“不同的EXEDLL身上有不同的C Runtime Library”而伤脑筋。

 

不幸的是,C编译器厂商不捧场,他们没有让每个人都使用CRTDLL.DLL。因此,每个EXE之内还是可能各有一个C Runtime Library。这种情况短时间内不会改变,所以我们最好知道这些runtime library的底层在做些什么。

 

我并不打算细究mallocfree。取而代之的是,我将给你足够的信息,让你足以进行自我判断,该如何设计你自己的内存管理体系。

 

到目前为止,我已经能够确定。BorlandMicrosoft以类似的方法实做其runtime libraryHeaps。事实上,除了大小之外,整个形势并没有和Windows 3.1时代有太多改变。每个EXEDLL都有它自己的Heap。程序如果使用三个DLLs,就会有四个独立的HeapsEXE有一个,DLLs各有一个)。DLL所分配的内存将来自DLLHeap。这和Win32HeapAlloc不同,后者一定是从Exe Heap中分配内存(假设你总是将进程的默认堆丢给它)。

 

C编译器的RTLsRuntime 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的做法。如果你需要复习,请回头看看hpCarvehpCommit这两个函数。

 

RTL并不会在你每次调用malloc时就调用VirtualAlloc。它们需要设定并维护内部结构并持续追踪什么区块被分配了,什么不是….。同时也保持一个自由链表,以便能够快速进行分配。

 

当你使用RTL提供的堆时,一个潜在的问题是其生命期。当一个DLL被剔出内存并收到DLL_PROCESS_DETACH消息时,Runtime Library会调用VirtualFree,将堆占用的内存释放掉。如果另一个DLL有个指针指向此块内存中的某个区块,指针会突然间失效。如果该DLL稍后也被剔出内存,并在其DLL_PROCESS_DETACH处理过程中使用了这些指针,程序就会完蛋了,而且很不容易发现这种错误。这段心得是从有痛苦经验的人那儿学来的。

 

那么,回答我最初的问题,malloc基本上是Windows 95HeapAlloc函数的编译器版本。但至少有两个关键差异。第一,每个EXEDLL都有它们自己的堆,由RTL提供,而HeapAlloc所分配的内存一概来自系统所设定的进程默认堆。在某种操作程序下,使用RTL的堆可能会造成问题。这并不是说你应该避免使用mallocnew,只是要告诉你它们是什么,以及让你了解其中的潜在性问题。

 

笔记:

   这两个函数,都属于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

 

 
-----------------第五章完-----------------

你可能感兴趣的:(windows,System,dll,library,编译器,Descriptor)