Windows XP Heap Manager

¿Qué es la administración de heap en Windows?

Es un subsistema usado por Windows para proveer la asignación de memoria dinámicamente. Esta se almacena en la parte superior de la memoria virtual provista por el kernel. Esta memoria puede ser manipulada por medio de las APIs VirtualAlloc() y VirtualFree(). Básicamente Heap Manager es responsable de proporcionar una capa de software de alto rendimiento para que el software pueda solicitar y liberar memoria utilizando malloc() y free(). RtlAllocateHeap() / RtlFreeHeap() y NtAllocateVirtualMemory() / NtFreeVirtualMemory() son las funciones que realmente forman la interface alrededor del Heap Manager.

Arquitectura del heap

Cada proceso comúnmente tiene múltiples heaps, y el programa puede crear nuevos heaps cuando sea requerido. Existe un heap por defecto del proceso, el cual es llamado como process heap, el puntero de este heap es almacenado en la estructura PEB (Process Environment Block). Todos los heaps de un proceso se almacenan en una lista enlazada en el PEB.

Administrador Front-End

El administrador Front-End se encarga del servicio de asignación y de las solicitudes de des-asignación de memoria.

Existen tres opciones para un administrador Front-End en Windows XP SP3:

  • Ninguno
  • LAL (Look-aside List)
  • LFH (Low-Fragmentation Heap)

El LAL front-end es el responsable de procesar las solicitudes de memoria inferiores a 1024 bytes. Utiliza una serie de estructuras de datos de listas únicas para realizar un seguimiento de varios trozos libres entre los tamaños 0 – 1024 bytes. El LFH front-end maneja solicitudes entre 0 – 16000 bytes, los tamaños mayores o iguales a 16000 son manejados por administrador Back-End.

Los administradores Front-End existen para optimizar el rendimiento del heap.

Administrador Back-End

El administrador Back-End se utiliza cuando el administrador Front-End no está presente, o no se puede realizar una solicitud. Esto puede ocurrir cuando el Front-End no tiene un trozo (chunk) apropiado para atender una solicitud específica, o si el sistema determina mediante heurística que las ventajas de rendimiento del Front-End pueden ser superadas. El administrador Back-End también es responsable de la asignación de grandes cantidades de memoria (> 1024 bytes cuando LAL está activado, > 16000 cuando LFH está activado).

Reservar y Comprometer (Commitment)

Windows distingue entre reservar memoria y comprometer memoria.

Lógicamente, un proceso primero reserva un rango de memoria, lo que hace que el kernel marque ese rango de direcciones de memoria virtual como no disponible. Reservar memoria no correlaciona nada con las direcciones virtuales, por lo que las escrituras, lecturas y ejecuciones de la memoria reservada seguirán causando una violación de acceso. El kernel tampoco intenta garantizar o preorganizar la memoria de respaldo para la memoria reservada. La reserva es esencialmente solo un mecanismo para proteger un rango de direcciones de la asignación desde el usuario.

Después de reservar una parte del espacio de direcciones, el proceso es libre de comprometer y des-comprometer la memoria dentro de él. La memoria comprometida es el acto de mapear y respaldar la memoria virtual. Los procesos pueden comprometer y des-comprometer libremente la memoria dentro de un trozo de memoria reservada sin tener que reservar la memoria (lo que se llama liberación de memoria).

Heap Base

Cada heap que es creado contiene una estructura de datos vital la cual es llamada heap base. El heap base contiene varios datos que son usados para administrar el seguimiento de los trozos libres u ocupados. El siguiente ejemplo muestra el contenido de la estructura _HEAP en un proceso creado bajo Windows XP SP3:

0:000> dt _HEAP 00150000
ntdll!_HEAP
   +0x000 Entry            : _HEAP_ENTRY
   +0x008 Signature        : 0xeeffeeff
   +0x00c Flags            : 0x50000062
   +0x010 ForceFlags       : 0x40000060
   +0x014 VirtualMemoryThreshold : 0xfe00
   +0x018 SegmentReserve   : 0x100000
   +0x01c SegmentCommit    : 0x2000
   +0x020 DeCommitFreeBlockThreshold : 0x200
   +0x024 DeCommitTotalFreeThreshold : 0x2000
   +0x028 TotalFreeSize    : 0xa0
   +0x02c MaximumAllocationSize : 0x7ffdefff
   +0x030 ProcessHeapsListIndex : 1
   +0x032 HeaderValidateLength : 0x608
   +0x034 HeaderValidateCopy : (null) 
   +0x038 NextAvailableTagIndex : 0
   +0x03a MaximumTagIndex  : 0
   +0x03c TagEntries       : (null) 
   +0x040 UCRSegments      : (null) 
   +0x044 UnusedUnCommittedRanges : 0x00150598 _HEAP_UNCOMMMTTED_RANGE
   +0x048 AlignRound       : 0x17
   +0x04c AlignMask        : 0xfffffff8
   +0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0x150050 - 0x150050 ]
   +0x058 Segments         : [64] 0x00150640 _HEAP_SEGMENT
   +0x158 u                : __unnamed
   +0x168 u2               : __unnamed
   +0x16a AllocatorBackTraceIndex : 0
   +0x16c NonDedicatedListLength : 1
   +0x170 LargeBlocksIndex : (null) 
   +0x174 PseudoTagEntries : (null) 
   +0x178 FreeLists        : [128] _LIST_ENTRY [ 0x152b08 - 0x152b08 ]
   +0x578 LockVariable     : 0x00150608 _HEAP_LOCK
   +0x57c CommitRoutine    : (null) 
   +0x580 FrontEndHeap     : 0x00150688 Void
   +0x584 FrontHeapLockCount : 0
   +0x586 FrontEndHeapType : 0x1 ''
   +0x587 LastSegmentIndex : 0 ''

Para listar un sumario de los heaps existentes en el proceso, se debe usar el comando:

!heap -s
0:000> !heap -s
NtGlobalFlag enables following debugging aids for new heaps:
    tail checking
    free checking
    validate parameters
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
00150000 50000062    1024     12     12      1     1     1    0      0   L  
00250000 50001062      64     24     24     13     1     1    0      0   L  
00260000 50008060      64     12     12     10     1     1    0      0      
-----------------------------------------------------------------------------
Administración de memoria

El Heap Manager organiza la memoria virtual usando heap segments y UCR segments para rastrear memoria des-cromprometida.

Heap Segments

El Back-End Heap Manager organiza su memoria por segmentos, donde cada segmento es un bloque contiguo de memoria virtual que es administrado por el sistema (Esta es una estructura de datos interna de Heap Manager y no está relacionada con la segmentación x86). Cuando es posible, el sistema utilizará la memoria comprometida para las solicitudes del servicio, pero si no hay suficiente memoria, el Heap Manager intentará reservar la memoria comprometida dentro de los segmentos existentes del heap para cumplir con la solicitud. Esto podría implicar la asignación de memoria al final del segmento o la asignación de memoria reservada al medio de los segmentos, estos agujeros se crearían al des-comprometer previamente la memoria.

De forma predeterminada, el sistema reserva un mínimo de 0x10000 bytes de memoria al crear un nuevo segmento de heap y compromete al menos 0x1000 bytes de memoria a la vez. El sistema crea nuevos segmentos de heap según sea necesario y los agrega a un array administrado en la base del heap. Cada vez que el heap crea un nuevo segmento, duplica su tamaño de reserva, haciendo que reserven secciones de memoria cada vez más grandes.

Segmento Base

Cada heap contiene un array de hasta 64 punteros a segmentos a +0x58 desde el heap base. Este array de 64 punteros de estructura _HEAP_SEGMENT contiene información sobre todos los segmentos asociados con un heap  dado. Cada segmento representa un bloque contiguo de memoria que es administrado por el sistema. Una estructura vacía se denota con NULL o 0x00000000 en el array. Para visualizar la estructura _HEAP_SEGMENT se debe usar el comando:

dt _HEAP_SEGMENT <dirección>
0:000> dt _HEAP_SEGMENT 0x00150640
ntdll!_HEAP_SEGMENT
   +0x000 Entry            : _HEAP_ENTRY
   +0x008 Signature        : 0xffeeffee
   +0x00c Flags            : 0
   +0x010 Heap             : 0x00150000 _HEAP
   +0x014 LargestUnCommittedRange : 0xfd000
   +0x018 BaseAddress      : 0x00150000 Void
   +0x01c NumberOfPages    : 0x100
   +0x020 FirstEntry       : 0x00150680 _HEAP_ENTRY
   +0x024 LastValidEntry   : 0x00250000 _HEAP_ENTRY
   +0x028 NumberOfUnCommittedPages : 0xfd
   +0x02c NumberOfUnCommittedRanges : 1
   +0x030 UnCommittedRanges : 0x00150588 _HEAP_UNCOMMMTTED_RANGE
   +0x034 AllocatorBackTraceIndex : 0
   +0x036 Reserved         : 0
   +0x038 LastEntryInSegment : 0x00152b00 _HEAP_ENTRY

