Skip to content
Article
Authors
On this page
Published on

进程虚拟地址空间划分

Article
Authors

引言

  • .text段(代码段):存放指令,只读。
  • .rodata段,即只读段,一般用来存放字符常量和const修饰的变量,只读。const char*p = "hello world", *p就是放在只读段,不可修改。
  • .data段存放已初始化的全局变量和已初始化的局部静态变量,且初始化的值不为0
  • .bss段存放未初始化的全局变量和未初始化的局部静态变量,或者初始化为0的全局变量和初始化为0局部静态变量。系统会把存放在该段的数据全都置为0,所以未初始化的全局变量或局部静态变量的值为0

注:以下代码的均执行在 x86系统 32位linux环境下

cpp
#include<iostream>
 
int gdata1 = 10;	// .data
int gdata2 = 0;	// .bss
int gdata3;	// .bss
 
static int gdata4 = 11;	// .data
static int gdata5 = 0; // .bss
static int gdata6;	// .bss
 
int main()
{
	int a = 12;
	int b = 0;
	int c;	// 打印c 会是无效值,下面的f会是0
 // 上面三行语句局部变量不会在符号表中产生符号,对应生成的是指令,比如第一行 mov dword ptr[a], 0ch, 所以这三行在.text 中
 // 程序运行时会加载在stack上
  
	static int d = 13; // .data
	static int e = 0;  // bss
	static int f; // bss
  cout<< g << endl;	// text
	return 0;	// text
}

首先我们定义了三个全局变量,然后定义了三个静态的全局变量,且我们将他们的值分别初始化为不为0的值,为0,和未初始化的。

接着我们又在主函数内部定义了三个局部变量和三个静态的局部变量,并且我们将他们的值也分别初始化为不为0的值,为0,和未初始化的。

接着当这段代码进行了 预处理->编译->汇编->链接后,就会产生一个可执行文件 xxx.exe,此时这个可执行文件存放在磁盘上,当我们运行这个可执行文件时,系统就会将这个可执行文件从磁盘加载到内存当中。在我们看来这个过程非常的简单,但从计算机的角度来看,这里面涉及了非常多的问题,例如,系统会将可执行文件的哪些东西加载到内存当中呢?加载到内存当中又是如何存放的呢?具体存放在内存的哪一个区域呢?存放在内存当中和存放在磁盘当中有什么区别呢?

这里还需要注意,当把这个可执行文件加载到内存时,不是直接加载到物理内存当中的。而是linux系统会给当前进程分配一个2^32位大小的一块空间,即4G。而这块空间被称为 进程的虚拟地址空间。

物理:存在看得见

透明:存在看不见

虚拟:不存在看得见

img

内核空间的默认大小为1G,但如果有需要,内核空间的大小可以被调整,用户空间的默认大小为3G。

【在用户空间中】

0x0000 0000~0x08048000 这段空间是不可以被访问的。我们通常指的零地址,即NULL,就是这块不可访问的地址。如果访问了这快地址,系统就会警告访问异常,导致程序崩溃。

.text段(代码段):存放指令,只读。

.rodata段,即只读段,一般用来存放字符常量和const修饰的变量,只读。const char*p = "hello world", *p就是放在只读段,不可修改。

.data段存放已初始化的全局变量和已初始化的局部静态变量,且初始化的值不为0

.bss段存放未初始化的全局变量和未初始化的局部静态变量,或者初始化为0的全局变量和初始化为0局部静态变量。系统会把存放在该段的数据全都置为0,所以未初始化的全局变量或局部静态变量的值为0

我们知道未初始化的全局变量和局部静态变量默认值都为0,本来它们也可以被放在data段的,但是因为它们都是0,所以为它们在data段分配空间并且存放数据0是没有必要的。程序运行的时候它们的确是要占内存空间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为bss段。所以bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。

stack:栈是由高地址向低地址增长的

heap:堆是由低地址向高地址增长的

总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和bss段属于程序数据。

很多人可能会有疑问:为什么要那么麻烦,把程序的指令和数据的存放分开?混杂地放在一个段里面不是更加简单?其实数据和指令分段的好处有很多。主要有如下几个方面

一方面是当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。 另外一方面是对于现代的CPU来说,它们有着极为强大的缓存( Cache)体系。由于缓存在现代的计算机中地位非常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。 第三个原因,其实也是最重要的原因,就是当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只须要保存一份改程序的指令部分。对于指令这种只读的区域来说是这样,对于其他的只读数据也一样,比如很多程序里面带有的图标、 图片、文本等资源也是属于可以共享的。当然每个副本进程的数据区域是不一样的,它们是进程私有的。不要小看这个共享指令的概念,它在现代的操作系统里面占据了极为重要的地位,特别是在有动态链接的系统中,可以节省大量的内存。比如我们常 用的 Windows Internet Explorer7.0运行起来以后,它的总虚存空间为11284KB,它的私有部分数据为15944KB,即有96900KB的空间是共享部分。如果系统屮运行了数百个进程,可以想象共享的方法来节省大量空间。 【内核空间中中】

ZONE_DMA:直接内存访问区,即从这块内存中获取数据时,不需要使用寄存器。16M

ZONE_NORMAL:常用部分,这一部分存放虚拟空间和内存的映射关系。800多M

ZONE_HIGHMEM:高端内存,内核中映射超过1GB的文件时使用。


上述代码中的 gdata1和gdata4已初始化且初始化不为0,因此存放在.data段,而gdata2,gdata5已初始化但初始化为0,以及gdata3,gdata6未初始化都存放在.bss段。且这些全局变量都会生成相应的符号。

a,b,c这些局部变量不会生成符号,而会生成指令,而指令是存放在.text段中的,只有当这些指令运行的时候才会由寄存器加载到栈上。d初始化且初始化不为0,所以存放在.data段,相应的e和f存放在.bss段。

注意:每一个进程的用户空间是私有的,但是内核空间是共享的。

在上述的图中,

.bss段和堆区之间存在偏移,图中未画出,

堆区和加载共享库之间从在偏移,图中未画出,

加载共享库和栈区之间从在偏移,图中未画出,

栈区和命令行参数,环境变量之间存在偏移。图中未画出,

命令行参数,环境变量和内核空间之间存在偏移,图中未画出。

2024 - future