MemAlloc the ultimate malloc function

Pour la petite histoire
Je suis en train de developper un nouveau demotool pour les faire une intro 64k en me basant sur du code des merveilleux tools de Brain Control et Conspiracy et j'arrivais à une étape crutiale ou je voulais compiler le player de demo pour avoir une toolchain de bou en bou (c'est une étape très importante car il n'a rien de pire que de debugger en demoparty quand on party prod). Et j'ai bien fait car j'ai découvert tout un tas de surprises !
Premierement les placement new n'était pas possible sans la library standard sous x86 win32 🙃 (or la lib standard c'est nein dans les intro 64k et l'operateur new de base est override par ImGui et ça foutais la merde).
Deuxiemement j'avais un gros leak et le code sur lequel je mettais basé (qui utilisais la fonction _malloc_dbg) n'était d'aucune utilité. En effet cette fonction reporte la liste les leak en fin de run donc qu'on se retrouve avec un gigantesque log tous allant sur des fichier de base (list.hpp ou str.hpp) ce qui ne m'aidais pas du tout à trouver mes soucis.
Enfin je suspectais des eventuel buffer overflows passant à travers les mailles des asserts. Car le code reportais de maniere àléatoire des heap corruptions toujours grace à _malloc_dbg malheursement bien trop tard
Fonctionalitées
Pour pouvoir me dépetrer de tout cela j'ai décidé de recoder ma fonction MemAlloc pour qu'elle puisse me permettre à la fois:
- La detection des buffer overflow en me mettant en triggerant un breakpoint directement sur mon caca 💩 (et non en me reportant le truc au prochain appel à malloc ou free comme avec _malloc_dbg).
- Enregistrer les alloc en distinguant les callstack pour pouvoir les visualiser avec implot (on ne remerciera jamais assez les devs de ImGui et ImPlot qui font gagner des kilo heures à l'industrie du jeu vidéo). Ultimement avoir un bouton breakpoint quand on aura reperer la callstack fautive ! Pour encore une fois (vous connaissez) avoir directement le 👃 dans le 💩 et pas chercher mille ans avec mille printf (d'ailleurs on peut écrire chercher mille fois un printf, chercher une fois mille printf mais pas chercher mille fois mille ... enfin bref).
- Gerer l'alignement sur l'adresse renvoyé (en effet pour utilier les instructions SIMD utilisés il faut être aligné sur 16 octets).
Pour cela on va bien sur utiliser une autre fonction que _malloc_dbg, vous commencez à comprendre ce que j'en pense (💩) de cette fonction ! Non aujourd'hui on va utiliser des merveilles trop peu connues que sont VirtualAlloc et StackWalk64.
On code ?
Dans un premier temps je voulais corrigé mes soucis d'overflow, pour cela il est possible d'utiliser les fonctions VirtualAlloc, VirtualProtect et bien sur VirtualFree. Ces fonctions permettent d'allouer directement des adresses mémoire virtuelles alignées sur les pages et de controller les droits d'accès pour chaque page. Comme l'adresse renvoyée n'est pas forcement l'addresse que l'on va renvoyer à l'utilisateur comme on veut aligner la mémoire, on commencera par définir un struct AllocHeader que l'on viendra placer juste avant l'adresse que l'on renverra.
#include <windows.h>
//===================================
//===================================
struct AllocHeader
{
void * baseAddr; // Address à donner à la méthode de free du system.
#ifdef TRACK_MEMORY_ALLOC_CALLSTACKS
AllocInfos infos; // Infos de tracking mémoire.
uint16 callStackTrackIndex; // Index dans la liste de tracking des allocs
// (pour pouvoir soustraire l'alloc info lors du free).
#endif // TRACK_MEMORY_ALLOC_CALLSTACK
};
On s'attadera sur la struct AllocInfos plus tard elle nous servira à tracker toutes nos allocations pour les visualiser et ainsi trouver le responsable du leak. J'ai mis des define TRACK_MEMORY_ALLOC_CALLSTACKS et DETECT_OVERFLOWS pour pouvoir activer ces features au moment ou j'en aurais besoin ou les désactiver si il pose trop de soucis de perfs. Passons au code de la fonction tant attendue 😀.
//===================================
//===================================
void * MemAllocAligned( uint32 _size, uint32 _alignment )
{
lassert( _alignment > 0 && IsPowerOf2( _alignment ), "%u is not a power of 2", _alignment );
/* Calcul de la taille nécéssaire pour aligner correctement notre pointeur. */
const uintptr alignedSize = _size + _alignment - 1;
/* Calcul de la taille totale nécéssaire alignement + header. */
const uintptr allocSize = alignedSize + sizeof( AllocHeader );
#if DETECT_OVERFLOWS
/* Si on veux detecter les overflows alors il nous faut utiliser les fonctions d'allocation de page virtuelles.
Le but est d'avoir une page protégée juste après notre notre allocation de cette façons le systeme levera une exception et avec le debugger on aura le breakpoint directement dans la situation d'overflow.
La taille d'une page peut varier (1024 octets chez moi) il est nécéssaire de la récupérer depuis les sytem infos. On pourrais faire cet appel une seule fois à l'init du programme (j'ai laissé comme ça et ce n'est pas lent).
Evidement l'inconvenient c'est les mini alloc qui vont se mettre à faire la taille de 2 pages mémoires 🤪 mais normalement on fait pas ça ! */
SYSTEM_INFO sysInfos;
GetSystemInfo( &sysInfos );
const uintptr pageSize = sysInfos.dwPageSize;
/* Maintenant on va calculer le nombre de pages qu'il faut allouer.
L'API de VirtualAlloc est un peu étrange et ne demande pas un nombre de pages mais une taille mémoire, donc on calcule cette taille, on ajoute la taille d'une page entiere pour notre page protégé et on alloue la mémoire. */
const uintptr lastPageUsedBytes = allocSize % pageSize;
const uintptr requiredRWPages = ( allocSize / pageSize ) + ( lastPageUsedBytes == 0 ? 0 : 1 ); // On en ajoute une si il y a un reste.
const uintptr totalSize = pageSize + ( requiredRWPages * pageSize );
const void * memPtr = VirtualAlloc( nullptr, totalSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE );
if ( !lverify( memPtr != nullptr, "VirtualAlloc failed while trying to alloc %u bytes: %s", totalSize, FormatWin32Error( GetLastError() ) ) ) {
return memPtr;
}
#else
/* Dans un scénario classique on alloue juste sur la pile du process (ça pourrais être n'importe quelle autre fonction malloc). */
const void * memPtr = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, allocSize );
if ( !lverify( memPtr != nullptr, "HeapAlloc failed while trying to alloc %llu bytes", totalSize ) ) {
return memPtr;
}
#endif // DETECT_OVERFLOWS
/* Maintenant on va prendre l'addresse aligné après le AllocHeader. memStartAddr est l'adresse sans alignement. alignedAddr une fois alignée. */
const uintptr memStartAddr = reinterpret_cast< uintptr >( memPtr ) + sizeof( AllocHeader );
uintptr alignedAddr = Align< uintptr >( ( static_cast< uintptr >( memStartAddr ) ) + _alignment - 1 ) & ( ~( _alignment - 1 ) );
#ifdef DETECT_OVERFLOWS
/* Maintenant qu'on a nos pages d'alloués il nous faut raprocher le plus possible l'allocation de la derniere page (celle qui sera protégée). Cependant il faut également respecter l'alignement */
// la page protégée commence à cette adresse.
const uintptr protectedAddr = reinterpret_cast< uintptr >( memPtr ) + ( requiredRWPages * pageSize );
// on regarde combien n % octets d'_alignment on gache actuellement.
const uintptr bytesBeforeProtectedAddr = protectedAddr - ( alignedAddr + _size );
// Si on a x * [ octets d'_alignment ] avant l'adresse protégée de dispo.
if ( ( bytesBeforeProtectedAddr / _alignment ) > 0 ) {
// on bouge à l'adresse de x * _alignment.
alignedAddr += _alignment * ( bytesBeforeProtectedAddr / _alignment );
}
// On check qu'on a pas fait d'erreur de calcul
lassert( protectedAddr >= alignedAddr + _size, "Protected address is before the end of our allocation" );
/* Normalement au pire l'utilisateur peut faire un overflow de _alignement - 1 sans qu'il soit detecté. */
lassert( protectedAddr - ( alignedAddr + _size ) < _alignment, "Non optimal protected page placement %u bytes untested", protectedAddr - ( alignedAddr + _size ) );
DWORD oldProtect = 0;
// Pour proteger la derniere page il faut donner à VirtualProtect son adresse.
if ( !lverify( VirtualProtect( reinterpret_cast< void * >( protectedAddr ), pageSize, PAGE_NOACCESS, &oldProtect ) != 0,
"Failed to protect last page while allocating %u bytes aligned on %u: %s", _size, _alignment, FormatWin32Error( GetLastError() ) ) )
{
VirtualFree( memPtr, 0, MEM_RELEASE );
return nullptr;
}
#else
lassert( alignedAddr + _size <= reinterpret_cast< uintptr >( memPtr ) + allocSize, "Start address overflow alloc size" );
#endif // DETECT_OVERFLOWS
/* Maintenant renvoyons notre adresse au user. On va stocker l'addresse que l'on devra donner à VirtualFree plus tard dans le header avant l'adresse pour le user pour cela on recule de sizeof( AllocHeader ) octets. */
void * alignedPtr = reinterpret_cast< void * >( alignedAddr );
uint8 * headerAddr = reinterpret_cast< uint8 * >( alignedPtr ) - sizeof( AllocHeader );
AllocHeader * header = reinterpret_cast< AllocHeader * >( headerAddr );
lassert( reinterpret_cast< uintptr >( header ) >= reinterpret_cast< uintptr >( memPtr ), "header address underflow the allocated ptr" );
header->baseAddr = memPtr;
return alignedPtr;
}
//===================================
//===================================
void MemFreeAligned( void * _ptr ) {
if ( _ptr != nullptr ) {
/*La fonction free est très simple il suffit de récuperer l'adresse de base dans le header*/
uint8 * headerAddr = reinterpret_cast< uint8 * >( _ptr ) - sizeof( AllocHeader );
AllocHeader * header = reinterpret_cast< AllocHeader * >( headerAddr );
#ifdef DETECT_OVERFLOWS
VirtualFree( header->baseAddr, 0, MEM_RELEASE );
#else
HeapFree( GetProcessHeap(), 0, header->baseAddr );
#endif // DETECT_OVERFLOWS
}
}
Et les leaks ?
Pour les leaks ce qui m'interessais c'est de remonter à une ligne interessante dans la callstack et d'enregistrer la taille alloué par chaque callstack dans une table.
À chaque alloc on regardera si la callstack à déjà alloué et on ajoutera la mémoire alloué dans l'entrée on sinon on crée simplement une nouvelle entrée. Lorsqu'un codepath alloue beaucoup de mémoire sans jamais faire un free on pourras le visualiser en affichant la table sous la forme de camembert grace implot et ainsi trouver la callstack responsable.
Pour chaque alloc il peut être interessant de calculer plusieurs taille alloué comme on a vu il ya la taille demandé par l'utilisateur, la taille nécéssaire pour l'alignement, et les tailles réellement allouées. Cela permettra d'avoir une idée de la mémoire gaspillé par le header ou l'alignement ou la detection des overflows.
#ifdef TRACK_MEMORY_ALLOC_CALLSTACKS
//===================================
//===================================
void InterlockedAddUintPtr( volatile uintptr & _res, uintptr _toAdd )
{
InterlockedAdd( reinterpret_cast< volatile LONG * >( &_res ), _toAdd );
}
//===================================
//===================================
void InterlockedSubUintPtr( volatile uintptr & _res, uintptr _toSub )
{
lassert( _toSub < INT32_MAX, "_toSub %u overflow (max %d)", _toSub, INT32_MAX );
InterlockedAdd( reinterpret_cast< volatile LONG * >( &_res ), -static_cast< LONG >( _toSub ) );
}
//===================================
//===================================
struct AllocInfos
{
uintptr requestedSize = 0; // Taille que l'utilisateur à demandé pour l'allocation
uintptr alignedSize = 0; // Taille nécéssaire à alouer pour satisfaire l'alignement
uintptr allocSize = 0; // Taille alloué sur le system (alignement + le header)
#ifdef DETECT_OVERFLOWS
uintptr virtualAllocRWPages = 0; // Nombre de pages mémoire nécéssaires pour réaliser l'allocation
uintptr virtualAllocSize = 0; // Taille totale de toutes les pages allouées pour l'allocation + page de protection
#endif // DETECT_OVERFLOWS
// Méthode pour pouvoir additionner / soustraire ces infos de mémoire en multithread
// qui seront utile pour additionner 2 AllocInfos infos qui proviennent de la même callstack
void ThreadSafeAdd( const AllocInfos & _infos ) {
InterlockedAddUintPtr( requestedSize, _infos.requestedSize );
InterlockedAddUintPtr( alignedSize, _infos.alignedSize );
InterlockedAddUintPtr( allocSize, _infos.allocSize );
#ifdef DETECT_OVERFLOWS
InterlockedAddUintPtr( virtualAllocRWPages, _infos.virtualAllocRWPages );
InterlockedAddUintPtr( virtualAllocSize, _infos.virtualAllocSize );
#endif // DETECT_OVERFLOWS
}
void ThreadSafeSub( const AllocInfos & _infos ) {
InterlockedSubUintPtr( requestedSize, _infos.requestedSize );
InterlockedSubUintPtr( alignedSize, _infos.alignedSize );
InterlockedSubUintPtr( allocSize, _infos.allocSize );
#ifdef DETECT_OVERFLOWS
InterlockedSubUintPtr( virtualAllocRWPages, _infos.virtualAllocRWPages );
InterlockedSubUintPtr( virtualAllocSize, _infos.virtualAllocSize );
#endif // DETECT_OVERFLOWS
}
};
#endif // TRACK_MEMORY_ALLOC_CALLSTACKS
Maintenant que l'on a notre structure on peut déclarer la table, on a besoin de retenir son nom donc il nous faut plus d'info que dans le header on crée donc une struct dédiée AllocOrigin.
//===================================
//===================================
#ifdef TRACK_MEMORY_ALLOC_CALLSTACKS
struct AllocOrigin
{
AllocInfos infos; // La somme des alloc pour cette callstack
static constexpr const uint32 MAX_SYM_NAME_LEN = 2001; // = MAX_SYM_NAME ( de la winapi ) + 1 for '\0'
char callstackLine[ MAX_SYM_NAME_LEN ]; // on stockera le nom de la ligne interessante dans la callsatck ici
uint32 callstackLineLen = 0; // 0 == non init
uint32 callstackLineHash = 0; // 0 == non init
bool shouldDebugBreak = false; // servira pour faire un bouton debug break dans imgui
static constexpr const uint32 MAX_CALLSTACK_TRACKED = 2000; // à sizer en fonction du projet, il faut pouvoir faire rentrer toutes les callstack possibles qui font des allocs
};
extern bool g_memAllocCallstackTrackingInitialized;
extern AllocOrigin g_memAllocCallstacks[ AllocOrigin::MAX_CALLSTACK_TRACKED ];
void InitializeMemAllocCallstackTracking();
void DeinitializeMemAllocCallstackTracking();
#endif // TRACK_MEMORY_ALLOC_CALLSTACKS
Récuperer la callstack
Visual studio exporte les données de debuggage dans des fichiers PDB, Microsoft fournie une API DbgHelp pour parser ces fichiers pour nous et extraire les nom de fonction des adresses de callstack. Cette section n'est pas forcement bien documenté et il m'a fallu tatonner sur des forums pour faire fonctionner le code donc j'espere qu'il pourra vous être utile.
#include
/* On doit avoir initialisé l'API avant de l'utiliser, comme les données statiques ou globales peuvent être construite avant le main() et qu'elles appelle potentiellement MemAlloc il est important de savoir si la librairie est déjà initialiser pour pouvoir enregistrer nos callstacks. */
bool g_memAllocCallstackTrackingInitialized = false;
AllocOrigin g_memAllocCallstacks[ AllocOrigin::MAX_CALLSTACK_TRACKED ];
//===================================
//===================================
BOOL CALLBACK EnumerateModules64Callback( const char * _moduleName, DWORD64 _baseOfDll, PVOID )
{
Printf( " - %s at address 0x%x\n", _moduleName, _baseOfDll );
return TRUE;
}
void InitializeMemAllocCallstackTracking() {
if ( SymInitialize( GetCurrentProcess(), NULL, TRUE ) ) {
Printf( "Initialized pdb symbols:\n" );
/* On log les modules chargés, cela peut-être utile pour savoir si on appelle pas SymInitialize trop tot, en effet une fois appellé si une DLL est chargée par la suite il faut rappeller à nouveau SymCleanup puis SymInitialize pour beneficier des symboles. */
SymEnumerateModules64( GetCurrentProcess(), &EnumerateModules64Callback, nullptr );
g_memAllocCallstackTrackingInitialized = true;
} else {
/* Peut arriver si on a pas le PDB de l'exe par exemple */
Errorf( "Could not initialize pdb symbols for callstack tracking, SymInitialize failed: %s", FormatWin32Error( GetLastError() ) );
}
}
//===================================
// Code de deinitialisation classique
//===================================
void DeinitializeMemAllocCallstackTracking() {
if ( g_memAllocCallstackTrackingInitialized ) {
if ( SymCleanup( GetCurrentProcess() ) ) {
Printf( "Deinitialized pdb symbols" );
/* On pourrais ici parcourir la table pour verifier qu'on pas des leaks et ainsi produire le même type de log que _malloc_dbg */
} else {
Errorf( "Could not deinitialize pdb symbols for callstack tracking, SymCleanup failed: %s", FormatWin32Error( GetLastError() ) );
}
}
}
//===================================
//===================================
static thread_local char s_lastSymbolName[ AllocOrigin::MAX_SYM_NAME_LEN ];
static Mutex s_stackWalkLock; // utilisez votre classe de mutex
/* Cette fonction essaye de chercher la premiere ligne interessant dans la callstack, dans mon cas je ne sauvegarde pas toute la callstack car ça serait pas facilement visualisable dans implot, il est donc possible que 2 callstack différentes arrive sur la même entrée dans la table, mais ce qui m'interesse c'est surtout quelle est la partie haut niveau au code qui pose problème.*/
const char * MemAllocGetValuableCallstackLine() {
/* On check qu'on a bien les PDB */
if ( g_memAllocCallstackTrackingInitialized ) {
CONTEXT context;
/* J'ai pas mal galeré avant de trouver cette ligne de code elle fait le café à partir de windows xp donc on est large. Elle nous permet de récuperer les valeurs des registres actuels, on pourrais aussi le faire en assembleur mais la flemme la 🥱... */
RtlCaptureContext( &context );
STACKFRAME64 stackFrame;
MemSet( &stackFrame, 0, sizeof( STACKFRAME64 ) );
/* Initialisation de stackFrame avec les bon registres selon la platform, merci les forums, je ne me rappelle plus de la source mais si vous vous reconnaissez je met un lien avec plaisir. */
#ifdef _M_IX86
DWORD imagetype = IMAGE_FILE_MACHINE_I386;
stackFrame.AddrPC.Offset = context.Eip;
stackFrame.AddrPC.Mode = AddrModeFlat;
stackFrame.AddrFrame.Offset = context.Ebp;
stackFrame.AddrFrame.Mode = AddrModeFlat;
stackFrame.AddrStack.Offset = context.Esp;
stackFrame.AddrStack.Mode = AddrModeFlat;
#elif _M_X64
imagetype = IMAGE_FILE_MACHINE_AMD64;
stackFrame.AddrPC.Offset = context.Rip;
stackFrame.AddrPC.Mode = AddrModeFlat;
stackFrame.AddrFrame.Offset = context.Rsp;
stackFrame.AddrFrame.Mode = AddrModeFlat;
stackFrame.AddrStack.Offset = context.Rsp;
stackFrame.AddrStack.Mode = AddrModeFlat;
#elif _M_IA64
imagetype = IMAGE_FILE_MACHINE_IA64;
stackFrame.AddrPC.Offset = context.StIIP;
stackFrame.AddrPC.Mode = AddrModeFlat;
stackFrame.AddrFrame.Offset = context.IntSp;
stackFrame.AddrFrame.Mode = AddrModeFlat;
stackFrame.AddrBStore.Offset = context.RsBSP;
stackFrame.AddrBStore.Mode = AddrModeFlat;
stackFrame.AddrStack.Offset = context.IntSp;
stackFrame.AddrStack.Mode = AddrModeFlat;
#else
#error Platform not supported!
#endif
/* Récupérer les informations de la fonction (nom etc) */
SYMBOL_INFO_PACKAGE symbol;
MemSet( &symbol, 0, sizeof( SYMBOL_INFO_PACKAGE ) );
/* SymFromAddr n'est pas thread safe, je n'ai pas testé pour StackWalk64 */
ScopedLock lock( s_stackWalkLock );
while ( StackWalk64( imagetype, GetCurrentProcess(), GetCurrentThread(), &stackFrame, &context, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL ) ) {
DWORD64 address = stackFrame.AddrPC.Offset;
symbol.si.SizeOfStruct = sizeof( SYMBOL_INFO );
symbol.si.MaxNameLen = MAX_SYM_NAME;
BOOL success = SymFromAddr( GetCurrentProcess(), address, 0, &symbol.si );
if ( success ) {
const uint32 len = StrLen( symbol.si.Name );
/* Dans mon cas la plupart de mes allocs ont lieu dans les List qui n'appellent pas les destructeur des élements (source des leaks), mon but est de trouver laquelle grossie sans faire son cleanup, je skip donc toutes les lignes contenant un symbole lié (ainsi que toutes les couches des fonction MemAlloc 🙃 qui ne nous intèresse pas. */
if ( StrFind( symbol.si.Name, len, TOSTRING( Str:: ) ) >= 0 ||
StrFind( symbol.si.Name, len, TOSTRING( List:: ) ) >= 0 ||
StrFind( symbol.si.Name, len, "MemAlloc" ) >= 0 ) {
} else {
/* On a trouvé la ligne interessante dans la callstack, on pourrais aussi sauvegarder la ligne dans le fichier, je n'y ai pas pensé mais je le ferais après avoir fini cet article. */
StrCopy( s_lastSymbolName, symbol.si.Name );
return s_lastSymbolName;
}
} else {
uint32 errCode = GetLastError();
if ( lverify( errCode == ERROR_MOD_NOT_FOUND, "SymFromAddr failed %s", FormatWin32Error( errCode ) ) ) {
/* DLL pour laquelle on a pas les PDB on skip simplement la ligne on peut ne pas en tenir compte et passer à la ligne suivante. */
} else {
return nullptr;
}
}
}
}
} // g_memAllocCallstackTrackingInitialized
return nullptr;
}
On a récupéré le symbole, il nous reste plus qu'à ajouter la taille alouée lors d'un MemAlloc dans l'entrée dans la table, pour celà rien de plus simple à la fin de la fonction on ajoute juste:
#ifdef TRACK_MEMORY_ALLOC_CALLSTACKS
header->infos.requestedSize = _size;
header->infos.alignedSize = alignedSize;
header->infos.allocSize = allocSize;
#ifdef DETECT_OVERFLOWS
header->infos.virtualAllocRWPages = requiredRWPages;
header->infos.virtualAllocSize = totalSize;
#endif // DETECT_OVERFLOWS
const char * callstack = MemAllocGetValuableCallstackLine();
if ( callstack != nullptr ) {
/* On stocke l'index dans la table pour facilement la retrouver lors du free et ainsi soustraire les tailles allouées. */
header->callStackTrackIndex = MemAllocAddOrCreateInfoForCallstack( callstack, header->infos );
} else {
header->callStackTrackIndex = -1;
}
#endif // TRACK_MEMORY_ALLOC_CALLSTACKS
Et au début de la fonction MemFree.
#ifdef TRACK_MEMORY_ALLOC_CALLSTACKS
if ( header->callStackTrackIndex < AllocOrigin::MAX_CALLSTACK_TRACKED ) {
g_memAllocCallstacks[ header->callStackTrackIndex].infos.ThreadSafeSub( header->infos );
}
#endif // TRACK_MEMORY_ALLOC_CALLSTACKS
Voici un exemple de fonction MemAllocAddOrCreateInfoForCallstack.
static Mutex s_allocInfoForCallstackLock;
uint16 MemAllocAddOrCreateInfoForCallstack( const char * _symbolName, const AllocInfos & _infos ) {
/* J'utilise un hash pour faire une premiere comparaison rapide et un compare de la string ensuite. */
uint32 hash = HashStr( _symbolName );
uint16 index = -1;
/* On peut lire regarder si l'entrée existe déjà sans locker le mutex comme on utilise des interlock pour additionner / soustraire les tailles. */
for ( uint16 i = 0; i < AllocOrigin::MAX_CALLSTACK_TRACKED; ++i ) {
if ( g_memAllocCallstacks[ i ].callstackLineHash == 0 ) {
break;
}
if ( hash == g_memAllocCallstacks[ i ].callstackLineHash && StrCompare( _symbolName, g_memAllocCallstacks[ i ].callstackLine ) == 0 ) {
index = i;
break;
}
}
/* Si elle n'existe pas alors on lock et on crée l'entrée. */
if ( index != -1 ) {
ScopedLock lock( s_allocInfoForCallstackLock );
/* On re-parcour la table une seconde fois au cas ou un thread avec la même callstack aurait lui aussi eu envie de crée l'entrée. */
for ( uint16 i = 0; i < AllocOrigin::MAX_CALLSTACK_TRACKED; ++i ) {
if ( g_memAllocCallstacks[ i ].callstackLineHash == 0 ) {
/* Personne d'autre n'a crée l'entrée en parallèle donc on l'a crée réellement cette fois. */
StrCopy( g_memAllocCallstacks[ i ].callstackLine, _symbolName );
g_memAllocCallstacks[ i ].callstackLineLen = StrLen( g_memAllocCallstacks[ i ].callstackLine );
g_memAllocCallstacks[ i ].callstackLineHash = hash;
index = i;
break;
}
if ( hash == g_memAllocCallstacks[ i ].callstackLineHash && StrCompare( _symbolName, g_memAllocCallstacks[ i ].callstackLine ) == 0 ) {
/* Un autre thread a créé l'entrée entre temps donc on l'utilise */
index = i;
break;
}
}
}
/* On vérifie qu'on a bien une table assez grande et on ajoute l'allocation courante. */
if ( lverify( index != -1, "Increase AllocOrigin::MAX_CALLSTACK_TRACKED ?" ) ) {
g_memAllocCallstacks[ index ].infos.ThreadSafeAdd( _infos );
}
return index;
}
Et voilà on a tout ce vous avez tout ce qu'il vous faut pour tracker vos allocations, faire un profiler mémoire, detecter les leaks ou visualiser tout ça dans ImPlot ! Je vous laisse avec quelques screens tiré du demotool 🙂.

