关于堆和栈

老生常谈了,引用了一些博客的描述,说的比较清晰。

网上很多文章都引用到了下面这段代码:

int a = 0; //全局初始化区
char *p1; //全局未初始化区
main()
{ 
    int b; //栈
    char s[] = "abc"; //栈
    char *p2; //栈
    char *p3 = "123456"; //123456\0在常量区,p3在栈上
    static int c =0; //全局(静态)初始化区
    p1 = (char *)malloc(10); //堆
    p2 = (char *)malloc(20);  //堆
}

并且如果你搜索关键词“内存堆栈图”,将很容易找到下面这张图:

 

 

程序在内存中的模型

在一个汇编程序中,常常把一个用户空间程序按习惯分为三个段:.data段,.bss段,.text段。

.data段

.data段包含了已经初始化了的数据项,这些数据在程序开始运行前就拥有自己的值,这些值是可执行文件的一部分,当可执行文件被加载到内存中用于执行时,这些数据也被加载到内存中。

定义的初始化数据越多,可执行文件就越大,运行它的时候也就需要更长的时间才能将它们从磁盘加载到内存。

一些全局或者静态的,且经过定义初始化过的变量,就属于该段。例如下面代码中的a,指针p以及b三个变量:

int a = 2;
int *p = &a;

int main () 
{
    static int b = 1;
...
...
    return 0;
}

.bss段

并不是所有数据项在程序开始之前都拥有值,例如你可以定义一个缓冲区来存在某些数据,这个缓冲区是.bss段中定义的。

分别定义.data段.bss段中的数据,它们一个重要的区别就是:.data段中的数据会添加到可执行文件的大小上,而.bss段中的则不会。即便你给.bss段定义一个1M字节的缓冲区,其最终可执行文件大小也几乎不变(除了大约50个字节用于描述外)。

程序在加载时知道哪些数据项没有初值,它会为这些数据项分配空间,而具有初值的数据线会与其初值一同读入。

一些全局或者静态的,且未经过初始化的变量,属于.bss段。例如上文中.data段段的三个变量,如果不进行初始化,就会存储在本段中。

.text段

以上两个段都是源程序所需要的数据,而真正组成程序的机器指令则存放在.text段中。一般情况下,在.text段中不进行数据项的定义。.text段包含名为标号(label)的符号,这些符号用于标识跳转和调用程序代码位置。

程序内存中的堆栈

先附上笔者在学习汇编时的一张笔记图,字比较丑,望各位见谅。

 

程序内存中的堆栈附笔记

 

通过该笔者图大家能够大概了解内存中上述三个段的位置。至于其中的堆栈以及笔记的含义请继续阅读后文。

编程过程中常见的几类变量

观察最开头经常被引用的那段代码,其中涵盖了几类最常见的变量以及其对应的存储位置。在上一小节中,我们已经说明了全局变量和静态变量存储的位置取决于是否进行过初始化。对于堆栈的解释我们留到下一小节。这里我们着重讲解文字常量区。

文字常量区

考虑如下代码:

char *p3 = "123456"; //123456\0在常量区,p3在栈上

这个文字常量区是什么?显然它与字符串存放有关。所谓字符串是指位于连续内存区域中的一个字符序列。字符串通过在起始处关联一个标号来进行定义。在汇编中,常见的字符串定义如下:

MSG: db "something"

它是位于.data段中的。和.data段中的所有变量一样,它也是一种已经初始化的数据:带有一个值,而不仅仅是一个在将来某时刻用于存放数据的内存空间。MSG标号和DB指令在内存中指定一个字节作为字符串的起点,而字符串中的字符数则告诉汇编编译器为该字符预留多少个字节的存储空间。

但高级语言中的字符串可能要比这里复杂一点,以C语言为例,针对printf函数中包含的字串。笔者认为其存储于.data段和.text段之间的一个名叫.rodata段的地方。即那张常见的“堆栈内存图”中底部绿色的“只读区”

大家可以发现,现在引出了更多的背后细节。因此,更为深入的说明我会留到第四个小节:底层的原理。

堆和栈的细节

下面进入第三小节,讲解堆和栈,这也是最开头代码中仍未涉及的两种变量存储位置。注意:我们在汇编中常说的堆栈,其实是栈,并不包含堆。

在此之前,推荐大家看一下stackoverflow的这个问答What and where are the stack and heap?

栈由系统管理。但是为什么呢?

首先,栈是一个后进先出(LIFO)结构。当把数据放入栈时,我们把数据push进入;当从栈取出数据时,我们把数据pop出来。栈随着数据被压入或者弹出而增长或者减小。最新压入栈的项被认为是在“栈的顶部”。当从栈中弹出一个项时,我们得到的是位于栈最顶部的那一个。就像给弹夹上子弹,只能在顶部进行操作。

在x86体系中,栈顶由堆栈指针寄存器ESP来标记,它是一个32位寄存器,里面存放着最后一个压入栈顶的项的内存地址。正因为有它,我们才能够随时操作到需要的项。需要注意的是,栈顶是朝着地内存方向增长的

再来看我拍的照片,为于.bss段之间有一段空余内存,C程序经常使用这种剩余内存空间来为那些为于堆内存中的,“已经在运行中”的变量分配空间。我们常说的堆就存在于这里。

