I'm looking for a code review of my custom memory allocation library (written for educational purposes), which includes malloc()
, calloc()
, realloc()
, and free()
functions.
Review Goals:
- Does the code ensure that the pointers returned by
*alloc()
are properly aligned for all types, as required by C standards, to avoid potential undefined behavior? - Does any part of my code invokes undefined behavior?
- General coding comments, style, etc.
Code:
liballoc.h:
#ifndef LIBALLOC_H
#define LIBALLOC_H
/**
* \brief The **malloc** function allocates space for an object whose size is specified by *size*
* and whose value is indeterminate.
* \param size - The size of the object to be allocated.
* \return The **malloc** function returns either a null pointer or a pointer to the allocated
* space. If the size of the space requested is zero, a null pointer is returned.
*/
void *malloc(size_t size);
/**
* \brief The **calloc** function allocates space for an array of *nmemb* objects, each of whose size
* is *size*. The space is initialized to all bits zero.
* \param nmemb - The number of objects.
* \param size - The size of each object.
* \return The **calloc** function returns either a null pointer or a pointer to the allocated space.
* If the size of the space requested is zero, a null pointer is returned.
*/
void *calloc(size_t nmemb, size_t size);
/**
* \brief The **realloc** function deallocates the old object pointed to by *ptr* and returns a pointer to
* a new object that has the size specified by *size*. The contents of the new object shall be
* the same as that of the old object prior to deallocation, upto the lesser of the new and
* old sizes. Any bytes in the new object beyond the size of the old object have indeterminate
* values.
*
* If *ptr* is a null pointer,the **realloc** function behaves like the **malloc** function for the specified *size*.
* Otherwise, if *ptr* does not match a pointer earlier returned by a memory management function, or if the
* space has been deallocated by a call to the **free** or **realloc** function, the behavior is undefined.
* If *size* is nonzero and memory for the new object is not allocated, the old object is not deallocated.
* If *size* is zero and memory for the new object is not allocated, the old object is not deallocated,
* and its value shall be unchanged.
*
* \param ptr - A pointer to the object to be reallocated.
* \param size - The new size.
* \return The **realloc** function returns a pointer to the new object, or a null pointer if the new object
* if the new object has not been allocated.
*
*/
void *realloc(void *ptr, size_t size);
/**
* \brief The **free** function causes the space pointed to by *ptr* to be deallocated, that is, made
* unavailable for further allocation. If *ptr* is a null pointer, no action occurs. Otherwise,
* if the argument does not match a pointer earlier returned by a memory management function,
* or if the space has been deallocated by a call to **free**, the behaviour is undefined.
* \param ptr - A pointer to the object to be deallocated.
* \return The **free** function returns no value.
*/
void free(void *ptr);
#endif /* LIBALLOC_H */
liballoc.c:
#ifdef _POSIX_C_SOURCE
#undef _POSIX_C_SOURCE
#endif
#ifdef _XOPEN_SOURCE
#undef _XOPEN_SOURCE
#endif
#define _POSIX_C_SOURCE 200819L
#define _XOPEN_SOURCE 700
#define _DEFAULT_SOURCE
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <stddef.h>
#include <stdalign.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include "liballoc.h"
#define MALLOC_LOCK() (pthread_mutex_lock (&global_malloc_lock))
#define MALLOC_UNLOCK() (pthread_mutex_unlock (&global_malloc_lock))
#define CHUNK_SIZE(chunk) ((chunk)->size)
#define NEXT_CHUNK(chunk) ((chunk)->next)
#define IS_CHUNK_FREE(chunk) ((chunk)->is_free)
#define MARK_CHUNK_FREE(chunk) ((chunk)->is_free = true)
#define MARK_CHUNK_NONFREE(chunk) ((chunk)->is_free = false)
#define SET_CHUNK_SIZE(chunk, size) ((chunk)->size = (size))
#define SET_NEXT_CHUNK(chunk, val) ((chunk)->next = (val))
#define SBRK_FAILED ((void *) -1)
/* The C Standard states that the pointer returned if the allocation succeeds is
* suitably aligned so that it may be assigned to a pointer to any type of object
* with a fundamental alignment requirement.
*/
typedef struct header {
_Alignas (max_align_t) struct header *next;
size_t size;
bool is_free;
} header_t;
static header_t *head, *tail;
static pthread_mutex_t global_malloc_lock;
static header_t *get_free_block(size_t size)
{
/* Ascertain if there's a free block that can accommodate requested size. */
for (header_t * curr = head; curr; curr = NEXT_CHUNK(curr)) {
if (IS_CHUNK_FREE(curr) && CHUNK_SIZE(curr) >= size) {
return curr;
}
}
return NULL;
}
void *malloc(size_t size)
{
/* The C Standard states that if the size of the space requested is zero,
* the behavior is implementation-defined: either a null pointer is returned
* to indicate an error, or the behavior is as if the size were some non zero
* value, except that the returned pointer shall not be used to access an object.
* We shall opt for the former.
*/
if (!size || size > SIZE_MAX - sizeof (header_t)) { /* Prevent overflow. */
return NULL;
}
MALLOC_LOCK();
header_t *header = get_free_block(size);
if (header) {
/* We have an apt free block. */
MARK_CHUNK_NONFREE(header);
MALLOC_UNLOCK();
return header + 1;
}
/* The size requested needs to be a multiple of alignof (max_align_t). */
size_t const padding = size % alignof (max_align_t);
if (padding) {
size += alignof (max_align_t) - padding;
}
intptr_t const total_size = (intptr_t) (sizeof (header_t) + size);
void *const chunk = sbrk(total_size);
if (chunk == SBRK_FAILED) {
MALLOC_UNLOCK();
return NULL;
}
header = chunk;
SET_CHUNK_SIZE(header, size);
MARK_CHUNK_NONFREE(header);
SET_NEXT_CHUNK(header, NULL);
if (!head) {
head = header;
}
if (tail) {
SET_NEXT_CHUNK(tail, header);
}
tail = header;
MALLOC_UNLOCK();
return header + 1;
}
void *calloc(size_t nmemb, size_t size)
{
if (!nmemb || !size) {
return NULL;
}
if (size < SIZE_MAX / nmemb) { /* Allocation too big. */
return NULL;
}
size *= nmemb;
void *const chunk = malloc(size);
if (!chunk) {
return NULL;
}
return memset(chunk, 0X00, size);
}
void *realloc(void *ptr, size_t size)
{
if (!ptr || !size) {
return malloc(size);
}
const header_t *const header = (header_t *) ptr - 1;
if (CHUNK_SIZE(header) >= size) {
return ptr;
}
void *const ret = malloc(size);
if (ret) {
/* Relocate the contents to the new chunkier chunk and
* free the old chunk.
*/
memcpy(ret, ptr, CHUNK_SIZE(header));
free(ptr);
}
return ret;
}
void free(void *ptr)
{
if (!ptr) {
return;
}
MALLOC_LOCK();
header_t *const header = (header_t *) ptr - 1;
/* Get a pointer to the current program break address.
*/
const void *const prog_brk = sbrk(0);
/* Ascertain whether the block to be freed is the last one in the
* list. If so, shrink the size of the heap and release the memory
* to the OS. Else, keep the block but mark it as free for use.
*/
if ((char *) ptr + CHUNK_SIZE(header) == prog_brk) {
if (head == tail) {
head = tail = NULL;
} else {
for (header_t * tmp = head; tmp; tmp = NEXT_CHUNK(tmp)) {
SET_NEXT_CHUNK(tmp, NULL);
tail = tmp;
}
}
/* sbrk() with a negative argument decrements the program break,
* which releases memory the memory back to the OS.
*/
sbrk((intptr_t) (0 - sizeof (header_t) - CHUNK_SIZE(header)));
/* This lock does not ensure thread-safety, for sbrk() itself
* is not thread-safe.
*/
MALLOC_UNLOCK();
return;
}
MARK_CHUNK_FREE(header);
MALLOC_UNLOCK();
}
makefile:
CFLAGS := -s
CFLAGS += -std=c2x
CFLAGS += -no-pie
#CFLAGS += -g3
#CFLAGS += -ggdb
CFLAGS += -Wall
CFLAGS += -Wextra
CFLAGS += -Warray-bounds
CFLAGS += -Wconversion
CFLAGS += -Wmissing-braces
CFLAGS += -Wno-parentheses
CFLAGS += -Wno-format-truncation
CFLAGS += -Wpedantic
CFLAGS += -Wstrict-prototypes
CFLAGS += -Wwrite-strings
CFLAGS += -Winline
CFLAGS += -O2
NAME := liballoc_$(shell uname -m)-$(shell uname -s)
LIBDIR := bin
SLIB := $(LIBDIR)/$(NAME).a
DLIB := $(LIBDIR)/$(NAME).so
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c, obj/%.o, $(SRCS))
all: $(SLIB) $(DLIB)
$(SLIB): $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DLIB): $(OBJS)
$(CC) $(CFLAGS) -fPIC -shared $(SRCS) -o $@
obj/%.o: src/%.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
$(RM) -rf $(OBJS)
fclean:
$(RM) $(SLIB) $(DLIB)
.PHONY: fclean clean all
.DELETE_ON_ERROR: