Перехват malloc()/realloc()/free() для отладки утечек памяти
Содержание
Введение⌗
В языке Си программист имеет полный контроль над выделением и освобождением памяти программмы: стандартные функции malloc()
, realloc()
, free()
и т.д.
Иногда полезно “перехватить” эти стандартные функции и, используя макросы __FILE__
, __FUNCTION__
и __LINE__
, хранить
информацию также и о контексте вызова: сколько байт, в каком файле, какой функцией и на какой строчке было запрошено/освобождено и т.д.
Такая техника помогает отслеживать любые утечки памяти на раз-два.
Интерфейс⌗
API наших функций будет идентичен стандартным:
#include <stdbool.h>
#include <stdlib.h>
/*
* Рабочие лошадки
*/
extern void* my_calloc_dbg(size_t number, size_t size, const char* file,
const char* func, int line);
extern void* my_malloc_dbg(size_t size, const char* file, const char* func,
int line);
extern void* my_realloc_dbg(void* mem, size_t size, const char* file,
const char* func, int line);
extern void my_free_dbg(void* mem, const char* file, const char* func,
int line);
/*
* Утилиты: включение трассировки управления памятью, печать статистики и т.п.
*/
extern void my_malloc_set_debug(bool value);
extern void my_malloc_print_stat(void);
/*
* Фасад: стандартный API malloc() & co.
*/
#define my_malloc(x) my_malloc_dbg(x, __FILE__, __FUNCTION__, __LINE__)
#define my_calloc(x, y) my_calloc_dbg(x, y, __FILE__, __FUNCTION__, __LINE__)
#define my_realloc(x, y) my_realloc_dbg(x, y, __FILE__, __FUNCTION__, __LINE__)
#define my_free(x) my_free_dbg(x, __FILE__, __FUNCTION__, __LINE__)
Трюк в том, чтобы использовать в программе вместо стандартных функций управления памятью подставные макросы, которые на местах вызова
разворачиваются препроцессором Си в “расширенные” функции со строковыми константами имени текущего файла, функции и номера строки в качестве аргументов.
Внутри “расширенных” функций xxx_dbg()
мы вместе с собственно вызовом самой системной функции malloc()
, free()
и т.д. сохраняем
в связном списке полученную информацию о контексте.
Реализация⌗
Для каждого выделения памяти мы храним следующую информацию:
// Отладочная информация (элемент списка таких объектов)
struct memchunk {
// на всякий случай, указатель на выделенную ОС память
void* mem;
// размер в байтах
size_t size;
// имя файла, в котором был вызов макроса
const char* file;
// имя функции, в которой был вызов макроса
const char* func;
// номер строки
int line;
// указатель на следующие элементы в списке
LIST_ENTRY(memchunk) entries;
};
// Определяем новый тип головы такого списка: struct listhead;
LIST_HEAD(listhead, memchunk);
// Статический список аллокаций
static struct listhead allocations = LIST_HEAD_INITIALIZER(allocations);
Здесь мы используем BSD реализацию связного списка из <sys/queue.h>
, однако это - не принципиально.
Вот как выглядит обёртка для malloc()
:
void* my_malloc_dbg(size_t size, const char* file, const char* func, int line) {
struct memchunk* chunk = malloc(sizeof(*chunk));
chunk->mem = malloc(size);
chunk->size = size;
chunk->file = file;
chunk->func = func;
chunk->line = line;
LIST_INSERT_HEAD(&allocations, chunk, entries);
// дополнительно: специально обнуляем полученную от ОС память
bzero(chunk->mem, chunk->size);
if (debug)
printf("%s:%d: my_malloc: allocated @%p (%zu bytes) in %s()\n", file, line,
chunk, chunk->size, chunk->func);
return chunk->mem;
}
Как видно, мы запрашиваем память под нашу структуру struct memchunk
и сохраняем в ней всю отладочную информацию. Второй раз
системный malloc()
используется для выделения собственно запрошенной пользователем памяти (void* mem
).
Статический флаг debug
контролирует печать трассировки активности всех наших функций управления памятью.
Реаллокация и освобождение выглядят так:
void* my_realloc_dbg(void* mem, size_t size, const char* file,
const char* func, int line) {
struct memchunk* chunk;
LIST_FOREACH(chunk, &allocations, entries) {
if (chunk->mem == mem) {
size_t oldsize = chunk->size;
chunk->mem = realloc(chunk->mem, size);
chunk->size = size;
chunk->file = file;
chunk->func = func;
chunk->line = line;
if (debug)
printf("%s:%d: my_realloc: @%p (%zu -> %zu bytes) in %s()\n", file,
line, chunk, oldsize, chunk->size, chunk->func);
return chunk->mem;
}
}
// если мы здесь, значит у нас нет информации об адресе mem; аллоцируем впервые
return my_malloc_dbg(size, file, func, line);
}
void my_free_dbg(void* mem, const char* file, const char* func, int line) {
if (LIST_EMPTY(&allocations)) return;
struct memchunk* chunk;
LIST_FOREACH(chunk, &allocations, entries) {
if (chunk->mem == mem) {
free(chunk->mem);
LIST_REMOVE(chunk, entries);
if (debug)
printf("%s:%d: my_free: free @%p (%zu bytes) in %s()\n", file, line,
chunk, chunk->size, chunk->func);
return;
}
}
// адрес mem не найден в нашем списке, значит мы не аллоцировали его; игнорим
return;
}
Всё довольно просто. В любой момент времени в нашем списке allocations
содержится информация обо всех контекстах аллокаций:
найти утечки памяти не составит особого труда.
Вот файл реализации целиком:
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/queue.h>
struct memchunk {
void* mem;
size_t size;
const char* file;
const char* func;
int line;
LIST_ENTRY(memchunk) entries;
};
LIST_HEAD(listhead, memchunk);
static bool debug = false;
static struct listhead allocations = LIST_HEAD_INITIALIZER(allocations);
void* my_malloc_dbg(size_t size, const char* file, const char* func,
int line) {
struct memchunk* chunk = malloc(sizeof(*chunk));
chunk->mem = malloc(size);
chunk->size = size;
chunk->file = file;
chunk->func = func;
chunk->line = line;
LIST_INSERT_HEAD(&allocations, chunk, entries);
bzero(chunk->mem, chunk->size);
if (debug)
printf("%s:%d: my_malloc: allocated @%p (%zu bytes) in %s()\n", file, line,
chunk, chunk->size, chunk->func);
return chunk->mem;
}
void* my_realloc_dbg(void* mem, size_t size, const char* file,
const char* func, int line) {
struct memchunk* chunk;
LIST_FOREACH(chunk, &allocations, entries) {
if (chunk->mem == mem) {
size_t oldsize = chunk->size;
chunk->mem = realloc(chunk->mem, size);
chunk->size = size;
chunk->file = file;
chunk->func = func;
chunk->line = line;
if (debug)
printf("%s:%d: my_realloc: @%p (%zu -> %zu bytes) in %s()\n", file,
line, chunk, oldsize, chunk->size, chunk->func);
return chunk->mem;
}
}
// chunk that contains 'mem' not found; malloc
return my_malloc_dbg(size, file, func, line);
}
void* my_calloc_dbg(size_t number, size_t size, const char* file,
const char* func, int line) {
return my_malloc_dbg(number * size, file, func, line);
}
void my_free_dbg(void* mem, const char* file, const char* func, int line) {
if (LIST_EMPTY(&allocations)) return; // XXX
struct memchunk* chunk;
LIST_FOREACH(chunk, &allocations, entries) {
if (chunk->mem == mem) {
free(chunk->mem);
LIST_REMOVE(chunk, entries);
if (debug)
printf("%s:%d: my_free: free @%p (%zu bytes) in %s()\n", file, line,
chunk, chunk->size, chunk->func);
return;
}
}
// not found; XXX not ours, so do not free() it
return;
}
void my_malloc_print_stat(void) {
struct memchunk* chunk;
size_t total = 0;
printf("Active allocations:\n");
printf("===================\n");
if (LIST_EMPTY(&allocations)) {
printf("No allocations yet.\n\n");
return;
}
LIST_FOREACH(chunk, &allocations, entries) {
printf("Chunk @%p (%zu bytes) in %s() at %s:%d\n", chunk, chunk->size,
chunk->func, chunk->file, chunk->line);
total += chunk->size;
}
printf("Total memory used: %lu bytes\n\n", total);
}
void my_malloc_set_debug(bool value) { debug = value; }
Использование⌗
Вот так легко с нашими функциями отследить утечку памяти:
#include <stdio.h>
#include "my_malloc.h"
void* foo;
double* d;
struct mystruct {
int a;
float b[4];
char* c;
} * s;
static void func1() {
foo = my_malloc(1024);
d = my_malloc(sizeof(*d));
s = my_malloc(sizeof(*s));
my_free(d);
}
static void func2() {
d = my_malloc(sizeof(*d));
my_free(s);
my_free(foo);
s = my_malloc(sizeof(*s));
s->c = my_malloc(16);
}
static void func3() { my_free(s); }
int main() {
my_malloc_set_debug(true);
func1();
my_malloc_print_stat();
func2();
my_malloc_print_stat();
func3();
my_malloc_print_stat();
return 0;
}
Вывод программы:
/home/nbrk/projects/cpp/untitled1/main.c:14: my_malloc: allocated @0x55a15791e2a0 (1024 bytes) in func1()
/home/nbrk/projects/cpp/untitled1/main.c:15: my_malloc: allocated @0x55a15791eb00 (8 bytes) in func1()
/home/nbrk/projects/cpp/untitled1/main.c:16: my_malloc: allocated @0x55a15791eb60 (32 bytes) in func1()
/home/nbrk/projects/cpp/untitled1/main.c:18: my_free: free @0x55a15791eb00 (8 bytes) in func1()
Active allocations:
===================
Chunk @0x55a15791eb60 (32 bytes) in func1() at /home/nbrk/projects/cpp/untitled1/main.c:16
Chunk @0x55a15791e2a0 (1024 bytes) in func1() at /home/nbrk/projects/cpp/untitled1/main.c:14
Total memory used: 1056 bytes
/home/nbrk/projects/cpp/untitled1/main.c:22: my_malloc: allocated @0x55a15791ebd0 (8 bytes) in func2()
/home/nbrk/projects/cpp/untitled1/main.c:23: my_free: free @0x55a15791eb60 (32 bytes) in func1()
/home/nbrk/projects/cpp/untitled1/main.c:24: my_free: free @0x55a15791e2a0 (1024 bytes) in func1()
/home/nbrk/projects/cpp/untitled1/main.c:26: my_malloc: allocated @0x55a15791ec10 (32 bytes) in func2()
/home/nbrk/projects/cpp/untitled1/main.c:27: my_malloc: allocated @0x55a15791ec50 (16 bytes) in func2()
Active allocations:
===================
Chunk @0x55a15791ec50 (16 bytes) in func2() at /home/nbrk/projects/cpp/untitled1/main.c:27
Chunk @0x55a15791ec10 (32 bytes) in func2() at /home/nbrk/projects/cpp/untitled1/main.c:26
Chunk @0x55a15791ebd0 (8 bytes) in func2() at /home/nbrk/projects/cpp/untitled1/main.c:22
Total memory used: 56 bytes
/home/nbrk/projects/cpp/untitled1/main.c:30: my_free: free @0x55a15791ec10 (32 bytes) in func2()
Active allocations:
===================
Chunk @0x55a15791ec50 (16 bytes) in func2() at /home/nbrk/projects/cpp/untitled1/main.c:27
Chunk @0x55a15791ebd0 (8 bytes) in func2() at /home/nbrk/projects/cpp/untitled1/main.c:22
Total memory used: 24 bytes