如何在C++中查找内存泄漏

7
有什么好的方法可以在嵌入式环境下检测C++内存泄漏?我尝试通过重载new运算符来记录每个数据分配,但我肯定做错了什么,这个方法不起作用。有其他人遇到过类似的情况吗?
这是new和delete运算符重载的代码。
编辑:
完全公开:我正在查找程序中的内存泄漏,并使用别人编写的此代码来重载new和delete运算符。我的问题部分是我不完全理解它的功能。我知道目标是记录调用方和上一个调用方的地址,分配的大小,如果我们进行分配,则为1,如果我们进行释放,则为2,以及正在运行的线程的名称。
感谢您所有给出的建议,我将尝试某个同事在这里建议的不同方法。如果它有效,我会在这里发布它。
再次感谢所有一流的程序员抽出时间来回答。
StackOverflow太棒了!
结论:
感谢所有答案。不幸的是,我必须转向不同更紧迫的问题。这种泄漏只发生在极不可能的情况下。我觉得不能完成它,如果有更多时间,我可能会回去看看它。我选择了我最有可能使用的答案。
#include <stdlib.h>
#include "stdio.h"
#include "nucleus.h"
#include "plus/inc/dm_defs.h"
#include "plus/inc/pm_defs.h"

#include "posix\inc\posix.h"

extern void* TCD_Current_Thread;
extern "C" void rd_write_text(char * text);
extern PM_PCB * PMD_Created_Pools_List;

typedef struct {
    void* addr;
    uint16_t size;
    uint16_t flags;
} MemLogEntryNarrow_t;

typedef struct {
    void* addr;
    uint16_t size;
    uint16_t flags;
    void* caller;
    void* prev_caller;
    void* taskid;
    uint32_t timestamp;
} MemLogEntryWide_t;

//size lookup table
unsigned char MEM_bitLookupTable[] = {
 0,1,1,2,1,2,2,3,1,2,2,3,1,3,3,4
};

//#pragma CODE_SECTION ("section_ramset1_0")
void *::operator new(unsigned int size)
{
   asm(" STR R14, [R13, #0xC]");  //save stack address temp[0]
   asm(" STR R13, [R13, #0x10]");  //save pc return address temp[1]

   if ( loggingEnabled )
   {
      uint32_t savedInterruptState;
      uint32_t currentIndex;

      // protect the thread unsafe section.
      savedInterruptState = NU_Local_Control_Interrupts(NU_DISABLE_INTERRUPTS);

      // Note that this code is FRAGILE.  It peeks backwards on the stack to find the return
      // address of the caller.  The location of the return address on the stack can be easily changed
      // as a result of other changes in this function (i.e. adding local variables, etc).
      // The offsets may need to be adjusted if this function is touched.
      volatile unsigned int temp[2];

      unsigned int *addr = (unsigned int *)temp[0] - 1;
      unsigned int count = 1 + (0x20/4);   //current stack space ***

      //Scan for previous store
      while ((*addr & 0xFFFF0000) != 0xE92D0000)
      {
         if ((*addr & 0xFFFFF000) == 0xE24DD000)
         {
            //add offset in words
            count += ((*addr & 0xFFF) >> 2);
         }
         addr--;
      }

      count += MEM_bitLookupTable[*addr & 0xF];
      count += MEM_bitLookupTable[(*addr >>4) & 0xF];
      count += MEM_bitLookupTable[(*addr >> 8) & 0xF];
      count += MEM_bitLookupTable[(*addr >> 12) & 0xF];

      addr = (unsigned int *)temp[1] + count;
      // FRAGILE CODE ENDS HERE

      currentIndex = currentMemLogWriteIndex;
      currentMemLogWriteIndex++;

      if ( memLogNarrow )
      {
         if (currentMemLogWriteIndex >= MEMLOG_SIZE/2 )
         {
            loggingEnabled = false;
            rd_write_text( "Allocation Logging is complete and DISABLED!\r\n\r\n");
         }
         // advance the read index if necessary.
         if ( currentMemLogReadIndex == currentMemLogWriteIndex )
         {
            currentMemLogReadIndex++;
            if ( currentMemLogReadIndex == MEMLOG_SIZE/2 )
            {
               currentMemLogReadIndex = 0;
            }
         }

         NU_Local_Control_Interrupts(savedInterruptState);

         //Standard operator 
         //(For Partition Analysis we have to consider that if we alloc size of 0 always as size of 1 then are partitions must be optimized for this)
         if (size == 0) size = 1;

         ((MemLogEntryNarrow_t*)memLog)[currentIndex].size = size;
         ((MemLogEntryNarrow_t*)memLog)[currentIndex].flags = 1;    //allocated

         //Standard operator
         void * ptr;
         ptr = malloc(size);

         ((MemLogEntryNarrow_t*)memLog)[currentIndex].addr = ptr;

         return ptr;
      }
      else
      {
         if (currentMemLogWriteIndex >= MEMLOG_SIZE/6 )
         {
            loggingEnabled = false;
            rd_write_text( "Allocation Logging is complete and DISABLED!\r\n\r\n");
         }
         // advance the read index if necessary.
         if ( currentMemLogReadIndex == currentMemLogWriteIndex )
         {
            currentMemLogReadIndex++;
            if ( currentMemLogReadIndex == MEMLOG_SIZE/6 )
            {
               currentMemLogReadIndex = 0;
            }
         }

         ((MemLogEntryWide_t*)memLog)[currentIndex].caller = (void *)(temp[0] - 4);
         ((MemLogEntryWide_t*)memLog)[currentIndex].prev_caller = (void *)*addr;
         NU_Local_Control_Interrupts(savedInterruptState);
         ((MemLogEntryWide_t*)memLog)[currentIndex].taskid = (void *)TCD_Current_Thread;
         ((MemLogEntryWide_t*)memLog)[currentIndex].size = size;
         ((MemLogEntryWide_t*)memLog)[currentIndex].flags = 1;    //allocated
         ((MemLogEntryWide_t*)memLog)[currentIndex].timestamp = *(volatile uint32_t *)0xfffbc410;   // for arm9

         //Standard operator
         if (size == 0) size = 1;

         void * ptr;
         ptr = malloc(size);

         ((MemLogEntryWide_t*)memLog)[currentIndex].addr = ptr;

         return ptr;
      }
   }
   else
   {
       //Standard operator
       if (size == 0) size = 1;

       void * ptr;
       ptr = malloc(size);

       return ptr;
   }
}
//#pragma CODE_SECTION ("section_ramset1_0")
void ::operator delete(void *ptr)
{
   uint32_t savedInterruptState;
   uint32_t currentIndex;

   asm(" STR R14, [R13, #0xC]");  //save stack address temp[0]
   asm(" STR R13, [R13, #0x10]");  //save pc return address temp[1]

   if ( loggingEnabled )
   {
      savedInterruptState = NU_Local_Control_Interrupts(NU_DISABLE_INTERRUPTS);

      // Note that this code is FRAGILE.  It peeks backwards on the stack to find the return
      // address of the caller.  The location of the return address on the stack can be easily changed
      // as a result of other changes in this function (i.e. adding local variables, etc).
      // The offsets may need to be adjusted if this function is touched.
      volatile unsigned int temp[2];

      unsigned int *addr = (unsigned int *)temp[0] - 1;
      unsigned int count = 1 + (0x20/4);   //current stack space ***

      //Scan for previous store
      while ((*addr & 0xFFFF0000) != 0xE92D0000)
      {
         if ((*addr & 0xFFFFF000) == 0xE24DD000)
         {
            //add offset in words
            count += ((*addr & 0xFFF) >> 2);
         }
         addr--;
      }

      count += MEM_bitLookupTable[*addr & 0xF];
      count += MEM_bitLookupTable[(*addr >>4) & 0xF];
      count += MEM_bitLookupTable[(*addr >> 8) & 0xF];
      count += MEM_bitLookupTable[(*addr >> 12) & 0xF];

      addr = (unsigned int *)temp[1] + count;
      // FRAGILE CODE ENDS HERE

      currentIndex = currentMemLogWriteIndex;
      currentMemLogWriteIndex++;

      if ( memLogNarrow )
      {
         if ( currentMemLogWriteIndex >= MEMLOG_SIZE/2 )
         {
            loggingEnabled = false;
            rd_write_text( "Allocation Logging is complete and DISABLED!\r\n\r\n");
         }
         // advance the read index if necessary.
         if ( currentMemLogReadIndex == currentMemLogWriteIndex )
         {
            currentMemLogReadIndex++;
            if ( currentMemLogReadIndex == MEMLOG_SIZE/2 )
            {
               currentMemLogReadIndex = 0;
            }
         }

         NU_Local_Control_Interrupts(savedInterruptState);

         // finish logging the fields.  these are thread safe so they dont need to be inside the protected section.
         ((MemLogEntryNarrow_t*)memLog)[currentIndex].addr = ptr;
         ((MemLogEntryNarrow_t*)memLog)[currentIndex].size = 0;
         ((MemLogEntryNarrow_t*)memLog)[currentIndex].flags = 2;    //unallocated
      }
      else
      {
         ((MemLogEntryWide_t*)memLog)[currentIndex].caller = (void *)(temp[0] - 4);
         ((MemLogEntryWide_t*)memLog)[currentIndex].prev_caller = (void *)*addr;

         if ( currentMemLogWriteIndex >= MEMLOG_SIZE/6 )
         {
            loggingEnabled = false;
            rd_write_text( "Allocation Logging is complete and DISABLED!\r\n\r\n");
         }
         // advance the read index if necessary.
         if ( currentMemLogReadIndex == currentMemLogWriteIndex )
         {
            currentMemLogReadIndex++;
            if ( currentMemLogReadIndex == MEMLOG_SIZE/6 )
            {
               currentMemLogReadIndex = 0;
            }
         }
         NU_Local_Control_Interrupts(savedInterruptState);

         // finish logging the fields.  these are thread safe so they dont need to be inside the protected section.
         ((MemLogEntryWide_t*)memLog)[currentIndex].addr = ptr;
         ((MemLogEntryWide_t*)memLog)[currentIndex].size = 0;
         ((MemLogEntryWide_t*)memLog)[currentIndex].flags = 2;    //unallocated
         ((MemLogEntryWide_t*)memLog)[currentIndex].taskid = (void *)TCD_Current_Thread;
         ((MemLogEntryWide_t*)memLog)[currentIndex].timestamp = *(volatile uint32_t *)0xfffbc410;   // for arm9
      }

      //Standard operator
      if (ptr != NULL) {
         free(ptr);
      }
   }
   else
   {
      //Standard operator
      if (ptr != NULL) {
        free(ptr);
      }
   }
}

