《Windows核心编程》---又是内存

一般情况下,应用程序使用的内存空间里有以下“默认”的区域:

1)栈:用于维护函数调用的上下文,离开了栈函数调用就没法实现。栈通常在用户空间的最高地址处分配,通常有数兆字节的大小;

2)堆:用来容纳应用程序动态分配的内存区域,当程序使用mallocnew分配内存时,得到的内存来自堆里。堆通常存在于栈的下方(低地址方向),在某些时候,堆也可能没有固定统一的存储区域,堆一般比栈大很多,可以有几十到数百兆字节的容量;

3)可执行文件映像:存储着可执行文件在内存中的映像。由装载器在装载时将可执行文件的内存读取或映射到这里。

4)保留区:这并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称,例如,大多数操作系统里,极小的地址通常都是不允许访问的,如NULL。通常C语言将无效指针赋值为0也是出于这个考虑,因为0地址上正常情况下不可能有有效的可访问数据。

 

Linux下,如果可执行文件依赖其他共享库,那么系统就会为它在从0x40000000开始的地址分配相应的空间,并将共享库载入该空间。在Linux中,栈向低地址方向增长,堆向高地址方向增长。

 

栈:

在经典的操作系统中,栈总是向下增长的。在i386下,栈顶由称为esp的寄存器进行定位。压栈操作使栈顶地址减小,弹出操作使栈顶地址增大,即栈的生长方向由高地址到低地址。

栈在程序设计中具有举足轻重的地位,栈保存了一个函数调用所需要的维护信息,这常常被称为堆栈帧(Stack Frame)或活动记录(Active Record)。堆栈帧一般包括如下几方面的内容:

1)函数的返回地址和参数;

2)临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量;

3)保存的上下文:包括在函数调用前后需要保持不变的寄存器;

i386中,一个函数的活动记录用ebpesp这两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也指向了当前函数的活动记录的顶部,而相对地,ebp寄存器指向了函数活动记录的一个固定位置,ebp寄存器又被称为帧指针(Frame Pointer)

一个i386下的函数总是这样调用的:

1)把所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递;

2)把当前指令的下一条指令的地址压入栈中;

3)跳转到函数体执行。

 

i386函数体的标准开头是这样的:

1push ebp:把ebp压入栈中(称为old ebp);

2mov ebp, espebp=esp(这时ebp指向栈顶,而此时栈顶就是old ebp);

3)【可选】sub esp, XXX:在栈上分配XXX字节的临时空间;

4)【可选】push YYY:如有必要,保存名为YYY的寄存器(可重复多个);

ebp压入栈中,是为了在函数返回时便于恢复以前的ebp;之所以可能要保存一些寄存器,在于编译器可能要求某些寄存器在调用前后保持不变。

在函数返回时,所进行的标准结尾与标准开头正好相反:

1)【可选】pop YYY:如有必要,恢复保存过的寄存器(可重复多个);

2mov esp ebp:恢复esp同时回收局部变量空间;

3pop ebp:从栈中恢复保存的ebp的值;

4ret:从栈中取得返回地址,并跳转到该位置。

 

我们在VC下调试程序时,常常会看到一些没有初始化的变量或内存区域的值是“烫”,这是因为分配的栈空间的每一字节都被初始化为0xCC,而0xCCCC(即两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC如果被当作文本看待就是“烫”。将未初始化数据设置为0xCC的理由是这样可以有助于判断一个变量是否没有初始化。如果一个指针变量的值是0xCCCCCCCC,那么我们就可以基本相信这个指针没有经过初始化。当然,有时编译器还会使用0xCDCDCDCD作为未初始化标记,此时我们就会看到汉字“屯屯”。

 

堆:

Windows的进程将地址空间分配给了各种EXEDLL文件、堆、栈。其中EXE文件一般位于0x00400000起始的地址;而一部分DLL位于0x10000000起始的地址,如运行库DLL;还有一部分DLL位于接近0x80000000的位置,如系统DLLNTDLL.dllKernel32.dll。栈的位置则在0x00030000EXE文件后面都有分布,这是因为Windows中每个线程的栈都是独立的,一个进程中有多少个线程,就应该有多少个对应的栈,每个线程默认的栈大小是1MB,在线程启动时,系统会为它在进程地址空间中分配相应的空间作为栈,线程栈的大小由函数CreateThread函数指定。

在分配完上面这些地址后,Windows的地址空间已经是支离破碎了,当程序向系统申请堆空间时,就只好从这些剩下的还没有被占用的地址空间上分配了。Windows提供VirtualAlloc()函数来向系统申请空间,事实上,VirtualAlloc()申请的空间不一定只用于堆,它仅仅是向系统预留了一块虚拟地址,应用程序可以按照需要随意使用。

在使用VirtualAlloc()函数申请空间时,系统要求空间大小必须是页的整数倍,即对于x86系统来说,必须有是4096字节的整数倍。显然,这将会造成内存碎片。此时Windows为我们提供了一个更合理的分配算法,这个算法实现位于堆管理器(Heap Manager)中,堆管理器提供了一套与堆相关的API用于创建、分配、释放和销毁堆空间:

HeapCreateHeapAllocHeapFreeHeapDestroy(详见:http://blog.csdn.net/ACE1985/archive/2010/07/25/5764917.aspx

堆管理器实际上存在于Windows的两个位置,一份是位于NTDLL.DLL中,这个DLLWindows操作系统用户层的最底层DLL,它负责Windows子系统DLLWindows内核之间的接口;而在Windows内核Ntoskrnl.exe中,还存在一份类似的堆管理器,它负责Windows内核的堆空间分配,内核堆管理器的接口都是由RtlHeap开头的。

 

堆分配算法:如何管理一大块连续的内存空间,如何能够按照需求分配空间,如何释放已申请的空间。

1)空闲链表:

实际上就是把堆中各个空闲的块按照链表的方式连接起来,当用户请求一块空间时,可以遍历链表,直到找到合适大小的块并且将它拆分;当用户释放空间时将它合并到空闲链表中。

空闲链表是这样一种结构,在堆里的每一个空闲空间的开头有一个头(header),头结构里记录了上一个(prev)和下一个(next)空闲块的地址,所有空闲块形成了一个链表。

 

2)位图:

核心思想是将整个堆划分为大量的块,每个块的大小相同。当用户申请内存时,总是分配整数个块的空间给用户,第一个块我们称为已分配区域的头(head),其余的称为已分配区域的主体(body)。而我们可以使用一个整数数组来记录块的使用情况,由于每个块只有头/主体/空闲三种状态,因此仅仅需要两位即可表示一个块,如用11表示Head10表示Body00表示Free;因此称为位图。

 

3)对象池:

如果每一次分配的空间大小是一样的,那么就可以按照这个每次请求分配的大小作为一个单位,将整个堆空间划分为大量的小块,每次请求的时候只需要找到一个小块就可以了。对象池的管理方法可以采用空闲链表,也可以采用位图,与它们的区别仅仅在于它假定了每次请求的都是一个固定的大小。

 

posted on 2010-07-29 11:21  android开发实例  阅读(219)  评论(0编辑  收藏  举报

导航