程序的地址空间 stack and heap

堆跟栈是个C++老话题了,但做应用程序的时候,也不怎么碰到问题,有时候用堆,有时候又觉得栈更方便点

总之,不要太离谱,都没啥问题,久而久之就不清楚怎么回事,稀里糊涂就过去了。

 

但,当你发展到一定程度的时候,比如要追求性能的时候,RAM受限的时候,此时就会关注这些细节,这往往也是架构师最基本素质之一

 

 

什么是堆?

堆(heap)

一般用于:是程序运行期间动态分配的 malloc/realloc  new的空间,相对栈来说这个空间比较大,但也不是无限大(或跟内存大小一致)

堆的生存期随进程持续性,从malloc/realloc new 到free delete一直存在

 

什么是栈?

栈(stack)

一般用于:函数或代码中的局部变量(非static变量)

栈的生存期随代码块持续性,代码块运行就给你分配空间,代码块结束,就自动回收空间。

值得注意:函数也是存在栈里面的。

 

堆栈的概念一般好理解,如果还是不能理解,可以参考以下代码:

main()  //函数,放在栈里面
{
    int b;    //栈。stack
    char s[] = "abc";    //栈 stack
    char *p2;    //栈 stack
    char *p3 = "123456";    //123456\0在常量区rodata,p3的栈上stack
    static int c = 0;    //全局(静态)初始化区 data
    p1 = (char*)malloc(10);    //堆 heap
    p2 = (char*)malloc(20);    //分配得来的10和20字节的区域就在堆区
    strcpy(p1,"123456");    //123456\0放在常量区 rodata,编译器可能会将它
}

 

以上是开胃菜,后面才是重点。

 

经典win32程序是如何分配地址空间的?

题目还是比较抽象,换个角度:printf("hello world!");   这个 “hello world”是存放在哪里?堆?栈?

 

经典win32最大能表示4G的地址,因此,也就最大支持4G内存空间。这4G大概可以分成以下部分:

请注意,内存地址的顺序。

 

这个也很重要,默认情况下是这样的,当然这些值大小可以在IDE手动修改的。

 

这里有个地方比较有趣。

从ELF到MMR有多少空间呢?

=(0x40000000-0x08048000)/(1024*1024)=895M

这个数字意味着什么?

意味着,如果我们一直new,最多也只能new 895M的空间(事实上肯定还不到,除了BSS Data ELF会占用以外,你代码其他堆的东西也会占用不少空间)

基于这个事实,我们是否又突然发现:有时候内存是足够的情况下,但new 了几百M 却是失败?对的,就是这个原因。

实际上不是这样的,据说windows下是2:2,也就是说最多2G;linux下是1:3,最多3G

也就是说,如果需要大容量内存的时候,系统会调整虚拟内容地址。但实际情况肯定到不了2G的(实测)

 

从图中,我们也看见,堆是自下而上的,栈是自上而下,两个是相向的,如果不是特殊情况下,分界线是0x40000000~~

 

进一步思考:

如果我们程序真的需要更大内存怎么办?

单个进程是有上限的,多个进程上限就是物理内存的上限。

也就是A进程里的内存地址跟B进程里内存地址,都是一样的规格,但即便数字是一样的,也不是指向同一块内存。

 

好了,再来看看栈。

再来张图:

 

插播一条知识点:

图中有几处 Random xxx offset,大概的意思是 随机偏移

 

那么问题来了,为什么要随机?不能固定吗?

其实,这也是为了安全考虑:

这是一种安全机制,ASLR(Address Space Layout Randomization),主要防止缓冲区溢出攻击。

也就是说,偶尔缓冲区溢出,也应该问题不大。

还有,就是地址不固定,从而导致无法推算,也在一定程度上保护了程序安全性。但准确来说,也带来调试分析上的麻烦。

Linux通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以免恶意程序通过计算获取访问栈、库函数等地址。

嗯,这也有些道理。

 

 

那么,除了堆跟栈,其他又是什么?都是干什么的?

先来看下,还有其他什么?

