C语言小课堂:动态内存管理及防御性编程

嵌入式ARM 2022-04-29 12:00

概述:C语言的优势是可以直接访问内存地址,也就是指针操作,但其缺陷也是因为直接内存访问。如何通过防御性编程提前发现问题,尽可能减少内存异常产生的后果,就是本文的重点。

1、内存划分

一般内存区域划分五段:

栈区(stack)有时也称为堆栈,重点在栈字,存放函数内部临时变量

堆区(heap)也就是动态申请(malloc)、释放(free)的内存区域

数据区(data)初始化的全局变量和静态变量, 占用可执行文件空间;rodata 固定不变const修饰的全局变量,不占内存空间

bss区未初始化的全局变量、静态变量(static关键字描述的),初始化为全0的全局变量,不占用可执行文件大小

代码区(text)程序二进制文件

最终下载的可执行文件包括代码(text)和数据(data)。内存的分配一般如下图:其中堆和栈的地址分配方向相反,栈比较特殊,下面微信公众号【嵌入式系统】以栈空间异常使用为例:

#include 

int main(void)
{
    int a=100;
    int b[3]={0};
    int c=200;

    printf("ori> a[%p]=%d,c[%p]=%d\r\n",&a,a,&c,c);

    printf("   > b[%p]\r\n",&b);
    b[0]=0;
    b[1]=1;
    b[2]=2;
    b[3]=3;//error ->a

    printf("new> a[%p]=%d,c[%p]=%d\r\n",&a,a,&c,c);

    return 0;
}

运行结果:

ori> a[0028FEBC]=100,c[0028FEAC]=200
   > b[0028FEB0]
new> a[0028FEBC]=3,c[0028FEAC]=200

结合打印的变量地址,栈空间分配如下图,因为数组b的操作越界,导致了变量a的值被覆盖。针对个人情况,一般情况下内存溢出都是使用数组越界,所以在异常值后或者前查看有没数组(全局变量可以查map文件),检查数组的操作是否正确。

除了堆区,其他几个区都是有编译器和系统运行时自动处理的,而堆区由开发者来操作的。这既是便利,也是隐患,一旦操作失误就是内存泄漏或溢出。

2、动态内存管理

在硬件资源固定的情况下,栈和堆的空间此消彼长,合理的定义堆的空间,为不同任务分配合适的栈空间也是至关重要的。以FreeRTOS内核代码为例,《FreeRTOS及其应用》分别解读其5种动态内存,也就是堆的分配方式,其他系统的原理差不多。参考Guide文档 https://www.freertos.org/Documentation/RTOS_book.html  Guide

FreeRTOS 内核提供了 5 种内存管理算法,源文件在Source\portable\MemMang 下,使用时选择其中一个。

heap_1.c内存管理方案简单,它只能申请内存而不能进行内存释放。一些低端嵌入式系统并不会经常动态申请与释放内存,在系统启动后申请,一直使用下去,永不释放,适合这种方式,也可近似理解为多个全局小数组合并的使用。

heap_2.c 方案支持申请和释放,但是它不能把相邻的两个小的内存块合成一个大的内存块, 随着不断的申请释放,空闲空间会分割为很多小片段,如下图持续申请、释放一定次数,就会出现剩余空间的和较大,但却申请不到内存的情况,如上图剩余空间是900,但无法申请600,因为没有连续的600空间。如果每次申请内存大小都是固定的,就不存在内存碎片问题,但实际不会这样,因此不推荐。

heap_3.c 方案只是封装了标准 C 库中的 malloc()和 free()函数,由编译器提供,需要通过编译器或者启动文件设置堆空间,封装是为了保证线程安全。

heap_4.c 方案是在heap_2.c 基础上,对内存碎片进行了改进。如图E到F,用户释放后,把相邻的空闲的内存块合并成一个更大的块,这样可以减少内存碎片。

heap_5.c 方案在实现动态内存分配时与 heap4.c 方案一样,采用最佳匹配算法和合并算法,并且允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配,比如做图形显示,可能芯片内部的 RAM 不足,额外扩展SDRAM,这种内存管理方案则比较合适。

