嵌入式技术
概述:C语言的优势是可以直接访问内存地址,也就是指针操作,但其缺陷也是因为直接内存访问。如何通过防御性编程提前发现问题,尽可能减少内存异常产生的后果,就是本文的重点。
1、内存划分
一般内存区域划分五段:
栈区(stack)有时也称为堆栈,重点在栈字,存放函数内部临时变量
堆区(heap)也就是动态申请(malloc)、释放(free)的内存区域
数据区(data)初始化的全局变量和静态变量, 占用可执行文件空间;rodata 固定不变const修饰的全局变量,不占内存空间
bss区未初始化的全局变量、静态变量(static关键字描述的),初始化为全0的全局变量,不占用可执行文件大小
代码区(text)程序二进制文件
最终下载的可执行文件包括代码(text)和数据(data)。内存的分配一般如下图:
其中堆和栈的地址分配方向相反,栈比较特殊,下面以栈空间异常使用为例:
#includeint main(void) { int a=100; int b[3]={0}; int c=200; printf("ori> a[%p]=%d,c[%p]=%d ",&a,a,&c,c); printf(" > b[%p] ",&b); b[0]=0; b[1]=1; b[2]=2; b[3]=3;//error ->a printf("new> a[%p]=%d,c[%p]=%d ",&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种动态内存,也就是堆的分配方式,其他系统的原理差不多。
FreeRTOS 内核提供了 5 种内存管理算法,源文件在SourceportableMemMang 下,使用时选择其中一个。
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() ", 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() ", error, chengj_memory_record[i].func_name); } else { printf("memory error 0x%02X %p ", error, *pv); } } memset(&chengj_memory_record[i], 0, sizeof(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 "); test(); return 0; }
运行结果:
embedded-system memory error 0x04 test()
表示test函数内申请的一段内存使用时溢出,尾部标记数据被覆盖。
也可以在memory_record_struct增加时间戳成员,记录内存申请时间,再扩展void chengj_pal_memory_record_print(void) 打印内存使用情况,查看长时间申请未释放的内存使用情况。
4、小结
内存记录调试方法,浪费了一定量的内存空间,而且不能排除问题,只是提早监测到异常,但对软件稳定性仍有较大意义,可以快速解决内存问题。建议只在debug版本启用,正式发布的release版本关闭记录功能。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !