1
0
forked from 0ad/0ad

Removes custom debug heap analysis on Windows.

Differential Revision: https://code.wildfiregames.com/D5045
This was SVN commit r27710.
This commit is contained in:
Vladislav Belov 2023-06-16 16:34:10 +00:00
parent 85bb745295
commit 7782aa95f1
3 changed files with 1 additions and 1041 deletions

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2021 Wildfire Games.
/* Copyright (C) 2023 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -30,10 +30,6 @@
#include "lib/sysdep/sysdep.h"
#include "lib/sysdep/vm.h"
#if OS_WIN
# include "lib/sysdep/os/win/wdbg_heap.h"
#endif
#include <cstdarg>
#include <cstring>
#include <cstdio>
@ -404,12 +400,6 @@ static ErrorReaction PerformErrorReaction(ErrorReactionInternal er, size_t flags
isExiting = 1; // see declaration
COMPILER_FENCE;
#if OS_WIN
// prevent (slow) heap reporting since we're exiting abnormally and
// thus probably leaking like a sieve.
wdbg_heap_Enable(false);
#endif
exit(EXIT_FAILURE);
case ERI_NOT_IMPLEMENTED:

View File

@ -1,971 +0,0 @@
/* Copyright (C) 2010 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "precompiled.h"
#include "lib/sysdep/os/win/wdbg_heap.h"
#include "lib/sysdep/os/win/win.h"
#include <crtdbg.h>
#include <excpt.h>
#include "lib/external_libraries/dbghelp.h"
#include "lib/sysdep/cpu.h" // cpu_AtomicAdd
#include "lib/sysdep/os/win/winit.h"
#include "lib/sysdep/os/win/wdbg.h" // wdbg_printf
#include "lib/sysdep/os/win/wdbg_sym.h" // wdbg_sym_WalkStack
WINIT_REGISTER_EARLY_INIT2(wdbg_heap_Init); // wutil -> wdbg_heap
WINIT_REGISTER_LATE_SHUTDOWN2(wdbg_heap_Shutdown); // last - no leaks are detected after this
void wdbg_heap_Enable(bool enable)
{
#ifdef _DEBUG // (avoid "expression has no effect" warning in release builds)
int flags = 0;
if(enable)
{
flags |= _CRTDBG_ALLOC_MEM_DF; // enable checks at deallocation time
flags |= _CRTDBG_LEAK_CHECK_DF; // report leaks at exit
#if 0
flags |= _CRTDBG_CHECK_ALWAYS_DF; // check during every heap operation (too slow to be practical)
flags |= _CRTDBG_DELAY_FREE_MEM_DF; // memory is never actually freed
#endif
}
_CrtSetDbgFlag(flags);
// Send output to stdout as well as the debug window, so it works during
// the normal build process as well as when debugging the test .exe
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG);
_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDOUT);
#else
UNUSED2(enable);
#endif
}
void wdbg_heap_Validate()
{
int ok = TRUE;
__try
{
// NB: this is a no-op if !_CRTDBG_ALLOC_MEM_DF.
// we could call _heapchk but that would catch fewer errors.
ok = _CrtCheckMemory();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
ok = FALSE;
}
wdbg_assert(ok == TRUE); // else: heap is corrupt!
}
//-----------------------------------------------------------------------------
// improved leak detection
//-----------------------------------------------------------------------------
// (this relies on the debug CRT; not compiling it at all in release builds
// avoids unreferenced local function warnings)
// (this has only been tested on IA32 and seems to have trouble with larger
// pointers and is horribly expensive, so it's disabled for now.)
#if !defined(NDEBUG) && ARCH_IA32 && 0
# define ENABLE_LEAK_INSTRUMENTATION 1
#else
# define ENABLE_LEAK_INSTRUMENTATION 0
#endif
#if ENABLE_LEAK_INSTRUMENTATION
// leak detectors often rely on macro redirection to determine the file and
// line of allocation owners (see _CRTDBG_MAP_ALLOC). unfortunately this
// breaks code that uses placement new or functions called free() etc.
//
// we avoid this problem by using stack traces. this implementation differs
// from other approaches, e.g. Visual Leak Detector (the safer variant
// before DLL hooking was used) in that no auxiliary storage is needed.
// instead, the trace is stashed within the memory block header.
//
// to avoid duplication of effort, the CRT's leak detection code is not
// modified; we only need an allocation and report hook. the latter
// mixes the improved file/line information into the normal report.
//-----------------------------------------------------------------------------
// memory block header
// the one disadvantage of our approach is that it requires knowledge of
// the internal memory block header structure. it is hoped that IsValid will
// uncover any changes. the following definition was adapted from dbgint.h:
struct _CrtMemBlockHeader
{
struct _CrtMemBlockHeader* next;
struct _CrtMemBlockHeader* prev;
char* file;
int line;
// fields reversed on Win64 to ensure size % 16 == 0
#if OS_WIN64
int blockType;
size_t userDataSize;
#else
size_t userDataSize;
int blockType;
#endif
long allocationNumber;
u8 gap[4];
bool IsValid() const
{
__try
{
if(prev && prev->next != this)
return false;
if(next && next->prev != this)
return false;
if((unsigned)blockType > 4)
return false;
if(userDataSize > 1*GiB)
return false;
if(allocationNumber == 0)
return false;
for(int i = 0; i < 4; i++)
{
if(gap[i] != 0xFD)
return false;
}
// this is a false alarm if there is exactly one extant allocation,
// but also a valuable indication of a block that has been removed
// from the list (i.e. freed).
if(prev == next)
return false;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return false;
}
return true;
}
};
static _CrtMemBlockHeader* HeaderFromData(void* userData)
{
_CrtMemBlockHeader* const header = ((_CrtMemBlockHeader*)userData)-1;
wdbg_assert(header->IsValid());
return header;
}
/**
* update our idea of the head of the linked list of heap blocks.
* called from the allocation hook (see explanation there)
*
* @return the current head (most recent allocation).
* @param operation the current heap operation
* @param userData allocation address (if reallocating or deallocating)
* @param hasChanged a convenient indication of whether the return value is
* different than that of the last call.
**/
static _CrtMemBlockHeader* GetHeapListHead(int operation, void* userData, bool& hasChanged)
{
static _CrtMemBlockHeader* s_heapListHead;
// first call: get the heap block list head
// notes:
// - there is no O(1) accessor for this, so we maintain a copy.
// - must be done here instead of in an initializer to guarantee
// consistency, since we are now under the _HEAP_LOCK.
if(!s_heapListHead)
{
_CrtMemState state = {0};
_CrtMemCheckpoint(&state); // O(N)
s_heapListHead = state.pBlockHeader;
wdbg_assert(s_heapListHead->IsValid());
}
// the last operation was an allocation or expanding reallocation;
// exactly one block has been prepended to the list.
if(s_heapListHead->prev)
{
s_heapListHead = s_heapListHead->prev; // set to new head of list
wdbg_assert(s_heapListHead->IsValid());
wdbg_assert(s_heapListHead->prev == 0);
hasChanged = true;
}
// the list head remained unchanged, so the last operation was a
// non-expanding reallocation or free.
else
hasChanged = false;
// special case: handle invalidation of the list head
// note: even shrinking reallocations cause deallocation.
if(operation != _HOOK_ALLOC && userData == s_heapListHead+1)
{
s_heapListHead = s_heapListHead->next;
wdbg_assert(s_heapListHead->IsValid());
hasChanged = false; // (head is now the same as last time)
}
return s_heapListHead;
}
//-----------------------------------------------------------------------------
// call stack filter
// we need to make the most out of the limited amount of frames. to that end,
// only user functions are stored; we skip known library and helper functions.
// these are determined by recording frames encountered in a backtrace.
/**
* extents of a module in memory; used to ignore callers that lie within
* the C runtime library.
**/
class ModuleExtents
{
public:
ModuleExtents()
: m_address(0), m_length(0)
{
}
ModuleExtents(const wchar_t* dllName)
{
HMODULE hModule = GetModuleHandleW(dllName);
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((u8*)hModule + ((PIMAGE_DOS_HEADER)hModule)->e_lfanew);
m_address = (uintptr_t)hModule + ntHeaders->OptionalHeader.BaseOfCode;
MEMORY_BASIC_INFORMATION mbi = {0};
VirtualQuery((void*)m_address, &mbi, sizeof(mbi));
m_length = mbi.RegionSize;
}
uintptr_t Address() const
{
return m_address;
}
uintptr_t Length() const
{
return m_length;
}
bool Contains(uintptr_t address) const
{
return (address - m_address) < m_length;
}
private:
uintptr_t m_address;
size_t m_length;
};
/**
* set data structure that avoids dynamic allocations because they would
* cause the allocation hook to be reentered (bad).
**/
template<typename T, size_t maxItems>
class ArraySet
{
public:
ArraySet()
{
m_arrayEnd = m_array;
}
void Add(T t)
{
if(m_arrayEnd == m_array+maxItems)
{
RemoveDuplicates();
wdbg_assert(m_arrayEnd < m_array+maxItems);
}
*m_arrayEnd++ = t;
}
bool Find(T t) const
{
return std::find(m_array, const_cast<const T*>(m_arrayEnd), t) != m_arrayEnd;
}
void RemoveDuplicates()
{
std::sort(m_array, m_arrayEnd);
m_arrayEnd = std::unique(m_array, m_arrayEnd);
}
private:
T m_array[maxItems];
T* m_arrayEnd;
};
class CallerFilter
{
public:
CallerFilter()
{
AddRuntimeLibraryToIgnoreList();
m_isRecordingKnownCallers = true;
CallHeapFunctions();
m_isRecordingKnownCallers = false;
m_knownCallers.RemoveDuplicates();
}
Status NotifyOfCaller(uintptr_t pc)
{
if(!m_isRecordingKnownCallers)
return INFO::SKIPPED;
// last 'known' function has been reached
if(pc == (uintptr_t)&CallerFilter::CallHeapFunctions)
return INFO::ALL_COMPLETE;
// pc is a 'known' function on the allocation hook's back-trace
// (e.g. _malloc_dbg and other helper functions)
m_knownCallers.Add(pc);
return INFO::OK;
}
bool IsKnownCaller(uintptr_t pc) const
{
for(size_t i = 0; i < numModules; i++)
{
if(m_moduleIgnoreList[i].Contains(pc))
return true;
}
return m_knownCallers.Find(pc);
}
private:
static const size_t numModules = 2;
void AddRuntimeLibraryToIgnoreList()
{
#if MSC_VERSION && _DLL // DLL runtime library
#ifdef NDEBUG
static const wchar_t* dllNameFormat = L"msvc%c%d" L".dll";
#else
static const wchar_t* dllNameFormat = L"msvc%c%d" L"d" L".dll";
#endif
const int dllVersion = (MSC_VERSION-600)/10; // VC2005: 1400 => 80
wdbg_assert(0 < dllVersion && dllVersion <= 999);
for(int i = 0; i < numModules; i++)
{
static const char modules[numModules] = { 'r', 'p' }; // C and C++ runtime libraries
wchar_t dllName[20];
swprintf_s(dllName, ARRAY_SIZE(dllName), dllNameFormat, modules[i], dllVersion);
m_moduleIgnoreList[i] = ModuleExtents(dllName);
}
#endif
}
static void CallHeapFunctions()
{
{
void* p1 = malloc(1);
void* p2 = realloc(p1, 111);
if(p2)
free(p2);
else
free(p1);
}
{
u8* p = new u8;
delete p;
}
{
u8* p = new u8[2];
delete[] p;
}
}
ModuleExtents m_moduleIgnoreList[numModules];
// note: this mechanism cannot hope to exclude every single STL helper
// function, which is why we need the module ignore list.
// however, it is still useful when compiling against the static CRT.
bool m_isRecordingKnownCallers;
ArraySet<uintptr_t, 500> m_knownCallers;
};
//-----------------------------------------------------------------------------
// stash (part of) a stack trace within _CrtMemBlockHeader
// this avoids the need for a mapping between allocation number and the
// caller information, which is slow, requires locking and consumes memory.
//
// callers := array of addresses inside functions that constitute the
// stack back-trace.
static const size_t numQuantizedPcBits = sizeof(uintptr_t)*CHAR_BIT - 2;
static uintptr_t Quantize(uintptr_t pc)
{
// postcondition: the return value lies within the same function as
// pc but can be stored in fewer bits. this is possible because:
// - linkers typically align functions to at least four bytes
// - pc is a return address and thus preceded by a call instruction and
// function prolog, which requires at least four bytes.
return pc/4;
}
static uintptr_t Expand(uintptr_t pc)
{
return pc*4;
}
static const size_t numEncodedLengthBits = 2;
static const size_t maxCallers = (sizeof(char*)+sizeof(int))*CHAR_BIT / (2+14);
static size_t NumBitsForEncodedLength(size_t encodedLength)
{
static const size_t numBitsForEncodedLength[1u << numEncodedLengthBits] =
{
8, // 1K
14, // 64K
20, // 4M
numQuantizedPcBits // a full pointer
};
return numBitsForEncodedLength[encodedLength];
}
static size_t EncodedLength(uintptr_t quantizedOffset)
{
for(size_t encodedLength = 0; encodedLength < 1u << numEncodedLengthBits; encodedLength++)
{
const size_t numBits = NumBitsForEncodedLength(encodedLength);
const uintptr_t maxValue = (1u << numBits)-1;
if(quantizedOffset <= maxValue)
return encodedLength;
}
wdbg_assert(0); // unreachable
return 0;
}
static uintptr_t codeSegmentAddress;
static uintptr_t quantizedCodeSegmentAddress;
static uintptr_t quantizedCodeSegmentLength;
static void FindCodeSegment()
{
const wchar_t* dllName = 0; // current module
ModuleExtents extents(dllName);
codeSegmentAddress = extents.Address();
quantizedCodeSegmentAddress = Quantize(codeSegmentAddress);
quantizedCodeSegmentLength = Quantize(extents.Length());
}
class BitStream
{
public:
BitStream(u8* storage, size_t storageSize)
: m_remainderBits(0), m_numRemainderBits(0)
, m_pos(storage), m_bitsLeft((size_t)storageSize*8)
{
}
size_t BitsLeft() const
{
return m_bitsLeft;
}
void Write(const size_t numOutputBits, uintptr_t outputValue)
{
wdbg_assert(numOutputBits <= m_bitsLeft);
wdbg_assert(outputValue < ((uintptr_t)1u << numOutputBits));
size_t outputBitsLeft = numOutputBits;
while(outputBitsLeft > 0)
{
const size_t numBits = std::min(outputBitsLeft, size_t(8));
m_bitsLeft -= numBits;
// (NB: there is no need to extract exactly numBits because
// outputValue's MSBs were verified to be zero)
const uintptr_t outputByte = outputValue & 0xFF;
outputValue >>= 8;
outputBitsLeft -= numBits;
m_remainderBits |= outputByte << m_numRemainderBits;
m_numRemainderBits += numBits;
if(m_numRemainderBits >= 8)
{
const u8 remainderByte = (m_remainderBits & 0xFF);
m_remainderBits >>= 8;
m_numRemainderBits -= 8;
*m_pos++ = remainderByte;
}
}
}
void Finish()
{
const size_t partialBits = m_numRemainderBits % 8;
if(partialBits)
{
m_bitsLeft -= 8-partialBits;
m_numRemainderBits += 8-partialBits;
}
while(m_numRemainderBits)
{
const u8 remainderByte = (m_remainderBits & 0xFF);
*m_pos++ = remainderByte;
m_remainderBits >>= 8;
m_numRemainderBits -= 8;
}
wdbg_assert(m_bitsLeft % 8 == 0);
while(m_bitsLeft)
{
*m_pos++ = 0;
m_bitsLeft -= 8;
}
}
uintptr_t Read(const size_t numInputBits)
{
wdbg_assert(numInputBits <= m_bitsLeft);
uintptr_t inputValue = 0;
size_t inputBitsLeft = numInputBits;
while(inputBitsLeft > 0)
{
const size_t numBits = std::min(inputBitsLeft, size_t(8));
m_bitsLeft -= numBits;
if(m_numRemainderBits < numBits)
{
const size_t inputByte = *m_pos++;
m_remainderBits |= inputByte << m_numRemainderBits;
m_numRemainderBits += 8;
}
const uintptr_t remainderByte = (m_remainderBits & ((1u << numBits)-1));
m_remainderBits >>= numBits;
m_numRemainderBits -= numBits;
inputValue |= remainderByte << (numInputBits-inputBitsLeft);
inputBitsLeft -= numBits;
}
return inputValue;
}
private:
uintptr_t m_remainderBits;
size_t m_numRemainderBits;
u8* m_pos;
size_t m_bitsLeft;
};
static void StashCallers(_CrtMemBlockHeader* header, const uintptr_t* callers, size_t numCallers)
{
// transform an array of callers into a (sorted and unique) set.
uintptr_t quantizedPcSet[maxCallers];
std::transform(callers, callers+numCallers, quantizedPcSet, Quantize);
std::sort(quantizedPcSet, quantizedPcSet+numCallers);
uintptr_t* const end = std::unique(quantizedPcSet, quantizedPcSet+numCallers);
const size_t quantizedPcSetSize = end-quantizedPcSet;
// transform the set into a sequence of quantized offsets.
uintptr_t quantizedOffsets[maxCallers];
if(quantizedPcSet[0] >= quantizedCodeSegmentAddress)
quantizedOffsets[0] = quantizedPcSet[0] - quantizedCodeSegmentAddress;
else
{
quantizedOffsets[0] = quantizedPcSet[0];
// make sure RetrieveCallers can differentiate between pointers and code-segment-offsets
wdbg_assert(quantizedOffsets[0] >= quantizedCodeSegmentLength);
}
for(size_t i = 1; i < numCallers; i++)
quantizedOffsets[i] = quantizedPcSet[i] - quantizedPcSet[i-1];
// write quantized offsets to stream
BitStream bitStream((u8*)&header->file, sizeof(header->file)+sizeof(header->line));
for(size_t i = 0; i < quantizedPcSetSize; i++)
{
const uintptr_t quantizedOffset = quantizedOffsets[i];
const size_t encodedLength = EncodedLength(quantizedOffset);
const size_t numBits = NumBitsForEncodedLength(encodedLength);
if(bitStream.BitsLeft() < numEncodedLengthBits+numBits)
break;
bitStream.Write(numEncodedLengthBits, encodedLength);
bitStream.Write(numBits, quantizedOffset);
}
bitStream.Finish();
}
static void RetrieveCallers(_CrtMemBlockHeader* header, uintptr_t* callers, size_t& numCallers)
{
// read quantized offsets from stream
uintptr_t quantizedOffsets[maxCallers];
numCallers = 0;
BitStream bitStream((u8*)&header->file, sizeof(header->file)+sizeof(header->line));
for(;;)
{
if(bitStream.BitsLeft() < numEncodedLengthBits)
break;
const size_t encodedLength = bitStream.Read(numEncodedLengthBits);
const size_t numBits = NumBitsForEncodedLength(encodedLength);
if(bitStream.BitsLeft() < numBits)
break;
const uintptr_t quantizedOffset = bitStream.Read(numBits);
if(!quantizedOffset)
break;
quantizedOffsets[numCallers++] = quantizedOffset;
}
if(!numCallers)
return;
// expand offsets into a set of callers
if(quantizedOffsets[0] <= quantizedCodeSegmentLength)
callers[0] = Expand(quantizedOffsets[0] + quantizedCodeSegmentAddress);
else
callers[0] = Expand(quantizedOffsets[0]);
for(size_t i = 1; i < numCallers; i++)
callers[i] = callers[i-1] + Expand(quantizedOffsets[i]);
}
//-----------------------------------------------------------------------------
// find out who called an allocation function
/**
* gather and store a (filtered) list of callers.
**/
class CallStack
{
public:
void Gather()
{
m_numCallers = 0;
CONTEXT context;
(void)debug_CaptureContext(&context);
(void)wdbg_sym_WalkStack(OnFrame_Trampoline, (uintptr_t)this, context);
std::fill(m_callers+m_numCallers, m_callers+maxCallers, 0);
}
const uintptr_t* Callers() const
{
return m_callers;
}
size_t NumCallers() const
{
return m_numCallers;
}
private:
Status OnFrame(const STACKFRAME64* frame)
{
const uintptr_t pc = frame->AddrPC.Offset;
// skip invalid frames
if(pc == 0)
return INFO::OK;
Status ret = m_filter.NotifyOfCaller(pc);
// (CallerFilter provokes stack traces of heap functions; if that is
// what happened, then we must not continue)
if(ret != INFO::SKIPPED)
return ret;
// stop the stack walk if frame storage is full
if(m_numCallers >= maxCallers)
return INFO::ALL_COMPLETE;
if(!m_filter.IsKnownCaller(pc))
m_callers[m_numCallers++] = pc;
return INFO::OK;
}
static Status OnFrame_Trampoline(const STACKFRAME64* frame, uintptr_t cbData)
{
CallStack* this_ = (CallStack*)cbData;
return this_->OnFrame(frame);
}
CallerFilter m_filter;
uintptr_t m_callers[maxCallers];
size_t m_numCallers;
};
//-----------------------------------------------------------------------------
// RAII wrapper for installing a CRT allocation hook
class AllocationHook
{
public:
AllocationHook()
{
wdbg_assert(s_instance == 0 && s_previousHook == 0);
s_instance = this;
s_previousHook = _CrtSetAllocHook(Hook);
}
~AllocationHook()
{
_CRT_ALLOC_HOOK removedHook = _CrtSetAllocHook(s_previousHook);
wdbg_assert(removedHook == Hook); // warn if we removed someone else's hook
s_instance = 0;
s_previousHook = 0;
}
/**
* @param operation either _HOOK_ALLOC, _HOOK_REALLOC or _HOOK_FREE
* @param userData is only valid (nonzero) for realloc and free because
* we are called BEFORE the actual heap operation.
**/
virtual void OnHeapOperation(int operation, void* userData, size_t size, long allocationNumber) = 0;
private:
static int __cdecl Hook(int operation, void* userData, size_t size, int blockType, long allocationNumber, const unsigned char* file, int line)
{
static bool busy = false;
wdbg_assert(!busy);
busy = true;
s_instance->OnHeapOperation(operation, userData, size, allocationNumber);
busy = false;
if(s_previousHook)
return s_previousHook(operation, userData, size, blockType, allocationNumber, file, line);
return 1; // continue as if the hook had never been called
}
// unfortunately static because we can't pass our `this' pointer through
// the allocation hook.
static AllocationHook* s_instance;
static _CRT_ALLOC_HOOK s_previousHook;
};
AllocationHook* AllocationHook::s_instance;
_CRT_ALLOC_HOOK AllocationHook::s_previousHook;
//-----------------------------------------------------------------------------
// our allocation hook
// ideally we would just stash the callers in the newly created header.
// unfortunately we are called BEFORE it (and the allocation) are actually
// created, so we need to keep the information around until the next call to
// AllocHook; only then can it be stored.
//
// unfortunately the CRT does not provide an O(1) means of getting at the
// most recent block header. instead, we do so once and then keep it
// up-to-date in the allocation hook. this is safe because we run under
// the _HEAP_LOCK and ensure the allocation numbers match.
static intptr_t s_numAllocations;
intptr_t wdbg_heap_NumberOfAllocations()
{
return s_numAllocations;
}
class AllocationTracker : public AllocationHook
{
public:
AllocationTracker()
: m_pendingAllocationNumber(0)
{
}
virtual void OnHeapOperation(int operation, void* userData, size_t size, long allocationNumber)
{
UNUSED2(size);
if(operation == _HOOK_ALLOC || operation == _HOOK_REALLOC)
cpu_AtomicAdd(&s_numAllocations, 1);
bool hasChanged;
_CrtMemBlockHeader* head = GetHeapListHead(operation, userData, hasChanged);
// if the head changed, the last operation was a (re)allocation and
// we now have its header; stash the pending call stack there.
if(hasChanged)
{
wdbg_assert(head->allocationNumber == m_pendingAllocationNumber);
// note: overwrite existing file/line info (even if valid) to avoid
// special cases in the report hook.
StashCallers(head, m_pendingCallStack.Callers(), m_pendingCallStack.NumCallers());
}
// remember the current caller for next time
m_pendingCallStack.Gather(); // NB: called for each operation, as required by the filter recording step
m_pendingAllocationNumber = allocationNumber;
}
private:
long m_pendingAllocationNumber;
CallStack m_pendingCallStack;
};
//-----------------------------------------------------------------------------
static void PrintCallStack(const uintptr_t* callers, size_t numCallers)
{
if(!numCallers || callers[0] == 0)
{
wdbg_printf(L"\n call stack not available.\n");
return;
}
wdbg_printf(L"\n partial, unordered call stack:\n");
for(size_t i = 0; i < numCallers; i++)
{
wchar_t name[DEBUG_SYMBOL_CHARS] = {'\0'}; wchar_t file[DEBUG_FILE_CHARS] = {'\0'}; int line = -1;
Status err = debug_ResolveSymbol((void*)callers[i], name, file, &line);
wdbg_printf(L" ");
if(err != INFO::OK)
wdbg_printf(L"(error %d resolving PC=%p) ", err, callers[i]);
if(file[0] != '\0')
wdbg_printf(L"%ls(%d) : ", file, line);
wdbg_printf(L"%ls\n", name);
}
}
static int __cdecl ReportHook(int reportType, wchar_t* message, int* out)
{
UNUSED2(reportType);
// set up return values to reduce the chance of mistakes below
*out = 0; // alternatives are failure (-1) and breakIntoDebugger (1)
const int ret = 0; // not "handled", continue calling other hooks
// note: this hook is transparent in that it never affects the CRT.
// we can't suppress parts of a leak report because that causes the
// rest of it to be skipped.
static enum
{
WaitingForDump,
WaitingForBlock,
IsBlock
}
state = WaitingForDump;
switch(state)
{
case WaitingForDump:
if(!wcscmp(message, L"Dumping objects ->\n"))
state = WaitingForBlock;
return ret;
case IsBlock:
{
// common case: "normal block at 0xPPPPPPPP, N bytes long".
const wchar_t* addressString = wcsstr(message, L"0x");
if(addressString)
{
const uintptr_t address = wcstoul(addressString, 0, 0);
_CrtMemBlockHeader* header = HeaderFromData((void*)address);
uintptr_t callers[maxCallers]; size_t numCallers;
RetrieveCallers(header, callers, numCallers);
PrintCallStack(callers, numCallers);
state = WaitingForBlock;
return ret;
}
// else: for reasons unknown, there's apparently no information
// about the block; fall through to the previous state.
}
case WaitingForBlock:
if(message[0] == '{')
state = IsBlock;
// suppress messages containing "file" and "line" since the normal
// interpretation of those header fields is invalid.
else if(wcschr(message, '('))
message[0] = '\0';
return ret;
default:
wdbg_assert(0); // unreachable
}
wdbg_assert(0); // unreachable
return 0;
}
#else
intptr_t wdbg_heap_NumberOfAllocations()
{
return 0;
}
#endif
//-----------------------------------------------------------------------------
#if ENABLE_LEAK_INSTRUMENTATION
static AllocationTracker* s_tracker;
#endif
static Status wdbg_heap_Init()
{
#if ENABLE_LEAK_INSTRUMENTATION
FindCodeSegment();
// load symbol information now (fails if it happens during shutdown)
wchar_t name[DEBUG_SYMBOL_CHARS]; wchar_t file[DEBUG_FILE_CHARS]; int line;
(void)debug_ResolveSymbol(wdbg_heap_Init, name, file, &line);
int ret = _CrtSetReportHookW2(_CRT_RPTHOOK_INSTALL, ReportHook);
if(ret == -1)
abort();
s_tracker = new AllocationTracker;
#endif
wdbg_heap_Enable(true);
return INFO::OK;
}
static Status wdbg_heap_Shutdown()
{
#if ENABLE_LEAK_INSTRUMENTATION
SAFE_DELETE(s_tracker);
#endif
return INFO::OK;
}

View File

@ -1,59 +0,0 @@
/* Copyright (C) 2022 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* improved debug heap using MS CRT
*/
#ifndef INCLUDED_WDBG_HEAP
#define INCLUDED_WDBG_HEAP
#ifdef _MSC_VER
# pragma warning(disable:4091) // hides previous local declaration
#endif
// this module provides a more convenient interface to the MS CRT's
// debug heap checks. it also hooks into allocations to record the
// caller/owner information without requiring macros (which break code
// using placement new or member functions called free).
/**
* enable or disable manual and automatic heap validity checking.
* (enabled by default during critical_init.)
**/
void wdbg_heap_Enable(bool);
/**
* check heap integrity.
* errors are reported by the CRT or via debug_DisplayError.
* no effect if called between wdbg_heap_Enable(false) and the next
* wdbg_heap_Enable(true).
**/
void wdbg_heap_Validate();
/**
* @return the total number of alloc and realloc operations thus far.
* used by the in-game profiler.
**/
intptr_t wdbg_heap_NumberOfAllocations();
#endif // #ifndef INCLUDED_WDBG_HEAP