Como es posible observar, entre la información se incluye, información del heap asociada a este segmento, FirstEntry y LastValidEntry. Con esta información es posible recorrer el heap y obtener todos los metadatos para cada trozo (chunk) del heap.

Rastreo UCR

Cada heap tiene una porción de memoria reservada para rastrear rangos de memoria des-comprometidos. Estos son utilizados por los segmentos para rastrear todos los espacios en sus rangos de direcciones reservadas. Los segmentos rastrean esto con estructuras de datos pequeñas llamadas entradas UCR (rango des-comprometido, del inglés Un-committed Range). El heap mantiene una lista global de estructuras de entrada de UCR libres que pueden solicitar los segmentos de heap, crece dinámicamente para atender las necesidades. En el heap base, UnusedUnCommittedRanges es una lista enlazada de estructuras de UCR vacías que pueden ser utilizadas por los segmentos de heap. UCRSegments (heap base) es una lista enlazada de segmentos especiales UCR utilizados para contener las estructuras UCR. Cuando un segmento usa un UCR, lo elimina de la lista enlaza UnusedUnCommittedRanges del heap y lo coloca en una lista enlazada en la cabecera del segmento llamado UnCommittedRanges. Los segmentos especiales de UCR se asignan dinámicamente. El sistema comienza reservando 0x10000 bytes para cada segmento UCR y asigna 0x1000 bytes (una página) a la vez a medida que se necesitan entradas de seguimiento UCR adicionales. Si el segmento UCR está lleno y se utilizan los 0x10000 bytes, el administrador de heap creará otro segmento UCR y lo agregará a la lista UCRSegments.

Administrador Front-End
Look-Aside List (LAL)

La array de listas Look-Aside se puede encontrar típicamente en el propio trozo de heap en + 0x688 desde la base del heap, apuntado por FrontEndHeap. La estructura de datos LAL es un array de 128 entradas, cada una de tamaño 0x30 bytes. Cada entrada en el array contiene varias variables relacionadas con el rendimiento, que incluyen el tamaño actual, el tamaño máximo y, lo que es más importante, un puntero a la lista enlazada de trozos que corresponden al índice de segmento. Si no hay trozos libres para un segmento dado, el puntero es NULL. De forma similar, el final de la lista simplemente enlazada se denota mediante un puntero de enlace directo de NULL.

Si la solicitud de memoria de un usuario es menor que (1024-8) bytes, puede ser realizada por el front-end LAL. El siguiente diagrama muestra un front-end LAL que tiene una entrada para un tamaño de 1016. Este tamaño corresponde al número de segmento 127, que serviría una solicitud de usuario de un tamaño de 1008 bytes. El LAL no tiene trozos libres para el segmento de tamaño 544 (número de entrada 68). Se debe tener en cuenta que los segmentos para el tamaño 0 y el tamaño 1 no se utilizan porque cada fragmento requiere al menos 8 bytes para la cabecera del trozo.

Low Fragmentation Heap (LFH)

El LFH es un front-end de heap complicado que aborda la fragmentación del heap, así como el rendimiento simultáneo. Este código no está documentado, aunque tiene un rol más central en las versiones posteriores de Windows XP SP3. Incluso con el LFH activo, las solicitudes que involucran tamaños mayores a 16384 bytes deberían ir automáticamente al back-end del heap.

Administrador Back-End

El Back-end Heap Manager mantiene varias listas doblemente enlazadas para rastrear bloques libres en el heap. Estos residen en un array en la base del heap, comenzando en el desplazamiento + 0x178. Hay listas separadas para cada tamaño de bloque posible menores a 1024 bytes, lo que da un total de 128 listas libres (los bloques de heap se clasifican en múltiplos de 8.) Cada lista libre doblemente enlazada tiene un nodo central ubicado en el array en la base de el heap. Cada nodo principal contiene dos punteros: un enlace directo (FLink) y un enlace posterior (BLink).

FreeList[0] es especial, lo cual se discutirá en breve. FreeList[1] no se utiliza, y FreeList[2] hasta FreeList[127] se llaman listas libres dedicadas. Para estas listas dedicadas, todos los bloques libres en la lista son del mismo tamaño, lo que corresponde al índice de array * 8.

Sin embargo, todos los bloques superiores o iguales al tamaño 1024 se mantienen en una sola lista libre en FreeList[0]. (Este espacio está disponible para su uso porque no hay bloques libres de tamaño 0). Los bloques libres en esta lista se ordenan desde el bloque más pequeño hasta el bloque más grande. Entonces, FreeList[0].Flink apunta al bloque libre más pequeño (de tamaño >= 1024), y FreeList[0].Blink apunta al bloque libre más grande (de tamaño >= 1024).

