Docs
README
Memory Leaks and Debugging in C
Table of Contents
- ā¢Introduction
- ā¢Understanding Memory Leaks
- ā¢Common Causes of Memory Leaks
- ā¢Memory Debugging Tools
- ā¢Using Valgrind
- ā¢Using AddressSanitizer
- ā¢Manual Debugging Techniques
- ā¢Memory Corruption Issues
- ā¢Best Practices for Prevention
- ā¢Memory Management Patterns
- ā¢Summary
Introduction
Memory leaks and memory-related bugs are among the most common and difficult-to-debug issues in C programming. Unlike languages with garbage collection, C requires programmers to manually manage memory allocation and deallocation. When memory is allocated but never freed, it creates a memory leak. When memory is accessed incorrectly, it causes memory corruption.
Why Memory Debugging Matters
Program Start
ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Allocations: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā Deallocations: āāāāāāāāāāāāāāāā ā
ā LEAKED: āāāāāāāāāāāāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
Over Time: Memory usage grows continuously
ā
Eventually: System runs out of memory / Program crashes
Types of Memory Problems
| Problem Type | Description | Consequence |
|---|---|---|
| Memory Leak | Allocated memory never freed | Memory exhaustion |
| Double Free | Freeing already freed memory | Crash, corruption |
| Use After Free | Accessing freed memory | Undefined behavior |
| Buffer Overflow | Writing beyond allocated bounds | Corruption, security issues |
| Buffer Underflow | Writing before allocated bounds | Corruption |
| Uninitialized Read | Reading uninitialized memory | Unpredictable behavior |
| Invalid Free | Freeing non-heap pointer | Crash |
Understanding Memory Leaks
What is a Memory Leak?
A memory leak occurs when dynamically allocated memory is no longer accessible but hasn't been freed.
void memory_leak_example(void) {
int *ptr = malloc(100 * sizeof(int));
// Do some work with ptr...
// PROBLEM: Function returns without freeing ptr
// The 400 bytes are now "leaked"
} // ptr goes out of scope, but memory is still allocated!
Visualizing Memory Leaks
Before function call:
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Heap Memory ā
ā [Free ] ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
During function:
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Heap Memory ā
ā [āāāāā ptr āāāā][Free ] ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ptr variable points here
After function returns (LEAK!):
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Heap Memory ā
ā [āāāāā LOST āāā][Free ] ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
No variable points here anymore!
Memory cannot be freed!
Types of Memory Leaks
1. Direct Leak (Lost Pointer)
void direct_leak(void) {
char *str = malloc(100);
str = malloc(200); // First allocation is now leaked!
free(str); // Only frees the second allocation
}
2. Indirect Leak (Leaked Data Structure)
typedef struct Node {
int data;
struct Node *next;
} Node;
void indirect_leak(void) {
Node *head = malloc(sizeof(Node));
head->next = malloc(sizeof(Node));
head->next->next = malloc(sizeof(Node));
free(head); // Only frees first node!
// Second and third nodes are leaked
}
3. Reachable but Not Freed (At Program Exit)
int *global_ptr;
int main() {
global_ptr = malloc(1000);
// Program ends without freeing global_ptr
// Memory is reachable but not freed
return 0;
}
Common Causes of Memory Leaks
1. Forgetting to Free
// WRONG
void process_data(void) {
char *buffer = malloc(1024);
// ... process data ...
return; // Oops! Forgot to free buffer
}
// CORRECT
void process_data_fixed(void) {
char *buffer = malloc(1024);
// ... process data ...
free(buffer); // Don't forget!
return;
}
2. Early Return Without Cleanup
// WRONG
int process_file(const char *filename) {
FILE *file = fopen(filename, "r");
char *buffer = malloc(1024);
if (file == NULL) {
return -1; // buffer is leaked!
}
if (buffer == NULL) {
fclose(file);
return -1;
}
// ...process...
free(buffer);
fclose(file);
return 0;
}
// CORRECT - Using goto for cleanup
int process_file_fixed(const char *filename) {
FILE *file = NULL;
char *buffer = NULL;
int result = -1;
file = fopen(filename, "r");
if (file == NULL) goto cleanup;
buffer = malloc(1024);
if (buffer == NULL) goto cleanup;
// ...process...
result = 0;
cleanup:
free(buffer); // free(NULL) is safe
if (file) fclose(file);
return result;
}
3. Reassigning Pointer Without Freeing
// WRONG
char *get_message(int type) {
char *msg = malloc(100);
if (type == 1) {
msg = "Hello"; // Original allocation leaked!
} else {
strcpy(msg, "Goodbye");
}
return msg;
}
// CORRECT
char *get_message_fixed(int type) {
char *msg = malloc(100);
if (msg == NULL) return NULL;
if (type == 1) {
strcpy(msg, "Hello");
} else {
strcpy(msg, "Goodbye");
}
return msg;
}
4. Lost Reference in Data Structures
// WRONG - When removing node, we lose reference to removed node
void list_remove_wrong(Node **head, int value) {
if (*head == NULL) return;
if ((*head)->data == value) {
*head = (*head)->next; // Original head is leaked!
return;
}
// ...
}
// CORRECT
void list_remove_correct(Node **head, int value) {
if (*head == NULL) return;
if ((*head)->data == value) {
Node *temp = *head;
*head = (*head)->next;
free(temp); // Free the removed node
return;
}
// ...
}
5. Exception-Like Control Flow
// WRONG - Using longjmp
#include <setjmp.h>
jmp_buf jump_buffer;
void risky_operation(void) {
char *data = malloc(1000);
if (some_error_condition) {
longjmp(jump_buffer, 1); // data is leaked!
}
free(data);
}
6. Lost Return Value
// WRONG
void waste_memory(void) {
malloc(1000); // Allocated but pointer not stored!
strdup("hello"); // strdup allocates, but we ignore result!
}
Memory Debugging Tools
Overview of Tools
| Tool | Platform | Type | Speed | Features |
|---|---|---|---|---|
| Valgrind | Linux, macOS | Dynamic | 10-50x slower | Comprehensive |
| AddressSanitizer | Cross-platform | Compile-time | 2x slower | Fast, detailed |
| Electric Fence | Linux | Library | Moderate | Simple |
| Dr. Memory | Windows, Linux | Dynamic | 10x slower | Similar to Valgrind |
| mtrace | Linux | Library | Minimal | Basic tracking |
When to Use Each Tool
Development Phase Recommended Tool
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Early development ā AddressSanitizer (fast)
Debugging specific ā Valgrind (detailed)
CI/CD pipeline ā AddressSanitizer
Production testing ā Valgrind memcheck
Windows development ā Dr. Memory or ASAN
Using Valgrind
Introduction to Valgrind
Valgrind is a powerful memory debugging tool that detects:
- ā¢Memory leaks
- ā¢Invalid memory access
- ā¢Use of uninitialized memory
- ā¢Double frees
- ā¢Memory corruption
Basic Usage
# Compile with debug symbols
gcc -g -O0 -o myprogram myprogram.c
# Run with Valgrind
valgrind ./myprogram
# Run with detailed leak check
valgrind --leak-check=full ./myprogram
# Show all leak sources
valgrind --leak-check=full --show-leak-kinds=all ./myprogram
# Track origins of uninitialized values
valgrind --track-origins=yes ./myprogram
Understanding Valgrind Output
// Example program with bugs
#include <stdlib.h>
int main() {
int *arr = malloc(10 * sizeof(int));
arr[10] = 5; // Buffer overflow
return 0; // Memory leak (no free)
}
==12345== Invalid write of size 4
==12345== at 0x400534: main (example.c:6)
==12345== Address 0x4c3a068 is 0 bytes after a block of size 40 alloc'd
==12345== at 0x4A06A2E: malloc (vg_replace_malloc.c:270)
==12345== by 0x400525: main (example.c:5)
==12345==
==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4A06A2E: malloc (vg_replace_malloc.c:270)
==12345== by 0x400525: main (example.c:5)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 40 bytes in 1 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 0 bytes in 0 blocks
==12345== still reachable: 0 bytes in 0 blocks
==12345== suppressed: 0 bytes in 0 blocks
Interpreting Leak Types
| Leak Type | Meaning |
|---|---|
| definitely lost | No pointer to the memory exists |
| indirectly lost | Memory pointed to by definitely lost memory |
| possibly lost | Pointer exists but may not point to start of block |
| still reachable | Memory not freed but still has valid pointer |
Valgrind Memcheck Options
# Full command with useful options
valgrind \
--leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
--verbose \
--log-file=valgrind-output.txt \
./myprogram
# For suppressions (ignore known leaks in libraries)
valgrind --suppressions=mysuppressions.supp ./myprogram
Creating Suppression Files
# Example suppression file
{
<insert_a_suppression_name_here>
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
obj:/usr/lib/libsomelib.so
}
Using AddressSanitizer
Introduction to AddressSanitizer (ASan)
AddressSanitizer is a compile-time memory error detector that's faster than Valgrind but requires recompilation.
Enabling AddressSanitizer
# GCC
gcc -fsanitize=address -g -O1 -o myprogram myprogram.c
# Clang
clang -fsanitize=address -g -O1 -o myprogram myprogram.c
# For memory leak detection (GCC/Clang)
gcc -fsanitize=address -fsanitize=leak -g -o myprogram myprogram.c
ASan Example Output
// Program with buffer overflow
#include <stdlib.h>
int main() {
int *arr = malloc(10 * sizeof(int));
arr[10] = 5; // Overflow!
free(arr);
return 0;
}
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000ef...
WRITE of size 4 at 0x60200000ef... thread T0
#0 0x4005d6 in main example.c:6
#1 0x7f0a12345 in __libc_start_main
0x60200000ef... is located 0 bytes to the right of 40-byte region [...]
SUMMARY: AddressSanitizer: heap-buffer-overflow example.c:6 in main
ASan Runtime Options
# Environment variables for ASan
export ASAN_OPTIONS="detect_leaks=1:halt_on_error=0:print_stats=1"
# Useful options:
# detect_leaks=1 - Enable leak detection
# halt_on_error=0 - Continue after error
# print_stats=1 - Print memory statistics
# check_initialization_order=1 - Check global init order
# detect_stack_use_after_return=1 - Detect stack use-after-return
Other Sanitizers
# Memory Sanitizer (uninitialized memory)
gcc -fsanitize=memory -g -o program program.c
# Undefined Behavior Sanitizer
gcc -fsanitize=undefined -g -o program program.c
# Thread Sanitizer (data races)
gcc -fsanitize=thread -g -o program program.c
# Combining sanitizers
gcc -fsanitize=address,undefined -g -o program program.c
Manual Debugging Techniques
1. Wrapper Functions for Tracking
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Debug mode toggle */
#define DEBUG_MEMORY 1
#if DEBUG_MEMORY
/* Allocation tracking */
typedef struct AllocationInfo {
void *ptr;
size_t size;
const char *file;
int line;
struct AllocationInfo *next;
} AllocationInfo;
static AllocationInfo *allocation_list = NULL;
static size_t total_allocated = 0;
static size_t total_freed = 0;
static int allocation_count = 0;
void *debug_malloc(size_t size, const char *file, int line) {
void *ptr = malloc(size);
if (ptr) {
AllocationInfo *info = malloc(sizeof(AllocationInfo));
info->ptr = ptr;
info->size = size;
info->file = file;
info->line = line;
info->next = allocation_list;
allocation_list = info;
total_allocated += size;
allocation_count++;
printf("[ALLOC] %zu bytes at %p (%s:%d)\n", size, ptr, file, line);
}
return ptr;
}
void debug_free(void *ptr, const char *file, int line) {
if (ptr == NULL) return;
AllocationInfo **current = &allocation_list;
while (*current) {
if ((*current)->ptr == ptr) {
AllocationInfo *to_free = *current;
*current = to_free->next;
printf("[FREE] %zu bytes at %p (%s:%d)\n",
to_free->size, ptr, file, line);
total_freed += to_free->size;
allocation_count--;
free(to_free);
free(ptr);
return;
}
current = &(*current)->next;
}
printf("[ERROR] Freeing unknown pointer %p (%s:%d)\n", ptr, file, line);
}
void debug_memory_report(void) {
printf("\n=== Memory Report ===\n");
printf("Total allocated: %zu bytes\n", total_allocated);
printf("Total freed: %zu bytes\n", total_freed);
printf("Currently allocated: %zu bytes\n", total_allocated - total_freed);
printf("Outstanding allocations: %d\n", allocation_count);
if (allocation_list) {
printf("\nLeaked allocations:\n");
for (AllocationInfo *info = allocation_list; info; info = info->next) {
printf(" %zu bytes at %p (%s:%d)\n",
info->size, info->ptr, info->file, info->line);
}
}
printf("====================\n");
}
#define malloc(size) debug_malloc(size, __FILE__, __LINE__)
#define free(ptr) debug_free(ptr, __FILE__, __LINE__)
#endif /* DEBUG_MEMORY */
/* Usage */
int main(void) {
int *arr = malloc(10 * sizeof(int));
char *str = malloc(100);
free(arr);
/* Note: str is not freed - will be reported */
debug_memory_report();
return 0;
}
2. Canary Values for Buffer Overflow Detection
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define CANARY_VALUE 0xDEADBEEF
#define CANARY_SIZE sizeof(uint32_t)
void *safe_malloc(size_t size) {
/* Allocate extra space for canaries */
size_t total = size + 2 * CANARY_SIZE;
char *raw = malloc(total);
if (!raw) return NULL;
/* Place canaries */
*(uint32_t *)raw = CANARY_VALUE;
*(uint32_t *)(raw + CANARY_SIZE + size) = CANARY_VALUE;
return raw + CANARY_SIZE;
}
int check_canaries(void *ptr, size_t size) {
char *raw = (char *)ptr - CANARY_SIZE;
uint32_t front = *(uint32_t *)raw;
uint32_t back = *(uint32_t *)(raw + CANARY_SIZE + size);
if (front != CANARY_VALUE) {
printf("ERROR: Front canary corrupted! (buffer underflow)\n");
return 0;
}
if (back != CANARY_VALUE) {
printf("ERROR: Back canary corrupted! (buffer overflow)\n");
return 0;
}
return 1;
}
void safe_free(void *ptr, size_t size) {
check_canaries(ptr, size);
free((char *)ptr - CANARY_SIZE);
}
int main(void) {
int *arr = safe_malloc(10 * sizeof(int));
/* Normal access */
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
/* This would trigger canary check: */
/* arr[10] = 100; */ /* Buffer overflow! */
safe_free(arr, 10 * sizeof(int));
return 0;
}
3. Memory Fill Patterns
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Fill patterns */
#define ALLOC_PATTERN 0xAA /* Freshly allocated */
#define FREE_PATTERN 0xDD /* Recently freed */
void *debug_calloc(size_t count, size_t size) {
void *ptr = malloc(count * size);
if (ptr) {
/* Fill with pattern (not zero) to detect uninitialized reads */
memset(ptr, ALLOC_PATTERN, count * size);
}
return ptr;
}
void debug_free(void *ptr, size_t size) {
if (ptr) {
/* Fill with pattern to detect use-after-free */
memset(ptr, FREE_PATTERN, size);
free(ptr);
}
}
int main(void) {
int *arr = debug_calloc(10, sizeof(int));
/* Check pattern - uninitialized read would show 0xAAAAAAAA */
printf("Uninitialized value: 0x%X\n", arr[0]);
arr[0] = 42;
printf("After assignment: %d\n", arr[0]);
debug_free(arr, 10 * sizeof(int));
/* Use after free would show 0xDDDDDDDD */
/* DANGEROUS: printf("Use after free: 0x%X\n", arr[0]); */
return 0;
}
Memory Corruption Issues
1. Buffer Overflow
/* WRONG - Writing past allocated memory */
void buffer_overflow_example(void) {
char *buffer = malloc(10);
strcpy(buffer, "This string is too long!"); // Overflow!
free(buffer);
}
/* CORRECT - Check bounds */
void buffer_overflow_fixed(void) {
char *buffer = malloc(10);
strncpy(buffer, "This string is too long!", 9);
buffer[9] = '\0'; // Ensure null termination
free(buffer);
}
2. Use After Free
/* WRONG */
void use_after_free_example(void) {
int *ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
printf("%d\n", *ptr); // Use after free!
}
/* CORRECT - Set pointer to NULL after free */
void use_after_free_fixed(void) {
int *ptr = malloc(sizeof(int));
*ptr = 42;
printf("%d\n", *ptr); // Use before free
free(ptr);
ptr = NULL; // Prevent accidental use
}
3. Double Free
/* WRONG */
void double_free_example(void) {
int *ptr = malloc(sizeof(int));
free(ptr);
free(ptr); // Double free!
}
/* CORRECT */
void double_free_fixed(void) {
int *ptr = malloc(sizeof(int));
free(ptr);
ptr = NULL; // Set to NULL
free(ptr); // free(NULL) is safe
}
4. Invalid Free
/* WRONG - Freeing stack memory */
void invalid_free_example(void) {
int x = 10;
int *ptr = &x;
free(ptr); // Error! Can't free stack memory
}
/* WRONG - Freeing middle of block */
void invalid_free_middle(void) {
int *arr = malloc(10 * sizeof(int));
int *middle = arr + 5;
free(middle); // Error! Not the start of allocation
}
5. Uninitialized Memory Access
/* WRONG */
void uninitialized_access(void) {
int *arr = malloc(10 * sizeof(int));
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += arr[i]; // Reading garbage values!
}
printf("Sum: %d\n", sum); // Unpredictable
free(arr);
}
/* CORRECT - Initialize memory */
void uninitialized_access_fixed(void) {
int *arr = calloc(10, sizeof(int)); // Zero-initialized
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += arr[i];
}
printf("Sum: %d\n", sum); // 0
free(arr);
}
Best Practices for Prevention
1. Allocation/Deallocation Pairing
/*
* Rule: Every allocation function should have a corresponding
* deallocation function at the same abstraction level
*/
typedef struct {
int *data;
size_t size;
} IntArray;
/* Paired create/destroy functions */
IntArray *intarray_create(size_t size) {
IntArray *arr = malloc(sizeof(IntArray));
if (!arr) return NULL;
arr->data = malloc(size * sizeof(int));
if (!arr->data) {
free(arr);
return NULL;
}
arr->size = size;
return arr;
}
void intarray_destroy(IntArray *arr) {
if (arr) {
free(arr->data);
free(arr);
}
}
2. Ownership Semantics
/*
* Clear ownership: who is responsible for freeing?
* Document it in comments/function names
*/
/* Returns newly allocated string - CALLER must free */
char *create_greeting(const char *name) {
char *greeting = malloc(strlen(name) + 10);
sprintf(greeting, "Hello, %s!", name);
return greeting; // Ownership transfers to caller
}
/* Borrows pointer - does NOT free */
void print_greeting(const char *greeting) {
printf("%s\n", greeting);
// Does NOT free - just borrows
}
/* Takes ownership - WILL free the pointer */
void consume_greeting(char *greeting) {
printf("%s\n", greeting);
free(greeting); // Takes ownership and frees
}
3. Defensive Coding
/* Set pointer to NULL after freeing */
#define SAFE_FREE(ptr) do { free(ptr); ptr = NULL; } while(0)
/* Check allocation success */
#define CHECK_ALLOC(ptr) \
do { \
if ((ptr) == NULL) { \
fprintf(stderr, "Allocation failed at %s:%d\n", \
__FILE__, __LINE__); \
exit(EXIT_FAILURE); \
} \
} while(0)
/* Usage */
int main(void) {
int *arr = malloc(100 * sizeof(int));
CHECK_ALLOC(arr);
// Use arr...
SAFE_FREE(arr);
// arr is now NULL
return 0;
}
4. RAII-Like Pattern in C
/* Resource Acquisition Is Initialization (adapted for C) */
typedef struct {
FILE *file;
char *buffer;
size_t buffer_size;
} FileReader;
FileReader *filereader_open(const char *filename, size_t bufsize) {
FileReader *reader = malloc(sizeof(FileReader));
if (!reader) goto fail;
reader->buffer = malloc(bufsize);
if (!reader->buffer) goto fail_buffer;
reader->file = fopen(filename, "r");
if (!reader->file) goto fail_file;
reader->buffer_size = bufsize;
return reader;
/* Cleanup on failure */
fail_file:
free(reader->buffer);
fail_buffer:
free(reader);
fail:
return NULL;
}
void filereader_close(FileReader *reader) {
if (reader) {
if (reader->file) fclose(reader->file);
free(reader->buffer);
free(reader);
}
}
/* Single point of cleanup */
5. Use Static Analysis Tools
# GCC static analysis
gcc -fanalyzer -Wall -Wextra myprogram.c
# Clang static analyzer
scan-build gcc myprogram.c
# cppcheck
cppcheck --enable=all myprogram.c
# PVS-Studio, Coverity, etc. (commercial)
Memory Management Patterns
1. Object Pool Pattern
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define POOL_SIZE 100
typedef struct {
int data;
int in_use;
} PoolObject;
typedef struct {
PoolObject objects[POOL_SIZE];
int free_count;
} ObjectPool;
ObjectPool *pool_create(void) {
ObjectPool *pool = calloc(1, sizeof(ObjectPool));
if (pool) {
pool->free_count = POOL_SIZE;
}
return pool;
}
PoolObject *pool_acquire(ObjectPool *pool) {
if (pool->free_count == 0) return NULL;
for (int i = 0; i < POOL_SIZE; i++) {
if (!pool->objects[i].in_use) {
pool->objects[i].in_use = 1;
pool->free_count--;
return &pool->objects[i];
}
}
return NULL;
}
void pool_release(ObjectPool *pool, PoolObject *obj) {
if (obj && obj->in_use) {
obj->in_use = 0;
obj->data = 0;
pool->free_count++;
}
}
void pool_destroy(ObjectPool *pool) {
free(pool); // All objects freed at once
}
2. Arena Allocator
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
typedef struct {
char *buffer;
size_t size;
size_t used;
} Arena;
Arena *arena_create(size_t size) {
Arena *arena = malloc(sizeof(Arena));
if (!arena) return NULL;
arena->buffer = malloc(size);
if (!arena->buffer) {
free(arena);
return NULL;
}
arena->size = size;
arena->used = 0;
return arena;
}
void *arena_alloc(Arena *arena, size_t size) {
/* Align to 8 bytes */
size = (size + 7) & ~(size_t)7;
if (arena->used + size > arena->size) {
return NULL; // Out of space
}
void *ptr = arena->buffer + arena->used;
arena->used += size;
return ptr;
}
void arena_reset(Arena *arena) {
arena->used = 0; // "Free" all allocations at once
}
void arena_destroy(Arena *arena) {
if (arena) {
free(arena->buffer);
free(arena);
}
}
/* No individual free needed! */
3. Reference Counting
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data;
int ref_count;
} SharedString;
SharedString *sharedstring_create(const char *str) {
SharedString *ss = malloc(sizeof(SharedString));
if (!ss) return NULL;
ss->data = strdup(str);
if (!ss->data) {
free(ss);
return NULL;
}
ss->ref_count = 1;
return ss;
}
SharedString *sharedstring_acquire(SharedString *ss) {
if (ss) {
ss->ref_count++;
}
return ss;
}
void sharedstring_release(SharedString *ss) {
if (ss) {
ss->ref_count--;
if (ss->ref_count <= 0) {
free(ss->data);
free(ss);
}
}
}
/* Usage */
int main(void) {
SharedString *s1 = sharedstring_create("Hello");
SharedString *s2 = sharedstring_acquire(s1); // ref_count = 2
SharedString *s3 = sharedstring_acquire(s1); // ref_count = 3
printf("%s (refs: %d)\n", s1->data, s1->ref_count);
sharedstring_release(s2); // ref_count = 2
sharedstring_release(s3); // ref_count = 1
sharedstring_release(s1); // ref_count = 0, freed
return 0;
}
Summary
Key Concepts
- ā¢Memory Leaks: Allocated memory that's never freed
- ā¢Memory Corruption: Invalid memory access (overflow, use-after-free, etc.)
- ā¢Tools: Valgrind, AddressSanitizer, static analyzers
- ā¢Prevention: Ownership semantics, defensive coding, proper cleanup
Debugging Checklist
- ⢠Compile with
-gflag for debug symbols - ⢠Run with Valgrind or AddressSanitizer
- ⢠Check all allocation return values
- ⢠Ensure every malloc/calloc has matching free
- ⢠Set pointers to NULL after freeing
- ⢠Use static analysis tools
- ⢠Review ownership semantics in code review
Common Commands
# Valgrind full leak check
valgrind --leak-check=full --show-leak-kinds=all ./program
# Compile with AddressSanitizer
gcc -fsanitize=address -g -o program program.c
# Run with leak detection
ASAN_OPTIONS=detect_leaks=1 ./program
# Static analysis
gcc -fanalyzer -Wall -Wextra program.c
cppcheck --enable=all program.c
Prevention Summary
| Problem | Prevention |
|---|---|
| Memory leak | Match every alloc with free |
| Double free | Set pointer to NULL after free |
| Use after free | Set pointer to NULL, avoid stale refs |
| Buffer overflow | Check bounds, use size-aware functions |
| Uninitialized read | Use calloc or memset |
Next Steps
After mastering memory debugging:
- ā¢Learn about memory-safe coding standards (CERT C, MISRA)
- ā¢Explore automated testing with fuzzing
- ā¢Study secure coding practices
- ā¢Implement custom memory allocators
This tutorial is part of the C Programming Learning Series.