2

My application is a dll based implementation and probably leaks memory during unload. I have noticed it during unload/reload cycles (when hosting process is not killed). the hosting process's virtual memory is increasing.

i have went through a code inspection to try and find the leaking code but didn't find any.

i'm looking for other techniques to detect memory leaks during unload (where objects are being destroyed).

EDIT: I am using win32 (XP) platform.

do you have experience in such tools/procedures? Thanks

NirMH
  • 4,769
  • 3
  • 44
  • 69
  • 1
    *Does* unloading a DLL actually invoke your destructors? It seems more likely to me that your objects are getting abandoned (rather than destructed) on unload and that's your problem. – Ben Jackson Jan 01 '12 at 09:13

4 Answers4

4

What I used to do a long time ago is this:

I wrote my own mymalloc, myrealloc and myfree (and overloaded new and delete so that they would call my functions.) Then I wrote malloc and realloc macros which invoked mymalloc and myrealloc, passing them __FILE__ and __LINE__. What mymalloc did is this: it invoked the malloc function of the standard library, allocating a slightly larger block, and it inserted __FILE__ and __LINE__ in that block. It also kept all allocated blocks in a linked list, so as to be able to traverse them later.

Upon program exit, I would traverse the list of blocks that had not been freed, and I would print out the file and line that were responsible for the memory leaks.

Nowadays I would assume that there would be ready-made tools that you could get to do that kind of stuff for you.

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142
  • 1
    +1: Yes I've used a similar method, have pasted an answer below for overriding new/delete in Visual Studio and printing the output. – Dr. Andrew Burnett-Thompson Jan 01 '12 at 10:26
  • +1: This is a good approach. Too bad you picked up bad names for those functions (leading underscore followed by a letter names are reserved in the global namespace). Did it happen because you didn't know or just because of some childish attraction for the forbidden? :-) – 6502 Jan 01 '12 at 10:32
  • @6502 I did not use these names back then, I used some considerably longer SentenceCase names. I only used these names here as a convenience. But you are right, I shouldn't even here, so I will fix this. Fixed it. – Mike Nakis Jan 01 '12 at 10:46
2

you didn't specify which platform you are looking for. I'm a windows developer so I can only recommend windows solutions. If that's what you are working with, there's a number of commercial and free tools available. Few that I personally worked with: Purify, BoundsChecker, UMDH, LeakDiag, DebugDiag.

Out of these, I typically prefer UMDH. It is free and comes as part of Debugging Tools for Windows (DTW) installation. I found it to actually be more reliable and less resource intensive than most other, including professional tools. It is very simply to use and documentation can be found in a .chm file which comes with DTW install. In the end, I personally find that UMDH has very high signal to noise ratio compared to many other tools.

DebugDiag is another good alternative. As far as I can tell, it uses almost same APIs as UMDH but is slightly more cumbersome to use because it's UI-based rather than command prompt so to get things done usually requires more clicks, but for someone new, I would recommend it over UMDH.

UPDATE:

It's interesting that majority preference was to insert custom hooks into malloc/free and then add even more code for custom hooks into operators new/delete.

I would strongly suggest you take a look at UMDH and learn how it works even if you don't find in necessary in this specific case. At the core of all memory allocations are windows functions, HeapAlloc/HeapFree. Microsoft, anticipating the need for leak detection methods, already provided hooks that we can use at that root level.

These are other advantages of using UMDH over custom allocator hooks:

  • You get full stack trace of each allocation instead of just what's provided by __FILE__ and __LINE__
  • It already has full reporting and aggregation of statistics which is something you'd have to write on top of just intercepting malloc/free. You get # of allocations for each trace, # of bytes allocated by each trace and a list of allocated memory buffers so you can actually analyze what kind of data was leaked.
  • Detecting malloc/free leaks which occurred in someone else's code, not specifically the DLL under your control
  • Detecting leaks from other memory allocation functions such as CoTaskMemAlloc or SysStringAlloc
  • Detecting leaks of COM objects which are not properly released
  • Detecting logic errors in your code when you call into third party API that returns a buffer which you forgot to free.
  • You can use UMDH instantly with any code base without having to add custom code over and over again.
  • The method you accepted only works in debug environment. UMDH can be used just as effectively without any code changes on production systems.

Pretty much, any time there's an upward trend in memory usage, the tool will tell you where it's coming from. Most of the time, I can find a leak within 10 minutes if its reproducible on the development machine (it takes a bit longer when debugging production code because symbol files must be matched, sometimes manually).

So if you get all this completely free with DTW installations (which btw has other awesome debugging features), why do people prefer to roll their own leak detection code?