以下内容参考: https://blog.csdn.net/C_Modesty/article/details/106333322

 

  • 每个进程看到的地址空间都是一样的
  • .text 都是从 0x80048000开始
  • 内核地址空间都是 0xC0000000 ~ 0xFFFFFFFF
  • 用户栈都是向低地址增长
  • 堆都是向高地址扩展

1.1 0x00000000 ~ 0x80048000

不能给用户访问,这里面是一些C运行库的内容,访问会报 segment fault 错误。

这个结合实际情况:当出现段错误的时候,大概使用了  null指针,也有可能使用了 未new的指针

 

1.2 0xC0000000 ~ 0xFFFFFFFF


内核的逻辑地址,在用户态访问会出错,权限不够;如果向访问,需要切换到内核态,可以通过系统调用等方式;

系统调用代表某个进程运行于内核,此时,相当于该进程可以访问这段内存虚拟地址(实际上只能访问该进程的某个8KB的内核栈)。

 

1.3 Random stack offset、Random mmap offset、Random brk offset

这些随机值意在防止恶意程序.这是一种安全机制ASLR(Address Space Layout Randomization),主要防止缓冲区溢出攻击。

Linux通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以免恶意程序通过计算获取访问栈、库函数等地址。

 

1.4 Stack

内核空间下面就是用户栈 Stack 地址段,栈的最大范围可以通过 prlimit 命令看到,默认情况下是8MB。

 

1.5 Memory Mapping Segment

这块地址是用来分配内存区域的,一般是用来把文件映射进内存用的,但是你也可以在这里申请内存空间来使用。

mmap()系统调用把一个文件映射到 Memory Mapping Segment 内存地址空间中

也可以匿名直接申请一段内存空间使用,mmap()不一定要在 Memory Mapping Segment

进行申请,你也可以指定任意的内存地址,当然只要不跟已有的虚拟地址冲突就好;这个地址也一定要是000结尾,才能使得页对齐(1KB)。

这块区域是风水宝地,大部分软件的瓶颈在IO这里,通过mmp方式可以大大提高IO访问速度。


1.6 start_brk和brk(program break)

分别标识了堆的起始地址和结束地址。

在Linux中可以通过 brk()和 sbrk()这两个函数来改变 program break 的位置。

当我们在程序中调用 malloc()的时候,一般就是在内部调用 sbrk()来调整途中 brk 标识的位置向上移动。

当调用 free()来释放内存空间的时候,传递给 sbrk()一个负值来使堆的 brk 标识向下移动。

当然, brk()和sbrk()所做的工作远不是简单地移动 brk 标识,还要处理将虚拟内存映射到物理内存地址等工作。

glibc 中当申请的内存空间不大于 MMAP_THRESHOLD的时候,malloc()使用 brk()/ sbrk()来调整 brk

标识的位置,这个时候所申请到的空间确实位于图中的 start_brk 和 brk 之间;当所申请的空间大于这个阈值的时候,

malloc()改用 mmap()来分配空间,这个时候所申请到的空间就位于图中的 Memory Mapping Segment 这一段内。


1.7 BSS Segment、Data Segment、Text Segment

BSS Segment存放未初始化的静态变量,所以也就是可以随意读写;

可读可写
存储未初始化的全局变量和未初始化的static变量
bss段中数据的生存期随进程持续性
bss段中的数据一般默认为0

Text Segment 其实就是存放二进制可执行代码的位置,所以它的权限是读与可执行;

又称文本段,用来存放指令,运行代码的一块内存空间
此空间大小在代码运行前就已经确定
内存空间一般属于只读,某些架构的代码也允许可写
在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

Data Segment 存放的是静态常量,所以该地址段权限是只读。

可读可写
存储初始化的全局变量和初始化的static变量
数据段中数据的生存期是随程序持续性(随进程持续性)
随进程持续性:进程创建就存在,进程死亡就消失

新的进程内存布局(默认进程内存布局)导致了栈空间的固定,而堆区域和MMAP区域共用一个空间,这在很大程度上增长了堆区域的大小。
 