1
你能解释一下“那种方法”吗?这里的专家们可以在几秒钟内修复它。 - Vijay Angelo
我添加了运算符重载的代码。问题似乎是保存到日志中的值(addr、size、flags、taskid)不是有效值,它们指向内存中错误的地址。另外,为了测试,我在另一个函数内添加了以下测试代码,但是当我调用该函数时,日志记录器将不会保存该函数的地址。char * obviousLeak = new char[64]; sprintf(obviousLeak, "这是一个测试,以查看明显的内存泄漏"); - embdeddCoder
“FRAGILE”一词应该在大多数程序员的脑海中引起警报。我认为你的堆栈窥视在很多层面上都是有缺陷的。我不确定你为什么要计算*addr中设置位的数量。你期望测试代码做什么? - Skizz
我想要查看日志,保存调用测试代码的函数地址。我认为堆栈窥视功能出现了问题。 - embdeddCoder
您的堆栈倒带计算内存位置中设置位的数量 - 这似乎完全错误。几乎可以确定调用您的operator new函数的函数不是调用new函数的函数 - 在两者之间将有一些运行时库代码。 - Skizz
15个回答

8

如果你正在使用Linux系统,我建议尝试使用Valgrind


1
+1 - Valgrind非常好用。它最好的特点之一是输出结果(至少在emacs中)看起来像标准编译器输出。当然,它还可以做其他事情,比如“massif”来显示你如何使用内存。你可能没有泄漏,但如果你有一个内存占用量很大的应用程序,那么massif会让你知道你正在使用最多的内存,潜在地显示出其他种类的问题——比如在完成后不立即释放内存。 - Richard Corden
1
这是一个很棒的工具,但它只适用于Linux。如果你在Windows上工作,可以使用Deleaker。它允许你搜索内存和资源泄漏,并以方便的形式显示结果。 - John Smith
我也使用Deleaker来搜索内存泄漏。非常棒的工具)) - MastAvalons

