Building a Linux C++ Memory Leak Detector with Custom New/Delete
This article explains how to implement a memory‑leak detection subsystem for Linux C++ programs by overloading the global new/delete operators, recording allocation sites with file and line information, handling mismatched deletions, supporting dynamic monitoring, and addressing nested‑delete and mutex re‑entrancy issues.
Development background
On Windows, Visual C++ provides a debugger that reports leaked heap blocks with file name, line number and size when a program exits in DEBUG mode. Linux lacks a comparable built‑in mechanism; developers can only observe total memory usage with top and cannot see which allocations were never freed. To fill this gap, a memory‑leak detection subsystem was designed and implemented for C++ programs running on Linux/Unix.
Principles of new and delete
The C++ language defines built‑in operator new and operator delete. operator new allocates raw memory (returning void*) and then calls the object's constructor; operator delete calls the destructor and releases the memory. For custom types the compiler adds a hidden “cookie” that stores the allocation size so that operator delete can know how much to free. Array forms use operator new[] and operator delete[], which also rely on a cookie.
To detect leaks, the four global functions must be overloaded:
void* operator new(size_t nSize, char* pszFileName, int nLineNum);
void* operator new[](size_t nSize, char* pszFileName, int nLineNum);
void operator delete(void* ptr);
void operator delete[](void* ptr);Basic implementation of the detection subsystem
The overloaded operator new forwards the allocation to the real global operator, then records the returned pointer together with file name and line number in a std::map<void*, Record>. The overloaded operator delete looks up the pointer in the map, removes the entry, and finally calls free. The map is created by a global object appMemory before any allocation occurs; its destructor prints any remaining entries as leaked blocks.
Two practical questions arise:
How to pass file name and line number to the custom operator new?
When and how to create the map, and when to output the leak report?
File and line information is obtained via the pre‑processor macros __FILE__ and __LINE__. A macro #define DEBUG_NEW new(__FILE__, __LINE__) is placed in a header MemRecord.h. Every source file that should be monitored includes this header and defines #define new DEBUG_NEW. When the MEM_DEBUG compile‑time flag is set, the macro is active; otherwise it is omitted to avoid runtime overhead.
Problems caused by mismatched delete
Using new and delete with inconsistent forms (e.g., new Test[10] with delete pObj) leads to undefined behaviour, which may crash the program or silently leak memory. The subsystem records the allocation form (single object vs. array) in the map. When a delete is invoked, the recorded form is compared with the actual form; a mismatch moves the entry to an ErrorDelete list and a warning is printed at program termination.
Because the pointer passed to operator delete may not match the pointer stored in the map (due to the compiler’s cookie), the warning mechanism also records the file and line of the offending delete using two global variables DELETE_FILE and DELETE_LINE. A macro #define DEBUG_DELETE locks a global mutex, saves __FILE__ and __LINE__ into those globals, and then calls the real delete. If the map lookup fails, the warning prints the saved location.
Dynamic memory‑leak monitoring
Static leak detection only reports blocks that remain at program exit. To catch “implicit” leaks that grow during long‑running execution, a snapshot module MemSnapShot periodically gathers allocation statistics from the running process. The subsystem’s global object sends allocation/deallocation events to a message queue keyed by the process PID. MemSnapShot reads the queue, aggregates memory usage per source location, and flags locations whose allocated‑but‑not‑freed total keeps increasing.
When the monitored process exits abnormally, the message queue would otherwise remain allocated. The implementation creates an orphan watchdog process (via double fork) that runs a tiny program MemCleaner. The watchdog periodically calls kill(pid,0); if the target process no longer exists, the watchdog deletes the message queue.
Implementation issues: nested delete
Nested deletion occurs when a destructor deletes another object (e.g., A::~A() deletes a member B*). The global DELETE_FILE/LINE values are overwritten, causing loss of the outer delete’s information. To protect the outer context, a std::stack is used: before overwriting the globals, the current values are pushed onto the stack; after the inner delete finishes, the previous values are popped and restored.
The subsystem also needed a re‑entrant mutex because the same thread may lock the mutex multiple times during nested deletes. The provided CCommonMutex wraps a POSIX semaphore and tracks the owning thread’s PID and lock count, allowing recursive locking while still enforcing exclusive access across threads.
Outlook
The first version of the memory‑leak detection subsystem is complete and has passed extensive testing. Future work includes building a memory‑allocation‑optimisation layer that pre‑allocates large blocks and manages them with custom data structures, thereby reducing the overhead of frequent new / delete calls.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITPUB
Official ITPUB account sharing technical insights, community news, and exciting events.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