一般选用heap_4.c。

3、动态内存防御性编程

内存只申请不释放,运行一段时间会因为内存不足而无法运行,即内存泄露;或者操作的内存区域超出了申请的空间,访问越界即内存溢出,导致各种随机异常。对于内存操作的不稳定因素,如何进行防御性编程,可以在调试阶段发现问题?

简单的说就是内存分配的时候,记录申请内存的函数名(或者扩展加上申请时间),申请内存大小的基础上额外增加空间,在其首尾加入特殊的标志位,释放该内存前对标志位进行校验;如果校验不通过,则将申请该内存的函数名打印出来,表示出现了内存溢出。也支持随时打印当前动态内存的使用情况,查看某些函数申请的内存释放一直未被释放,人工判断是否内存泄露。

下面是完整源码:

//pal_memory.h
#ifndef _PAL_MEMORY_H
#define _PAL_MEMORY_H

//配置是否开启内存记录功能
#define __MEMORY_DEBUG__

typedef unsigned char   uint8_t;
typedef unsigned int    uint32_t;

extern void *chengj_pal_memory_malloc(uint32_t size, const char *func);
extern void chengj_pal_memory_free(void **pv);
extern void chengj_pal_memory_record_print(void);

#define chengj_malloc(size)     chengj_pal_memory_malloc(size, __FUNCTION__)
#define chengj_free(pv)         chengj_pal_memory_free(&pv)

#endif  /* _PAL_MEMORY_H */

具体实现:

/**********************************************************************
 * 
 * Copyright(c)  embedded-systems rights reserved
 * 
 * Description:
 *        memory management 
 *
 *      [微信公众号: 嵌入式系统]
 * 
 *********************************************************************/

#include 
#include 
#include "pal_memory.h"

//适配平台内存管理接口
#define PAL_MALLOC  malloc
#define PAL_FREE    free

#if defined (__MEMORY_DEBUG__)

#define MEMORY_RECORD_COUNT_MAX 100

//len[4]+head[4]+...[data]...+tail[2]
#define MEMORY_EXTRA_SIZE 10

//magic
#define MEMORY_DATA_MAGIC_HEAD 0x43
#define MEMORY_DATA_MAGIC_TAIL 0x4A

typedef struct
{

    const char *func_name;
    void *pointer;
    //可扩展保存 时间戳 等信息
} memory_record_struct;

//记录申请内存的函数
static memory_record_struct chengj_memory_record[MEMORY_RECORD_COUNT_MAX] = {0};

#endif /* __MEMORY_DEBUG__ */

/*
 *输出未被释放的申请函数名和指针地址
 */

void chengj_pal_memory_record_print(void)
{
#if defined (__MEMORY_DEBUG__)
    uint32_t i = 0;

    for(; i < MEMORY_RECORD_COUNT_MAX; i++)
    {
        if(chengj_memory_record[i].pointer != NULL)
        {
            printf("[%d] %s()\r\n", i, chengj_memory_record[i].func_name);
        }
    }
#endif /* __MEMORY_DEBUG__ */
}

/*
 *malloc
 */

