内容总结: 一般来讲,应用程序使用的内存空间里有如下“默认”的区域。 1. 栈:栈用于维护函数调用的上下文,离开了栈函数调用就没法实现。栈通常在用户空间的最高地址处分配,通常有数兆字节的大小。 2. 堆:堆是用来容纳应用程序动态分配的内存区域,当程序使用 malloc或new分配内存时,得到的内存来自堆里。堆通常存在于的下方(低地址方向),在某些时候,堆也可能没有固定统一的存储区域。堆一般比栈大很多,可以有几十至数百兆字节的容量。 3. 可执行文件映像:这里存储着可执行文件在内存里的映像,由装载器在装载时将可执行文件的内存读取或映射到这里。 4. 保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称,例如,大多数操作系统里极小的地址通常都是不允许访问的,如NULL通常C语言将无效指针赋值为0也是出于这个考虑,因为0地址上正常情况下不可能有有效的可访问数据。
栈在程序运行中具有举足轻重的地位。最重要的,栈保存了一个函数调用所需要的维护信息,这常常被称为堆栈帧(Stack Frame)或活动记录(Activate Record),堆栈帧一般包括如下几方面内容: 1. 函数的返回地址和参数 2. ·临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。 3. 保存的上下文:包括在函数调用前后需要保持不变的寄存器。
在参数之后的数据(包括参数)即是当前函数的活动记录,ebp固定在图中所示的位置,不随这个函数的执行而变化,相反地,esp始终指向栈顶,因此随着函数的执行,esp会不断变化。固定不变的cbp可以用来定位函数活动记录中的各个数据。在ebp之前首先是这个函数的返回地址,它的地址是ebp4,再往前是压入栈中的参数,它们的地址分别是ebp-8、ebp-12等,视参数数量和大小而定。ebp所直接指向的数据是调用该函数前ebp的值,这样在函数返回的时候,ebp可以通过读取这个值恢复到调用前的值。
一个调用惯例一般会规定如下几个方面的内容。 1. 函数参数的传递顺序和方式,函数参数的传递有很多种方式,最常见的一种是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方,将参数压栈的顺序:是从左至右,还是从右至左有些调用惯例还允许使用寄存器传递参数,以提高性能。 2. 栈的维护方式,在函数将参数压栈之后,函数体会被调用,此后需要将被压入中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成。 3. 名字修饰(Name -mangling-)的策略,为了链接的时候对调用惯例进行区分,调用管理要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。 堆是一块巨大的内存空间,常常占据整个虚拟空间的绝大部分。在这片空间里,程序可以请求一块连续内存,并自由地使用,这块内存在程序主动放弃之前都会一直保持有效。
Windows的进程将地址空间分配给了各种EXE、DLL文件、堆、栈。其中EXE文件一般位于0x004000起始的地址;而一部分DLL位于0x10000000起始的地址,如运行库DLL;还有一部分DLL位于接近0x800000位置,如系统DLL, NTDLLDLL、 Kernel32.DLL. 每个线程的栈都是独立的,所以一个进程中有多少个线程,就应该有多少个对应的栈,对于 Windows来说,每个线程默认的大小是1MB,在线程启动时,系统会为它在进程地址空间中分配相应的空间作为栈,线程栈的大小可以由创建线程时 Create Thread的参数指定。
当然,在 Windows下我们也可以自己实现一个分配的算法,首先通过 VirtualAlloc向操作系统一次性批发大量空间,比如10MB,然后再根据需要分配给程序。不过这么常用的分配算法已经被各种系统、库实现了无数遍,在 Windows中,这个算法的实现位于堆管理器(HeapManager)堆管理器提供了一套与堆相关的API可以用来创建、分配、释放和销毁堆空间:
- HeapCreate:创建一个堆。
- HeapAlloc:在一个堆里分配内存。
- HeapFree:释放已经分配的内存。
- HeapDestroy:摧毁一个堆。
《程序员的自我修养》
|