一.
node
在 咱们我的编程的过程中,内存泄露虽然不会像内存溢出那样形成各类莫名奇妙的问题,可是它的危害也是不可忽视的。一方面,内存的泄露致使咱们的软件在运行 过程当中占用了愈来愈多的内存,占有资源而又得不到及时清理,这会致使咱们程序的效率愈来愈低;另外一方面,它会影响咱们用户的体验,失去市场的竞争能力。编程
常见的内存泄露是这样的:缓存
- void process(int size)
- {
- char* pData = (char*)malloc(size);
-
- /* other code */
-
- return; /* forget to free pData */
- }
如上图所示,咱们在函数process的处理过程当中,每一次都须要对内存进行申请,可是在函数结束的时候却没有进行释放。若是这样的一段代码出如今业务 侧,那么后果是不可思议的。举个例子来讲,若是咱们服务器每秒钟须要接受100个用户的并发访问,每一个用户过来的数据,咱们都须要本地申请内存从新保存一 份。处理结束以后,若是内存没有获得很好地释放,就会致使咱们服务器可用的物理内存愈来愈少。一旦达到某一个临界点以后,操做系统不得不经过内外存的调度 来知足咱们申请新内存的需求,这在另外一方面来说又会下降服务器服务的质量。
内存泄露的危害是不言而喻的,可是查找内存泄露倒是一件苦难并且复杂的工做。咱们都知道,解决bug是一件很是简单的事情,可是寻找bug的出处倒是一 件很是吃力的事情。所以,咱们有必要在本身编写代码的时候,就把查找内存泄露的工做放在很重要的位置上面。那么有没有什么办法来解决这一问题呢?服务器
我想要作到解决内存泄露,必须作到下面两个方面:数据结构
(1)必须记录内存在哪一个函数申请的,具体文件的行数是多少并发
(2)内存应该何时被释放函数
要完成第1个条件其实并不困难。咱们能够用节点的方法记录咱们申请的内存:测试
a)设置节点的数据结构spa
- typedef struct _MEMORY_NODE
- {
- char functionName[64];
- int line;
- void* pAddress;
- struct _MEMORY_NODE* next;
-
- }MEMORY_NODE;
其中 functionName记录函数名称,line记录行数, pAddress记录分配的地址, next记录下一个内存节点。
b)修改内存的分配函数操作系统
对业务侧的malloc进行函数修改,添加下面一句宏语句
#define malloc(param) MemoryMalloc(__FUNCTION__, __LINE__, param)
在桩函数侧书写下面的代码
- void* MemoryMalloc(const char* name, int line, int size)
- {
- void* pData = (void*)malloc(size);
- MEMORY_NODE* pMemNode = NULL;
- if(NULL == pData) return NULL;
- memset((char*)pData, 0, size);
-
- pMemNode = (MEMORY_NODE*)malloc(sizeof(MEMORY_NODE));
- if(NULL == pMemNode){
- free(pData);
- return NULL;
- }
- memset((char*)pMemNode, 0, sizeof(MEMORY_NODE));
- memmove(pMemNode->functionName, name, strlen(name));
- pMemNode->line = line;
- pMemNode->pAddress = pData;
- pMemNode->next = NULL;
- add_memory_node(pMemNode);
-
- return pData;
- }
内存的分配过程当中还涉及到了节点的添加,因此咱们还须要添加下面的代码
- static MEMORY_NODE* gMemNode = NULL;
-
- void add_memory_node(MEMORY_NODE* pMemNode)
- {
- MEMORY_NODE* pNode = gMemNode;
- if(NULL == pMemNode) return;
- if(NULL == gMemNode){
- gMemNode = pMemNode;
- return;
- }
-
- while(NULL != pNode->next){
- pNode = pNode->next;
- }
- pNode->next = pMemNode;
- return;
- }
文中gMemNode表示全部内存节点的根节点,咱们每增长一次malloc过程就会对内存节点进行记录。在记录过程当中,咱们还会记录调用malloc的函数名称和具体文件行数,这主要是为了方便咱们在后面进行故障定位的时候更好地查找。
完成了第一个条件以后,咱们就要对第二个条件进行完成。
a)内存何时释放,这取决于咱们在函数中是怎么实现的,可是咱们在编写测试用例的时候倒是应该知道内存释放没有,好比说若是测试用例所有结束了,咱们有理由相信assert(gMemNode == NULL)这应该是恒等于真的。
b)内存释放的时候,咱们应该作些什么?和节点的添加同样,咱们在内存释放的时候须要free指定的内存,free节点,free节点的内存,下面就是在释放的时候咱们须要进行的操做
对业务侧的free函数进行修改,添加下面一句宏代码,
#define free(param) MemoryFree(param)
在桩函数侧输入下面的代码:
- void MemoryFree(void* pAddress)
- {
- if(NULL == pAddress) return;
- delete_memory_node(pAddress);
- free(pAddress);
- }
在删除内存的时候,须要删除节点,删除节点的内存
- void delete_memory_node(void* pAddress)
- {
- MEMORY_NODE* pHead = gMemNode;
- MEMORY_NODE* pMemNode = gMemNode;
- while(NULL != pMemNode){
- if(pAddress == pMemNode->pAddress)
- break;
- pMemNode = pMemNode->next;
- }
- if(NULL == pMemNode) {
- assert(1 == 0);
- return;
- }
-
- while(pMemNode != pHead->next){
- pHead = pHead->next;
- }
-
- if(pMemNode == gMemNode){
- gMemNode = gMemNode->next;
- }else{
- pHead->next = pMemNode->next;
- }
- free(pMemNode);
- return;
- }
有了上面一小段代码的帮助,咱们在编写测试用例的时候,就能够在函数执行后,经过判断内存节点是否为空的方法判断内存是否已经释放。若是内存没有释放,咱们还能经过节点的信息帮助咱们是哪里发生了错误,可是这个方法还有两个缺点:
(1)没有考虑缓存的状况,好多内存分配了以后并不会在函数中立刻释放,而是放在缓存池中等待下一次调用,这就须要咱们准确把握和判断了。
(2)代码中节点删除和添加的时候没有考虑多进程的情形,应该考虑用一个互斥锁或者是信号量加以保护。
二.
内存越界是咱们软件开发中常常遇到的一个问题。不经意间的复制经常致使很严重的后果。常用memset、memmove、strcpy、 strncpy、strcat、sprintf的朋友确定对此印象深入,下面就是我我的在开发中实际遇到的一个开发问题,颇具典型。
- #define MAX_SET_STR_LENGTH 50
- #define MAX_GET_STR_LENGTH 100
-
- int* process(char* pMem, int size)
- {
- char localMemory[MAX_SET_STR_LENGTH] = {0};
- int* pData = NULL;
-
- /* code process */
- memset(localMemory, 1, MAX_GET_STR_LENGTH);
- memmove(pMem, localMemory, MAX_GET_STR_LENGTH);
- return pData;
- }
这段代码看上去没有什么问题。咱们本意是对localMemory进行赋值,而后拷贝到pMem指向的内存中去。其实问题就出在这一句memset的大 小。根据localMemory初始化定义语句,咱们能够看出localMemory其实最初的申明大小只有MAX_SET_STR_LENGTH,可是 咱们赋值的时候,却设置成了MAX_GET_STR_LENGTH。之因此会犯这样的错误,主要是由于MAX_GET_STR_LENGTH和 MAX_SET_STR_LENGTH极其类似。这段代码编译后,产生的后果是很是严重的,不断冲垮了堆栈信息,还把返回的int*设置成了非法值。
那么有没有什么好的办法来处理这样一个问题?咱们能够换一个方向来看。首先咱们查看,在软件中存在的数据类型主要有哪些?无非就是全局数据、堆数据、栈 临时数据。搞清楚了须要控制的数据以后,咱们应该怎么对这些数据进行监控呢,一个简单有效的办法就是把memset这些函数替换成咱们本身的函数,在这些 函数中咱们严格对指针的复制、拷贝进行判断和监督。
(1)事实上,通常来讲malloc的数据是不须要咱们监督的,由于内存分配的时候,一般库函数会比咱们要求的size多分配几个字节,这样在free的时候就能够判断内存的开头和结尾处有没有指针溢出。朋友们能够试一下下面这段代码。
- void heap_memory_leak()
- {
- char* pMem = (char*)malloc(100);
- pMem[-1] = 100;
- pMem[100] = 100;
- free(pMem);
- }
pMem[-1] = 100是堆左溢出, pMem[100]是堆右溢出。
(2)堆全局数据和栈临时数据进行处理时,咱们利用memset初始化记录全局指针或者是堆栈临时指针
a) 首先对memset处理,添加下面一句宏语句
#define memset(param, value, size) MEMORY_SET_PROCESS(__FUNCTION__, __LINE__, param, value, size)
b) 定义内存节点结构
- typedef struct _MEMORY_NODE
- {
- char functionName[64];
- int line;
- void* pAddress;
- int size;
- struct _MEMORY_NODE* next;
-
- }MEMORY_NODE;
其中functionName记录了函数名称,line记录文件行数, pAddress记录了指针地址, size指向了pAddress指向的内存大小,next指向下一个结构节点。
c)记录内存节点属性
在MEMORY_SET_PROCESS处理过程当中,不只须要调用memset函数,还须要对当前内存节点进行记录和保存。能够经过使用单链表节点的方 法进行记录。可是若是发现pAddress指向的内存是malloc时候分配过的,此时就不须要记录了,由于堆内存指针溢出的问题lib库已经帮咱们解决 了。
d)改造原有内存指针操做函数
好比对memmove等函数进行改造,不失去通常性,咱们就以memmove做为范例。
添加宏语句 #define memmove(dst, src, size) MEMMOVE_PROCESS(dst, src, size)
- void MEMMOVE_PROCESS(void* dst, const void* src, int size)
- {
- MEMORY_NODE* pMemNode = check_node_exist(dst);
- if(NULL == pMemNode) return;
-
- assert(dst >= (pMemNode->pAddress));
- assert(((char*)dst + size) <= ((char*)pMemNode->pAddress + pMemNode->size));
- memmove(dst, src, size);
- return;
- }
e)下面就是内存节点的删除工做。
咱们知道函数是须要反复使用堆栈的。不一样时间相同的堆栈地址对应的是彻底不一样的指针内容,这就要求咱们在函数返回的时候对内存地址进行清理,把内存节点从对应的链表删除。
咱们知道在函数运行后,ebp和esp之间的内存就是一般意义上临时变量的生存空间,因此下面的一段宏就能够记录函数的内存空间。
- #ifdef MEMORY_LEAK_TEST
- #define FUNCTION_LOCAL_SPACE_RECORD()\
- {\
- int* functionBpRecord = 0;\
- int* functionSpRecord = 0;\
- }
- #else
- #define FUNCTION_LOCAL_SPACE_RECORD()
- #endif
-
- #ifdef MEMORY_LEAK_TEST
- #define FUNCTION_LEAVE_PROCESS()\
- {\
- __asm { mov functionBpRecord, bp\
- mov functionSpRecord, sp}\
- FREE_MEMORY_NODE(functionBpRecord, functionSpRecord)\
- }
- #else
- #define FUNCTION_LEAVE_PROCESS()
- #endif
这两段宏代码,须要插在函数的起始位置和结束的位置,这样在函数结束的时候就能够根据ebp和esp删除堆栈空间中的全部内存,方便了堆栈的重复使用。若是是全局内存,由于函数的变化不会致使地址的变化,因此没有必要进行全局内存节点的处理。
内存溢出检查流程总结:
(1)对memset进行从新设计,记录除了malloc指针外的一切内存;
(2)对memmove, strcpy, strncpy,strcat,sprintf等所有函数进行从新设计,由于咱们须要对他们的指针运行范围进行判断;
(3)在函数的开头和结尾位置添加宏处理。函数运行返回前进行节点清除。