I've been following an article that walks through implementing primitive versions of malloc and free. After finishing the allocator, I wanted to test it using Valgrind, so I added the following lines to my malloc and free functions, respectively:
VALGRIND_MALLOCLIKE_BLOCK(block, size, 0, 0);
VALGRIND_FREELIKE_BLOCK(block, 0);
Valgrind shows that memory allocations are being tracked correctly using VALGRIND_MALLOCLIKE_BLOCK() and VALGRIND_FREELIKE_BLOCK(). However, one block is still marked as "still reachable" at exit.
HEAP SUMMARY:
==74425== in use at exit: 20 bytes in 1 blocks
==74425== total heap usage: 2 allocs, 2 frees, 1,044 bytes allocated
==74425==
==74425== 20 bytes in 1 blocks are still reachable in loss record 1 of 1
==74425==
==74425== LEAK SUMMARY:
==74425== definitely lost: 0 bytes in 0 blocks
==74425== indirectly lost: 0 bytes in 0 blocks
==74425== possibly lost: 0 bytes in 0 blocks
==74425== still reachable: 20 bytes in 1 blocks
==74425== suppressed: 0 bytes in 0 blocks
The article adds a "stub" member to the union, with a fixed size of 16, however it is unnecessary, since sizeof(header_t) = 24.
Here's my implementation of the two functions:
pthread_mutex_t global_malloc_lock;
header_t *head, *tail;
header_t *get_free_block(size_t size) {
header_t *curr = head;
while (curr) {
if (curr->s.is_free && curr->s.size >= size) {
return curr;
}
curr = curr->s.next;
}
return NULL;
}
void *my_malloc(size_t size) {
size_t total_size;
void *block;
header_t *header;
if (!size) {
return NULL;
}
pthread_mutex_lock(&global_malloc_lock);
header = get_free_block(size);
if (header) {
header->s.is_free = 0;
pthread_mutex_unlock(&global_malloc_lock);
return (void*)(header+1);
}
total_size = sizeof(header_t) + size;
block = sbrk(total_size);
if (block == (void*) -1) {
pthread_mutex_unlock(&global_malloc_lock);
return NULL;
}
header = block;
header->s.is_free = 0;
header->s.size = size;
header->s.next = NULL;
if (!head) {
head = header;
}
if (tail) {
tail->s.next = header;
}
tail = header;
pthread_mutex_unlock(&global_malloc_lock);
VALGRIND_MALLOCLIKE_BLOCK(block, size, 0, 0);
return (void*)(header+1);
}
void my_free(void* block) {
header_t *header, *tmp;
void *programbreak;
if (!block) {
return;
}
pthread_mutex_lock(&global_malloc_lock);
header = (header_t*)block - 1;
programbreak = sbrk(0);
if ((char*)block + header->s.size == programbreak) {
if (head == tail) {
head = tail = NULL;
}
else {
tmp = head;
while(tmp) {
if (tmp->s.next == tail) {
tmp->s.next = NULL;
tail = tmp;
}
tmp = tmp->s.next;
}
}
sbrk(0 - sizeof(header_t) - header->s.size);
pthread_mutex_unlock(&global_malloc_lock);
VALGRIND_FREELIKE_BLOCK(block, 0);
return;
}
header->s.is_free = 1;
pthread_mutex_unlock(&global_malloc_lock);
}
(The definition of the union as well as the signatures for the functions are in a separate header file)
Here's the main function:
int main(int argc, char const *argv[])
{
printf("sizeof(header_t) = %zu\n", sizeof(header_t));
int* block = my_malloc(5*sizeof(int));
my_free(block);
return 0;
}
My question is: why does Valgrind report memory as "still reachable" even though I call my_free() and VALGRIND_FREELIKE_BLOCK()? Is it because sbrk() isn't releasing the memory? Or is there something missing in my free logic?
The article: https://arjunsreedharan.org/post/148675821737/memory-allocators-101-write-a-simple-memory
headandtailare used before initializing them.staticor otherwise the program design is fishy at best.--leak-check=yes --show-leak-kinds=allto get the callstack where memory was allocated.