Detecting Memory Leaks by Overriding New in C++
Introduction
Most modern C++ compilers, such as g++, have the ability to compile
in tooling to detect memory leaks. In the case of g++ and gcc, the
-fsanitize=address option will compile in tooling that
hooks to memory allocations and print useful debugging information when
there is a memory leak, use-after-free, null pointer de-reference and
other memory issues. However, some compilers, such as those used for
older or proprietary systems, do not have this functionality, making
detecting and fixing memory leaks using dynamic analysis harder.
This article will explain how basic C++ features can be used to add similar, if more basic, functionality. The examples used will all compile using C++ 98 with the minimum number of required headers.
Overriding New and Delete
The new operator is used by C++ to allocate memory on
the heap in a similar way to malloc in C. The delete
operator is used to deallocate said memory, in a similar manner to
free in C. Both of these operators can be overwritten to
add additional tooling:
#include <iostream>
#include <cstdlib>
void* operator new(size_t size, const char* file, int line)
{
void * ptr = malloc(size);
std::cout << "New called in " << file << " on line " << line << std::endl;
return ptr;
}
void operator delete(void* ptr)
{
std::cout << "Delete called" << std::endl;
free(ptr);
}
#define new new(__FILE__, __LINE__)In the case above, the tooling is simply printing when
new and delete get called. In C++ 11 the
noexcept keyword should be added after
delete(void* ptr).
Adding Useful Tooling
Just printing when new and delete get
called isn’t all that useful. Instead, we want to keep an updated list
of all allocated memory and where in our code it has been allocated.
Then, at the end of our program, we can print out all the locations
where we have leaked memory. We create a structure with the following
data:
- The memory address allocated, which will be used as our unique key.
- The name of the file where the memory is allocated.
- The line-number where the memory is allocated.
We will implement the list as a vector in its own library. The header
file will define the API for printing the currently allocated memory as
well as the overwritten new and delete
operators:
#include <cstddef>
#ifndef __LEAK_CHECK_HH__
#define __LEAK_CHECK_HH__
void* operator new(size_t, const char*, int);
void operator delete(void*);
void PrintMemory();
#define new new(__FILE__, __LINE__)
#endifThe source file will then contain the implementation of these:
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
struct MemoryAllocation {
void* ptr;
std::string filename;
int line;
};
static std::vector<MemoryAllocation> mem_al_;
void* operator new(size_t sz, const char* f, int line) {
void* output = malloc(sz);
MemoryAllocation mem;
mem.ptr = output;
mem.filename = std::string(f);
mem.line = line;
mem_al_.push_back(mem);
return output;
}
void operator delete(void *ptr) {
for (size_t i = 0; i < mem_al_.size(); i++) {
if (mem_al_.at(i).ptr == ptr) {
mem_al_.erase(mem_al_.begin() + i);
return;
}
}
}
void PrintMemory() {
std::cout << "Currently allocated memory: " << std::endl;
for (size_t i = 0; i < mem_al_.size(); i++) {
std::cout << mem_al_.at(i).filename << " " << mem_al_.at(i).line <<
std::endl;
}
}An Example
We can test this implementation to see if it works with the following test:
#include <iostream>
#include "leak_check.hh"
class Object {
public:
Object() {
std::cout << "Constructor Called" << std::endl;
}
~Object() {
std::cout << "Destructor Called" << std::endl;
}
};
int main() {
int* i = new int;
int* j = new int;
int* k = new int;
Object* obj = new Object;
delete obj;
delete i;
delete j;
PrintMemory();
}This can be compiled and run using the following:
g++ -std=c++98 -c leak_check.cpp -o leak_check.o
g++ -std=c++98 test.cpp leak_check.o -o test
./test
This produces the following output showing that constructors and
destructors are still called using the overwritten new and
delete operators, as well as showing that memory allocated
on line 19 is actually being leaked:
Constructor Called
Destructor Called
Currently allocated memory:
test.cpp 19
Limitations
For ultimate compatibility the presented implementation is not thread
safe. In order to make it thread-safe, some form of mutex protection for
the mem_al_ vector would be required. Using the C++ 11
threading library this could be implemented as follows:
static std::vector<MemoryAllocation> mem_al_;
std::mutex mtx_;
void* operator new(size_t sz, const char* f, int line) {
void* output = malloc(sz);
MemoryAllocation mem;
mem.ptr = output;
mem.filename = std::string(f);
mem.line = line;
mtx.lock();
mem_al_.push_back(mem);
mtx.unlock();
return output;
}
void operator delete(void *ptr) {
mtx.lock();
for (size_t i = 0; i < mem_al_.size(); i++) {
if (mem_al_.at(i).ptr == ptr) {
mem_al_.erase(mem_al_.begin() + i);
mtx.unlock();
return;
}
}
mtx.unlock();
}