Whenever I work on C code of reasonable complexity, I find Valgrind to be an indispensable tool for memory debugging, and leak detection. It is all too easy to miss a free() somewhere, especially in fairly complex code. It really is an indispensable resource.
This weekend, as I was playing around with some LD_PRELOAD tricks to gather statistics on memory allocations, I realized I already had half of a rudimentary memory leak detector written. I didn't have any plans, aside from watching football, so I decided to follow through and see what I could come up with. It is interesting how quickly a working prototype could be hacked together. Sure, it is no Valgrind, but it works.
I started by creating a shared library (loadable through LD_PRELOAD) that wrapped three functions malloc(), free(), and exit(). When any of the functions are called, the allocations and deallocations are tracked as they happen. I had a relatively optimized binary tree implementation lying around, so I leveraged that, though a hash table would probably be better suited in this scenario. The code (with many implementation details omitted for brevity) looks something like this:
void *malloc(size_t size) {
void *(*real_malloc)(size_t size) = NULL;
real_malloc = dlsym(RTLD_NEXT, "malloc");
...
return real_malloc(size);
}
void free(void *ptr) {
void *(*real_free)(void *ptr) = NULL;
real_free = dlsym(RTLD_NEXT, "free");
...
real_free(ptr);
}
The major problem is that it will alert you to the existence of a memory leak, without providing any details on where the leak occurred. Fortunately, libc supports the generation of backtraces, so it is just a matter of tracking the backtraces along with the allocation information. Should be simple to add this feature -- but when is anything as easy as it initially seems?.
It turns out that the libc backtrace implementation has some serious problems when called from a wrapped implementation of malloc() -- backtrace() itself calls malloc(), leading to an infinite loop. Instead of detecting memory leaks, we just started the application down the path to a segfault. Great.
Back to square one. Fortunately once again, it is quite easy to write our own backtrace implementation. Below is a simplified implementation printing only the information on the calling function (1 level away on the stack)
void print_stacktrace() {
Dl_info *info = (DL_info *)malloc(sizeof(Dl_info));
void *addr = __builtin_return_address(1);
int ret = dladdr(addr, info);
printf("%s(%s) [%p]\n", info->dli_fname, info->dli_sname, info->dli_saddr);
free(info);
}
Overall, I think things worked out pretty well. I can't say it will be very useful, but in some simple cases it might be of help. In any event, it was a nice way to spend a Sunday morning, and I got to learn a few new tricks.