Freelist Bitmap

Las listas libres también tienen un mapa de bits correspondiente, llamado FreeListsInUseBitmap, que se usa para escanear rápidamente a través del array FreeList. Cada bit en el mapa de bits corresponde a una lista libre, y el bit se establece si hay bloques libres en la lista correspondiente. El mapa de bits está ubicado en + 0x158 desde la base del almacenamiento dinámico y proporciona una ruta optimizada para que el sistema dé servicio a una solicitud de asignación entrante de las listas libres dedicadas. Hay 128 bits en el mapa de bits (4 DWORDS), que corresponden a las 128 listas libres que manejan asignaciones de tamaño <1016.

Para una solicitud de asignación dada para un tamaño <1016, primero se le da la oportunidad al servicio de atenderlo. Suponiendo que LAL o LFH no existe o no sirve la solicitud, el sistema mira directamente a la lista enlazada FreeList[n] para el tamaño dado. Se debe tener en cuenta que en este caso, el mapa de bits no es consultado. Si la entrada FreeList[n] correspondiente al tamaño solicitado por el usuario está vacía, entonces el sistema procede a usar el mapa de bits. A partir de este punto, busca en el mapa de bits un bit establecido, lo que hará que encuentre el siguiente bloque libre más grande en las listas libres. Si el sistema se queda sin mapa de bits, intenta extraer un bloque de FreeList[0].

Por ejemplo, si un usuario solicita 32 bytes del heap y no hay un trozo en LAL[5], y FreeList[5] está vacío, el mapa de bits se utiliza para localizar un trozo >40 bytes en las listas libres dedicadas (iniciando la búsqueda del mapa de bits en FreeList[6]).

Heap Cache

Como ya se ha comentado, todos los bloques libres con un tamaño superior o igual a 1024 se almacenan en FreeList[0]. Esta es una lista doblemente enlazada, clasificada por tamaño, desde la más pequeña hasta la más grande, sin mejoras adicionales para la velocidad. En consecuencia, si FreeList[0] crece para contener un gran número de bloques, el heap manager tendrá que recorrer múltiples nodos de lista cada vez que busque en la lista.

El heap cache es una mejora del rendimiento que intenta minimizar el costo de recorridos frecuentes de FreeList[0]. Lo hace creando un índice externo para los bloques en FreeList[0]. Es importante tener en cuenta que el Heap Manager en realidad no mueve ningún bloque libre al caché. Los bloques libres todavía se conservan en FreeList[0], pero el caché contiene varios punteros en los nodos dentro de FreeList[0], que se utilizan como atajos para acelerar el recorrido.

La memoria caché es un array simple de segmentos, donde cada segmento tiene un tamaño de intptr_t bytes y contiene un puntero NULL o un puntero a un bloque en FreeList[0]. De forma predeterminada, el array contiene 896 segmentos, lo que representa cada tamaño de bloque posible entre 1024 y 8192. Este es un tamaño configurable, al que nos referiremos aquí como el índice máximo de caché.

Cada segmento contiene un único puntero al primer bloque en FreeList[0] con el tamaño representado por ese segmento. Si no hay ninguna entrada en FreeList[0] con ese tamaño exacto, el segmento contiene NULL. El último segmento en la memoria caché de almacenamiento dinámico es único: en lugar de representar el tamaño específico 8192, representa todos los tamaños superiores o iguales al índice máximo de caché. Por lo tanto, apuntará al primer bloque en FreeList[0] que sea más grande que el tamaño máximo. (por ejemplo, >= 8192 bytes).

La mayoría de los segmentos están vacíos, por lo que hay un mapa de bits adicional que se utiliza para realizar búsquedas rápidas en el array. Este mapa de bits funciona igual que el mapa de bits utilizado para acelerar la lista libre.

Virtual Alloc List

Cada heap tiene un límite de asignación virtual VirtualMemoryThreshold. Por defecto, esto es 0xfe00, correspondiente a trozos de memoria de tamaño 508k o superior. Los bloques ocupados y virtualmente asignados se mantienen en una lista doblemente enlazada en la base del heap. Cuando los bloques vuelven al sistema, son liberados directamente al kernel por el back-end Heap Manager. (VirtualAllocdBlocks a + 0x50 y + 0x54).

Fuente
Compartir

Agregar un comentario