二者分别存储什么以及原因

可以看到栈有一个ESP寄存器管理,从底层就实现了一种“自动化”,而堆似乎并没有额外的东西来帮助管理。

此外,栈的大小需要有一定的限制,栈的增长是向低地址扩展,如照片中看到的,如果栈不断地增加,很可能会与.bss段发生碰撞,这是不堪设想的,系统会发出错误并终止程序。

栈应该被看成一个短期存储数据的地方,存在在栈中的数据项没有名字,只是按照后进先出来操作罢了。栈经常可以用来在寄存器紧张的情况下,临时存储一些数据,并且十分安全。当寄存器空闲后,我们可以从栈中弹出该数据,供寄存器使用。这种临时存放数据的特性,使得它经常用来存储局部变量,函数参数,上下文环境等。

相反,堆相对于栈,更加强调需要进行控制。常见的就是我们手动申请,手动释放。因此可以分配更大的空间,但开销也会更多。

底层原理

可执行目标文件

程序在运行前以可执行文件的形式存储在磁盘中,我们先来看一下这张图:

典型的ELF可执行目标文件


ELF格式是类UNIX系统中可执行文件的常见格式,在众多表项中我们重点关注:.text,.rodata,.data,.bss这四个小段(节)。可以看到.text和.rodata属于只读存储器段(代码段),而.data,.bss属于可读可写存储器段(数据段)。下面具体说明这四个小段。

 

.text

存放已编译程序段机器代码。

.rodata

存放只读数据,如C语言中printf语句中的格式串和开关语句的跳转表。

所谓开关语句的跳转表,一个典型的例子就是switch(开关)语句的汇编实现,其使用了数组来映射代码块的地址,以此构成一张跳转表,相关的内容存储于只读数据中。

.data

已初始化的全局C变量。局部C变量在运行时保存在栈中,既不出现在.data中,也不在.bss中。

.bss

未初始化的全局C变量。如前文汇编语言讲解中提到的,它在目标文件中不占据实际空间,仅仅是一个占位符。目标文件格式区分初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。

值得一提的是,.bss原本是IBM704汇编语言(大约在1957年)中Block Started by Simple指令的首字母缩写,并沿用至今。不过在今天,我们只需要记住区别.data.bss的最简单的方法就是把.bss看成是“更好地节省空间”(Better Save Space)的缩写!

有一些特例

  • 标记有static静态标志的局部变量不在栈中管理,而是根据有无初始化,在.data或者.bss中。
  • 对于GCC编译器,初始化为0的变量存储在.bss中。

所以说,如果想真的搞清楚来龙去脉,仍旧需要你自己去阅读各类文献。

加载可执行目标文件

可执行文件在内存中运行时,有一个运行时存储器印象,我们来看一下这其中的情况,如下图:

 

Linux 运行时存储器映像

 

这张图涵盖了本文所讲的大多数知识点。相比于前文的那张汇编语言内存图,更加细分了。

  • 代码段总是从地址0x08048000处开始。
  • 数据段在接下来的下一个4KB对齐的地址处。
  • 运行时读/写段(数据段)之后接下来的第一个4KB对齐的地址处,并通过malloc库往上(高地址方向)增长。
  • 中间还有一个段是为共享库(shared library)保留的。
  • 用户总是从最大的合法用户地址开始,向下增长(低地址方向)
  • 栈上方的段是为操作系统驻留存储器部分(也就是内核)的代码和数据保留的。
  • 当程序开始运行时,加载器在可执行文件中段头部表的指引下,将可执行文件的相关内容拷贝到代码段和数据段。

上述的一些名词,比如共享库,其含义可能需要你自己去研究。Tips:Windows的.DLL。另外,笔者在参考各类文献时发现,上述诸如数据段data段等名字经常包含不同的含义,且经常一个概念有多种说法。例如只读段又可以被认为是代码段。这里大家需要注意我们所说的数据段不是指data段,而是data段bss段

其实完全细分的名称会与操作系统和CPU架构有关,笔者在这里只能针对共通的地方加以概括。

动态存储器分配

这里重点讲一下堆。

动态存储器分配维护这一个进程的虚拟存储器区域,称为堆(heap)。我们假设堆是一个请求二进制零的区域,它紧接在未初始化的.bss区域后开始,并向上(高地址方向)生长。对于每一个进程,内核维护这一个变量brk(读作"break"),它指向堆堆顶部。如下图:

 

引用链接:https://www.jianshu.com/p/52b5a1879aa1

对于keil而言:

Program Size: Code=86496 RO-data=9064 RW-data=1452 ZI-data=16116 

 

Code是代码占用的空间;

RO-data是 Read Only 只读常量的大小,如const型;

RW-data是(Read Write) 初始化了的可读写变量的大小;

ZI-data是(Zero Initialize) 没有初始化的可读写变量的大小。ZI-data不会被算做代码里因为不会被初始化;

 

简单的说就是在烧写的时候是FLASH中的被占用的空间为:Code + RO Data + RW Data

程序运行的时候,芯片内部RAM使用的空间为:               RW Data + ZI Data

通过.map文件可以清楚的看到这些信息。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值