Don't lose your mind over the different types of memory leaks

Introduction

Memory leaks, work enough time with systems programming and you're bound to face a few. While not as terrible to diagnose and fix as the dreaded race conditions, memory leaks are silent problems. They might lie dormant for a few hours, days, or weeks to then cause your TSR code to crash the production server due to memory exhaustion. Fortunately, we have Valgrind, an excellent tool to diagnose memory leaks while we wait for a magic language that safely does it all*.

Valgrind

Valgrind is very simple to use, but understanding its report format is a little more complicated. I won't go into much detail about the different leak types reported by Valgrind. Memcheck's manual is the canonical reference and you should read it. Mark Wielaard's blog post has an excellent explanation about them as well, it was my main source. In a nutshell, the different types of leaks are:

  • definitely lost
  • indirectly lost
  • possibly lost
  • still reachable
  • suppressed

Why is it important to understand the different memory leak types? Why not just get the code location and fix it? Because the different types have different causes and require different fixes. Forgetting to call free() on a pointer might be obvious in a toy example, but memory management in real-world applications is seldom simple. Overwrite a pointer buried in an array of structures and you've got a memory leak with no obvious source.

After reading the sources above, I was still feeling possibly lost (yeah, bad pun) as the examples aren't really dead simple (at least for me), so I set out to write a set of very simple functions that show the different types of leaks reported by Valgrind and what they mean.

#include <stdlib.h>
#include <string.h>

void leaky_definitely(void)
{
??? char *p;

??? /*
??? This is definitely lost because the pointer is a local variable
??? and once we exit the function we lose the reference to the
??? allocated block, so there's no way to free this block.
??? */
??? p = malloc(sizeof(char));
}

char *g_p;

void leaky_still_reachable(void)
{
??? /*
??? This is still reachable because the pointer is a global variable
??? that's preserved until the end of our program, so we could have
??? called free on it.
??? */
??? g_p = malloc(sizeof(char));
}

char *leaky_array;

void leaky_possibly(void)
{
??? leaky_array = malloc(3*sizeof(char));

??? /*
??? This is possibly lost because the allocated base pointer was lost
??? as we incremented it twice, but because leaky_array still points
??? somwewhere in the allocated block, we could have done something
??? to recover the base pointer and then free it.

??? To transform this possibly lost leak into a definitely lost leak,
??? just change '2' to '3' in the loop.
??? */
??? for(int i = 0; i < 2; i++) {
??????? *leaky_array = i;
??????? leaky_array++;
??? }
}

char **double_pointer;

void leaky_indirectly(void)
{
??? double_pointer = malloc(sizeof(char*));
??? *double_pointer = malloc(sizeof(char));
??? /*
??? This will cause a definitely lost leak because double_pointer is
??? lost. It will also cause an indirectly lost leak because
??? double_pointer contined a pointer to another memory block.

??? Indirectly lost leaks are definitely lost leaks indirectly caused
??? by another definitely lost leak.
???? */

??? /*
??? By changing the line bellow to `free(double_pointer)` we would
??? create a definitely lost leak on `*double_pointer` instead of a
??? indirectly lost.
???? */
??? // free(double_pointer);
??? double_pointer = NULL;
}

int main(void)
{
??? leaky_definitely();
??? leaky_still_reachable();
??? leaky_possibly();
??? leaky_indirectly();

??? return 0;
}        

Using GCC to build:

gcc -Wall -O0 -g -o test test.c        

Finally, use Valgrind to detect the memory leaks:

$ valgrind --show-reachable=yes --leak-check=full ./test
==26907== Memcheck, a memory error detector
==26907== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==26907== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==26907== Command: ./test
==26907==
==26907==
==26907== HEAP SUMMARY:
==26907==???? in use at exit: 14 bytes in 5 blocks
==26907==?? total heap usage: 5 allocs, 0 frees, 14 bytes allocated
==26907==
==26907== 1 bytes in 1 blocks are still reachable in loss record 1 of 5
==26907==??? at 0x4C336AD: malloc (vg_replace_malloc.c:381)
==26907==??? by 0x400518: leaky_still_reachable (test.c:24)
==26907==??? by 0x4005BF: main (test.c:74)
==26907==
==26907== 1 bytes in 1 blocks are indirectly lost in loss record 2 of 5
==26907==??? at 0x4C336AD: malloc (vg_replace_malloc.c:381)
==26907==??? by 0x40059B: leaky_indirectly (test.c:53)
==26907==??? by 0x4005C9: main (test.c:76)
==26907==
==26907== 1 bytes in 1 blocks are definitely lost in loss record 3 of 5
==26907==??? at 0x4C336AD: malloc (vg_replace_malloc.c:381)
==26907==??? by 0x400503: leaky_definitely (test.c:13)
==26907==??? by 0x4005BA: main (test.c:73)
==26907==
==26907== 3 bytes in 1 blocks are possibly lost in loss record 4 of 5
==26907==??? at 0x4C336AD: malloc (vg_replace_malloc.c:381)
==26907==??? by 0x400534: leaky_possibly (test.c:31)
==26907==??? by 0x4005C4: main (test.c:75)
==26907==
==26907== 9 (8 direct, 1 indirect) bytes in 1 blocks are definitely lost in loss record 5 of 5
==26907==??? at 0x4C336AD: malloc (vg_replace_malloc.c:381)
==26907==??? by 0x400583: leaky_indirectly (test.c:52)
==26907==??? by 0x4005C9: main (test.c:76)
==26907==
==26907== LEAK SUMMARY:
==26907==??? definitely lost: 9 bytes in 2 blocks
==26907==??? indirectly lost: 1 bytes in 1 blocks
==26907==????? possibly lost: 3 bytes in 1 blocks
==26907==??? still reachable: 1 bytes in 1 blocks
==26907==???????? suppressed: 0 bytes in 0 blocks
==26907==
==26907== For lists of detected and suppressed errors, rerun with: -s
==26907== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)t        

I've also shared this code in this SO question.

Memory Management Tips for C

I like very much to use a function pattern that resembles the constructor/destructor paradigm, in which a function does all the memory allocation as another function all de freeing:

#include <stdlib.h>

struct my_struct
{
??? int *member;? ?
};

struct my_struct *create_my_struct()
{
??? struct my_struct *p;

??? p = malloc(sizeof(struct my_struct));
??? if( p == NULL ) return NULL;

??? p->member = malloc(sizeof(int));
??? if( p->member == NULL ) {
??????? free(p);
??????? return NULL;
??? }

??? return p;
}

void destroy_my_struct(struct my_struct *p)
{
??? free(p);
}

int main()
{
??? struct my_struct *p;
??? p = create_my_struct();
??? destroy_my_struct(p);

??? return 0;?? ?
}        

The example above has an obvious problem, but let's go with it and use Valgrind to tell us where the problem is. After building, running it with Valgrind produces:

==31847== Memcheck, a memory error detector
==31847== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31847== Using Valgrind-3.17.0 and LibVEX; rerun with -h for copyright info
==31847== Command: ./valgrind_simple
==31847== ?
==31847== ?
==31847== HEAP SUMMARY:
==31847== ????in use at exit: 4 bytes in 1 blocks
==31847== ??total heap usage: 2 allocs, 1 frees, 12 bytes allocated
==31847== ?
==31847== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==31847== ???at 0x4842839: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==31847== ???by 0x10919A: create_my_struct (valgrind_simple.c:16)
==31847== ???by 0x1091FE: main (valgrind_simple.c:33)
==31847== ?
==31847== LEAK SUMMARY:
==31847== ???definitely lost: 4 bytes in 1 blocks
==31847== ???indirectly lost: 0 bytes in 0 blocks
==31847== ?????possibly lost: 0 bytes in 0 blocks
==31847== ???still reachable: 0 bytes in 0 blocks
==31847== ????????suppressed: 0 bytes in 0 blocks
==31847== ?
==31847== For lists of detected and suppressed errors, rerun with: -s
==31847== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)        

OK, line 16, what is it?

p->member = malloc(sizeof(int));        

Oh, yes, we're allocating member but never freeing it. Because of the function pattern, the problem had to be in the destroy_my_struct function.

Conclusion

Thanks for sticking until the end. Hopefully, this little article helped you understand a little better the different memory leak types reported by Valgrind and how to solve them.

*Note on Rust: This is a joke. I think Rust is a great language and I really hope it helps us, developers, to write better and safer code.

要查看或添加评论,请登录

社区洞察

其他会员也浏览了