0%

进程内存布局

进程的用户空间

Windows系统中,系统空间与用户空间的分界线为0x80000000,该地址以上为系统空间,以下为用户空间,各占2GB(默认情况下)。2GB的用户空间不是全部可访问的,它被划分成了以下几个部分:

用户空间的两端各有64KB的隔离区,是不允许访问的。上端的隔离区将用户空间与系统空间隔离开,下端的隔离区就是我们编程时遇到的NULL,因此NULL实际所表示的区域不是一个地址,而是一个64KB的地址范围。这样,实际可访问的用户空间地址范围为0x10000-0x7ffeffff。

进程的内存布局

程序加载到内存后的布局如下图所示:

(注:图片来自潘爱民编著的《Windows内核原理与实现》)

程序中不同的部分在上图中占据不同的空间。主要为以下几部分:

  1. 代码段。为程序编译后的机器码指令,然后映射到进程地址空间。
  2. 已初始化数据段。在程序中已经显式初始化的全局变量和静态变量。这些变量的值在编译时保存于可执行文件中,程序加载到进程中时读取这些变量的值,映射到相应的内存空间。
  3. 未初始化数据段。程序中声明而没有初始化的全局变量和静态变量。为了缩小可执行文件的大小,这些变量的值在编译时是不会保存到可执行文件中,可执行文件中只记录了所需要的大小,加载程序时才分配空间,映射到相应的内存空间。
  4. 堆。运行时动态为变量分配的内存,如new和malloc,堆是由低地址向高地址增长的。
  5. 栈。运行时函数调用占用的内存,每个当前调用的函数占据一个栈帧,其中保存了局部变量,参数,返回地址等。栈是由高地址向低地址增长的。

《Linux/UNIX编程手册》第6章中用实例进行了讲解,这里就不重复列出来了。该章的末尾有一个练习,说示例中声明了一个10MB的数组,为什么编译后的可执行文件大小远小于10MB?如下:

1
static char mbuf[10240000];

这就是前面所说的未初始化的静态变量,在可执行文件中不会保存实际的数据,因而只占用很少的空间。

程序可自由分配的空间

不知道大家有没有产生这样的疑问,结合前面所述的,用户空间除去隔离区域,其它都是编程时可以自由支配的吗?首先明确一点,可自由支配的空间是一定的,因此,不要认为new一定会成功,如果不断地分配而不释放,它总会失败的。而且,大家编程时也遇到过无限递归的错误,无限递归同样造成栈帧的分配导致空间耗尽,所以同样会失败。所以,我们到底可以支配多少空间呢?

回答这个问题之前,先看上面的图,想想编程时几乎必不可少的一个东西——动态链接库(DLL),我们知道,一般情况下,进程只能访问自己的地址空间,而程序要访问动态链接库(包括Windows提供的和我们自己定义的)中的函数和变量,就必须把DLL映射到自己的地址空间,因而,这些动态链接库会消耗一部分空间。此处讲一点题外话,不同的进程使用同一个DLL都会映射到自己的地址空间,但是同一个DLL在物理页面上只有一份,这是通过共享映射区(Section)实现的,使用了同一个DLL的进程的虚拟页面对应同一组物理页面。

如果程序使用到大量DLL,则会占用比较多的空间。同时,进程还有一些自己的数据需要保存,如用户空间的进程结构,这些空间会从堆上分配。因此,程序实际可以自由支配的空间还要压缩。

下面我们实际对一个进程的虚拟地址空间中的空闲页面进行统计,看看有多少可以分配的空间。Windows提供了函数VirtualQuery可以查询当前进程空间中虚拟页面的信息,具体的用法就不说了。下面是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <Windows.h>
#include <stdio.h>

int main( int argc, char* argv[] )
{
SIZE_T retVal; // VirtualQuery返回值
MEMORY_BASIC_INFORMATION mbi; // 返回页面信息
DWORD dwStartAddress = 0x0; // 起始地址
DWORD dwTotalFreeSize = 0; // 空闲页面总大小

while( true )
{
ZeroMemory(&mbi, sizeof(MEMORY_BASIC_INFORMATION));
retVal = VirtualQuery( (LPCVOID)dwStartAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION) );

if( 0 == retVal ) // 返回0表示失败
break;

// 判断mbi中的State标识,累加为MEM_FREE的区间
if( MEM_FREE == mbi.State )
{
dwTotalFreeSize += mbi.RegionSize;
}

// 下一个区间
dwStartAddress += mbi.RegionSize;
}

// 输出总的空闲页面大小
printf("%lfGB\n", (double)dwTotalFreeSize/1024/1024/1024);

system("pause...");

return 0;
}

运行结果:

你可以分配全局变量来查看结果的变化。说明一点,上面我写的程序只统计了MEM_FREE的页面,实际上还要判断页面的访问类型,除去特殊如无法访问的页面。


本文地址:http://xnerv.wang/process-memory-layout/
转载自:进程内存布局