在嵌入式C语言开发中,动态内存管理是一把双刃剑。它提供了灵活性,但使用不当极易引入难以追踪的错误,如内存泄漏、野指针和缓冲区溢出。本文将深入剖析动态内存使用的常见陷阱,并提供一套行之有效的自动查错机制,助你写出更健壮的嵌入式代码。
常见错误与预防措施
1. 分配后忘记释放内存
void func(void) {
p = malloc(len);
do_something(p);
return; /* 错误!函数退出时未释放内存 */
}
预防:在编写代码时,应确保 malloc() 和 free() 成对出现。一种良好的习惯是,在写下 malloc 的瞬间,就在附近写下对应的 free 调用框架,然后再填充业务逻辑。
更要警惕的是,在所有函数返回路径上,都必须确保内存被释放。
int func(void) {
p = malloc(len);
if (condition)
return -1; /* 错误!此分支退出时未释放内存 */
free(p);
return 0;
}
预防:使用动态内存后,必须仔细检查函数的每一个退出分支(如条件返回、错误提前返回),确保内存已被正确回收。
2. 使用错误指针释放内存
void func(void) {
p = malloc(len);
val = *p++; /* 错误!移动了动态内存的句柄指针 */
free(p); /* 此时p已不再指向分配内存的起始地址 */
}
预防:切勿修改指向动态内存的原始句柄指针。如需进行指针运算访问内存,应使用另一个临时指针变量进行操作,始终保持原始指针用于最终的 free 释放。
3. 分配内存不足导致溢出
void func(void) {
len = strlen(str);
p = malloc(len);
strcpy(p, str); /* 错误!未为字符串结尾的 '\0' 分配空间 */
}
预防:分配内存前务必仔细计算所需大小。对于字符串操作,尤其要牢记标准库函数如 strcpy 会拷贝结束符,因此所需内存是 strlen(str) + 1。
构建自动查错机制
即便在开发中严格遵守原则并进行测试,内存泄漏等错误依然可能潜伏。为系统增加一层自动检查机制能极大提升排错效率。
一种有效的方法是建立内存分配日志。核心思想是:每次分配内存时,在一个独立的日志池中记录该内存块的指针和大小;释放内存时,从日志中移除对应记录。程序结束时,检查日志池,任何未被移除的记录都指向了潜在的内存泄漏。
以下是一个结合了日志功能的内存分配/释放函数实现示例。通过预编译宏 DMEM_DBG 和 DBG_VER 控制,仅在调试版本中启用日志功能,从而不影响最终发布版本的性能。
代码实现
首先定义日志块结构体:
/* 动态内存使用日志 */
typedef struct _dmem_log {
struct _dmem_log *p_stNext; /* 指向下一个日志 */
const void *p_vDMem; /* 指向分配的内存块 */
INT32S iSize; /* 分配的内存大小 */
} DMEM_LOG;
初始化日志池及相关全局变量:
static DMEM_LOG *s_pstFreeLog; /* 指向空闲日志池 */
static INT8U s_byNumUsedLog;
static DMEM_LOG *s_pstHeadLog; /* 指向已使用日志链表的头部 */
#define NUM_DMEM_LOG 20
static DMEM_LOG s_astDMemLog[NUM_DMEM_LOG]; /* 静态日志池 */
接下来是日志的核心操作函数:初始化、插入日志(分配时调用)、移除日志(释放时调用)。这部分代码涉及到Linux等系统编程中常见的资源管理思想。
/* 初始化动态内存日志池 */
static void InitDMemLog(void) {
INT16S nCnt;
for (nCnt = 0; nCnt < NUM_DMEM_LOG; ++nCnt) {
s_astDMemLog[nCnt].p_stNext = &s_astDMemLog[nCnt + 1];
}
s_astDMemLog[NUM_DMEM_LOG - 1].p_stNext = NULL;
s_pstFreeLog = &s_astDMemLog[0];
return;
}
/* 记录一次内存分配 */
static void LogDMem(const void *p_vAddr, INT32S iSize) {
ASSERT(p_vAddr && iSize > 0);
DMEM_LOG *p_stLog;
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr;
#endif
/* 从空闲池获取一个日志块 */
OS_ENTER_CRITICAL(); /* 防止对s_pstFreeLog的竞态条件 */
if (!s_pstFreeLog) {
OS_EXIT_CRITICAL();
PRINTF("Allocate DMemLog failed.\r\n");
return;
}
p_stLog = s_pstFreeLog;
s_pstFreeLog = s_pstFreeLog->p_stNext;
OS_EXIT_CRITICAL();
p_stLog->p_vDMem = p_vAddr;
p_stLog->iSize = iSize;
/* 将该日志块插入已使用链表头部 */
OS_ENTER_CRITICAL(); /* 防止竞态条件 */
p_stLog->p_stNext = s_pstHeadLog;
s_pstHeadLog = p_stLog;
++s_byNumUsedLog;
OS_EXIT_CRITICAL();
return;
}
/* 移除一次内存分配记录 */
static void UnlogDMem(const void *p_vAddr) {
ASSERT(p_vAddr);
DMEM_LOG *p_stLog, *p_stPrev;
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr;
#endif
/* 在已使用链表中查找对应的日志块 */
OS_ENTER_CRITICAL();
p_stLog = p_stPrev = s_pstHeadLog;
while (p_stLog) {
if (p_vAddr == p_stLog->p_vDMem) {
break;
}
p_stPrev = p_stLog;
p_stLog = p_stLog->p_stNext;
}
if (!p_stLog) {
OS_EXIT_CRITICAL();
PRINTF("Search Log failed.\r\n");
return;
}
/* 从已使用链表中移除 */
if (p_stLog == s_pstHeadLog) {
s_pstHeadLog = s_pstHeadLog->p_stNext;
} else {
p_stPrev->p_stNext = p_stLog->p_stNext;
}
--s_byNumUsedLog;
OS_EXIT_CRITICAL();
p_stLog->p_vDMem = NULL;
p_stLog->iSize = 0;
/* 将该日志块归还到空闲池 */
OS_ENTER_CRITICAL();
p_stLog->p_stNext = s_pstFreeLog;
s_pstFreeLog = p_stLog;
OS_EXIT_CRITICAL();
return;
}
最后,实现带日志功能的包装函数 MallocExt 和 FreeExt。这些函数在调试模式下会填充初始值并记录日志,对于处理多线程环境下的并发问题也做了考虑。
/* 扩展的内存分配函数 */
void *MallocExt(INT32S iSize) {
ASSERT(iSize > 0);
void *p_vAddr;
p_vAddr = malloc(iSize);
if (!p_vAddr) {
PRINTF("malloc failed at %s line %d.\r\n", __FILE__, __LINE__);
} else {
#if (DMEM_DBG && DBG_VER)
memset(p_vAddr, 0xA3, iSize); /* 填充特定值便于调试 */
LogDMem(p_vAddr, iSize); /* 记录分配日志 */
#endif
}
return p_vAddr;
}
/* 扩展的内存释放函数 */
void FreeExt(void *p_vMem) {
ASSERT(p_vMem);
#if (DMEM_DBG && DBG_VER)
UnlogDMem(p_vMem); /* 从日志中移除记录 */
#endif
free(p_vMem);
return;
}
通过这套机制,在调试阶段可以清晰地跟踪每一块动态内存的生命周期。程序运行结束后,检查 s_byNumUsedLog 或遍历 s_pstHeadLog 链表,即可快速定位未被释放的内存块,极大提升了排查内存泄漏问题的效率。将良好的编程习惯与自动化工具相结合,是保障嵌入式系统稳定性的关键。