6

有几种形式的operator new:

void *operator new (size_t);
void *operator new [] (size_t);
void *operator new (size_t, void *);
void *operator new [] (size_t, void *);
void *operator new (size_t, /* parameters of your choosing! */);
void *operator new [] (size_t, /* parameters of your choosing! */);

以上所有内容均可存在于全局和类范围内。对于每个 operator new,都有一个等效的 operator delete。如果您想要这样做,您需要确保将日志记录添加到所有版本的操作符中。

理想情况下,您希望系统在存在或不存在内存日志记录时表现相同。例如,MS VC 运行时库在调试模式下分配更多的内存,因为它在内存分配之前加入了更大的信息块,并在分配的开头和结尾添加了守卫块。最好的解决方案是将所有内存日志信息保存在单独的内存块中,并使用映射来跟踪内存。这也可以用于验证传递给 delete 的内存是否有效。

new
  allocate memory
  add entry to logging table

delete
  check address exists in logging table
  free memory

然而,嵌入式软件通常会面临内存有限的问题。因此,在这些系统上通常不建议使用动态内存分配,理由如下:
  1. 你知道可以分配多少内存,因此你提前知道可以分配多少个对象。分配应该永远不返回null,因为通常没有简单的方法回到一个健康的系统。
  2. 分配和释放内存会导致内存碎片。随着时间的推移,你可以分配的对象数量会减少。你可以编写内存压缩器来移动已分配的对象以释放更大的内存块,但这会影响性能。正如第1点所述,一旦获取null,情况就变得棘手了。
因此,在进行嵌入式工作时,你通常提前知道可以为各种对象分配多少内存,知道这一点之后,你可以编写针对每个对象类型的更有效的内存管理器,当内存耗尽时可以采取适当的措施-丢弃旧项目、崩溃等。
编辑
如果你想知道是什么调用了内存分配,则最好使用宏(我知道,宏通常很糟糕):
#define NEW new (__FILE__, __LINE__, __FUNCTION__)

并定义一个 new 操作符:

void *operator new (size_t size, char *file, int line, char *function)
{
   // log the allocation somewhere, no need to strcpy file or function, just save the 
   // pointer values
   return malloc (size);
}

然后像这样使用:

SomeObject *obj = NEW SomeObject (parameters);

你的编译器可能没有__FUNCTION__预处理器定义,因此你可以安全地省略它。

5

http://www.linuxjournal.com/article/6059

根据我的经验,为嵌入式系统创建内存池并使用自定义分配器/释放器总是更好的选择。这样我们可以很容易地识别泄漏情况。例如,我们曾经为vxworks创建一个简单的自定义内存管理器,在其中存储了任务ID和分配的内存块的时间戳。


3

一种方法是通过指针将分配内存的模块的文件名和行号字符串插入到数据的分配块中。使用 C++ 标准宏 "__FILE__" 和 "__LINE__" 处理文件和行号。在内存被释放时,该信息会被删除。

