Введение

В языке Си программист имеет полный контроль над выделением и освобождением памяти программмы: стандартные функции 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