程序的内存结构,也叫内存布局,也叫存储映像


一、什么是内存结构?

前面已经介绍过,其实就是程序运行时在内存中存储结构

不管是裸机还是基于OS虚拟内存运行的情况,内存布局基本都差不多,
所以我们这里只介绍程序基于OS虚拟内存运行时的内存布局,这个内存布局很重要,
希望大家理解并记住它,至于说为什么重要,后面的课程会体现出来

程序在内存中存储时,就存储两个东西,一是指令,二是数据


二、指令

指令存储在代码段中的.init.text节中:

  • .init节:放启动代码相关的指令
  • .text节:主要放我们自己所写程序的指令

三、数据

数据的存储形态,有两种存储形态:

  1. 第一种:数据存储在常量空间中
    数据存储在常量空间中时,由于常量空间只能读不能写,因此常量保存的数据将一直不变
  2. 第二种:数据存储在变量空间中
    由于变量空间的读写权限为可读/可写,所以数据存在变量中时,是可以被新的数据改写的

四. 常量空间

常量空间要求是只读的,在程序的内存布局中,只有代码段是只读的,因此常量空间肯定只能在代码段中,
在代码段中有两个地方可以用于开辟常量空间,一个是在.text中,另一个是在.rodata

1. 在.text中的情况

此时,直接作为指令的一部分,放在了.text中。

比如:

a = a + 100;

编译后100直接作为指令的一部分存储在了.text中,由于.text只能读不能写,因此数据100就保存在了常量中

2. 在.rodata中的情况

比如:

int a = 100;
printf("a = %d", a);

char *p = "hello world";

字符串"a = %d"/"hello world"被保存在了.rodata中,由于.rodata是只读的,
因此"a = %d"/"hello world"储空间是一个常量,
在一般情况下为了好称呼,我们直接将"a = %d"/"hello world"称为常量

有关char *p = "hello world"这种情况,我们后面讲字符串时,还会详细的介绍到。

疑问:为什么不将这些字符串和指令一起直接保存在.text中,因为太长了,无法作为指令的一部分存储在.text


五. 变量空间

变量空间只能开辟于在数据段,因为只有数据段是可读可写的

