07--进程的内存布局

C进程内存布局

任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究才能清楚内存布局,逐个了解不同内存区域的特性。

每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。

  • PM:Physical Memory,物理内存。
  • VM:Virtual Memory,虚拟内存。

 

将其中一个C语言含如进程的虚拟内存放大来看,会发现其内部包下区域:

  • 栈(stack)
  • 堆(heap)
  • 数据段
  • 代码段

 

虚拟内存中,内核区段对于应用程序而言是禁闭的,它们用于存放操作系统的关键性代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x0 ~ 0x08048000 之间也有一段禁闭的区段,该区段也是不可访问的。

虚拟内存中各个区段的详细内容:

栈内存 

  • 什么东西存储在栈内存中?
    • 环境变量
    • 命令行参数 (./a.out 123 Hello Even ... )
    • 局部变量(包括形参)
      • 定义在函数体内{ }的变量成为局部变量,反之则成为全局变量
      • 形参则是在函数头中的参数列表内定义的变量 成为形参 int main(int argc, char const *argv[])
  • 栈内存有什么特点?
    • 空间有限,尤其在嵌入式环境下(STM32 / 51...)。因此不可以用来存储尺寸太大的变量。(如果超出范围则会出现栈溢出程序崩溃)
    • 每当一个函数被调用,栈就会向下增长一段(被分配),用以存储该函数的局部变量。
    • 每当一个函数退出,栈就会向上缩减一段(被回收),将该函数的局部变量所占内存归还给系统。

栈内存的分配和释放,都是由系统规定的,我们无法干预。

局部作用域

  • 概念:在代码块中定义的变量(局部变量),其可见范围从其定义处开始,到代码块结束为止。
  • 示例:
    int main()
    {  // 主函数的作用域
        // a = 123 ; // [错误] 需要在定义语句后,才能正确访问变量
        int a=1;   // a的作用域从该语句运行结束后开
        int b=2;     // 变量 c 的作用域是第4行到第9行
    
    
        printf("LINE:%d\ta:%d\n" , __LINE__ ,a );
    
    
        { // 👇👇👇👇👇👇新的作用域的开始👇👇👇👇👇👇
            int c=4;
            int d=5; // 变量 d 的作用域是第7行到第8行
    
            printf("LINE:%d\ta:%d\n" , __LINE__ ,a );
            printf("LINE:%d\tc:%d\n" , __LINE__ ,c );
            
            // 这里又定义了一个a 但是当前的a 属于一个小范围的作用域,
            // 在该作用域的a 会临时掩盖大作用域中同名的变量
            int a = 100; 
            printf("LINE:%d\ta:%d\n" , __LINE__ ,a );
        }// 👆👆👆👆👆新的作用域的结束👆👆👆👆👆👆👆👆
    
        // 当我们离开小作用域后,该作用域中所有的变量都会被释放 (交给系统重新分配)
        // printf("LINE:%d\tc:%d\n" , __LINE__ ,c ); // [编译保存] 未定义
    
    
        printf("LINE:%d\ta:%d\n" , __LINE__ ,a );
    } // 直到该 大括号 }  表示结束

  • 要点:
    • 代码块指的是一对花括号 { } 括起来的区域。
    • 代码块可以嵌套包含,外层的标识符(变量名...)会被内嵌的同名标识符临时掩盖变得不可见。
    • 代码块作用域的变量,由于其可见范围是局部的,因此被称为局部变量。
  • 自动存储期

在栈内存中分配的变量,统统拥有自动存储期,因此也都被称为自动变量。这里自动的含义,指的是这些变量的内存管理不需要开发者操心,都是全自动的:在变量定义处自动分配,出了变量的作用域后自动释放。

  • 以下三个概念是等价的:
    • 自动变量:从存储期的角度,描述变量的时间特性。
    • 临时变量:同上。
    • 局部变量:从作用域(作用访问是局部 { 只能在代码块内部使用 })的角度,描述变量的空间特性。
  • 注意:

数据段与代码段

  • 数据段细分成如下几个区域:
    • .bss(Block Started by Symbol) 段:存放未初始化的静态数据,它们将被系统自动初始化为0
    • .data段:存放已初始化的静态数据
    • .rodata段:存放常量数据 (该区域的所有数据都是不允许被修改的)
  • 代码段细分成如下几个区域:
    • .text段:存放用户代码
    • .init段:存放系统初始化代码(编译器根据实际的目标系统自动添加)

int a;       // 未初始化的全局变量,放置在.bss 中
int b = 100; // 已初始化的全局变量,放置在.data 中

int main(void)
{
    static int c;       // 未初始化的静态局部变量,放置在.bss 中
    static int d = 200; // 已初始化的静态局部变量,放置在.data 中
    
    // 以上代码中的常量100、200防止在.rodata 中
}
  • 注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预。

静态数据

概念: 数据的生存周期是固定, 不会因为程序执行某些操作而申请或释放它所占用的内存。

他们的内存是在程序运行之处就被申请出来, 直到程序运行结束才会释放回收。

C语言中,静态数据有两种:

  • 全局变量:定义在函数外部的变量。
  • 静态局部变量:定义在函数内部,且被static修饰的变量。
  • 示例:
    int a; // 全局变量,退出整个程序之前不会释放
    void f(void)
    {
        static int b; // 静态局部变量,退出整个程序之前不会释放
        printf("%d\n", b);
        b++;
    }
    
    int main(void)
    {
        f();
        f(); // 重复调用函数 f(),会使静态局部变量 b 的值不断增大
    }

  • 为什么需要静态数据?
    1. 全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数间访问的数据提供操作上的方便。
    2. 当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。
  • 注意1:
    • 若定义静态数据 ( 全局变量 、 被static修饰的局部变量 ) 时未初始化,则系统会将所有的静态数据自动初始化为0
    • 静态数据初始化语句,只会执行一遍。
    • 静态数据从程序开始运行时便已存在,直到程序退出时才释放。
  • 注意2 static关键字的作用:
    • static修饰局部变量:使之由栈内存临时数据,变成了静态数据。
    • static修饰全局变量:使之由各文件可见的静态数据,变成了本文件可见的静态数据。
    • static修饰函数:使之由各文件可见的函数,变成了本文件可见的静态函数。
#include <stdio.h>

// 全局变量定义
int Num1 = 456 ;   // 该变量Num 的可见范围(作用域)是整个程序(可能由多个.c文件组成)可见
                    // 如果在其他文件也定义一样全局变量,那么将会发成重定义的冲突问题
                    // 不利于模块化编程

static int Num2 = 567 ; // 使用static 修饰的全局变量,他的作用域(可见范围)尽本文件可见
                    // 如果在其他文件也定义一样全局变量,那么将bu会发成重定义的冲突问题 
                    // 有利于模块化编程
                    // 如果其他文件确实需要使用到该变量,
                    // 那么一般就会把该变量定义的语句写入到头文件中,需要用到的文件直接包含头文件即可



int main(int argc, char const *argv[])
{
    
    int a = 123 ; // 该变量a 属于函数main的局部变量
                // 当该函数被执行的时候 自动分配出来 它所在区域为 栈空间
                // 当函数运行结束时会被系统自动回收


    static int b = 456 ; // 该变量是在程序被执行的时候直接分配
                        // 该变量的存储区域 是 数据段 的.data段
                        // 他的生命周期与程序保持一致 , 不会因为函数运行结束而释放
                        //  (只要程序没有退出,他的值可以一直延用)
                        // 该数据为静态数据,因此无法手动对他进行释放
                        
    char * p = "Hello Even\n" ; // 指针 p 是一个局部变量.他的存储区在栈空间
                            // 它指向了 数据段中 .rodata 中"Hello Even\n" 的入口地址

    static char * p1 = "Hello Even\n" ; // 指针 p1 是一个静态的局部变量, 它的存储区在 数据段的 .data 段
                    // 它指向了 数据段中 .rodata 中"Hello Even\n" 的入口地址
                    // p 会因为函数的运行结束而释放, p1 则不会, 他的生命周期与程序保持一致

    return 0;
}