void *chengj_pal_memory_malloc(uint32_t size, const char *func)
{
    void *pv = NULL;
    uint32_t i = 0;
#if defined (__MEMORY_DEBUG__)
    uint8_t *pdata;
#endif

    if(size == 0 || func == NULL)
    {
        return NULL;
    }

#if defined (__MEMORY_DEBUG__)
    size = size + MEMORY_EXTRA_SIZE;
#endif

    pv = PAL_MALLOC(size);

    if(pv == NULL)
    {
        return NULL;
    }
    memset(pv, 0, size);

#if defined (__MEMORY_DEBUG__)

    pdata = (uint8_t *)pv;

    pdata[0] = (size >> 24) & 0xFF;
    pdata[1] = (size >> 16) & 0xFF;
    pdata[2] = (size >> 8) & 0xFF;
    pdata[3] = size & 0xFF;

    pdata[4] = MEMORY_DATA_MAGIC_HEAD;
    pdata[5] = MEMORY_DATA_MAGIC_HEAD;
    pdata[6] = MEMORY_DATA_MAGIC_HEAD;
    pdata[7] = MEMORY_DATA_MAGIC_HEAD;

    pdata[size - 2] = MEMORY_DATA_MAGIC_TAIL;
    pdata[size - 1] = MEMORY_DATA_MAGIC_TAIL;

    for(; i < MEMORY_RECORD_COUNT_MAX; i++) //过多不记录
    {
        if(chengj_memory_record[i].pointer == NULL)
        {
            chengj_memory_record[i].func_name = func;
            chengj_memory_record[i].pointer = pv;
            break;
        }
    }
    
    return &pdata[8];

#else

    return pv;

#endif /* __MEMORY_DEBUG__ */

}

/*
 *free
 */

