A naive (but fast) memory leak detection system
El objetivo de este post es el plantear un sistema de deteccion de leaks que sea lo suficientemente rapido como para ser usado en aplicaciones complejas o en aplicaciones que hagan un uso desmedido del heap. El objetivo es un sistema que sin un uso extra de memoria excesivo, y con una perdida de rendimiento minimo, sea capaz de detectar los leaks de nuestra aplicacion.
El codigo y las ideas que voy a exponer son solo un esbozo y una pequeña prueba de concepto. Quedaria un gran trabajo por hacer para llegar a obtener algo realmente funcional.
Y dicho esto, vamos a meternos de lleno en el tema.
La idea es bastante sencilla. Sabemos que el Heap de Windows devuelve bloques de memoria que estan alineados a ocho bytes. Sabemos que Windows divide a partes iguales el espacio de direcciones entre Kernel mode y User mode (vamos a ignorar el modo de inicio /3gb). En IA32 esto nos deja un espacio de direcciones de 2gb para el usuario. Ahora bien, si quisiesemos terner un bitmap que pudiese representar cualquier reserva de memoria necesitariamos
2gb/(8*8) = 32mb
O lo que es lo mismo. Con solo gastar 32mb podemos terner un bitmap que represente cualquier reserva. Con esto conseguimos una estructura a traves de la cual gestionar los mallocs que el usuario tiene en un momento dado.
Por otro lado, un sistema de este tipo necesita gestionar mas informacion a parte de los punteros de los mallocs. ¿Donde almacenamos esta informacion? Lo que haremos es añadir esa informacion al malloc que hagamos para satisfacer la peticion del usuario.
En este modelo tenemos dos ventajas: No hacemos mallocs extra para nuestras gestiones, y solo necesitamos tener en cuenta la sincronia en el bitmap que representa las peticiones de los usuarios que han sido satisfechas.
Comenzaremos definiendo una cabecera para las reservas que pidan los usuarios:
{
DWORD UserSize;
}ALLOCATION_HEADER, *LPALLOCATION_HEADER;
Y algunos defines:
#define BYTE_BITS (ULONG)(8)
#define HEAP_ALIGNMENT (ULONG)(8)
#define LONG_BITS (ULONG)(BYTE_BITS * sizeof(DWORD))
#define USER_ADDRESS_SPACE (ULONG)(2 * _1GB_)
#define CONTROL_SIZE (ULONG)(USER_ADDRESS_SPACE / (HEAP_ALIGNMENT * LONG_BITS))
El define CONTROL_SIZE sera el tamaño de un array de LONG’s que tiene 32MB de tamaño. Esto es, nuestro bitmap:
Ahora un par de funciones hacer set/clear de un bit en el bitmap (Añadir o quitar un malloc). Estas funciones son triviales, y tan solo hay que tener cuidado de asegurarnos de que se puede acceder correctamente desde varios hilos de forma concurrente. En este caso el tiempo que tarda en ejecutarse el codigo que hay que proteger es despreciable por lo que no tiene sentido usar una seccion critica (Si queremos asegurar la correcta sincronia y no queremos implementarla nosotros mismos una alternativa aceptable podria ser una seccion critica, pero siempre con un spinlock adecuado)
__inline
void
SetMalloc(LPVOID Buffer)
{
DWORD MallocInfo = (DWORD)(ULONG_PTR)Buffer / HEAP_ALIGNMENT;
LONG *SegmentAddr = &(AllocControl[MallocInfo / LONG_BITS]);
DWORD NewBit = 1 << (MallocInfo % LONG_BITS);
DWORD Segment;
do
{
Segment = *SegmentAddr;
}while(InterlockedCompareExchange(SegmentAddr, Segment | NewBit, Segment) != Segment);
}
static
__inline
void
ClearMalloc(LPVOID Buffer)
{
DWORD MallocInfo = (DWORD)(ULONG_PTR)Buffer / HEAP_ALIGNMENT;
LONG *SegmentAddr = &(AllocControl[MallocInfo / LONG_BITS]);
DWORD OldBit = 1 << (MallocInfo % LONG_BITS);
DWORD Segment;
do
{
Segment = *SegmentAddr;
}
while(InterlockedCompareExchange(SegmentAddr, Segment & ~OldBit, Segment) != Segment);
}
La funcion malloc:
{
size_t RealSize;
LPALLOCATION_HEADER Header;
LPVOID Result = NULL;
RealSize = size + sizeof(ALLOCATION_HEADER);
Header = (LPALLOCATION_HEADER) malloc(RealSize);
if(Header != NULL)
{
SetMalloc(Header);
Header->UserSize = size;
Result = Header + 1;
}
return Result;
}
El calloc:
{
size_t RealSize;
LPALLOCATION_HEADER Header;
LPVOID Result = NULL;
size *= n;
RealSize = size + sizeof(ALLOCATION_HEADER);
Header = (LPALLOCATION_HEADER) calloc(1, RealSize);
if(Header != NULL)
{
SetMalloc(Header);
Header->UserSize = size;
Result = Header + 1;
}
return Result;
}
El free:
{
if(ptr != NULL)
{
LPALLOCATION_HEADER Header = (LPALLOCATION_HEADER) ptr;
Header -= 1;
ClearMalloc(Header);
free(Header);
}
}
Y el realloc. En el caso del realloc es importante el orden en el que se llevan a cabo las acciones. Este orden es evidente si revisamos el significado estricto de las estructuras de datos que utilizamos. Nuestro bitmap representa los mallocs que hemos satisfecho. Antes del realloc es necesario eliminar la reserva del usuario del Bitmap (Aunque mas tarde tengamos que volvera a añadirla). Si no lo hiciesemos asi necesitariamos estructuras de control extra para evitar problemas de sincronia:
{
LPALLOCATION_HEADER Old = NULL;
LPALLOCATION_HEADER New = NULL;
LPVOID Result = NULL;
DWORD RealSize = 0;
if(ptr != NULL)
{
Old = (LPALLOCATION_HEADER)ptr;
Old -= 1;
ClearMalloc(Old);
}
RealSize = size;
if(RealSize != 0)
{
RealSize += sizeof(ALLOCATION_HEADER);
}
New = (LPALLOCATION_HEADER)realloc(Old, RealSize);
if(RealSize != 0)
{
if(New != NULL)
{
SetMalloc(New);
New->UserSize = size;
Result = New + 1;
}
else
{
if(Old != NULL)
{
SetMalloc(Old);
}
}
}
return Result;
}
Y por ultimo, una funcion que genera un fichero de dump muyh basico con los leaks de nuestra aplicacion:
{
FILE *File;
File = fopen(FilePath, "a+t");
if(File != NULL)
{
DWORD ControlSegment = 0;
DWORD i;
for(i = 0; i < CONTROL_SIZE ; ++i)
{
DWORD SegMent = AllocControl[i];
DWORD Offset = 0;
while(SegMent != 0)
{
if((SegMent & 1) != 0)
{
LPALLOCATION_HEADER Leak = (LPALLOCATION_HEADER)(ControlSegment + Offset);
LPBYTE Buffer;
int Size;
Size = Leak->UserSize;
Buffer = (LPBYTE)(Leak + 1);
fprintf(File, "\nLeak of %d bytes at 0x%0p\nDump:\n", Size, Buffer);
while(Size != 0)
{
int LineSize = min(Size, 16);
int i = 0;
for(i = 0 ; i < LineSize ; i++)
{
fprintf(File, "%02x ", Buffer[i]);
}
for(;i < 16 ; i++)
{
fprintf(File, " ");
}
fprintf(File, "| ");
for(i = 0 ; i < LineSize ; i++)
{
if(Buffer[i] < 32 || Buffer[i] == 127)
{
fprintf(File, " ");
}
else
{
fprintf(File, "%c", Buffer[i]);
}
}
for(;i < 16 ; i++)
{
fprintf(File, " ");
}
fprintf(File, "\n");
Size -= LineSize;
Buffer += LineSize;
}
fprintf(File, "\n");
}
SegMent >>= 1;
Offset += HEAP_ALIGNMENT;
}
ControlSegment += LONG_BITS * HEAP_ALIGNMENT;
}
fclose(File);
}
}
En las pruebas que he llevado a cabo la perdida de rendimiento no se aprecia, y el gasto de memoria extra es de 32MB mas un DWORD por cada malloc. Lo cierto es que el bitmap de 32MB puede reducirse considerablemente con una implementacion adecuada, aunque una implementacion mas util añadiria algo mas de overhead a cada malloc.
Sea como sea, al prueba de concepto parece valida, y a partir de aqui se podria seguir construyendo un Leak Detector bastante rapido.
Esto es una prueba de concepto que he implementado en un par de horas, asi que pido disculpas publicamente porque lo cierto es que el codigo esta bastante descuidado.
Y esto es to to todo amigos!!