DXM
  • 4,413
  • 1
  • 19
  • 29
  • i take your advice again, downloaded the SDK from the link for win32 bits , but i can't find the UMDH tool... can you help? – NirMH Jan 02 '12 at 07:27
  • If you follow the link I provided, you will find winsdk_web.exe. Then you can select DTW either from "Common Utilities" or "Redistributable Packages". Redist places installation files under C:\Program Files\Microsoft SDKs\Windows\v7.1\Redist\Debugging Tools for Windows. The other one installs DTW and places it into C:\Program Files\Debugging Tools for Windows (x64) (or similar depending on 64-bit vs. 32-bit OS). In that directory you should find umdh.exe You will also find debugger.chm which documents how to use the tool – DXM Jan 02 '12 at 08:32
  • i've installed and profiles my apps, the compare files are 17MB of size. do you have experience with how to filter them? and/or how to use this approach to detect d-tor leakage (unload sequence)? – NirMH Jan 05 '12 at 13:41
  • What do you consider a leak? (maybe I should've started with that) Does your app leak memory while it is running, because you there are times when it loads/unloads the same DLL? Or are you simply concerned that some memory is not released when the app exits? Memory left over on exit isn't a leak since it's all reclaimed once process goes away and many libraries/frameworks leave things allocated all the time. However, if you see memory usage continuously growing while the app is running, then you have a leak. In that case, figure out which action causes the growth and make that action occur... – DXM Jan 05 '12 at 14:24
  • ... 10k times (hack in a for-loop if you have to, or use one of UI automation tools). UMDH diff files are already sorted so that biggest leak is listed first, so by repeatedly doing the action that causes the leak, you'll effectively make that leak pop right to the top of the list. – DXM Jan 05 '12 at 14:26
  • my scenario is a shell process loads my app implemented as a dll. i have an ability to "unload" my app (which eventually unloads the dll) and reload it again without stopping the shell process. i am observing the shell's process virtual memory increase in every reload cycle. thus i'm thinking my app is not releasing some memory during the unload process. – NirMH Jan 11 '12 at 13:37
1

If you can compile your code on a supported platform, you should certainly give Valgrind's Memcheck tool a try. Read The Valgrind Quick Start Guide to see how.

Here's a quick example...

Source:

$ cat -n leaky.cpp 
     1  struct leaky
     2  {
     3      leaky()
     4          :bytes(new char[256])
     5      {
     6      }
     7  
     8      char* bytes;
     9  };
    10  
    11  int main()
    12  {
    13      leaky sieve;
    14      return sizeof sieve;
    15  }
    16  

Build:

$ make leaky
g++ -Wall -Wextra -Wshadow -pedantic -Wno-long-long -Wfloat-equal -Wcast-qual -g -I/opt/local/include -Weffc++ -Wall -I /opt/local/include -L/opt/local/lib  leaky.cpp   -o leaky

Check:

$ valgrind --leak-check=full ./leaky
==85800== Memcheck, a memory error detector
==85800== Copyright (C) 2002-2011, and GNU GPL'd, by Julian Seward et al.
==85800== Using Valgrind-3.7.0 and LibVEX; rerun with -h for copyright info
==85800== Command: ./leaky
==85800== 
==85800== 
==85800== HEAP SUMMARY:
==85800==     in use at exit: 2,367 bytes in 33 blocks
==85800==   total heap usage: 33 allocs, 0 frees, 2,367 bytes allocated
==85800== 
==85800== 256 bytes in 1 blocks are definitely lost in loss record 6 of 9
==85800==    at 0xB823: malloc (vg_replace_malloc.c:266)
==85800==    by 0x5768D: operator new(unsigned long) (in /usr/lib/libstdc++.6.0.9.dylib)
==85800==    by 0x576DA: operator new[](unsigned long) (in /usr/lib/libstdc++.6.0.9.dylib)
==85800==    by 0x100000EE7: leaky::leaky() (leaky.cpp:4)
==85800==    by 0x100000EB3: main (leaky.cpp:13)
==85800== 
==85800== LEAK SUMMARY:
==85800==    definitely lost: 256 bytes in 1 blocks
==85800==    indirectly lost: 0 bytes in 0 blocks
==85800==      possibly lost: 0 bytes in 0 blocks
==85800==    still reachable: 2,111 bytes in 32 blocks
==85800==         suppressed: 0 bytes in 0 blocks
==85800== Reachable blocks (those to which a pointer was found) are not shown.
==85800== To see them, rerun with: --leak-check=full --show-reachable=yes
==85800== 
==85800== For counts of detected and suppressed errors, rerun with: -v
==85800== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 1 from 1)
johnsyweb
  • 136,902
  • 23
  • 188
  • 247
  • 2
    he talks about dlls, therefore it's not supported – BЈовић Jan 01 '12 at 08:52
  • 1
    Not supported does not mean it will not work. Have a look at Wine. – BatchyX Jan 01 '12 at 09:33
  • I'm using win32 platform (winXP and VS2005) - is this tool supported? – NirMH Jan 01 '12 at 09:59
  • @NirMH: From the link in my answer: "Windows is not under consideration here because porting to it would require so many changes it would almost be a separate project. (However, Valgrind + Wine can be made to work with some effort.)". That said, if you can compile the relevant part of your project on Linux, then you may be able to track down your problem. – johnsyweb Jan 01 '12 at 10:28
1

In addition to Mike's answer (overriding Malloc) you can also overide the new and delete operators in Visual Studio.

Disclaimer: I found this code on the net back in 2004 and included it in a C++ project. I don't know the original source

Below is a code sample (which I included as a header file, memleak.h). This is pretty old code so it may have errors compiling! It does illustrate however how to override new and delete. It also dumps unfreed memory to a file. This code becomes effective if the define _DEBUG is included in your code.

Best regards,

#include <iostream>
#include <list>
using namespace std;

//void DumpUnfreed();
//void AddTrack(DWORD addr,  DWORD asize,  const char *fname, DWORD lnum);
//void RemoveTrack(DWORD addr);

typedef struct 
{
    DWORD   address;
    DWORD   size;
    char    file[64];
    DWORD   line;
} ALLOC_INFO;      

typedef list<ALLOC_INFO*> AllocList;   

AllocList *allocList; 

void AddTrack(DWORD addr,  DWORD asize,  const char *fname, DWORD lnum)
{
    ALLOC_INFO *info;         
    if(!allocList) 
    {
        allocList = new(AllocList);
    }         
    info = new(ALLOC_INFO);
    info->address = addr;
    strncpy(info->file, fname, 63);
    info->line = lnum;
    info->size = asize;
    allocList->insert(allocList->begin(), info);
};      

void RemoveTrack(DWORD addr)
{
    AllocList::iterator i;        
    if(!allocList)
        return;
    for(i = allocList->begin(); i != allocList->end(); i++)
    {
        if((*i)->address == addr)
        {
            allocList->remove((*i));
            break;
        }
    }
};

void DumpUnfreed()
{
    AllocList::iterator i;
    DWORD totalSize = 0;
    char buf[1024];   
    sprintf(buf, "-----------------------------------------------------------\n");
    OutputDebugString(buf);
    OutputDebugString("DSP.DLL: Detecting unfreed memory...\n");
    if(!allocList)
    {
        OutputDebugString("No memory allocations were tracked!\n");
        return;       
    }
    for(i = allocList->begin(); i != allocList->end(); i++) 
    {
        sprintf(buf, "%-50s:\t\tLINE %d,\t\tADDRESS %d\t%d unfreed\n",
            (*i)->file, (*i)->line, (*i)->address, (*i)->size);
        OutputDebugString(buf);
        totalSize += (*i)->size;
    }
    sprintf(buf, "-----------------------------------------------------------\n");
    OutputDebugString(buf);
    sprintf(buf, "DSP.DLL Total Unfreed: %d bytes\n", totalSize);
    OutputDebugString(buf);
    sprintf(buf, "-----------------------------------------------------------\n");
    OutputDebugString(buf);
};

#ifdef _DEBUG
inline void * __cdecl operator new(unsigned int size,
                                         const char *file, int line)
{
    void *ptr = (void *)malloc(size);
    AddTrack((DWORD)ptr, size, file, line);
    return(ptr);
};
inline void __cdecl operator delete(void *p)
{
    RemoveTrack((DWORD)p);
    free(p);
};
inline void * __cdecl operator new[ ] (unsigned int size, const char *file, int line)
{  
    void *ptr = (void *)malloc(size);
    AddTrack((DWORD)ptr, size, file, line);
    return(ptr);                            
};
inline void __cdecl operator delete[ ] (void *p)
{
    RemoveTrack((DWORD)p);
    free(p);
};
#endif

#ifdef _DEBUG
#define DEBUG_NEW new(__FILE__, __LINE__)
#else
#define DEBUG_NEW new
#endif
#define new DEBUG_NEW
Dr. Andrew Burnett-Thompson
  • 20,980
  • 8
  • 88
  • 178
  • 1
    Why does he copy the entire filename into the memory block header? This is totally unnecessary! The filename is a constant string, so he only needs to store a pointer to it! – Mike Nakis Jan 01 '12 at 10:30
  • Thanks - i'll give it a try, it looks promising – NirMH Jan 01 '12 at 12:24