堆和栈

简介

  C/C++,一般内存模型从低到高分别为:text代码段,data全局/静态已初始区域,bss全局/静态未初始化区域,heap堆,stack栈,内核区。如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SOXhmeCO-1578724361581)(https://i.niupic.com/images/2020/01/11/6h4a.png)]

为什么栈向下增长

  堆和栈之间有很大的地址空间空闲着,在需要分配空间的时候,堆向上涨,栈往下涨。

  **这样设计可以使得堆和栈能够充分利用空闲的地址空间。**如果栈向上涨的话,我们就必须得指定栈和堆的一个严格分界线,但这个分界线怎么确定呢?平均分?但是有的程序使用的堆空间比较多,而有的程序使用的栈空间比较多。所以就可能出现这种情况:一个程序因为栈溢出而崩溃的时候,其实它还有大量闲置的堆空间呢,但是我们却无法使用这些闲置的堆空间。所以呢,最好的办法就是让堆和栈一个向上涨,一个向下涨,这样它们就可以最大程度地共用这块剩余的地址空间,达到利用率的最大化!

为什么栈要比堆快

  栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

为什么要使用栈来保存函数参数或者局部变量

  因为局部变量和函数参数都是有其作用域的,当出了作用域由系统自动释放,而且释放比较简单只需要移动栈顶指针即可。

  在VS下,函数参数入栈是从右往左(一般都是用__cdecl和__stdcall),然后在压入函数的地址(4个字节),而函数内部的局部变量是从上往下压入栈的。

如何验证栈的增长方向

  一般上栈地址是高到低,堆地址是低到高,但是这是依赖于硬件的。

  那如何验证栈内存的生长方向呢,可以用函数来验证(因为函数会用到栈)。一般会想到函数里的局部变量,先声明的先入栈,如果第一个变量的地址如果是高的,则从上往下增长,但是先声明先入栈也要依赖于编译期。同样对于函数参数压栈顺序也要依赖于编译期的实现。

  函数如何调用。执行一个函数时,这个函数的相关信息都会出现栈之中,比如参数、返回地址和局部变量。当它调用另一个函数时,在它栈信息保持不变的情况下,会把它调用那个函数的信息放到栈中。所以,两个函数的相关信息位置是固定的,肯定是先调用的函数其信息先入栈,后调用的函数其信息后入栈。设计两个函数,一个作为调用方,另一个作为被调用方。被调用方以一个地址(也就是指针)作为自己的入口参数,调用方传入的地址是自己的一个局部变量的地址,然后,被调用方比较这个地址和自己的一个局部变量地址,由此确定栈的增长方向。

  为什么一个函数解决不了这个问题。函数的相关信息会一起送入栈,这些信息就包括了参数、返回地址和局部变量等等,在计算机的术语里,有个说法叫栈帧,指的就是这些与一次函数调用相关的东西,而在一个栈帧内的这些东西其相对顺序是由编译器决定的,所以,仅仅在一个栈帧内做比较,都会有对编译器的依赖。

栈帧

  机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储。为单个过程(函数调用)分配的那部分栈称为栈帧。栈帧其实 是两个指针寄存器,寄存器%ebp为帧指针(指向该栈帧的最底部),而寄存器%esp为栈指针(指向该栈帧的最顶部),当程序运行时,栈指针可以移动(大多数的信息的访问都是通过帧指针的,换句话说,就是如果该栈存在,%ebp帧指针是不移动的,访问栈里面的元素可以用-4(%ebp)或者8(%ebp)访问%ebp指针下面或者上面的元素)。总之 一句话,栈帧的主要作用是用来控制和保存一个过程的所有信息的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VuiatE5p-1578724361582)(https://i.niupic.com/images/2020/01/11/6h4l.jpg)]

  此处注意:这里面有一个错误,即:“保存的寄存器、局部变量和临时值”处应该是ebp-4。并且最下面的esp应该是栈指针ebp。

  假设过程P(调用者)调用过程Q(被调用者),则Q的参数放在P的栈帧中。另外,当P调用Q时,P中的返回地址被压入栈中,形成P的栈帧的末尾 (返回地址就是当程序从Q返回时应该继续执行的地方)。Q的栈帧从保存的帧指针的值开始,后面到新的栈指针之间就是该过程的部分了。

函数参数压栈顺序

  压栈顺序一般是从右到左,但这也与编译器有关。

  C语言为什么压栈顺序是从右到左。这样的好处就是可以动态变化参数个数。以printf()函数举例:printf(const char* format,…)。如果知道其参数个数,需要靠format,编译器通过format中%占位符的个数来确定参数个数。

  现在我们假设参数的压栈顺序是从左到右的,这时,函数调用的时候,format最先进栈,之后是各个参数进栈,最后pc进栈,此时,由于format先进栈了,上面压着未知个数的参数,想要知道参数的个数,必须找到format,而要找到format,必须要知道参数的个数,这样就陷入了一个无法求解的死循环了!

char s[] = "123", *p;  
p  = s;   
printf("%c%c%c\n", *p++, *p++, *p++);   // 输出是321  
栈溢出

  从物理上讲,栈是就是一段连续分配的内存空间。在一个程序中,会声明各种变量。静态全局变量是位于数据段并且在程序开始运行的时候被加载。而程序的动态的局部变量则分配在堆栈里面。

 &emsp从操作上来讲,栈是一个先入后出的队列。他的生长方向与内存的生长方向正好相反。我们规定内存的生长方向为向上,则栈的生长方向为向下。压栈的操作push=ESP-4,出栈的操作是pop=ESP+4.换句话说,堆栈中老的值,其内存地址,反而比新的值要大。请牢牢记住这一点,因为这是堆栈溢出的基本理论依据。

  在一次函数调用中,堆栈中将被依次压入:参数,返回地址,EBP。如果函数有局部变量,接下来,就在堆栈中开辟相应的空间以构造变量。函数执行结束,这些局部变量的内容将被丢失。但是不被清除。在函数返回的时候,弹出EBP,恢复堆栈到函数调用的地址,弹出返回地址到EIP以继续执行程序。(但是这个顺序也依赖编译器实现)

  栈溢出是指向向栈中写入了超出限定长度的数据,溢出的数据会覆盖栈中其它数据,从而影响程序的运行。栈的大小一般为1M-2M。为什么栈溢出会影响程序,因为栈溢出会将栈中元素数据覆盖,比如说返回地址,则程序当然可能会被影响。

栈溢出例子

不要再构造函数中去调用构造函数

class AA {  
public:    
    AA() { AA(0); }    
    AA(int x)  : m_val(x) {}    
    int  m_val;     
};     
int main() {    
    AA  a;    
    cout <<  a.m_val << endl;  
}  

  为这里的AA(0)其实是创造的了一个临时对象,对于自己的对象毫无影响。构造函数的相互调用引起的后果不是死循环,而是栈溢出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值