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

时间:2009-06-15 14:40:17

标签: c++ memory-leaks embedded

在嵌入式环境中检测C ++内存泄漏的好方法是什么?我尝试重载新运算符来记录每个数据分配,但我一定做错了,这种方法不起作用。还有其他人遇到过类似的情况吗?

这是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);
      }
   }
}

15 个答案:

答案 0 :(得分:8)

如果您正在运行Linux,我建议您尝试Valgrind

答案 1 :(得分: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,事情就变得棘手了。
  3. 因此,在进行嵌入式工作时,您通常会预先知道可以为各种对象分配多少内存,并且知道这一点,您可以为每种对象类型编写更高效的内存管理器,以便在内存耗尽时采取适当的操作 - 丢弃旧物品,崩溃等等。

    修改

    如果你想知道什么称为内存分配,最好的办法就是使用一个宏(我知道,宏通常很糟糕):

    #define NEW new (__FILE__, __LINE__, __FUNCTION__)
    

    并定义一个new 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__预处理器定义,因此您可以安全地省略它。

答案 2 :(得分:5)

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

实际上根据我的经验,最好为嵌入式系统创建内存池并使用自定义分配器/解除分配器。我们可以轻松识别泄漏。例如,我们为vxworks提供了一个简单的自定义内存管理器,我们将任务ID,时间戳存储在已分配的mem块中。

答案 3 :(得分:3)

一种方法是将模块分配内存的文件名和行号字符串(通过指针)插入分配的数据块中。使用C ++标准“__FILE__”和“__LINE__”宏处理文件和行号。取消分配内存时,将删除该信息。

我们的一个系统具有此功能,我们将其称为“记忆猪报告”。因此,无论何时从我们的CLI,我们都可以打印出所有已分配的内存以及已分配内存的大量信息列表。此列表按哪个代码模块分配的内存最多排序。很多时候,我们会随着时间的推移以这种方式监视内存使用情况,最终内存耗尽(泄漏)会冒泡到列表的顶部。

答案 4 :(得分:1)

如果密切关注,

重载newdelete应该有效。

也许你可以向我们展示一下这种方法没有用处?

答案 5 :(得分:1)

我不是嵌入式环境专家,因此我唯一可以建议的是使用您最喜欢的免费或专有工具在开发机器上测试尽可能多的代码。用于特定嵌入式平台的工具也可能存在,您可以将它们用于最终测试。但是最强大的工具适用于台式机。

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


答案 6 :(得分:1)

如果覆盖类的构造函数和析构函数,则可以打印到屏幕或日志文件。这样做可以让您了解何时创建内容,创建内容以及删除相同的信息。

为了便于浏览,您可以添加临时全局变量“INSTANCE_ID”,并在每个构造函数/析构函数调用上打印此(和增量)。然后你可以通过ID浏览,它应该会更容易。

答案 7 :(得分:1)

我们使用C 3D工具包的方式是创建自定义的new / malloc和删除宏,将每个分配和释放记录到文件中。我们必须确保所有代码当然都称为我们的宏。对日志文件的写入由运行时标志控制,只在调试时发生,因此我们不必重新编译。

运行完成后,后处理器会运行匹配分配到解除分配的文件,并报告任何不匹配的分配。

它的性能受到了打击,但我们只需要在一段时间内完成它。

答案 8 :(得分:1)

是否真的需要推出自己的内存泄漏检测?

假设您无法使用动态内存检查程序,例如Linux上的开源valgrind工具,则可能会使用商业产品Coverity PreventKlocwork Insight等静态分析工具。我已经使用了所有三个,并且对所有这些都有非常好的结果。

答案 9 :(得分:1)

很多好的答案。

我只想指出,如果该程序是一个像小型命令行实用程序一样运行一段时间然后将其所有内存释放回操作系统的程序,那么内存泄漏可能没什么坏处。 / p>

答案 10 :(得分:0)

您可以使用第三方工具执行此操作。

您可以通过在New和Delete调用中添加内存计数器来增加/减少内存计数器,并在应用程序关闭时打印出报告,从而检测自己类结构中的泄漏。但是,这不会检测在类系统外部分配的内存的内存泄漏 - 第三方工具可以执行此操作。

答案 11 :(得分:0)

您能用日志方法描述什么“无效”吗?

你没有得到预期的日志吗?或者,他们是否显示一切都很好,但你仍然有泄漏?

你是如何确认这绝对是泄密而不是其他类型的腐败?

检查重载的一种方法是正确的:每个类实例化一个计数器对象,在new中递增它,并在类的delete中递减它。如果你的人数越来越多,你就会有泄漏。现在,您希望您的日志行与增量和减量点重合。

答案 12 :(得分:0)

不是专门用于嵌入式开发,但我们过去常常使用BoundsChecker

答案 13 :(得分:0)

使用智能指针,永远不要再考虑它了,周围有很多官方类型,但也非常容易自己动手:

class SmartCoMem
{
public:
SmartCoMem() : m_size( 0 ), m_ptr64( 0 ) {
}

~SmartCoMem() {
    if( m_size )
        CoTaskMemFree((LPVOID)m_ptr64);
}

void copyin( LPCTSTR  in, const unsigned short size )
{
    LPVOID ptr;
    ptr = CoTaskMemRealloc( (LPVOID)m_ptr64, size );
    if( ptr == NULL )
        throw std::exception( "SmartCoMem: CoTaskMemAlloc Failed" );
    else
    {
        m_size = size;
        m_ptr64 = (__int64)ptr;
        memcpy( (LPVOID)m_ptr64, in, size );
    }
}

std::string copyout( ) {
    std::string out( (LPCSTR)m_ptr64, m_size );
    return out;
}

__int64* ptr() {
    return &m_ptr64;
}

unsigned short size() {
    return m_size;
}

unsigned short* sizePtr() {
    return &m_size;
}

bool loaded() {
    return m_size > 0;
}

private:
    //don't allow copying as this is a wrapper around raw memory
    SmartCoMem (const SmartCoMem &);
    SmartCoMem & operator = (const SmartCoMem &);

__int64 m_ptr64;
unsigned short m_size;

};

由于我正在使用的API,但是这个示例中没有封装,但仍然比使用完全原始指针更好。

答案 14 :(得分:0)

对于这样的测试,尝试为Linux(或您使用的任何操作系统)本地编译嵌入式代码,并使用像Valgrind这样的成熟工具来测试内存泄漏。这样做可能是一个挑战,但您只需要使用一些模拟适合您测试的代码替换任何直接访问硬件的代码。

我发现使用SWIG将我的嵌入式代码转换为Linux本机库并从Python脚本运行代码非常有效。您可以使用可用于非嵌入式项目的所有工具,并测试除硬件驱动程序之外的所有代码。