尝试使用画图工具把以上示例代码中各个变量在内存中进行标记。

堆内存

堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。

  • 堆内存基本特征:
    • 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
    • 相比栈内存,堆内存从下往上增长。
    • 堆内存是匿名的,只能由指针来访问。
    • 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。
  • 相关API:
    • 申请堆内存:malloc ( ) / calloc ( )
    • 清零堆内存:bzero ( )
    • 释放堆内存:free ( )
    • 重新申请 realloc ( )

分析堆内存的API:

申请堆内存空间,并返回该内存的入口地址, 但是不会对该内存进行初始化操作。
头文件:
    #include <stdlib.h>
函数原型:
    void *malloc(size_t size);
参数分析:
    size --> 需要申请的内存的大小(字节) 
返回值:
    成功 返回一个已经分配好的内存
    失败 返回 NULL 
                                   
释放内存空间:       
函数原型:                             
    void free(void *ptr);
参数分析:
    ptr --> 需要释放的堆内存的入口地址 (必须是堆内存+入口地址)
返回值:
    成功  无
    失败  无
    
函数原型(更适用于数组的申请)并对申请得到的内存进行初始化为 0 :           
    void *calloc(size_t nmemb, size_t size);
参数分析:
    nmemb --> 数组元素的个数
    size --> 每一个元素的大小    
返回值:
    成功 返回申请到的内存入口地址
    失败 返回 NULL 
    
函数原型:           
    void *realloc(void *ptr, size_t size);
参数分析:
    ptr --> 旧的堆空间入口地址
    size --> 新空间的内存大小
返回值:  
    成功 返回新的入口地址 (如果没有换到新的内存下则返回值与参数ptr相同)
    失败 返回null 
    
     
void *reallocarray(void *ptr, size_t nmemb, size_t size);

  • 注意:
    • malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero() 来清零(拓展)。
    • calloc()申请的堆内存,默认情况下是已经清零了的,不需要再清零。
    • free()只能释放堆内存,并且只能释放整块堆内存(必须是某一块堆空间的入口地址),不能释放别的区段的内存或者释放一部分堆内存。
  • 释放内存的含义:
    • 释放内存意味着将内存的使用权归还给系统。
    • 释放内存并不会改变指针的指向(会出现野指针)。
    • 释放内存并不会对内存做任何修改,更不会将内存清零(所以内存中会有随机值)。
  • 尝试自己画一下内存布局图,过程中回顾一下各个区域的特点

  • 编写代码验证一下内存布局(地址大小关系)
    #include <stdio.h>
    #include <stdlib.h>
    
    // 全局变量 (静态数据)
    int a ;   // a 是静态数据, 没有初始化因此它的存储区域在 .bss  默认初始值为 0 
    int b = 333 ; // b 是静态数据,有进行初始化因此它存储的区域在  .data 
    
    int main(int argc, char const *argv[])
    {
    
        printf("argv[0]:%p\n" , argv[0]); // 命令行参数
    
        printf("&argc的地址:%p\n" , &argc);  // 在参数列表中定义的变量属于该函数的局部变量
    
        int Num = 123 ;
        printf("&Num的地址:%p\n" , &Num);  // 在函数体内部定义,因此它属于该函数的局部变量
    
    
        // 在堆内存中申请了10个字节的内存空间并把该内存的入口地址 存入到 msg 指针变量中
        char * msg = calloc(10 , sizeof(char) ) ; 
        printf("msg:%p\n" , msg); 
    
        // 数据段的内存
        printf("a的地址:%p\n" , &a); 
        printf("b的地址:%p\n" , &b); 
    
        // 使用指针p 指向了常量区的 "Hello Even" 的入口地址
        char * p = "Hello Even" ;
    
        printf("p:%p\n" , p); 
        
        // 代码段
        printf("main 主函数的入口地址:%p\n" , main);
        printf("&main 主函数的入口地址:%p\n" , &main);
    
        return 0;
    }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值