基于链表的C语言堆内存检测
说明
本文基于链表实现C语言堆内存的检测机制,可检测内存泄露、越界和重复释放等操作问题。
本文仅提供即视代码层面的检测机制,不考虑编译链接级的注入或钩子。此外,该机制暂未考虑并发保护。
相关性文章参见:
一 原理
堆内存泄漏检测机制的基本原理是截获对内存分配和释放函数的调用,从而跟踪每块内存的生命周期。例如,每次成功分配一块内存后,将内存分配信息(如指向它的指针、文件名、函数名、行号和申请字节数等)加入一个全局链表中;每当释放一块内存时,再从链表中删除相应的内存信息。这样,当程序结束时,链表中剩余的内存信息结点就对应那些未被释放的内存。
当运行环境支持断点调试时,记录下内存分配的序号会很有用;当运行环境支持堆栈回溯时,可记录回溯信息以便观察泄露时的函数调用栈。此外,可根据使用者指定的文件名和行号信息过滤链表结点,以输出指定内存申请代码处的回溯信息。
对于隐式内存泄露,可在程序运行过程中监控当前内存的总使用量和分配释放情况。以分配内存时的文件名和行号为索引,遍历链表结点即可计算出各处已分配但未释放的内存总量。若在连续多个时间间隔内,某文件中某行所分配的内存总量不断增长,则基本可确定属于隐式内存泄露(尤其是多线程引起的)。
当模块提供动态内存管理的封装接口时,可采用“红区”技术检测内存越界。例如,接口内每次申请比调用者所需更大的内存,将其首尾若干字节(守护字节,Guard Bytes)设置为特殊值,仅将中间部分的内存返回给调用者使用。通过检查特殊字节是否被改写,即可获知是否发生内存越界。其结构示意图如下:
需要注意的是,这些检测机制均会造成目标程序性能上的损失,因此最好能在编译时(通过编译选项)或运行时(通过设置使能标志等)可选地激活。
二 实现
本节将基于《C语言通用双向循环链表操作函数集》一文中的链表接口,实现堆内存检测机制。
文中“OMCI_”和“Omci”前缀为代码所在模块名信息,使用接口时可按需修改这些前缀。
2.1 数据结构
定义内存管理信息结构体如下:
1 typedef struct{ 2 const CHAR *pFileName; //内存分配函数调用处所在文件名 3 const CHAR *pFuncName; //内存分配函数调用处所在函数名 4 INT32U dwCodeLine; //内存分配函数调用处所在代码行号 5 INT32U dwMemSize; //内存分配函数申请到的内存字节数 6 VOID *pvMemAddr; //内存分配函数返回的内存指针地址 7 }OMCI_MEM_INFO;
这些信息将作为结点数据插入下面的全局链表中:
1 T_OMCI_LIST gtMemLeakCheckList = {.pHead=NULL, .pTail=NULL, .dwNodeNum=0, .dwNodeDataSize=0};
为统计内存分配情况,还定义如下结构体:
1 typedef struct{ 2 INT32U dwAllocBytes; //申请内存的总字节数 3 INT32U dwAllocTimes; //申请内存的总次数 4 INT32U dwFreeBytes; //申请内存的总字节数 5 INT32U dwFreeTimes; //释放内存的总次数 6 }T_MEM_STATIS;
这些统计值将存入下面的全局结构中:
1 static T_MEM_STATIS gtMemStatis = {0};
2.2 宏代码
为完成基本的内存分配和释放工作,必须调用系统提供的相应库函数(如calloc和free)。但直接使用库函数无法得到文件名等信息,因此需要借助宏对库函数进行封装:
1 //动态内存宏,使用方式同系统函数calloc和free,但不可与后者混用 2 #define OMCI_ALLOC(dwMemSize) OmciAlloc((dwMemSize), __FILE__, FUNC_NAME, __LINE__) 3 #define OMCI_FREE(pvMemBuf) OmciFree((pvMemBuf), __FILE__, FUNC_NAME, __LINE__)
此处考虑到OmciAlloc/ OmciFree函数可内置内存统计和越界检查,且相比内存泄露检测对系统资源和性能的影响不大,因此未使用条件编译控制。若不需要这些额外的好处,可采用下面的封装方式:
1 #ifdef OMCI_MEM_CHECK 2 #define OMCI_ALLOC(dwMemSize) OmciAlloc((dwMemSize), __FILE__, FUNC_NAME, __LINE__) 3 #define OMCI_FREE(pvMemBuf) OmciFree((pvMemBuf), __FILE__, FUNC_NAME, __LINE__) 4 #else 5 #define OMCI_ALLOC(dwMemSize) calloc((dwMemSize), 1) 6 #define OMCI_FREE(pvMemBuf) free((pvMemBuf)) 7 #end
OmciAlloc函数内调用calloc库函数来申请内存,OmciFree函数内则调用free库函数来释放内存。使用者在开发时一律使用OMCI_ALLOC和OMCI_FREE宏。注意,OmciAlloc函数未考虑对strdup/strndup/realloc等库函数的替换。
要检测内存越界,需要预留一些守护字节,其长度和内容定义如下:
1 //OMCI申请内存时的头守护字节数 2 #define OMCI_MEM_HEAD_SIZE 8 3 4 //OMCI内存管理特征码0xA5(二进制10100101) 5 #define OMCI_MEM_IDEN_CODE (CHAR)0xA5 6 7 //OMCI内存管理特征域所占字节数 8 #define OMCI_MEM_IDEN_SIZE 4 9 10 //OMCI申请内存时的尾守护字节数 11 #define OMCI_MEM_TAIL_SIZE 2
可见,OMCI_ALLOC在所申请的内存头部预留8个守护字节,其中Byte0~3标记OMCI内存管理特征码0xA5(二进制10100101),前两字节用于越界检测,后两字节用于调用匹配。Byte4~7记录申请的内存长度供OMCI_FREE释放时获取待释放内存大小。此外,所申请的内存尾部预留2个守护字节用于越界检测。守护字节对外屏蔽,即只能使用位于首尾守护字节之间的内存。建议头守护长度为4字节整数倍,以确保可用内存的对齐(否则可能降低CPU处理效率甚至导致访问崩溃)。此外,考虑到库函数分配的内存块通常大于所申请的内存(向上圆整),尾守护字节的开销可以忽略。
若在OMCI_ALLOC中向内存检测链表添加结点以存储待申请内存的信息,在OMCI_FREE中删除指向待删除内存指针所对应的结点,内存检测链表中剩余的结点即对应未释放(疑似泄露)的内存。需要注意的是,若未执行OMCI_ALLOC宏(如直接调用calloc等库函数或使用OMCI_ALLOC宏的分支被跳过),则无法检测到相应的动态内存。
内存泄露检测需要使用链表,对资源和性能影响较大,因此需要使用条件编译控制。同时,对于链表结点的操作比较固定,因此也用宏定义加以封装:
1 #ifdef __MEM_LEAK_CHECK 2 #define OMCI_INIT_MEMINFO() do{ \ 3 OmciInitList(>MemLeakCheckList, sizeof(OMCI_MEM_INFO)); \ 4 }while(0) 5 #define OMCI_INSERT_MEMINFO(pFile, pFunc, dwLine, dwMemBytes, pvMemBufAddr) do{ \ 6 OMCI_MEM_INFO tMemInfo = {0}; \ 7 tMemInfo.pFileName = (pFile); \ 8 tMemInfo.pFuncName = (pFunc); \ 9 tMemInfo.dwCodeLine = (dwLine); \ 10 tMemInfo.dwMemSize = (dwMemBytes); \ 11 tMemInfo.pvMemAddr = (pvMemBufAddr); \ 12 OmciAppendListNode(>MemLeakCheckList, &tMemInfo); \ 13 }while(0) 14 #define OMCI_REMOVE_MEMINFO(pFile, pFunc, dwLine, dwMemBytes, pvMemBufAddr) do{ \ 15 OMCI_MEM_INFO tMemInfo = {0}; \ 16 tMemInfo.pFileName = (pFile); \ 17 tMemInfo.pFuncName = (pFunc); \ 18 tMemInfo.dwCodeLine = (dwLine); \ 19 tMemInfo.dwMemSize = (dwMemBytes); \ 20 tMemInfo.pvMemAddr = (pvMemBufAddr); \ 21 T_OMCI_LIST_NODE *pDeleteNode = NULL; \ 22 pDeleteNode = OmciLocateListNode(>MemLeakCheckList, &tMemInfo, CompareNodeMem); \ 23 OmciRemoveListNode(>MemLeakCheckList, pDeleteNode); \ 24 }while(0) 25 #define OMCI_REPORT_MEMCHECK() do{ \ 26 OmciCheckMemLeak(>MemLeakCheckList); \ 27 }while(0) 28 #else 29 #define OMCI_INIT_MEMINFO() 30 #define OMCI_INSERT_MEMINFO(pFile, pFunc, dwLine, dwMemBytes, pvMemAddr) 31 #define OMCI_REMOVE_MEMINFO(pFile, pFunc, dwLine, dwMemBytes, pvMemAddr) 32 #define OMCI_REPORT_MEMCHECK() 33 #endif
条件编译表达式__MEM_LEAK_CHECK(对应Makefile编译选项-D__MEM_LEAK_CHECK)用于控制内存检测功能的开启和关闭。
2.3 函数接口
2.3.1 私有函数
通过链表储存内存分配信息时,链表自身也需要分配和释放内存。若不与外界的普通内存分配和释放区分,将会出现无限循环递归调用。因此,首先对链表函数集稍加改造:
1 static T_OMCI_LIST_NODE *CreateListNode(T_OMCI_LIST *pList, VOID *pvNodeData) 2 { 3 T_OMCI_LIST_NODE *pInsertNode = NULL; 4 #ifdef __MEM_LEAK_CHECK //避免内存检测链表创建结点时陷入无限循环 5 extern T_OMCI_LIST gtMemLeakCheckList; 6 if(pList == >MemLeakCheckList) 7 { 8 pInsertNode = (T_OMCI_LIST_NODE*)calloc((sizeof(T_OMCI_LIST_NODE)+pList->dwNodeDataSize), 1); 9 } 10 else 11 #endif 12 { 13 pInsertNode = (T_OMCI_LIST_NODE*)OMCI_ALLOC((sizeof(T_OMCI_LIST_NODE)+pList->dwNodeDataSize)); 14 } 15 if(NULL == pInsertNode) 16 { 17 printf("[%s]pList(%p) failed to alloc for pInsertNode!\n", FUNC_NAME, pList); 18 return NULL; 19 } 20 21 pInsertNode->pvNodeData = (INT8U *)pInsertNode + sizeof(T_OMCI_LIST_NODE); 22 if(NULL != pvNodeData) 23 { //创建非头结点时 24 memmove(pInsertNode->pvNodeData, pvNodeData, pList->dwNodeDataSize); 25 } 26 27 return pInsertNode; 28 } 29 static LIST_STATUS RemoveListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE *pNode) 30 { 31 OMCI_ISOL_NODE(pNode); 32 33 //释放链表结点 34 #ifdef __MEM_LEAK_CHECK //避免内存检测链表销毁结点时陷入无限循环 35 extern T_OMCI_LIST gtMemLeakCheckList; 36 if(pList == >MemLeakCheckList) 37 { 38 free(pNode); 39 } 40 else 41 #endif 42 { 43 OMCI_FREE(pNode); 44 } 45 46 return OMCI_LIST_OK; 47 } 48 static LIST_STATUS DestroyListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE **pNode) 49 { 50 //释放链表结点 51 #ifdef __MEM_LEAK_CHECK //避免内存检测链表销毁结点时陷入无限循环 52 extern T_OMCI_LIST gtMemLeakCheckList; 53 if(pList == >MemLeakCheckList) 54 { 55 free(*pNode); 56 } 57 else 58 #endif 59 { 60 OMCI_FREE(*pNode); 61 } 62 *pNode = NULL; 63 64 return OMCI_LIST_OK; 65 }
创建链表结点时判断链表指针是否指向gtMemLeakCheckList。若是,表示检测机制内部申请/释放内存,此时调用calloc/free函数;若否,表示外部使用者申请/释放内存,此时调用OMCI_ALLOC/ OMCI_FREE宏。由此亦知,检测机制不会检测自身的内存泄露。
为隐藏细节和管理方便,首尾守护字节的初始化和解析等操作也封装函数:
1 /********************************************************************** 2 * 函数名称: GenerateMemChecker 3 * 功能描述: 构造动态内存检测区 4 * 输入参数: VOID *pMemBuffer :内存管理函数申请到的内存地址 5 * INT32U dwMemSize :内存管理函数申请到的内存字节数 6 * 输出参数: NA 7 * 返 回 值: CHAR *:指向可用动态内存(跳过信息头)的指针 8 ***********************************************************************/ 9 static CHAR *GenerateMemChecker(VOID *pvMemBuf, INT32U dwMemSize) 10 { 11 CHAR *pHeader = (CHAR *)pvMemBuf; 12 memset(pHeader, OMCI_MEM_IDEN_CODE, OMCI_MEM_IDEN_SIZE); 13 memmove(&pHeader[OMCI_MEM_IDEN_SIZE], &dwMemSize, sizeof(dwMemSize)); 14 memset(&pHeader[OMCI_MEM_HEAD_SIZE+dwMemSize], OMCI_MEM_IDEN_CODE, OMCI_MEM_TAIL_SIZE); 15 16 return pHeader + OMCI_MEM_HEAD_SIZE; 17 } 18 19 /********************************************************************** 20 * 函数名称: DeriveMemHeader 21 * 功能描述: 提取动态内存信息头 22 * 输入参数: VOID *pvMemBuf :内存管理函数申请到的内存地址 23 * 输出参数: INT32U *dwMemSize :内存管理函数申请到的内存字节数 24 * 返 回 值: CHAR *:指向动态内存信息头的指针 25 ***********************************************************************/ 26 static CHAR *DeriveMemHeader(VOID *pvMemBuf, INT32U *dwMemSize) 27 { 28 CHAR *pHeader = (CHAR *)pvMemBuf - OMCI_MEM_HEAD_SIZE; 29 30 //若动态内存由OMCI_ALLOC申请,则由信息头获取待释放内存大小 31 if((OMCI_MEM_IDEN_CODE == pHeader[2]) && (OMCI_MEM_IDEN_CODE == pHeader[3])) 32 { 33 *dwMemSize = *(INT32U*)&(pHeader[OMCI_MEM_IDEN_SIZE]); 34 } 35 36 return pHeader; 37 } 38 39 /********************************************************************** 40 * 函数名称: IsMemHeadOverrun/IsMemTailOverrun 41 * 功能描述: 检查动态内存是否存在头(低地址)/尾(高地址)越界 42 ***********************************************************************/ 43 static BOOL IsMemHeadOverrun(CHAR *pMemHeader) 44 { 45 return ((OMCI_MEM_IDEN_CODE != *pMemHeader) || (OMCI_MEM_IDEN_CODE != *(pMemHeader+1))); 46 } 47 static BOOL IsMemTailOverrun(VOID *pvMemBuf, INT32U dwMemSize) 48 { 49 return ((OMCI_MEM_IDEN_CODE != *((CHAR*)pvMemBuf+dwMemSize)) || 50 (OMCI_MEM_IDEN_CODE != *((CHAR*)pvMemBuf+dwMemSize+1))); 51 }
内存泄露检测相关的函数如下:
1 /********************************************************************** 2 * 函数名称: PrintListMem 3 * 功能描述: 打印OMCI内存泄露检测结果 4 * 输入参数: VOID *pvNodeData :链表结点数据指针 5 * INT32U dwNodeNum :链表结点数目 6 ***********************************************************************/ 7 static VOID PrintListMem(VOID *pvNodeData, INT32U dwNodeNum) 8 { 9 OMCI_MEM_INFO *ptMemInfo = (OMCI_MEM_INFO *)pvNodeData; 10 printf("[%s(%d)<%s>]%uBytes(Address:%p) were allocated, but unfreed!\n", 11 ptMemInfo->pFileName, ptMemInfo->dwCodeLine, ptMemInfo->pFuncName, 12 ptMemInfo->dwMemSize, (CHAR*)ptMemInfo->pvMemAddr); 13 } 14 15 /********************************************************************** 16 * 函数名称: OmciCheckMemLeak 17 * 功能描述: OMCI内存泄露检测 18 * 输入参数: T_OMCI_LIST *gpMemLeakCheckList :泄露结点链表指针 19 ***********************************************************************/ 20 static VOID OmciCheckMemLeak(T_OMCI_LIST *gpMemLeakCheckList) 21 { 22 INT32U dwMemLeakNum = OmciGetListNodeNum(gpMemLeakCheckList); 23 if(0 == dwMemLeakNum) 24 { 25 printf("No Memory Leakage Detected!\n"); 26 return; 27 } 28 printf("Suspected Memory Leakage Occurrence(%u Time(s)):\n", dwMemLeakNum); 29 OmciPrintListNode(gpMemLeakCheckList, PrintListMem); 30 } 31 32 /********************************************************************** 33 * 函数名称: CompareNodeMem 34 * 功能描述: 可疑泄露结点信息比较 35 * 输入参数: VOID *pNodeData :链表结点数据指针 36 * VOID *pData :待比较外部数据指针 37 * INT32U dwNodeDataSize :链表结点数据大小 38 * 输出参数: NA 39 * 返 回 值: 0:Equal; !0:Unequal 40 * 注意事项: 比较长度为结点数据字节数,即默认与外部数据大小一致 41 ***********************************************************************/ 42 static INT8U CompareNodeMem(VOID *pNodeData, VOID *pData, INT32U dwNodeDataSize) 43 { 44 return (((OMCI_MEM_INFO *)pNodeData)->pvMemAddr != ((OMCI_MEM_INFO *)pData)->pvMemAddr); 45 }
CompareNodeMem函数中,内存指针匹配时内存大小不一定匹配,此时仍可正常释放,但头守护字节的内存大小字段被越界改写。若要检测这种错误(意义不大),可将函数改写为:
1 INT8U CompareNodeMem(VOID *pNodeData, VOID *pData, INT32U dwNodeDataSize) 2 { 3 OMCI_MEM_INFO *ptMemInfo1 = (OMCI_MEM_INFO *)pNodeData; 4 OMCI_MEM_INFO *ptMemInfo2 = (OMCI_MEM_INFO *)pData; 5 6 if(ptMemInfo1->pvMemBufAddr != ptMemInfo2->pvMemBufAddr) 7 return 1; 8 9 if(ptMemInfo1->dwMemSize != ptMemInfo2->dwMemSize) 10 { //因内存指针匹配,故仍可正常释放,但提示某处可能存在内存越界 11 printf("MemSize Field of Head Guard was Overwritten([%s(%u)<%s>]Addr:%p, Size:%u->%u)!\n", 12 ptMemInfo2->pFileName, ptMemInfo2->dwCodeLine, ptMemInfo2->pFuncName, 13 ptMemInfo2->pvMemBufAddr, ptMemInfo1->dwMemSize, ptMemInfo2->dwMemSize); 14 } 15 16 return 0; 17 }
基于上述私有函数,可进一步构建内存管理和检测的基本接口。
2.3.2 公共接口
若要启用内存泄露检测机制,则分配内存前必须先初始化全局链表gtMemLeakCheckList:
1 /********************************************************************** 2 * 函数名称: OmciInitMemCheck 3 * 功能描述: 初始化内存泄露检测机制 4 * 注意事项: 检测内存泄露时必须先调用本函数进行初始化,然后分配内存 5 ***********************************************************************/ 6 VOID OmciInitMemCheck(VOID) 7 { 8 OMCI_INIT_MEMINFO(); 9 }
完成链表初始化工作后,内存分配接口内就可向其插入内存信息:
1 /********************************************************************** 2 * 函数名称: OmciAlloc 3 * 功能描述: 动态分配内存并初始化 4 * 输入参数: INT32U dwMemSize :内存管理函数申请到的内存字节数 5 * const CHAR *pFileName :内存管理函数调用处所在文件名 6 * const CHAR *pFuncName :内存管理函数调用处所在函数名 7 * INT32U dwCodeLine :内存管理函数调用处所在代码行号 8 * 输出参数: NA 9 * 返 回 值: VOID *:内存管理函数返回的内存指针地址 10 * 注意事项: 必须通过OMCI_ALLOC宏间接调用,且不可与free配对使用 11 ***********************************************************************/ 12 VOID *OmciAlloc(INT32U dwMemSize, const CHAR *pFileName, const CHAR *pFuncName, INT32U dwCodeLine) 13 { 14 if(0 == dwMemSize) 15 { 16 printf("[%s(%u)<%s>] Memmory to be allocated must be larger than Zero Byte!\n", 17 pFileName, dwCodeLine, pFuncName); 18 return NULL; 19 } 20 21 VOID *pvMemBuf = calloc((dwMemSize+OMCI_MEM_HEAD_SIZE+OMCI_MEM_TAIL_SIZE), 1); 22 if(NULL == pvMemBuf) 23 { 24 printf("[%s(%d)<%s>] Allocating %uBytes failed, no memory available!\n", 25 pFileName, dwCodeLine, pFuncName, dwMemSize); 26 return NULL; 27 } 28 29 gtMemStatis.dwAllocBytes += dwMemSize; 30 gtMemStatis.dwAllocTimes++; 31 32 pvMemBuf = GenerateMemChecker(pvMemBuf, dwMemSize); 33 OMCI_INSERT_MEMINFO(pFileName, pFuncName, dwCodeLine, dwMemSize, pvMemBuf); 34 35 return pvMemBuf; 36 }
通过头尾守护字节和内存分配信息,释放时可检测内存越界并删除链表结点:
1 /********************************************************************** 2 * 函数名称: OmciFree 3 * 功能描述: 释放通过OMCI_ALLOC分配的动态内存 4 * 输入参数: VOID* pvMemBuf :指向待释放内存的指针 5 * const CHAR *pFileName :内存管理函数调用处所在文件名 6 * const CHAR *pFuncName :内存管理函数调用处所在函数名 7 * INT32U dwCodeLine :内存管理函数调用处所在代码行号 8 * 输出参数: NA 9 * 返 回 值: FUNC_STATUS 10 * 注意事项: 必须通过OMCI_FREE宏间接调用,且不可与malloc/calloc等配对使用 11 ***********************************************************************/ 12 FUNC_STATUS OmciFree(VOID* pvMemBuf, const CHAR* pFileName, const CHAR* pFuncName, INT32U dwCodeLine) 13 { 14 if(NULL == pvMemBuf) 15 { 16 printf("[%s(%d)<%s>] Cannot free Null Address!\n", pFileName, dwCodeLine, pFuncName); 17 return S_NULL_POINTER; 18 } 19 20 INT32U dwMemSize = 0; 21 CHAR *pMemHeader = DeriveMemHeader(pvMemBuf, &dwMemSize); 22 23 //对于Double Free,无论头守护字节是否已被改写,free均会发生段错误; 24 //对于非OMCI_ALLOC申请的内存,free可能造成越界释放。 25 //但本函数难以区分两种情况。综合考虑,决定拦截并警告。 26 if(0 == dwMemSize) 27 { 28 printf("Warning: [%s(%d)<%s>]Free Memory(%p) that was 1) not allocated by OMCI_ALLOC or 2) freed by OMCI_FREE;\n", 29 pFileName, dwCodeLine, pFuncName, pvMemBuf); 30 printf(" Both cases are critical, and NO free action takes place to minimize the casualties!!!\n"); 31 return S_ILLEGAL_PARAM; 32 } 33 34 if(IsMemHeadOverrun(pMemHeader)) 35 { 36 printf("Warning: [%s(%d)<%s>]Overrun Occurs at Lower Address(%p);\n", 37 pFileName, dwCodeLine, pFuncName, pvMemBuf); 38 } 39 if(IsMemTailOverrun(pvMemBuf, dwMemSize)) 40 { 41 printf("Warning: [%s(%d)<%s>]Overrun Occurs at Higher Address(%p);\n", 42 pFileName, dwCodeLine, pFuncName, pvMemBuf); 43 } 44 45 OMCI_REMOVE_MEMINFO(pFileName, pFuncName, dwCodeLine, dwMemSize, pvMemBuf); 46 47 free(pMemHeader); 48 49 gtMemStatis.dwFreeBytes += dwMemSize; 50 gtMemStatis.dwFreeTimes++; 51 52 return S_OK; 53 }
程序执行过程中,可随时调用OmciShowMemInfo查看内存分配的统计信息及泄露情况:
1 /********************************************************************** 2 * 函数名称: OmciShowMemInfo 3 * 功能描述: 查看OMCI模块当前内存申请与释放信息 4 * 输入参数: NA 5 * 输出参数: NA 6 * 返 回 值: VOID 7 * 注意事项: 当__MEM_LEAK_CHECK已定义时,本函数同时显示可能存在的内存泄露 8 ***********************************************************************/ 9 VOID OmciShowMemInfo(VOID) 10 { 11 printf("-------------------Omci Memory Info-------------------\n"); 12 printf("%-15s%-15s%-15s%-15s%-15s\n", "AllocBytes", "AllocTimes", "FreeBytes", "FreeTimes", "<UnFreeBytes>"); 13 printf("%-15u%-15u%-15u%-15u%-15u\n", gtMemStatis.dwAllocBytes, gtMemStatis.dwAllocTimes, 14 gtMemStatis.dwFreeBytes, gtMemStatis.dwFreeTimes, gtMemStatis.dwAllocBytes-gtMemStatis.dwFreeBytes); 15 16 OMCI_REPORT_MEMCHECK(); 17 }
三 测试
本节将对上文实现的堆内存检测机制进行测试。
1 CHAR *gpGlobalMem = NULL; 2 VOID GlobalAllocTest(VOID){ 3 gpGlobalMem = OMCI_ALLOC(50); 4 } 5 VOID GlobalFreeTest(VOID){ 6 OMCI_FREE(gpGlobalMem); 7 } 8 9 INT32S main(VOID) 10 { 11 INT8U ucTestIndex = 1; 12 OMCI_INIT_MEMINFO(); 13 14 printf("\n<Test Case %u>: Simple Alloc-Free within Function!\n", ucTestIndex++); 15 CHAR *ptr = OMCI_ALLOC(10); 16 CHAR *ptr1 = OMCI_ALLOC(20); 17 CHAR *ptr2 = OMCI_ALLOC(30); 18 CHAR *ptr3 = OMCI_ALLOC(40); 19 strcpy(ptr, "123456789"); 20 printf("ptr=%s(PreFree)\n", ptr); 21 OMCI_FREE(ptr); //为测试需要,此处释放内存后不置ptr为NULL 22 printf("ptr=%s(PostFree)\n", ptr); 23 OmciShowMemInfo(); 24 25 printf("\n<Test Case %u>: Simple Alloc-Free outside Function!\n", ucTestIndex++); 26 GlobalAllocTest(); 27 OmciShowMemInfo(); 28 29 printf("\nTest Case %u: Free all Allocated Memories!\n", ucTestIndex++); 30 OMCI_FREE(ptr1); 31 OMCI_FREE(ptr2); 32 OMCI_FREE(ptr3); 33 GlobalFreeTest(); 34 OmciShowMemInfo(); 35 36 printf("\nTest Case %u: Overrun Downward!\n", ucTestIndex++); 37 CHAR *ptr5 = OMCI_ALLOC(33); 38 *(ptr5-8) = 10; 39 OMCI_FREE(ptr5); 40 41 printf("\nTest Case %u: Overrun Upward!\n", ucTestIndex++); 42 CHAR *ptr6 = OMCI_ALLOC(5); 43 strcpy(ptr6, "123456789"); 44 OMCI_FREE(ptr6); 45 46 printf("\nTest Case %u: Doubly Free!\n", ucTestIndex++); 47 OMCI_FREE(ptr6); 48 49 printf("\nTest Case %u: Free Memory not allocated by OMCI_ALLOC!\n", ucTestIndex++); 50 CHAR *ptr7 = malloc(44); 51 OMCI_FREE(ptr7); 52 53 return 0; 54 }
关闭-D__MEM_LEAK_CHECK编译选项时,测试结果如下所示(不检查堆内存泄露):
1 <Test Case 1>: Simple Alloc-Free within Function! 2 ptr=123456789(PreFree) 3 ptr=123456789(PostFree) 4 -------------------Omci Memory Info------------------- 5 AllocBytes AllocTimes FreeBytes FreeTimes <UnFreeBytes> 6 100 4 10 1 90 7 8 <Test Case 2>: Simple Alloc-Free outside Function! 9 -------------------Omci Memory Info------------------- 10 AllocBytes AllocTimes FreeBytes FreeTimes <UnFreeBytes> 11 150 5 10 1 140 12 13 Test Case 3: Free all Allocated Memories! 14 -------------------Omci Memory Info------------------- 15 AllocBytes AllocTimes FreeBytes FreeTimes <UnFreeBytes> 16 150 5 150 5 0 17 18 Test Case 4: Overrun Downward! 19 Warning: [Omci_Main.c(61)<main>]Overrun Occurs at Lower Address(0x8e38050); 20 21 Test Case 5: Overrun Upward! 22 Warning: [Omci_Main.c(67)<main>]Overrun Occurs at Higher Address(0x8e38010); 23 24 Test Case 6: Doubly Free! 25 Warning: [Omci_Main.c(70)<main>]Free Memory(0x8e38010) that was 1) not allocated by OMCI_ALLOC or 2) freed by OMCI_FREE; 26 Both cases are critical, and NO free action takes place to minimize the casualties!!! 27 28 Test Case 7: Free Memory Not Allocated by OMCI_ALLOC! 29 Warning: [Omci_Main.c(74)<main>]Free Memory(0x8e38048) that was 1) not allocated by OMCI_ALLOC or 2) freed by OMCI_FREE; 30 Both cases are critical, and NO free action takes place to minimize the casualties!!!
开启-D__MEM_LEAK_CHECK编译选项时,测试结果如下所示:
1 <Test Case 1>: Simple Alloc-Free within Function! 2 ptr=123456789(PreFree) 3 ptr=123456789(PostFree) 4 -------------------Omci Memory Info------------------- 5 AllocBytes AllocTimes FreeBytes FreeTimes <UnFreeBytes> 6 100 4 10 1 90 7 Suspected Memory Leakage Occurrence(3 Time(s)): 8 [Omci_Main.c(37)<main>]20Bytes(Address:0x8428078) were allocated, but unfreed! 9 [Omci_Main.c(38)<main>]30Bytes(Address:0x84280c8) were allocated, but unfreed! 10 [Omci_Main.c(39)<main>]40Bytes(Address:0x8428120) were allocated, but unfreed! 11 12 13 <Test Case 2>: Simple Alloc-Free outside Function! 14 -------------------Omci Memory Info------------------- 15 AllocBytes AllocTimes FreeBytes FreeTimes <UnFreeBytes> 16 150 5 10 1 140 17 Suspected Memory Leakage Occurrence(4 Time(s)): 18 [Omci_Main.c(37)<main>]20Bytes(Address:0x8428078) were allocated, but unfreed! 19 [Omci_Main.c(38)<main>]30Bytes(Address:0x84280c8) were allocated, but unfreed! 20 [Omci_Main.c(39)<main>]40Bytes(Address:0x8428120) were allocated, but unfreed! 21 [Omci_Main.c(17)<GlobalAllocTest>]50Bytes(Address:0x8428180) were allocated, but unfreed! 22 23 24 Test Case 3: Free all Allocated Memories! 25 -------------------Omci Memory Info------------------- 26 AllocBytes AllocTimes FreeBytes FreeTimes <UnFreeBytes> 27 150 5 150 5 0 28 No Memory Leakage Detected! 29 30 Test Case 4: Overrun Downward! 31 Warning: [Omci_Main.c(61)<main>]Overrun Occurs at Lower Address(0x84280c8); 32 33 Test Case 5: Overrun Upward! 34 Warning: [Omci_Main.c(67)<main>]Overrun Occurs at Higher Address(0x8428038); 35 36 Test Case 6: Doubly Free! 37 Warning: [Omci_Main.c(70)<main>]Free Memory(0x8428038) that was 1) not allocated by OMCI_ALLOC or 2) freed by OMCI_FREE; 38 Both cases are critical, and NO free action takes place to minimize the casualties!!! 39 40 Test Case 7: Free Memory Not Allocated by OMCI_ALLOC! 41 Warning: [Omci_Main.c(74)<main>]Free Memory(0x84280c0) that was 1) not allocated by OMCI_ALLOC or 2) freed by OMCI_FREE; 42 Both cases are critical, and NO free action takes place to minimize the casualties!!!
四 后记
在大型项目中,系统支撑层面通常会封装统一的内存申请/释放接口,以便管理和优化。发生内存泄露时,外部检测工具所报告的泄露代码位于接口内,对排障帮助有限(除非提供堆栈回溯)。而本文提供的检测机制天然地寄生于管理接口之上,能准确地指示泄露内存的分配现场。
若要直接接管malloc/free等库函数调用,可采用如下方式:
1 //Omci_Alloc.h 2 #ifndef OMCI_INNER_ALLOC 3 #define malloc(dwMemSize) OmciAlloc((dwMemSize), __FILE__, FUNC_NAME, __LINE__) 4 #define free(pvMemBuf) OmciFree((pvMemBuf), __FILE__, FUNC_NAME, __LINE__) 5 #endif 6 7 //Omci_Alloc.c 8 #define OMCI_INNER_ALLOC 9 #include "Omci_Alloc.h"
如此可避免OmciAlloc/ OmciFree内递归调用库函数。若在Omci_Alloc.c内实现内存泄露检测的链表操作(单链表即可),则不必“污染”双向循环链表函数集。
此外,为使检测代码的简单易懂,文中未对其进行优化和异常保护。