一般来说,还有rodata区域

只读数据
比如printf语句中的格式字符串和开关语句的跳转表。也就是你所说的常量区。例
如,全局作用域中的constintival=10,ival存放在.rodata段;再如,函数局部作用域中
的printf("Helloworld%d\n",c);语句中的格式字符串"Helloworld%d\n",也存放在.rodata
段。

int a = 0;//全局初始化区。data
char *p1;//全局未初始化区bss
main()
{
 int b;//栈。stack
 char s[]="abc";//栈stack
 char *p2;//栈stack
 char *p3="123456";//123456\0在常量区rodata,p3的栈上stack
 static int c=0;//全局(静态)初始化区data
 p1 = (char*)malloc(10);//堆heap
 p2 = (char*)malloc(20);//分配得来的10和20字节的区域就在堆区
 strcpy(p1,"123456");//123456\0放在常量区rodata,编译器可能会将它
}

 

压栈是怎么样工作?

进程地址空间中最顶部的段是栈,大多数编程语言将之用于存储局部变量和函数参数。

调用一个方法或函数会将一个新的栈桢(stackframe)压入栈中。栈桢在函数返回时被清理。

栈溢出常见几种:

  • 如果一个函数,是嵌套函数,且存在死循环嵌套,这样就会到栈溢出。(查一下)
  • 由于局部变量也是要压栈的,因此,如果栈空间剩余有限时,来一个大一点局部变量,也会导致栈溢出。(改成newdelete)

 

栈不够之后,程序会崩溃吗?

一般据说8M(这个可以设置的)

开始的时候,栈不够时并不会崩溃,一般系统会捕获,产生页故障(pagefault)并被Linux的expand_stack()处理,它会调用acct_stack_growth()来检查是否还有合适的地方用于栈的增长。

并调用一个方法,尝试去找一些空间。如果不够的话,会产生栈溢出(stackoverflow),程序收到一个段错误(SegmentationFault)

此时,如果程序,仍然不处理,会导致栈不再回收了,那就比较麻烦,可能随便都会崩溃。


数组越界会导致什么结果?

个人见解:

1.首先,越界之后做了什么。如果仅仅是读取,那么应该就是读取内容不对(不可预见),也可能是下一个变量的值或半个值

2.如果是进行赋值操作的话:

     a)看看该数组是存放在哪里,如果是new出来的,是放在堆里,此时会破坏下一个new出来的变量(为什么是下一个呢?是因为堆地址是从小到大,你溢出了,自然破坏到下一个对象里面),问题很严重,有时候也很隐蔽,毕竟大家都是new出来的,下次轮到你的时候,会重新new之类。

     b)如果该数组是局部变量,放在栈里面,同时放在栈里面,还有函数,所以此时可能会破坏函数,导致函数异常。也有可能是上一个 存放在栈里的变量

--------------

以上现象:分明我这个变量没有修改,被莫名其妙的修改了。


)如果该数组是static,还要分初始化未初始化的,越界了,会导致data或bss错误之类。

d)还有就是越界也越区了,那就更加离谱了,比如你访问rodata区的话,可能就会段错误。

e)总之要分析越界到哪里,往往越界本身系统是捕获不到的,但越界后果可以被系统捕获到,但也很多情况下是没有后果的,

比如说:越界地方也没有被申请,你用了就用了吧。也有些地方是考虑对齐的,对不齐的地方,会有几个字节预留,此时,越界一点点,也没有什么问题

f) 静态检查工具,可以检查部分错误,但复杂点不行


常见段错误有哪些?

  • 访问不存在内存地址:比如空指针,然后进行操作
  • 访问系统包含地址(尝试去改):地址指向一个固定值,然后进行操作
  • 访问只读内存地址:指向常量,然后去修改
  • 栈溢出:

 

当程序出现崩溃时,如何设计一套机制,可快速定位到问题?

  1. 监控程序,程序崩溃会产生dump文件,
  2. 后台程序可以将dump文件传输过来
  3. 然后用windbg或GDB进行分析,大致猜测崩溃位置

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值