void chengj_pal_memory_free(void **pv)
{
    uint32_t i = 0;
#if defined (__MEMORY_DEBUG__)
    uint32_t size;
    uint8_t *pdata;
    uint8_t error = 0;
#endif

    if(pv == NULL || *pv == NULL)
    {
        return;
    }

#if defined (__MEMORY_DEBUG__)

    pdata = (uint8_t *)(*pv) - 8;
    *pv = (void*)pdata;

    size = ((pdata[0] << 24) | (pdata[1] << 16) | (pdata[2] << 8) | (pdata[3]));

    if(size <= MEMORY_EXTRA_SIZE)
    {
        error = error | 0x01;
    }

    if((pdata[4] != MEMORY_DATA_MAGIC_HEAD) || (pdata[5] != MEMORY_DATA_MAGIC_HEAD)\
            || (pdata[6] != MEMORY_DATA_MAGIC_HEAD) || (pdata[7] != MEMORY_DATA_MAGIC_HEAD))
    {
        error = error | 0x02;
    }

    if((pdata[size - 2] != MEMORY_DATA_MAGIC_TAIL) || (pdata[size - 1] != MEMORY_DATA_MAGIC_TAIL))
    {
        error = error | 0x04;
    }


    for(; i < MEMORY_RECORD_COUNT_MAX; i++)
    {
        if(chengj_memory_record[i].pointer == *pv)
        {
            if(error != 0)
            {
                if(chengj_memory_record[i].func_name != NULL)
                {
                    printf("memory error 0x%02X %s()\r\n", error, chengj_memory_record[i].func_name);
                }
                else
                {
                    printf("memory error 0x%02X %p\r\n", error, *pv);
                }
            }
            memset(&chengj_memory_record[i], 0sizeof(memory_record_struct));
            break;
        }
    }

    if(error != 0
    {
        //ASSERT调试
        return;
    }
#endif /* CY_PAL_MEMORY_DEBUG */

    PAL_FREE(*pv);
    *pv = NULL;

    return;
}

可以测试下效果:

#include "pal_memory.h"

//微信公众号: 嵌入式系统
//申请10字节但使用20字节
void test(void)
{
    uint8_t *p;
    uint8_t i;

    p=chengj_malloc(10);
    for(i=0;i<20;i++)
    {
        p[i]=i;
    }
    chengj_free(p);
}


int main(int argc, char *argv[])
{
    printf("embedded-system \r\n");
    test();
    return 0;
}

运行结果:

embedded-system
memory error 0x04 test()

表示test函数内申请的一段内存使用时溢出,尾部标记数据被覆盖。

也可以在memory_record_struct增加时间戳成员,记录内存申请时间,再扩展void chengj_pal_memory_record_print(void) 打印内存使用情况,查看长时间申请未释放的内存使用情况。

4、小结

内存记录调试方法,浪费了一定量的内存空间,而且不能排除问题,只是提早监测到异常,但对软件稳定性仍有较大意义,可以快速解决内存问题。建议只在debug版本启用,正式发布的release版本关闭记录功能。

END

来源:嵌入式系统

版权归原作者所有,如有侵权,请联系删除。

推荐阅读
电子元器件项链,下血本了!不知道女朋友会不会喜欢
代码编程规范-排版风格,提升你代码逼格的小技巧
10年经验的嵌入式工程师最后都选择了什么职位?

→点关注,不迷路←
嵌入式ARM 关注这个时代最火的嵌入式ARM,你想知道的都在这里。
评论
  • 「他明明跟我同梯进来,为什么就是升得比我快?」许多人都有这样的疑问:明明就战绩也不比隔壁同事差,升迁之路却比别人苦。其实,之间的差异就在于「领导力」。並非必须当管理者才需要「领导力」,而是散发领导力特质的人,才更容易被晓明。许多领导力和特质,都可以通过努力和学习获得,因此就算不是天生的领导者,也能成为一个具备领导魅力的人,进而被老板看见,向你伸出升迁的橘子枝。领导力是什么?领导力是一种能力或特质,甚至可以说是一种「影响力」。好的领导者通常具备影响和鼓励他人的能力,并导引他们朝着共同的目标和愿景前
    优思学院 2025-01-08 14:54 96浏览
  •  在全球能源结构加速向清洁、可再生方向转型的今天,风力发电作为一种绿色能源,已成为各国新能源发展的重要组成部分。然而,风力发电系统在复杂的环境中长时间运行,对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦(光电耦合器)作为一种电气隔离与信号传输器件,凭借其优秀的隔离保护性能和信号传输能力,已成为风力发电系统中不可或缺的关键组件。 风力发电系统对隔离与控制的需求风力发电系统中,包括发电机、变流器、变压器和控制系统等多个部分,通常工作在高压、大功率的环境中。光耦在这里扮演了
    晶台光耦 2025-01-08 16:03 88浏览
  • 故障现象一辆2017款东风风神AX7车,搭载DFMA14T发动机,累计行驶里程约为13.7万km。该车冷起动后怠速运转正常,热机后怠速运转不稳,组合仪表上的发动机转速表指针上下轻微抖动。 故障诊断 用故障检测仪检测,发动机控制单元中无故障代码存储;读取发动机数据流,发现进气歧管绝对压力波动明显,有时能达到69 kPa,明显偏高,推断可能的原因有:进气系统漏气;进气歧管绝对压力传感器信号失真;发动机机械故障。首先从节气门处打烟雾,没有发现进气管周围有漏气的地方;接着拔下进气管上的两个真空
    虹科Pico汽车示波器 2025-01-08 16:51 112浏览
  • 在过去十年中,自动驾驶和高级驾驶辅助系统(AD/ADAS)软件与硬件的快速发展对多传感器数据采集的设计需求提出了更高的要求。然而,目前仍缺乏能够高质量集成多传感器数据采集的解决方案。康谋ADTF正是应运而生,它提供了一个广受认可和广泛引用的软件框架,包含模块化的标准化应用程序和工具,旨在为ADAS功能的开发提供一站式体验。一、ADTF的关键之处!无论是奥迪、大众、宝马还是梅赛德斯-奔驰:他们都依赖我们不断发展的ADTF来开发智能驾驶辅助解决方案,直至实现自动驾驶的目标。从新功能的最初构思到批量生
    康谋 2025-01-09 10:04 88浏览
  • 一个真正的质量工程师(QE)必须将一件产品设计的“意图”与系统的可制造性、可服务性以及资源在现实中实现设计和产品的能力结合起来。所以,可以说,这确实是一种工程学科。我们常开玩笑说,质量工程师是工程领域里的「侦探」、「警察」或「律师」,守护神是"墨菲”,信奉的哲学就是「墨菲定律」。(注:墨菲定律是一种启发性原则,常被表述为:任何可能出错的事情最终都会出错。)做质量工程师的,有时会不受欢迎,也会被忽视,甚至可能遭遇主动或被动的阻碍,而一旦出了问题,责任往往就落在质量工程师的头上。虽然质量工程师并不负
    优思学院 2025-01-09 11:48 110浏览
  • 职场是人生的重要战场,既是谋生之地,也是实现个人价值的平台。然而,有些思维方式却会悄无声息地拖住你的后腿,让你原地踏步甚至退步。今天,我们就来聊聊职场中最忌讳的五种思维方式,看看自己有没有中招。1. 固步自封的思维在职场中,最可怕的事情莫过于自满于现状,拒绝学习和改变。世界在不断变化,行业的趋势、技术的革新都在要求我们与时俱进。如果你总觉得自己的方法最优,或者害怕尝试新事物,那就很容易被淘汰。与其等待机会找上门,不如主动出击,保持学习和探索的心态。加入优思学院,可以帮助你快速提升自己,与行业前沿
    优思学院 2025-01-09 15:48 87浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球中空长航时无人机产值达到9009百万美元,2024-2030年期间年复合增长率CAGR为8.0%。 环洋市场咨询机构出版了的【全球中空长航时无人机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球中空长航时无人机总体规模,包括产量、产值、消费量、主要生产地区、主要生产商及市场份额,同时分析中空长航时无人机市场主要驱动因素、阻碍因素、市场机遇、挑战、新产品发布等。报告从中空长航时
    GIRtina 2025-01-09 10:35 91浏览
  • 1月7日-10日,2025年国际消费电子产品展览会(CES 2025)盛大举行,广和通发布Fibocom AI Stack,赋智千行百业端侧应用。Fibocom AI Stack提供集高性能模组、AI工具链、高性能推理引擎、海量模型、支持与服务一体化的端侧AI解决方案,帮助智能设备快速实现AI能力商用。为适应不同端侧场景的应用,AI Stack具备海量端侧AI模型及行业端侧模型,基于不同等级算力的芯片平台或模组,Fibocom AI Stack可将TensorFlow、PyTorch、ONNX、
    物吾悟小通 2025-01-08 18:17 80浏览
  • 在当前人工智能(AI)与物联网(IoT)的快速发展趋势下,各行各业的数字转型与自动化进程正以惊人的速度持续进行。如今企业在设计与营运技术系统时所面临的挑战不仅是技术本身,更包含硬件设施、第三方软件及配件等复杂的外部因素。然而这些系统往往讲究更精密的设计与高稳定性,哪怕是任何一个小小的问题,都可能对整体业务运作造成严重影响。 POS应用环境与客户需求以本次分享的客户个案为例,该客户是一家全球领先的信息技术服务与数字解决方案提供商,遭遇到一个由他们所开发的POS机(Point of Sal
    百佳泰测试实验室 2025-01-09 17:35 90浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2025-01-09 09:58 77浏览
  • HDMI 2.2 规格将至,开启视听新境界2025年1月6日,HDMI Forum, Inc. 宣布即将发布HDMI规范2.2版本。新HDMI规范为规模庞大的 HDMI 生态系统带来更多选择,为创建、分发和体验理想的终端用户效果提供更先进的解决方案。新技术为电视、电影和游戏工作室等内容制作商在当前和未来提供更高质量的选择,同时实现多种分发平台。96Gbps的更高带宽和新一代 HDMI 固定比率速率传输(Fixed Rate Link)技术为各种设备应用提供更优质的音频和视频。终端用户显示器能以最
    百佳泰测试实验室 2025-01-09 17:33 101浏览
  • 在智能网联汽车中,各种通信技术如2G/3G/4G/5G、GNSS(全球导航卫星系统)、V2X(车联网通信)等在行业内被广泛使用。这些技术让汽车能够实现紧急呼叫、在线娱乐、导航等多种功能。EMC测试就是为了确保在复杂电磁环境下,汽车的通信系统仍然可以正常工作,保护驾乘者的安全。参考《QCT-基于LTE-V2X直连通信的车载信息交互系统技术要求及试验方法-1》标准10.5电磁兼容试验方法,下面将会从整车功能层面为大家解读V2X整车电磁兼容试验的过程。测试过程揭秘1. 设备准备为了进行电磁兼容试验,技
    北汇信息 2025-01-09 11:24 93浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