1.内存分布概述
每个进程都有自己的虚拟地址空间,对于32位进程来说,这个地址空间的大小为4GB,这是因为32位指针可以表示从0x00000000到0xFFFFFFFF之间的任意一值。对于64位进程来说,由于64位指针可以表示从0x00000000’00000000到0xFFFFFFFF’FFFFFFFF之间的任一值,因此这个地址空间大小为16EB。因为每个进程都有自己专用的地址空间,当进程中的各线程运行时,它们只能访问属于该进程的内存。线程既看不到属于其他进程的内存,也无法访问它们。
2.虚拟地址空间分区
应用程序虽然有这么大的地址空间可用,但是这只是虚拟地址空间,不是物理存储器。这个地址空间只不过是一个内存的地址区间。
每个进虚拟地址空间被划分为许多分区(partion)。由于地址空间的分区依赖于操作系统的底层实现。因此会随着Windows内核的不同而略有变化。
分区 |
x86 32位 Windows |
3G用户模式下的 x86 32位Windows |
x64 64位Windows |
IA-64 64位的 Windows |
空指针 赋值分区 |
0x00000000 0xFFFFFFFF |
0x00000000 0xFFFFFFFF |
0x00000000’00000000 0x00000000’0000FFFF |
0x00000000’00000000 0x00000000’0000FFFF |
用户模式 分区 |
0x00010000 0x7FFEFFFF |
0x00010000 0xBFFEFFFF |
0x00000000’00010000 0x000007FF’FFFFFFFF |
0x00000000’00010000 0x000006FB’FFFEFFFF |
64K禁入 分区 |
0x7FFF0000 0x7FFFFFFF |
0xBFFF0000 0xBFFFFFFF |
0x000007FF’FFFF0000 0x000007FF’FFFFFFFF |
0x000006FB’FFFF0000 0x000006FB’FFFFFFFF |
内核模式 分区 |
0x80000000 0xFFFFFFFF |
0xC0000000 0xFFFFFFFF |
0x00000800’00000000 0xFFFFFFFF’FFFFFFFF |
0x000006FC’00000000 0xFFFFFFFF’FFFFFFFF |
从上表中可能看到,32位Winows内核和64位Windows内核的分区基本一致,唯一的不同在于分区的大小和分区的位置。
2.1.空指针赋值分区
这一分区是进程地址空间中从0x00000000到0x0000FFFF的闭区间,保留该分区的目的是为了帮助程序捕获对于空指针的赋值。没有任何办法可以让我们分配到位于这一地址空间的虚拟内存,即使是使用Win32的应用程序编程接口(appliction programming interface,通常简称API)也不例外。如果进程中的线程试图读取或写入于这一分区内的内存地址,就会引发访问违规。
如果malloc无法分配足够的内在,那么它会返回NULL。地址空间中的这一分区是禁止访问的,所以会引发内存访问违规并导致进程被终止。这一特性可以帮助开发人员发现应用程序中的缺陷。
2.2.用户模式分区
这一分区是进程地址空间的驻地,可用的地址空间和用户模式分区的大小取决于CPU体系结构。进程无法通过指针来读取、写入或以任何方式,访问驻留在这一分区中的其他进程的数据。对所有应用程序来说,进程的大部分数据都保存在这一分区。由于每个进程都有自己的数据分区。因此一个应用程序破另一个应用程序的可能性就非常小,从而使得整个系统更加坚固。
在早期版本的Windows中,Microsoft不允许用户程序访问2GB以上的地址空间,为了让此类应用程序即使在用模式分区大于2GB的环境下仍能正常运行,Microsoft提供代了一种模式来增大用户模式分区,最多不超过3GB。
当前系统即将运行一个应用程序时,它会检查应用程序在链接时是否使用了/LARGEADDRESSAWARE链接器开关。如果是,则相应于应用程序在声明它会充分利用大用户模式地址空间,而不会对内在地址进行任何不当的操作。反之,如果应用程序在链接时没有使用/LARGEADDRESSAWARE开关,那么操作系统会保留用户模式分区中2GB以上到内核模式开始处的整个部分。
2.2.1.程序内存使用分布
可参考https://en.wikipedia.org/wiki/Data_segment#Program_memory
2.2.1.1.Text
【原文】
The code segment, also known as a text segment or simply as text, is where a portion of an object file or the corresponding section of the program's virtual address space that contains executable instructions is stored and is generally read-only and fixed size.
代码段,通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读。
在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
代码段,也称为文本段,或简称为文本,是指存储有可执行指令的对象文件或程序的虚拟地址空间的相应部分的一部分,并且通常是只读的和固定大小的。
2.2.1.2.Data
【原文】
The .data segment contains any global or static variables which have a pre-defined value and can be modified. That is any variables that are not defined within a function (and thus can be accessed from anywhere) or are defined in a function but are defined as static so they retain their address across subsequent calls. Examples, in C, include:
int val = 3;
char string[] = "Hello World";
The values for these variables are initially stored within the read-only memory (typically within .text) and are copied into the .data segment during the start-up routine of the program.
数据段包含任何具有预定义值并可被修改的全局变量或静态变量。这是任何函数中未定义的变量(因此可以从任何地方访问)或在函数中定义,但被定义为静态的,因此它们在随后的调用中保留它们的地址。C中的例子包括:
int val = 3;
char string[] = "Hello World";
这些变量的值最初存储在只读存储器(通常在.text中),并在程序的启动例程期间复制到.DATA段。
2.2.1.3.BSS
【原文】
The BSS segment, also known as uninitialized data, is usually adjacent to the data segment. The BSS segment contains all global variables and static variables that are initialized to zero or do not have explicit initialization in source code. For instance, a variable defined as static int i; would be contained in the BSS segment.
BBS段,也称为未初始化数据段,通常与数据段相邻,包含初始化为或在源码中未进行显式初始化的全局变量和静态变量。例如,定义为静态整型变量i将被包含在BSS段中。
2.2.1.4.Heap
【原文】
The heap area commonly begins at the end of the .bss and .data segments and grows to larger addresses from there. The heap area is managed by malloc, calloc, realloc, and free, which may use the brk and sbrk system calls to adjust its size (note that the use of brk/sbrk and a single "heap area" is not required to fulfill the contract of malloc/calloc/realloc/free; they may also be implemented using mmap/munmap to reserve/unreserve potentially non-contiguous regions of virtual memory into the process' virtual address space). The heap area is shared by all threads, shared libraries, and dynamically loaded modules in a process.
堆区域通常在.BSS和.DATA段的末尾开始,并从那里扩展到更大的地址。堆区域由Maloc、CaloC、ReLoLc和空闲管理,可以使用BRK和SBRK系统调用来调整其大小(注意使用BRK/SBRK和单个堆区)不需要履行MalC/CaloC/ReAlOLC/FILE的合同;也可以使用MMAP/MunMMAP来实现。将虚拟内存的潜在非相邻区域保留/不保留到进程的“虚拟地址空间”中。堆区域由进程中的所有线程、共享库和动态加载模块共享。
2.2.1.5.Stack
【原文】
Main article: Call stack
The stack area contains the program stack, a LIFO structure, typically located in the higher parts of memory. A "stack pointer" register tracks the top of the stack; it is adjusted each time a value is "pushed" onto the stack. The set of values pushed for one function call is termed a "stack frame". A stack frame consists at minimum of a return address. Automatic variables are also allocated on the stack.
The stack area traditionally adjoined the heap area and they grew towards each other; when the stack pointer met the heap pointer, free memory was exhausted. With large address spaces and virtual memory techniques they tend to be placed more freely, but they still typically grow in a converging direction. On the standard PC x86 architecture the stack grows toward address zero, meaning that more recent items, deeper in the call chain, are at numerically lower addresses and closer to the heap. On some other architectures it grows the opposite direction.
主要文章:调用堆栈
堆栈区域包含程序栈,即LIFO结构,通常位于内存的较高部分。一个“堆栈指针”寄存器跟踪堆栈的顶部;每当一个值被“推”到堆栈时,它就被调整。一个函数调用所推的值集称为“堆栈框架”。堆栈帧包括返回地址的最小值。自动变量也被分配到堆栈上。
堆栈区域传统上毗邻堆区域,它们彼此生长;当堆栈指针遇到堆指针时,空闲内存被耗尽。随着大的地址空间和虚拟内存技术,它们倾向于更自由地放置,但它们仍然通常在收敛的方向上生长。在标准的PC x86架构上,堆栈向地址零增长,这意味着在调用链中较近的项在数值较低的地址上更接近堆。在其他一些架构上,它生长了相反的方向。
2.3.内核模式分区
这一分区是操作系统代码的驻地,与线程调度、内存管理、文件系统支持、网络支持以及设备驱动程序相关的代码都载入到该分区。在这一分区内的任何东西为所有进程共有。虽然这一分区就在每个进程中用户模式分区的上方,但该分区中的所有代码和数据都被完全保护起来。如果一个应用程序试图读取或写入这一分区中的内存地址,会引发违规。在默认情况下,访问违规会导致系统先向用户显示一个消息框,然后结束应用程序。
3.堆栈段说明
3.1.栈(Stack)
当系统创建线程时,会为线程栈预订一块空间区域(每个线程都有自己的栈),并给区域调拨一些物理存储器。默认情况下,系统会预订1MB的地址空间并调拨两个页面的存储器。但是,在构建应用程序时开发人员可以通过两种方法来改变该默认值,一种方法是使用Microsoft C++编译器的/F选项,另一种方法是使用Microsoft C++链接器的/STACK选项。在构建应用程序时,链接器会把想要的栈的大小写入到.exe或.dll文件的PE文件头中。当前系统创线栈的时候,会根据PE文件头中的大小来预订地址空间的区域。但是在调用CreateThread或_beginthreadex函数时,开发人员也可以另外指定需要在一开始就调拨的存储器数量。这两个函数都有一个参数,可以用来指定一开始要调拨给线程栈的地址空间区域的存储器的大小。如果该参数设为0,那么系统会使用PE文件头中指定的大小,所使用的都是默认值(即区域大小为1MB),每次调拨一个存储页面。
3.2.堆(Heap)
堆非常适用分配大量的小型数据。堆是用来管理链表和树的最佳方式。堆的优点是它能让我们专心解决手头上的问题,而不必理会分配粒度和页面边界这类事情。堆的缺点是分配和释放内存块的速度比其他方式慢,而且也无法对物理存储器的调拨和摊销进行直接控制。
进程初始化时候,系统会在进程的地址空间中创建一个堆,这个堆被称为进程的默认堆(Default heep)。在默认情况下,这个堆的地址空间区域的大小是1MB。但是系统可以增大进程的默认堆,使它大于1MB。我们也可以在创建应用程序的时候用/HEAP链接器开关来改变默认区域大小。由于动态链接库(.DLL)没有与之关联的堆,因此在创建DLL的时候不应使用/HEAP开关。