进程的虚拟地址空间

每个程序运行起来后,都拥有一个自己的虚拟地址空间(注意是虚拟的,不是实际存在的),这个虚拟地址空间的大小由计算机的硬件平台

关于虚拟的概念,当时IBM给出了一种说法很形象生动:
它存在,你能看得见,它是物理的
它存在,你看不见,它是透明的
它不存在,你却看得见,它是虚拟的
它不存在,你也看不见,它被删除了!

决定,具体地说是由CPU位数决定的。硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小,比如32位的硬件平台决定了虚拟地址空间的地址为0到2^32 - 1,即0x00000000~0xFFFFFFFF,也就是我们常说的4GB虚拟空间大小,也就是它只有4GB的寻址能力。
现在让我们来看一下x86体系下32位Linux环境中进程虚拟地址空间:
在这里插入图片描述
了解了地址空间,我们就能对程序有更深入的理解。
这里介绍一下每一节中存放的内容:

.text:已编译程序的机器代码
.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表
.data:已初始化的全局和静态变量。注意局部变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中
.bss:未初始化的全局和静态变量,以及所有被初始化为0的全局或静态变量。这个节不占据实际的空间,它仅仅是一个占位符。
区分已初始化和未初始化变量是为了空间效率,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0

一种记住.data和.bss节之间区别的简单方法是把“bss”看成是“更好地节省空间(Better Save Space)的缩写”(.bss中的值都为0)

举个例子:

int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main()
{
	int a = 12;
	int b = 0;
	int c;

	static int e = 13;
	static int f = 0;
	static int g;

    cout << c << g << endl;
    //c不为零,g为零
    
	system("pause");
	return 0;
}

根据上面的规则,各个变量存放在哪块内存呢?

gdata1和gdata4初始化且不为0,存放在.data段
gdata2、gdata3、gdata5、gdata6未初始化或初始化为0,存放在.bss段

a,b,c不会产生符号,编译成指令,指令存放在.text段
mov dword ptr[a], 0ch

e初始化了,且不为0,存放在.data段
f和g初始化为0,或未初始化,存放在.bss段

注意一点,虽然a变量是局部变量,存放在栈上,
但有一点需要明确a = 12编译后生成指令,存放在.text段,只是在运行时,在栈上开辟空间

知道了每节存放的内容,或许就能更好的理解程序
比如,为什么有时会发生栈溢出(stack overflow)?
栈向低地址增长,栈空间是有限的,当递归函数递归太深就会爆栈(关于函数调用是如何开辟栈空间的之后再谈)

为什么程序常常会出现”段错误(segment fault)“或”非法操作,该内存地址不能read/write“的错误信息?
这往往是因为指针指向一个不允许读或写的内存地址,而程序却试图利用指针来读写该地址的时候,就会出现这个错误。在Linux或Windows的内存布局中,有些地址是始终不能读写的,例如0地址。

还有这样的程序为什么报错?

char *p = "hello, world";
*p = 'a';

因为”hello, world“作为字符串常量存放在只读数据段中,修改只读数据段当然不可以。
等等,当我们之后遇到此类问题时,都可以想想它在内存中是怎么存储的,从而明白为什么出错。
一些小点:
1.栈通常只有数兆字节的大小,而堆一般比栈大很多可以有几十至数百兆字节的容量。程序使用mallocnew分配内存时得到的内存都来自堆里。
2.Windows在默认情况下会将高地址的2GB空间分配给内核(也可配置为1GB),而Linux默认情况下将高地址的1GB空间分配给内核。

思考,数据什么时候可以放在栈上,什么时候又需要放在堆上呢?
栈是程序运行的基础,函数调用的过程中,通过移动栈指针开辟足够的空间,用来存放函数使用到的局部变量,以及函数中使用到的通用寄存器的副本(以便在函数调用结束后,通过副本可以恢复到函数调用之前)。编译器编译并优化代码时,一个函数就是一个最小的编译单元,所以在编译阶段,编译器就得知道要存放哪些局部变量和寄存器,以便预留空间。所以编译期可以确定大小的”放在“(预留,运行时开辟)栈上,对于无法确定大小或大小可以改变的数据,最好放在堆上。除了动态大小的需要放在堆上,动态生命周期的内存也需要分配到堆上。因为栈上的内存在函数调用结束后,会被回收(实则是改变栈指针,数据并不会被清空),对于栈上内存的生命周期是不受开发人员控制的,局限在当前调用栈中。而堆上开辟的内存需要显式释放,因此堆上的内存就有更加灵活的生命周期,可以在不同的调用栈之间共享数据。总结下来,栈上的数据是静态的,大小固定,生命周期固定,而堆上的数据是动态的,大小不固定,生命周期不固定。

以上便是进程的虚拟地址空间的介绍,这里只关注了几个常用的节,关于这部分知识点很多,之后继续总结。
正文结束,这里通过一个小例子来测试指针在不同平台下、不同类型下的大小:
一般来说,C语言指针大小的位数与虚拟空间的位数相同,如32位平台下的指针为4字节,64位平台下的指针为8字节。

这里有一个经典的面试题,就是问指针大小,比如32位操作系统下,int *p和char *p,p的大小是多大?
都是4字节,因为p是指针类型,p存放的是内存地址,与类型无关。

32位环境下:
在这里插入图片描述
64位环境下:
在这里插入图片描述
参考资料:
[1] 龚奕利,贺莲译.深入理解计算机系统[M].北京:机械工业出版社,2016.
[2] 俞甲子,石凡,潘爱民著.程序员的自我修养:链接、装载与库[M].北京:电子工业出版社,2009.4

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高二的笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值