变量分为两种,一种是静态变量,另一种是动态变量

  • 静态变量:空间开辟于静态数据段(.data/.bss
  • 动态变量:空间开辟于动态数据段(堆/栈)

1. 静态变量

分两种情况

  • .data:初始化了的静态变量
  • .bss:未初始化的静态变量
(a) .data

其实.data中的内容在编译时就决定好了,加载程序时,只需要将“可执行目标文件”的.data的内容,拷贝到内存即可

初始化了的静态变量分两种:初始化了的全局变量 和 初始化了的静态局部变量,它们都在.data

初始化了的全局变量

int a = 100;
int main(void)
{
    ...
}

初始化了的静态局部变量

int main(void)
{
    static int a = 100; //main函数的初始化了的静态局部变量
    ...
}
(b) .bss

同样两种情况:

未初始化的全局变量

int a;
int main(void)
{
    ...
}

未初始化的静态局部变量

int main(void)
{
    static int a;
    ...
}

在“可执行目标文件”中,.bss并不存在,程序加载后才会开辟.bss的空间,
然后再在.bss里面开辟未初始化静态变量的空间,并自动初始化为0

这就是以前常说的,未初始化的全局变量和静态局部变量,会被自动初始化为0


2. 动态变量

之所称为动态的,是因为变量空间并不是在编译时决定的,而是在程序运行时才有的

动态变量分两种:

  • 自动局部变量:空间开辟在栈中
  • 手动开辟的变量:空间开辟在堆中
(a)自动局部变量(开辟于栈中)

没有加static修饰的函数局部变量都是自动局部变量,我们这里将形参也归到自动局部变量里面

为什么称为自动局部变量?
变量空间是函数运行时自动开辟,运行结束时自动释放的,所以把它称为“自动”的,
正是由于是自动开辟和释放的,因此栈也被称为自动存储区

int fun(int a) //a形参
{
    int b = 100; //没有加static修饰的局部变量
    int c;

    return a+b+c;
}

a、b、c都是开辟于栈中

疑问:编译得到可执行目标文件后,函数中的自动局部变量是一个什么样存在?

fun函数被编译后,自动局部变量的定义会变为操作栈的指令(压栈指令push、弹栈指令pop),

压栈

函数运行时会调用压栈指令,会将“栈顶指针”向低地址方向移动需要的字节数,挪出来的这个空间就是自动局部变量的空间

  • 如果有初始化的话,就将初始化值写到空间中。
  • 如果没有初始化的话,空间中的内容就是别人之前使用后遗留的内容。

这就是上一篇文章说的,如果函数的自动局部变量不初始化的话,就是一个随机值

因此我们要求必须给自动局部变量赋一个明确的值,特别是指针变量更是如此,如果你不知道赋什
么值,那最起码要赋一个NULL空指针,以防止出现因随机值所导致的野指针

弹栈

函数中的弹栈指令时,栈顶指针会向高地址方向回退相应的字节数,这样就把变量的空间给释放出来了,
释放时并不会对空间清0,因此这一次使用的值,就变成了下一次别人使用这个空间时的随机值


(b)手动开辟的变量(开辟于堆中)

栈中空间是自动开辟和释放的,但是堆空间不是的,对于堆来说,需要程序员自己在程序手动的调malloc函数,
按需从堆中分配空间,当不再需要该空间时,就调用free函数来释放

因为堆空间是手动开辟和释放的,因此在堆也被称为手动存储区

malloc、free的使用举例:

int main(void)
{
    int *p = NULL;

    p = malloc(4); //在堆中开辟4字节的空间,然后将首字节地址给p,以便访问空间

    if(p == NULL) 
    {
        printf("malloc fail\n");
        exit(-1); //开辟空间失败就结束进程
    }

    bzero(p, 4); //将开辟的空间清零
    *p = 100;    //使用开辟的空间
    free(p);     //删除p所指向的4字节空间
}

堆空间也存在和栈一样的随机值问题,所以我们从堆中开辟出空间后,必须主动清零,否者会影响写入的数据
比如程序中调用bzero(p, 4)的目的,就是将p所指向的4个字节空间清0

疑问:如果没有调用free来释放开辟的堆空间的话,程序运行结束后,堆空间会释放吗?

当然,程序结束后,不仅堆空间,所占用所有的空间都会释放

疑问:既然程序终止时会释放所有堆空间,那为什么还需要调用free函数来释放呢?

因为真实的程序,很多一旦运行起来后会长时间运行,甚至有些可能会永久运行,
如果程序在运行的过程中每次malloc后都不free的话,程序的堆空间会越用越少,
严重的话堆和栈的空间会顶到一起,相互篡改数据,最后导致程序死机,
对于很多重要程序来说是绝不能死机的,因为死机所带来的经济损失可能是非常巨大的

其实正常情况下,栈和堆之间的空间距离非常大,如果正常操作(开辟后及时释放)的话,
它们之间是不可能会碰到一起的,但是如果堆空降只开辟不释放的话,这种情况就有可能发生


六、有关堆变量和栈变量,再提两句

1. 栈变量

从栈中开辟变量时,为了避免随机值带来的问题,一定要初始化

1)基于OS虚拟内存运行时,随机值来源有两个

  • 最开始时来自于子进程以前栈的遗留
  • 新程序运行一段段时间后,随机值来自于自己使用后的的遗留

2)裸机运行时

  • 没有OS虚拟内存,不存在父进程一说,上电运行时内存默认就是清零的,所以随机值来自于自己使用后的遗留

2. 堆变量

1)基于OS虚拟内存运行时,随机值来源有两个

  • 最开始时来自于子进程以前堆的遗留
  • 来自于自己使用后的遗留

2)裸机运行时

  • 没有OS虚拟内存时,不存在父进程一说,所以随机值也是来自于自己使用后的遗留

3)mallocfree函数对堆内存的管理

(a)malloc从堆中开辟的变量空间往往大于实际想要的空间

  • malloc为了能够更好的管理堆内存,开辟空间时,并不是你想开辟多少,就刚好开辟多少,
    而是会做圆整处理,比如我想申请212字节,最终malloc实际开辟时会“圆整”为256字节

(b)如果忘了free,实际上导致的堆内存泄漏,要大于明面上所开辟的空间

  • 这个道理很简单,我想开辟212字节,但是malloc实际上开辟了256字节,
    所以如果忘了free,实际泄露的堆内存空间为256,大于明面上的212字节