我们的一个系统有这个功能,我们称之为 "内存耗用报告"。因此,我们可以随时从 CLI 打印出所有分配的内存以及分配内存的许多信息。这个列表按照哪个代码模块分配了最多的内存进行排序。很多时候,我们会通过这种方式监测内存使用情况,并最终找出内存泄漏问题。


我不认为我完全理解。您是否修改每个对象以具有指向分配位置的字符串指针?如果是这样,那么您如何检测无法修改的第三方对象?如果不是,请问我漏掉了什么? - Alex B
我不知道你们的内存管理是如何设计的,所以我会谈谈我们的。我们根本不修改对象。所有这些都在调用new和内存分配代码中处理。我们有自己的块管理器来处理堆。在块内,我们为文件名(ptr)和行号(ptr)保留了字节。我们修改了new函数,使其能够通过宏(调试构建)将此信息传递到内存块中。我们还在另一个系统上有一个函数,可以查找堆栈以找到调用者。 - Jay Atkinson
其实,仔细想想,查找调用堆栈以确定是谁调用了new并将其存储在内存块中可能更容易。您可以使用地址来确定代码位置。 - Jay Atkinson

1

如果你仔细观察,重载newdelete应该可以正常工作。

也许你可以向我们展示一下这种方法有什么问题?


这种方法假定所有的分配都是用 new 和 delete 运算符完成的。它无法捕捉使用 malloc 分配的内存。 - laalto

1

我不是嵌入式环境专家,所以我能给的唯一建议就是尽可能在开发机上使用你喜欢的免费或专有工具测试尽可能多的代码。特定嵌入式平台的工具也可能存在,你可以用它们进行最终测试。但大多数强大的工具都是针对桌面的。

在桌面环境中,我喜欢 DevPartner Studio 所做的工作。这是为 Windows 和专有的。Linux 也有免费的工具可用,但我没有太多的经验。例如,有 EFence



1
如果你重写类的构造函数和析构函数,你可以将信息打印到屏幕或日志文件中。这样做可以让你了解什么时候创建了什么东西,以及删除相同信息。
为了方便浏览,你可以添加一个临时全局变量 "INSTANCE_ID",并在每个构造函数/析构函数调用时打印它(并递增)。然后你就可以按 ID 浏览,这应该会使事情变得更容易一些。

1
我们在C 3D工具包中的做法是创建自定义的new/malloc和delete宏,将每个分配和释放记录到文件中。当然,我们必须确保所有代码都调用了我们的宏。写入日志文件由运行时标志控制,并且仅在调试模式下发生,因此我们不必重新编译。
一旦运行完成,后处理器会遍历文件,匹配分配和释放,并报告任何不匹配的分配。
这会对性能产生影响,但我们只需要偶尔这样做一次。

1

需要自己开发内存泄漏检测工具吗?

如果你不能使用Linux上的开源valgrind等动态内存检查工具,商业产品Coverity PreventKlocwork Insight等静态分析工具可能会有所帮助。我已经使用过这三个工具,并且取得了非常好的结果。


1
静态工具在查找泄漏方面与动态工具相比存在显著的局限性。其中之一是正确处理堆分配的资源 - 一旦你的资源被分配给全局变量或实例字段,很难推断会发生什么。商业工具中没有一个能够很好地解决这个问题 - 它们要么将所有这些报告为缺陷,导致高假阳性率,要么忽略它们全部,削减了潜在的错误数量。 - Michael Donohue
我并不是建议仅使用静态分析工具。它们肯定是动态工具的有用补充,尤其是如果它们可以被纳入自动化或周期性手动构建中。 - Void

1

很多好的答案。

我只想指出,如果程序像一个小的命令行实用程序一样运行了一段时间,然后释放所有内存回到操作系统,那么内存泄漏可能不会造成任何伤害。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接