Как найти утечку памяти в C ++ - PullRequest
7 голосов
/ 15 июня 2009

Что может быть хорошим способом обнаружения утечки памяти C ++ во встроенной среде? Я пытался перегрузить новый оператор для регистрации каждого распределения данных, но я, должно быть, сделал что-то не так, этот подход не работает. Кто-нибудь еще сталкивался с подобной ситуацией?

Это код перегрузки оператора new и delete.

EDIT:

Полное раскрытие: я ищу утечку памяти в моей программе и использую этот код, который кто-то другой написал для перегрузки оператора 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 ]

8 голосов
/ 15 июня 2009

Если вы работаете в Linux, я предлагаю попробовать Valgrind .

6 голосов
/ 15 июня 2009

Существует несколько форм оператора 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! */);

Все вышеперечисленное может существовать как на глобальном уровне, так и на уровне класса. Для каждого нового оператора существует эквивалентный оператор удаления. Вы должны убедиться, что вы добавляете логирование ко всем версиям оператора, если вы так хотите.

В идеале вы хотели бы, чтобы система вела себя одинаково независимо от того, присутствует ли запись в память или нет. Например, библиотека времени выполнения MS VC выделяет больше памяти при отладке, чем при выпуске, потому что она ставит префикс выделения памяти с большим информационным блоком и добавляет защитные блоки в начало и конец выделения. Лучшее решение состоит в том, чтобы хранить всю информацию о регистрации в памяти как отдельный фрагмент или память и использовать карту для отслеживания памяти. Это также можно использовать для проверки правильности памяти, переданной для удаления.

new
  allocate memory
  add entry to logging table

delete
  check address exists in logging table
  free memory

Однако вы пишете встроенное программное обеспечение, в котором память обычно ограничена. В этих системах обычно предпочтительно избегать динамического выделения памяти по нескольким причинам:

  1. Вы знаете, сколько там памяти, поэтому заранее знаете, сколько объектов вы можете выделить. Распределение никогда не должно возвращать ноль, так как это обычно является терминалом без простого способа вернуться к исправной системе.
  2. Выделение и освобождение памяти приводит к фрагментации. Количество объектов, которые вы можете выделить, будет со временем уменьшаться. Вы можете написать компактор памяти для перемещения выделенных объектов, чтобы высвободить большие объемы памяти, но это повлияет на производительность. Как и в пункте 1, когда вы получаете ноль, все становится сложнее.

Итак, когда вы выполняете встроенную работу, вы обычно заранее знаете, сколько памяти можно выделить для различных объектов, и, зная это, вы можете написать более эффективные менеджеры памяти для каждого типа объектов, которые могут предпринять соответствующие действия, когда память закончится - выбрасывание старых предметов, разрушение и т. д.

EDIT

Если вы хотите узнать, что называется распределением памяти, лучше всего использовать макрос (я знаю, что макросы обычно плохие):

#define NEW new (__FILE__, __LINE__, __FUNCTION__)

и определите новый оператор:

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 голосов
/ 15 июня 2009

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

На самом деле, исходя из моего опыта, всегда лучше создавать пулы памяти для встраиваемых систем и использовать собственный распределитель / выделитель. Мы можем легко идентифицировать утечки. Например, у нас был простой пользовательский менеджер памяти для vxworks, в котором мы храним идентификатор задачи, отметку времени в выделенном блоке mem.

3 голосов
/ 15 июня 2009

Один из способов - вставить строки имени файла и номера строки (через указатель) модуля, выделяющего память, в выделенный блок данных. Файл и номер строки обрабатываются с помощью стандартных макросов C * "__FILE__" и "__LINE__". Когда память освобождается, эта информация удаляется.

Одна из наших систем имеет эту функцию, и мы называем ее «отчет о переполнении памяти». Поэтому в любое время из нашего CLI мы можем распечатать всю выделенную память вместе с большим списком информации о том, кто выделил память. Этот список отсортирован по тому, какому модулю кода выделено наибольшее количество памяти. Много раз мы будем отслеживать использование памяти таким образом с течением времени, и в конечном итоге задержка памяти (утечка) будет подниматься вверх по списку.

1 голос
/ 02 июля 2009

Много хороших ответов.

Я просто хотел бы указать, что если это программа, которая, как небольшая утилита командной строки, работает в течение короткого периода времени, а затем высвобождает всю свою память обратно в ОС, утечки памяти, вероятно, не причинят вреда.

1 голос
/ 15 июня 2009

Есть ли реальная необходимость развернуть собственное обнаружение утечки памяти?

Предполагается, что вы не можете использовать средства динамической проверки памяти, например инструмент с открытым исходным кодом valgrind в Linux, инструменты статического анализа, такие как коммерческие продукты Coverity Prevent и Klocwork Insight может быть полезным. Я использовал все три, и у всех были очень хорошие результаты.

1 голос
/ 15 июня 2009

То, как мы делали это с нашим набором инструментов C 3D, заключалось в создании пользовательских новых / malloc и удалении макросов, которые регистрировали каждое выделение и освобождение в файл. Конечно, мы должны были убедиться, что весь код вызвал наши макросы. Запись в файл журнала контролировалась флагом времени выполнения и происходила только при отладке, поэтому нам не пришлось перекомпилировать.

Как только запуск завершился, постпроцессор перебрал распределение файлов, сопоставив его с освобождениями, и сообщил о любых несопоставленных распределениях.

У него был удар по производительности, но нам нужно было сделать это только один раз.

1 голос
/ 15 июня 2009

Если вы переопределяете конструктор и деструктор ваших классов, вы можете распечатать их на экране или в файле журнала. Это даст вам представление о том, когда что-то создается, что создается, а также о той же информации для удаления.

Для удобства просмотра вы можете добавить временную глобальную переменную «INSTANCE_ID» и распечатать ее (и увеличить) при каждом вызове конструктора / деструктора. Затем вы можете просматривать по идентификатору, и это должно немного облегчить работу.

1 голос
/ 15 июня 2009

Я не эксперт по встроенным средам, поэтому могу только посоветовать протестировать как можно больше кода на своей машине для разработки с помощью ваших любимых бесплатных или фирменных инструментов. Инструменты для конкретной встроенной платформы также могут существовать, и вы можете использовать их для окончательного тестирования. Но самые мощные инструменты для настольных компьютеров.

В среде рабочего стола мне нравится работа DevPartner Studio. Это для Windows и проприетарно. Для Linux есть бесплатные инструменты, но я не особо разбираюсь в них. Как пример есть EFence


1 голос
/ 15 июня 2009

перегрузка new и delete должны работать, если вы обратите пристальное внимание.

Может быть, вы можете показать нам, что не работает с этим подходом?

...