diff --git a/.gitignore b/.gitignore index 547178fc..39d22c03 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,4 @@ api/python/__pycache__ # TTD files *.run *.idx +__pycache__/ diff --git a/api/debuggerapi.h b/api/debuggerapi.h index 2d3cf9e5..3bef5ecf 100644 --- a/api/debuggerapi.h +++ b/api/debuggerapi.h @@ -517,6 +517,31 @@ namespace BinaryNinjaDebuggerAPI { TTDCallEvent() : threadId(0), uniqueThreadId(0), functionAddress(0), returnAddress(0), returnValue(0), hasReturnValue(false) {} }; + struct TTDHeapEvent + { + std::string eventType; // Event type (always "Heap" for TTD.Heap objects) + std::string action; // Heap action: Alloc, ReAlloc, Free, Create, Protect, Lock, Unlock, Destroy + uint32_t threadId; // OS thread ID of thread that made the heap call + uint32_t uniqueThreadId; // Unique ID for the thread across the trace + uint64_t heap; // Handle for the Win32 heap + uint64_t address; // Address of the allocated object (if applicable) + uint64_t previousAddress; // Address before reallocation (for ReAlloc) + uint64_t size; // Size of allocated object (if applicable) + uint64_t baseAddress; // Base address of allocated object (if applicable) + uint64_t flags; // Heap API flags (meaning depends on API) + uint64_t result; // Result of heap API call (non-zero = success) + uint64_t reserveSize; // Amount of memory to reserve (for Create) + uint64_t commitSize; // Initial committed size (for Create) + uint64_t makeReadOnly; // Non-zero = make heap read-only (for Protect) + std::vector parameters; // Raw parameters from the heap call + TTDPosition timeStart; // Position when heap operation started + TTDPosition timeEnd; // Position when heap operation ended + + TTDHeapEvent() : threadId(0), uniqueThreadId(0), heap(0), address(0), previousAddress(0), + size(0), baseAddress(0), flags(0), result(0), reserveSize(0), + commitSize(0), makeReadOnly(0) {} + }; + typedef BNDebugAdapterConnectionStatus DebugAdapterConnectionStatus; typedef BNDebugAdapterTargetStatus DebugAdapterTargetStatus; @@ -687,6 +712,7 @@ namespace BinaryNinjaDebuggerAPI { // TTD Memory Analysis Methods std::vector GetTTDMemoryAccessForAddress(uint64_t address, uint64_t size, TTDMemoryAccessType accessType = TTDMemoryRead); std::vector GetTTDCallsForSymbols(const std::string& symbols, uint64_t startReturnAddress = 0, uint64_t endReturnAddress = 0); + std::vector GetTTDHeapObjects(); TTDPosition GetCurrentTTDPosition(); bool SetTTDPosition(const TTDPosition& position); diff --git a/api/debuggercontroller.cpp b/api/debuggercontroller.cpp index 859995ab..d64c5692 100644 --- a/api/debuggercontroller.cpp +++ b/api/debuggercontroller.cpp @@ -1063,6 +1063,64 @@ std::vector DebuggerController::GetTTDCallsForSymbols(const std::s } +std::vector DebuggerController::GetTTDHeapObjects() +{ + std::vector result; + + size_t count = 0; + BNDebuggerTTDHeapEvent* events = BNDebuggerGetTTDHeapObjects(m_object, &count); + + if (events && count > 0) + { + result.reserve(count); + for (size_t i = 0; i < count; i++) + { + TTDHeapEvent event; + event.eventType = events[i].eventType ? std::string(events[i].eventType) : ""; + event.action = events[i].action ? std::string(events[i].action) : ""; + event.threadId = events[i].threadId; + event.uniqueThreadId = events[i].uniqueThreadId; + event.heap = events[i].heap; + event.address = events[i].address; + event.previousAddress = events[i].previousAddress; + event.size = events[i].size; + event.baseAddress = events[i].baseAddress; + event.flags = events[i].flags; + event.result = events[i].result; + event.reserveSize = events[i].reserveSize; + event.commitSize = events[i].commitSize; + event.makeReadOnly = events[i].makeReadOnly; + event.timeStart.sequence = events[i].timeStart.sequence; + event.timeStart.step = events[i].timeStart.step; + event.timeEnd.sequence = events[i].timeEnd.sequence; + event.timeEnd.step = events[i].timeEnd.step; + + // Convert parameters array + if (events[i].parameters && events[i].parameterCount > 0) + { + event.parameters.reserve(events[i].parameterCount); + for (size_t j = 0; j < events[i].parameterCount; j++) + { + if (events[i].parameters[j]) + { + event.parameters.push_back(std::string(events[i].parameters[j])); + } + else + { + event.parameters.push_back(""); + } + } + } + + result.push_back(event); + } + BNDebuggerFreeTTDHeapEvents(events, count); + } + + return result; +} + + bool DebuggerController::IsInstructionExecuted(uint64_t address) { return BNDebuggerIsInstructionExecuted(m_object, address); diff --git a/api/ffi.h b/api/ffi.h index b2f489ae..36d0919c 100644 --- a/api/ffi.h +++ b/api/ffi.h @@ -339,6 +339,28 @@ extern "C" BNDebuggerTTDPosition timeEnd; // Position when call ended } BNDebuggerTTDCallEvent; + typedef struct BNDebuggerTTDHeapEvent + { + char* eventType; // Event type (always "Heap" for TTD.Heap objects) + char* action; // Heap action: Alloc, ReAlloc, Free, Create, Protect, Lock, Unlock, Destroy + uint32_t threadId; // OS thread ID of thread that made the heap call + uint32_t uniqueThreadId; // Unique ID for the thread across the trace + uint64_t heap; // Handle for the Win32 heap + uint64_t address; // Address of the allocated object (if applicable) + uint64_t previousAddress; // Address before reallocation (for ReAlloc) + uint64_t size; // Size of allocated object (if applicable) + uint64_t baseAddress; // Base address of allocated object (if applicable) + uint64_t flags; // Heap API flags (meaning depends on API) + uint64_t result; // Result of heap API call (non-zero = success) + uint64_t reserveSize; // Amount of memory to reserve (for Create) + uint64_t commitSize; // Initial committed size (for Create) + uint64_t makeReadOnly; // Non-zero = make heap read-only (for Protect) + char** parameters; // Array containing raw parameters from the heap call + size_t parameterCount; // Number of parameters + BNDebuggerTTDPosition timeStart; // Position when heap operation started + BNDebuggerTTDPosition timeEnd; // Position when heap operation ended + } BNDebuggerTTDHeapEvent; + // This should really be a union, but gcc complains... typedef struct BNDebuggerEventData @@ -555,10 +577,12 @@ extern "C" uint64_t address, uint64_t size, BNDebuggerTTDMemoryAccessType accessType, size_t* count); DEBUGGER_FFI_API BNDebuggerTTDCallEvent* BNDebuggerGetTTDCallsForSymbols(BNDebuggerController* controller, const char* symbols, uint64_t startReturnAddress, uint64_t endReturnAddress, size_t* count); + DEBUGGER_FFI_API BNDebuggerTTDHeapEvent* BNDebuggerGetTTDHeapObjects(BNDebuggerController* controller, size_t* count); DEBUGGER_FFI_API BNDebuggerTTDPosition BNDebuggerGetCurrentTTDPosition(BNDebuggerController* controller); DEBUGGER_FFI_API bool BNDebuggerSetTTDPosition(BNDebuggerController* controller, BNDebuggerTTDPosition position); DEBUGGER_FFI_API void BNDebuggerFreeTTDMemoryEvents(BNDebuggerTTDMemoryEvent* events, size_t count); DEBUGGER_FFI_API void BNDebuggerFreeTTDCallEvents(BNDebuggerTTDCallEvent* events, size_t count); + DEBUGGER_FFI_API void BNDebuggerFreeTTDHeapEvents(BNDebuggerTTDHeapEvent* events, size_t count); // TTD Code Coverage Analysis Functions DEBUGGER_FFI_API bool BNDebuggerIsInstructionExecuted(BNDebuggerController* controller, uint64_t address); diff --git a/api/python/debuggercontroller.py b/api/python/debuggercontroller.py index a99b1a39..6f8cac8a 100644 --- a/api/python/debuggercontroller.py +++ b/api/python/debuggercontroller.py @@ -721,6 +721,93 @@ def __repr__(self): return f"" +class TTDHeapEvent: + """ + TTDHeapEvent represents a heap operation event in a TTD trace. It has the following fields: + + * ``event_type``: type of the event (always "Heap" for TTD.Heap objects) + * ``action``: heap action that occurred (Alloc, ReAlloc, Free, Create, Protect, Lock, Unlock, Destroy) + * ``thread_id``: OS thread ID that performed the heap operation + * ``unique_thread_id``: unique thread ID across the trace + * ``heap``: handle for the Win32 heap + * ``address``: address of the allocated object (if applicable) + * ``previous_address``: address before reallocation (for ReAlloc operations) + * ``size``: size of allocated object (if applicable) + * ``base_address``: base address of allocated object (if applicable) + * ``flags``: heap API flags (meaning depends on the specific API) + * ``result``: result of heap API call (non-zero means success) + * ``reserve_size``: amount of memory to reserve (for Create operations) + * ``commit_size``: initial committed size (for Create operations) + * ``make_read_only``: non-zero indicates request to make heap read-only + * ``parameters``: list of raw parameters from the heap call + * ``time_start``: TTD position when heap operation started + * ``time_end``: TTD position when heap operation ended + """ + + def __init__(self, event_type: str, action: str, thread_id: int, unique_thread_id: int, + heap: int, address: int, previous_address: int, size: int, base_address: int, + flags: int, result: int, reserve_size: int, commit_size: int, make_read_only: int, + parameters: List[str], time_start: TTDPosition, time_end: TTDPosition): + self.event_type = event_type + self.action = action + self.thread_id = thread_id + self.unique_thread_id = unique_thread_id + self.heap = heap + self.address = address + self.previous_address = previous_address + self.size = size + self.base_address = base_address + self.flags = flags + self.result = result + self.reserve_size = reserve_size + self.commit_size = commit_size + self.make_read_only = make_read_only + self.parameters = parameters + self.time_start = time_start + self.time_end = time_end + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return (self.event_type == other.event_type and + self.action == other.action and + self.thread_id == other.thread_id and + self.unique_thread_id == other.unique_thread_id and + self.heap == other.heap and + self.address == other.address and + self.previous_address == other.previous_address and + self.size == other.size and + self.base_address == other.base_address and + self.flags == other.flags and + self.result == other.result and + self.reserve_size == other.reserve_size and + self.commit_size == other.commit_size and + self.make_read_only == other.make_read_only and + self.parameters == other.parameters and + self.time_start == other.time_start and + self.time_end == other.time_end) + + def __ne__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return not (self == other) + + def __hash__(self): + return hash((self.event_type, self.action, self.thread_id, self.unique_thread_id, + self.heap, self.address, self.previous_address, self.size, self.base_address, + self.flags, self.result, self.reserve_size, self.commit_size, self.make_read_only, + tuple(self.parameters), self.time_start, self.time_end)) + + def __setattr__(self, name, value): + try: + object.__setattr__(self, name, value) + except AttributeError: + raise AttributeError(f"attribute '{name}' is read only") + + def __repr__(self): + return f"" + + class DebuggerController: """ The ``DebuggerController`` object is the core of the debugger. Most debugger operations can be performed on it. @@ -2042,6 +2129,58 @@ def get_ttd_calls_for_symbols(self, symbols: str, start_return_address: int = 0, dbgcore.BNDebuggerFreeTTDCallEvents(events, count.value) return result + def get_ttd_heap_objects(self) -> List[TTDHeapEvent]: + """ + Get TTD heap operation events. + + This method is only available when debugging with TTD (Time Travel Debugging). + Use the is_ttd property to check if TTD is available before calling this method. + + :return: list of TTDHeapEvent objects representing heap operations + :raises: May raise an exception if TTD is not available + """ + count = ctypes.c_ulonglong() + events = dbgcore.BNDebuggerGetTTDHeapObjects(self.handle, count) + + if not events: + return [] + + result = [] + for i in range(count.value): + event = events[i] + time_start = TTDPosition(event.timeStart.sequence, event.timeStart.step) + time_end = TTDPosition(event.timeEnd.sequence, event.timeEnd.step) + + # Convert parameters array to Python list + parameters = [] + if event.parameters and event.parameterCount > 0: + for j in range(event.parameterCount): + parameters.append(event.parameters[j]) + + heap_event = TTDHeapEvent( + event_type=event.eventType if event.eventType else "", + action=event.action if event.action else "", + thread_id=event.threadId, + unique_thread_id=event.uniqueThreadId, + heap=event.heap, + address=event.address, + previous_address=event.previousAddress, + size=event.size, + base_address=event.baseAddress, + flags=event.flags, + result=event.result, + reserve_size=event.reserveSize, + commit_size=event.commitSize, + make_read_only=event.makeReadOnly, + parameters=parameters, + time_start=time_start, + time_end=time_end + ) + result.append(heap_event) + + dbgcore.BNDebuggerFreeTTDHeapEvents(events, count.value) + return result + def __del__(self): if dbgcore is not None: dbgcore.BNDebuggerFreeController(self.handle) diff --git a/core/adapters/dbgengttdadapter.cpp b/core/adapters/dbgengttdadapter.cpp index 3a3b40ab..ffede4c7 100644 --- a/core/adapters/dbgengttdadapter.cpp +++ b/core/adapters/dbgengttdadapter.cpp @@ -1350,6 +1350,492 @@ bool DbgEngTTDAdapter::ParseTTDCallObjects(const std::string& expression, std::v } +std::vector DbgEngTTDAdapter::GetTTDHeapObjects() +{ + std::vector events; + + if (!QueryTTDHeapObjects(events)) + { + LogError("Failed to query TTD heap objects"); + } + + return events; +} + + +bool DbgEngTTDAdapter::QueryTTDHeapObjects(std::vector& events) +{ + try + { + LogInfo("Querying TTD heap objects using @$cursession.TTD.Data.Heap()"); + + // Use the data model to query heap objects + std::string expression = "@$cursession.TTD.Data.Heap()"; + + // Execute the query and parse results + return ParseTTDHeapObjects(expression, events); + } + catch (const std::exception& e) + { + LogError("Exception in QueryTTDHeapObjects: %s", e.what()); + return false; + } +} + + +bool DbgEngTTDAdapter::ParseTTDHeapObjects(const std::string& expression, std::vector& events) +{ + try + { + LogInfo("Parsing TTD heap objects from expression: %s", expression.c_str()); + + // Convert expression to wide string + std::wstring wExpression(expression.begin(), expression.end()); + + // Create context for evaluation + ComPtr hostContext; + if (FAILED(m_debugHost->GetCurrentContext(&hostContext))) + { + LogError("Failed to get debug host context"); + return false; + } + + // Evaluate the data model expression + ComPtr resultObject; + ComPtr resultMetadata; + if (FAILED(m_hostEvaluator->EvaluateExpression( + hostContext.Get(), + wExpression.c_str(), + nullptr, + &resultObject, + &resultMetadata))) + { + LogError("Failed to evaluate TTD heap expression: %s", expression.c_str()); + return false; + } + + // Check if result is iterable + ComPtr iterableConcept; + if (FAILED(resultObject->GetConcept(__uuidof(IIterableConcept), + reinterpret_cast(iterableConcept.GetAddressOf()), nullptr))) + { + LogError("TTD heap result is not iterable"); + return false; + } + + // Get iterator + ComPtr iterator; + if (FAILED(iterableConcept->GetIterator(resultObject.Get(), &iterator))) + { + LogError("Failed to get iterator for TTD heap objects"); + return false; + } + + // Iterate through heap objects + while (true) + { + ComPtr current; + ComPtr currentMetadata; + + HRESULT hr = iterator->GetNext(¤t, ¤tMetadata, nullptr); + if (hr == E_BOUNDS) + { + // End of iteration + break; + } + if (FAILED(hr)) + { + LogError("Failed to get next heap object from iterator"); + break; + } + + // Parse individual heap object + TTDHeapEvent heapEvent; + heapEvent.eventType = "Heap"; + + // Parse Action field + ComPtr actionObj; + if (SUCCEEDED(current->GetKeyValue(L"Action", &actionObj, nullptr))) + { + VARIANT actionVar; + VariantInit(&actionVar); + if (SUCCEEDED(actionObj->GetIntrinsicValue(&actionVar))) + { + if (actionVar.vt == VT_BSTR && actionVar.bstrVal) + { + std::wstring wAction(actionVar.bstrVal); + heapEvent.action = std::string(wAction.begin(), wAction.end()); + } + VariantClear(&actionVar); + } + } + + // Parse Heap field + ComPtr heapObj; + if (SUCCEEDED(current->GetKeyValue(L"Heap", &heapObj, nullptr))) + { + VARIANT heapVar; + VariantInit(&heapVar); + if (SUCCEEDED(heapObj->GetIntrinsicValue(&heapVar))) + { + if (heapVar.vt == VT_UI8) + { + heapEvent.heap = heapVar.ullVal; + } + else if (heapVar.vt == VT_UI4) + { + heapEvent.heap = heapVar.ulVal; + } + VariantClear(&heapVar); + } + } + + // Parse Address field (conditional) + ComPtr addressObj; + if (SUCCEEDED(current->GetKeyValue(L"Address", &addressObj, nullptr))) + { + VARIANT addressVar; + VariantInit(&addressVar); + if (SUCCEEDED(addressObj->GetIntrinsicValue(&addressVar))) + { + if (addressVar.vt == VT_UI8) + { + heapEvent.address = addressVar.ullVal; + } + else if (addressVar.vt == VT_UI4) + { + heapEvent.address = addressVar.ulVal; + } + VariantClear(&addressVar); + } + } + + // Parse PreviousAddress field (conditional) + ComPtr prevAddressObj; + if (SUCCEEDED(current->GetKeyValue(L"PreviousAddress", &prevAddressObj, nullptr))) + { + VARIANT prevAddressVar; + VariantInit(&prevAddressVar); + if (SUCCEEDED(prevAddressObj->GetIntrinsicValue(&prevAddressVar))) + { + if (prevAddressVar.vt == VT_UI8) + { + heapEvent.previousAddress = prevAddressVar.ullVal; + } + else if (prevAddressVar.vt == VT_UI4) + { + heapEvent.previousAddress = prevAddressVar.ulVal; + } + VariantClear(&prevAddressVar); + } + } + + // Parse Size field (conditional) + ComPtr sizeObj; + if (SUCCEEDED(current->GetKeyValue(L"Size", &sizeObj, nullptr))) + { + VARIANT sizeVar; + VariantInit(&sizeVar); + if (SUCCEEDED(sizeObj->GetIntrinsicValue(&sizeVar))) + { + if (sizeVar.vt == VT_UI8) + { + heapEvent.size = sizeVar.ullVal; + } + else if (sizeVar.vt == VT_UI4) + { + heapEvent.size = sizeVar.ulVal; + } + VariantClear(&sizeVar); + } + } + + // Parse BaseAddress field (conditional) + ComPtr baseAddressObj; + if (SUCCEEDED(current->GetKeyValue(L"BaseAddress", &baseAddressObj, nullptr))) + { + VARIANT baseAddressVar; + VariantInit(&baseAddressVar); + if (SUCCEEDED(baseAddressObj->GetIntrinsicValue(&baseAddressVar))) + { + if (baseAddressVar.vt == VT_UI8) + { + heapEvent.baseAddress = baseAddressVar.ullVal; + } + else if (baseAddressVar.vt == VT_UI4) + { + heapEvent.baseAddress = baseAddressVar.ulVal; + } + VariantClear(&baseAddressVar); + } + } + + // Parse Flags field (conditional) + ComPtr flagsObj; + if (SUCCEEDED(current->GetKeyValue(L"Flags", &flagsObj, nullptr))) + { + VARIANT flagsVar; + VariantInit(&flagsVar); + if (SUCCEEDED(flagsObj->GetIntrinsicValue(&flagsVar))) + { + if (flagsVar.vt == VT_UI8) + { + heapEvent.flags = flagsVar.ullVal; + } + else if (flagsVar.vt == VT_UI4) + { + heapEvent.flags = flagsVar.ulVal; + } + VariantClear(&flagsVar); + } + } + + // Parse Result field (conditional) + ComPtr resultObj; + if (SUCCEEDED(current->GetKeyValue(L"Result", &resultObj, nullptr))) + { + VARIANT resultVar; + VariantInit(&resultVar); + if (SUCCEEDED(resultObj->GetIntrinsicValue(&resultVar))) + { + if (resultVar.vt == VT_UI8) + { + heapEvent.result = resultVar.ullVal; + } + else if (resultVar.vt == VT_UI4) + { + heapEvent.result = resultVar.ulVal; + } + VariantClear(&resultVar); + } + } + + // Parse ReserveSize field (conditional) + ComPtr reserveSizeObj; + if (SUCCEEDED(current->GetKeyValue(L"ReserveSize", &reserveSizeObj, nullptr))) + { + VARIANT reserveSizeVar; + VariantInit(&reserveSizeVar); + if (SUCCEEDED(reserveSizeObj->GetIntrinsicValue(&reserveSizeVar))) + { + if (reserveSizeVar.vt == VT_UI8) + { + heapEvent.reserveSize = reserveSizeVar.ullVal; + } + else if (reserveSizeVar.vt == VT_UI4) + { + heapEvent.reserveSize = reserveSizeVar.ulVal; + } + VariantClear(&reserveSizeVar); + } + } + + // Parse CommitSize field (conditional) + ComPtr commitSizeObj; + if (SUCCEEDED(current->GetKeyValue(L"CommitSize", &commitSizeObj, nullptr))) + { + VARIANT commitSizeVar; + VariantInit(&commitSizeVar); + if (SUCCEEDED(commitSizeObj->GetIntrinsicValue(&commitSizeVar))) + { + if (commitSizeVar.vt == VT_UI8) + { + heapEvent.commitSize = commitSizeVar.ullVal; + } + else if (commitSizeVar.vt == VT_UI4) + { + heapEvent.commitSize = commitSizeVar.ulVal; + } + VariantClear(&commitSizeVar); + } + } + + // Parse MakeReadOnly field (conditional) + ComPtr makeReadOnlyObj; + if (SUCCEEDED(current->GetKeyValue(L"MakeReadOnly", &makeReadOnlyObj, nullptr))) + { + VARIANT makeReadOnlyVar; + VariantInit(&makeReadOnlyVar); + if (SUCCEEDED(makeReadOnlyObj->GetIntrinsicValue(&makeReadOnlyVar))) + { + if (makeReadOnlyVar.vt == VT_UI8) + { + heapEvent.makeReadOnly = makeReadOnlyVar.ullVal; + } + else if (makeReadOnlyVar.vt == VT_UI4) + { + heapEvent.makeReadOnly = makeReadOnlyVar.ulVal; + } + VariantClear(&makeReadOnlyVar); + } + } + + // Parse TimeStart field + ComPtr timeStartObj; + if (SUCCEEDED(current->GetKeyValue(L"TimeStart", &timeStartObj, nullptr))) + { + // Parse sequence + ComPtr sequenceObj; + if (SUCCEEDED(timeStartObj->GetKeyValue(L"Sequence", &sequenceObj, nullptr))) + { + VARIANT sequenceVar; + VariantInit(&sequenceVar); + if (SUCCEEDED(sequenceObj->GetIntrinsicValue(&sequenceVar))) + { + if (sequenceVar.vt == VT_UI8) + { + heapEvent.timeStart.sequence = sequenceVar.ullVal; + } + else if (sequenceVar.vt == VT_UI4) + { + heapEvent.timeStart.sequence = sequenceVar.ulVal; + } + VariantClear(&sequenceVar); + } + } + + // Parse step + ComPtr stepObj; + if (SUCCEEDED(timeStartObj->GetKeyValue(L"Steps", &stepObj, nullptr))) + { + VARIANT stepVar; + VariantInit(&stepVar); + if (SUCCEEDED(stepObj->GetIntrinsicValue(&stepVar))) + { + if (stepVar.vt == VT_UI8) + { + heapEvent.timeStart.step = stepVar.ullVal; + } + else if (stepVar.vt == VT_UI4) + { + heapEvent.timeStart.step = stepVar.ulVal; + } + VariantClear(&stepVar); + } + } + } + + // Parse TimeEnd field + ComPtr timeEndObj; + if (SUCCEEDED(current->GetKeyValue(L"TimeEnd", &timeEndObj, nullptr))) + { + // Parse sequence + ComPtr sequenceObj; + if (SUCCEEDED(timeEndObj->GetKeyValue(L"Sequence", &sequenceObj, nullptr))) + { + VARIANT sequenceVar; + VariantInit(&sequenceVar); + if (SUCCEEDED(sequenceObj->GetIntrinsicValue(&sequenceVar))) + { + if (sequenceVar.vt == VT_UI8) + { + heapEvent.timeEnd.sequence = sequenceVar.ullVal; + } + else if (sequenceVar.vt == VT_UI4) + { + heapEvent.timeEnd.sequence = sequenceVar.ulVal; + } + VariantClear(&sequenceVar); + } + } + + // Parse step + ComPtr stepObj; + if (SUCCEEDED(timeEndObj->GetKeyValue(L"Steps", &stepObj, nullptr))) + { + VARIANT stepVar; + VariantInit(&stepVar); + if (SUCCEEDED(stepObj->GetIntrinsicValue(&stepVar))) + { + if (stepVar.vt == VT_UI8) + { + heapEvent.timeEnd.step = stepVar.ullVal; + } + else if (stepVar.vt == VT_UI4) + { + heapEvent.timeEnd.step = stepVar.ulVal; + } + VariantClear(&stepVar); + } + } + } + + // Parse Parameters field (raw parameters as strings) + ComPtr parametersObj; + if (SUCCEEDED(current->GetKeyValue(L"@\"Parameters\"", ¶metersObj, nullptr))) + { + // Check if parameters is iterable + ComPtr paramIterableConcept; + if (SUCCEEDED(parametersObj->GetConcept(__uuidof(IIterableConcept), + reinterpret_cast(paramIterableConcept.GetAddressOf()), nullptr))) + { + ComPtr paramIterator; + if (SUCCEEDED(paramIterableConcept->GetIterator(parametersObj.Get(), ¶mIterator))) + { + while (true) + { + ComPtr paramObj; + HRESULT paramHr = paramIterator->GetNext(¶mObj, nullptr, nullptr); + if (paramHr == E_BOUNDS) + { + break; + } + if (FAILED(paramHr)) + { + break; + } + + VARIANT paramVar; + VariantInit(¶mVar); + if (SUCCEEDED(paramObj->GetIntrinsicValue(¶mVar))) + { + std::string paramStr; + if (paramVar.vt == VT_UI8) + { + paramStr = fmt::format("0x{:x}", paramVar.ullVal); + } + else if (paramVar.vt == VT_UI4) + { + paramStr = fmt::format("0x{:x}", paramVar.ulVal); + } + else if (paramVar.vt == VT_BSTR && paramVar.bstrVal) + { + std::wstring wParam(paramVar.bstrVal); + paramStr = std::string(wParam.begin(), wParam.end()); + } + + if (!paramStr.empty()) + { + heapEvent.parameters.push_back(paramStr); + } + VariantClear(¶mVar); + } + } + } + } + } + + events.push_back(heapEvent); + } + + if (events.empty()) + { + LogWarn("No TTD heap events found"); + } + else + { + LogInfo("Successfully parsed %zu TTD heap events from data model", events.size()); + } + return true; + } + catch (const std::exception& e) + { + LogError("Exception in ParseTTDHeapObjects: %s", e.what()); + return false; + } +} + + void BinaryNinjaDebugger::InitDbgEngTTDAdapterType() { static DbgEngTTDAdapterType localType; diff --git a/core/adapters/dbgengttdadapter.h b/core/adapters/dbgengttdadapter.h index 30ed9749..42ba9354 100644 --- a/core/adapters/dbgengttdadapter.h +++ b/core/adapters/dbgengttdadapter.h @@ -54,6 +54,9 @@ namespace BinaryNinjaDebugger { // TTD Calls Analysis Methods std::vector GetTTDCallsForSymbols(const std::string& symbols, uint64_t startReturnAddress = 0, uint64_t endReturnAddress = 0) override; + // TTD Heap Analysis Methods + std::vector GetTTDHeapObjects() override; + void GenerateDefaultAdapterSettings(BinaryView* data); Ref GetAdapterSettings() override; @@ -65,6 +68,10 @@ namespace BinaryNinjaDebugger { bool QueryCallsForSymbols(const std::vector& symbols, uint64_t startReturnAddress, uint64_t endReturnAddress, std::vector& events); bool ParseTTDCallObjects(const std::string& expression, std::vector& events); + // Helper methods for TTD heap analysis + bool QueryTTDHeapObjects(std::vector& events); + bool ParseTTDHeapObjects(const std::string& expression, std::vector& events); + // Data model helper methods std::string EvaluateDataModelExpression(const std::string& expression); bool ParseTTDMemoryObjects(const std::string& expression, TTDMemoryAccessType accessType, std::vector& events); diff --git a/core/debugadapter.cpp b/core/debugadapter.cpp index 52360905..0f4d0155 100644 --- a/core/debugadapter.cpp +++ b/core/debugadapter.cpp @@ -193,6 +193,13 @@ std::vector DebugAdapter::GetTTDCallsForSymbols(const std::string& } +std::vector DebugAdapter::GetTTDHeapObjects() +{ + // Default implementation returns empty results for adapters that don't support TTD + return {}; +} + + TTDPosition DebugAdapter::GetCurrentTTDPosition() { // Default implementation returns an empty position for adapters that don't support TTD diff --git a/core/debugadapter.h b/core/debugadapter.h index 548c023a..9359b4d7 100644 --- a/core/debugadapter.h +++ b/core/debugadapter.h @@ -330,6 +330,7 @@ namespace BinaryNinjaDebugger { // TTD (Time Travel Debugging) methods - default implementations return empty results virtual std::vector GetTTDMemoryAccessForAddress(uint64_t startAddress, uint64_t endAddress, TTDMemoryAccessType accessType = TTDMemoryRead); virtual std::vector GetTTDCallsForSymbols(const std::string& symbols, uint64_t startReturnAddress = 0, uint64_t endReturnAddress = 0); + virtual std::vector GetTTDHeapObjects(); virtual TTDPosition GetCurrentTTDPosition(); virtual bool SetTTDPosition(const TTDPosition& position); diff --git a/core/debuggercontroller.cpp b/core/debuggercontroller.cpp index 3f389b1e..ba8b9674 100644 --- a/core/debuggercontroller.cpp +++ b/core/debuggercontroller.cpp @@ -2902,6 +2902,20 @@ std::vector DebuggerController::GetTTDCallsForSymbols(const std::s } +std::vector DebuggerController::GetTTDHeapObjects() +{ + std::vector events; + + if (!IsTTD()) + { + LogError("Current adapter does not support TTD"); + return events; + } + + return m_adapter->GetTTDHeapObjects(); +} + + TTDPosition DebuggerController::GetCurrentTTDPosition() { TTDPosition position; diff --git a/core/ffi.cpp b/core/ffi.cpp index c3f77a86..029882eb 100644 --- a/core/ffi.cpp +++ b/core/ffi.cpp @@ -1272,6 +1272,101 @@ void BNDebuggerFreeTTDCallEvents(BNDebuggerTTDCallEvent* events, size_t count) } +BNDebuggerTTDHeapEvent* BNDebuggerGetTTDHeapObjects(BNDebuggerController* controller, size_t* count) +{ + if (!count) + return nullptr; + + *count = 0; + + auto events = controller->object->GetTTDHeapObjects(); + if (events.empty()) + return nullptr; + + *count = events.size(); + auto result = new BNDebuggerTTDHeapEvent[events.size()]; + + for (size_t i = 0; i < events.size(); ++i) + { + // Copy string fields + result[i].eventType = BNAllocString(events[i].eventType.c_str()); + result[i].action = BNAllocString(events[i].action.c_str()); + + // Copy primitive fields + result[i].threadId = events[i].threadId; + result[i].uniqueThreadId = events[i].uniqueThreadId; + result[i].heap = events[i].heap; + result[i].address = events[i].address; + result[i].previousAddress = events[i].previousAddress; + result[i].size = events[i].size; + result[i].baseAddress = events[i].baseAddress; + result[i].flags = events[i].flags; + result[i].result = events[i].result; + result[i].reserveSize = events[i].reserveSize; + result[i].commitSize = events[i].commitSize; + result[i].makeReadOnly = events[i].makeReadOnly; + + // Copy parameters array + result[i].parameterCount = events[i].parameters.size(); + if (result[i].parameterCount > 0) + { + result[i].parameters = new char*[result[i].parameterCount]; + for (size_t j = 0; j < result[i].parameterCount; ++j) + { + result[i].parameters[j] = BNAllocString(events[i].parameters[j].c_str()); + } + } + else + { + result[i].parameters = nullptr; + } + + // Copy TTD positions + result[i].timeStart.sequence = events[i].timeStart.sequence; + result[i].timeStart.step = events[i].timeStart.step; + result[i].timeEnd.sequence = events[i].timeEnd.sequence; + result[i].timeEnd.step = events[i].timeEnd.step; + } + + return result; +} + + +void BNDebuggerFreeTTDHeapEvents(BNDebuggerTTDHeapEvent* events, size_t count) +{ + if (!events || count == 0) + return; + + // Free all strings for each event + for (size_t i = 0; i < count; ++i) + { + if (events[i].eventType) + { + BNFreeString(events[i].eventType); + } + if (events[i].action) + { + BNFreeString(events[i].action); + } + + // Free parameter strings + if (events[i].parameters && events[i].parameterCount > 0) + { + for (size_t j = 0; j < events[i].parameterCount; ++j) + { + if (events[i].parameters[j]) + { + BNFreeString(events[i].parameters[j]); + } + } + delete[] events[i].parameters; + } + } + + delete[] events; +} + + void BNDebuggerPostDebuggerEvent(BNDebuggerController* controller, BNDebuggerEvent* event) { diff --git a/debuggerui.qrc b/debuggerui.qrc index f1495492..2218c7ed 100644 --- a/debuggerui.qrc +++ b/debuggerui.qrc @@ -24,6 +24,7 @@ icons/stop.png icons/ttd-memory.png icons/ttd-calls.png + icons/ttd-heap.png icons/ttd-timestamp.png diff --git a/docs/ttd-python-api.md b/docs/ttd-python-api.md index 2d073638..7905000e 100644 --- a/docs/ttd-python-api.md +++ b/docs/ttd-python-api.md @@ -85,6 +85,36 @@ class TTDCallEvent: """ ``` +### TTDHeapEvent + +Represents a heap operation event in a TTD trace. + +```python +class TTDHeapEvent: + """ + TTDHeapEvent represents a heap operation event in a TTD trace. + + Attributes: + event_type (str): Type of the event (always "Heap" for TTD.Heap objects) + action (str): Heap action that occurred (Alloc, ReAlloc, Free, Create, Protect, Lock, Unlock, Destroy) + thread_id (int): OS thread ID that performed the heap operation + unique_thread_id (int): Unique thread ID across the trace + heap (int): Handle for the Win32 heap + address (int): Address of the allocated object (if applicable) + previous_address (int): Address before reallocation (for ReAlloc operations) + size (int): Size of allocated object (if applicable) + base_address (int): Base address of allocated object (if applicable) + flags (int): Heap API flags (meaning depends on the specific API) + result (int): Result of heap API call (non-zero means success) + reserve_size (int): Amount of memory to reserve (for Create operations) + commit_size (int): Initial committed size (for Create operations) + make_read_only (int): Non-zero indicates request to make heap read-only + parameters (List[str]): List of raw parameters from the heap call + time_start (TTDPosition): TTD position when heap operation started + time_end (TTDPosition): TTD position when heap operation ended + """ +``` + ## Constants and Access Types ### DebuggerTTDMemoryAccessType Enum @@ -188,6 +218,22 @@ def get_ttd_calls_for_symbols( """ ``` +### get_ttd_heap_objects() + +```python +def get_ttd_heap_objects(self) -> List[TTDHeapEvent]: + """ + Get TTD heap operation events. + + This method queries all heap operations that occurred during the TTD trace. + It provides information about heap allocations, deallocations, reallocations, + and other heap management operations. + + Returns: + List of TTDHeapEvent objects representing heap operations + """ +``` + ### get_current_ttd_position() ```python @@ -264,6 +310,58 @@ for call in call_events: print(f" Return value: {call.return_value:#x}") ``` +### Heap Analysis + +```python +# Analyze heap operations during the trace +heap_events = dbg.get_ttd_heap_objects() + +print(f"Found {len(heap_events)} heap operations") + +# Group by action type +actions = {} +for event in heap_events: + if event.action not in actions: + actions[event.action] = [] + actions[event.action].append(event) + +# Display summary +for action, events in actions.items(): + print(f"{action}: {len(events)} operations") + +# Find large allocations +large_allocs = [e for e in heap_events + if e.action == "Alloc" and e.size > 1024*1024] # > 1MB + +print(f"Found {len(large_allocs)} large allocations (>1MB)") +for alloc in large_allocs: + print(f" {alloc.size} bytes at {alloc.address:#x} (heap {alloc.heap:#x})") + print(f" Time: {alloc.time_start}") + +# Analyze heap usage patterns +heap_stats = {} +for event in heap_events: + heap_handle = event.heap + if heap_handle not in heap_stats: + heap_stats[heap_handle] = {'allocs': 0, 'frees': 0, 'total_allocated': 0} + + if event.action == "Alloc": + heap_stats[heap_handle]['allocs'] += 1 + heap_stats[heap_handle]['total_allocated'] += event.size + elif event.action == "Free": + heap_stats[heap_handle]['frees'] += 1 + +print("\nHeap usage statistics:") +for heap_handle, stats in heap_stats.items(): + print(f"Heap {heap_handle:#x}:") + print(f" Allocations: {stats['allocs']}") + print(f" Frees: {stats['frees']}") + print(f" Total allocated: {stats['total_allocated']} bytes") + leaked = stats['allocs'] - stats['frees'] + if leaked > 0: + print(f" Potential leaks: {leaked} allocations") +``` + ### TTD Navigation ```python diff --git a/icons/ttd-heap.png b/icons/ttd-heap.png new file mode 100644 index 00000000..83d80059 Binary files /dev/null and b/icons/ttd-heap.png differ diff --git a/ui/ttdheapwidget.cpp b/ui/ttdheapwidget.cpp new file mode 100644 index 00000000..ab87d66e --- /dev/null +++ b/ui/ttdheapwidget.cpp @@ -0,0 +1,634 @@ +/* +Copyright 2020-2025 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "ttdheapwidget.h" +#include "fmt/format.h" +#include +#include +#include + +using namespace BinaryNinjaDebuggerAPI; + +// Helper class for numerical sorting +class NumericalTableWidgetItem : public QTableWidgetItem +{ +public: + NumericalTableWidgetItem(const QString &text, uint64_t numValue) : QTableWidgetItem(text), m_numValue(numValue) {} + + bool operator<(const QTableWidgetItem &other) const override + { + const NumericalTableWidgetItem* numOther = dynamic_cast(&other); + if (numOther) + return m_numValue < numOther->m_numValue; + return QTableWidgetItem::operator<(other); + } + +private: + uint64_t m_numValue; +}; + +// TTDHeapQueryWidget implementation +TTDHeapQueryWidget::TTDHeapQueryWidget(QWidget* parent, BinaryViewRef data) + : QWidget(parent), m_data(data) +{ + m_controller = DebuggerController::GetController(m_data); + if (!m_controller) + { + LogError("Failed to get debugger controller"); + return; + } + + // Initialize column names and visibility + m_columnNames << "Index" << "Event Type" << "Action" << "Time Start" << "Time End" + << "Heap" << "Address" << "Previous Address" << "Size" << "Base Address" + << "Flags" << "Result" << "Reserve Size" << "Commit Size" << "Make Read Only" + << "Thread ID" << "Unique Thread ID" << "Parameters"; + + // Default visibility - show most important columns by default + m_columnVisibility << true << true << true << true << true // Index, Event Type, Action, Time Start, Time End + << true << true << false << true << false // Heap, Address, Previous Address, Size, Base Address + << false << true << false << false << false // Flags, Result, Reserve Size, Commit Size, Make Read Only + << false << false << false; // Thread ID, Unique Thread ID, Parameters + + setupUI(); + setupUIActions(); +} + +TTDHeapQueryWidget::~TTDHeapQueryWidget() +{ + if (m_contextMenuManager) + delete m_contextMenuManager; +} + +void TTDHeapQueryWidget::setupUI() +{ + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + // Controls section + auto* controlsWidget = new QWidget(); + auto* controlsLayout = new QHBoxLayout(controlsWidget); + controlsLayout->setContentsMargins(8, 8, 8, 8); + + m_queryButton = new QPushButton("Query Heap Objects"); + m_clearButton = new QPushButton("Clear Results"); + + controlsLayout->addWidget(m_queryButton); + controlsLayout->addWidget(m_clearButton); + controlsLayout->addStretch(); + + // Status label + m_statusLabel = new QLabel("Ready to query TTD heap objects"); + m_statusLabel->setStyleSheet("QLabel { color: #666; font-style: italic; }"); + controlsLayout->addWidget(m_statusLabel); + + layout->addWidget(controlsWidget); + + // Results table + m_resultsTable = new QTableWidget(); + setupTable(); + layout->addWidget(m_resultsTable); + + // Connect signals + connect(m_queryButton, &QPushButton::clicked, this, &TTDHeapQueryWidget::performQuery); + connect(m_clearButton, &QPushButton::clicked, this, &TTDHeapQueryWidget::clearResults); + connect(m_resultsTable, &QTableWidget::cellDoubleClicked, this, &TTDHeapQueryWidget::onCellDoubleClicked); +} + +void TTDHeapQueryWidget::setupTable() +{ + m_resultsTable->setColumnCount(m_columnNames.size()); + m_resultsTable->setHorizontalHeaderLabels(m_columnNames); + m_resultsTable->horizontalHeader()->setStretchLastSection(true); + m_resultsTable->setAlternatingRowColors(true); + m_resultsTable->setSelectionBehavior(QAbstractItemView::SelectRows); + m_resultsTable->setSortingEnabled(true); + m_resultsTable->setContextMenuPolicy(Qt::CustomContextMenu); + + // Set column widths + m_resultsTable->setColumnWidth(IndexColumn, 80); + m_resultsTable->setColumnWidth(EventTypeColumn, 100); + m_resultsTable->setColumnWidth(ActionColumn, 100); + m_resultsTable->setColumnWidth(TimeStartColumn, 120); + m_resultsTable->setColumnWidth(TimeEndColumn, 120); + m_resultsTable->setColumnWidth(HeapColumn, 120); + m_resultsTable->setColumnWidth(AddressColumn, 120); + m_resultsTable->setColumnWidth(PreviousAddressColumn, 120); + m_resultsTable->setColumnWidth(SizeColumn, 100); + m_resultsTable->setColumnWidth(BaseAddressColumn, 120); + m_resultsTable->setColumnWidth(FlagsColumn, 100); + m_resultsTable->setColumnWidth(ResultColumn, 100); + m_resultsTable->setColumnWidth(ReserveSizeColumn, 100); + m_resultsTable->setColumnWidth(CommitSizeColumn, 100); + m_resultsTable->setColumnWidth(MakeReadOnlyColumn, 100); + m_resultsTable->setColumnWidth(ThreadIdColumn, 100); + m_resultsTable->setColumnWidth(UniqueThreadIdColumn, 120); + + updateColumnVisibility(); + + connect(m_resultsTable, &QTableWidget::customContextMenuRequested, this, &TTDHeapQueryWidget::showContextMenu); +} + +void TTDHeapQueryWidget::updateStatus(const QString& message) +{ + m_statusLabel->setText(message); + QApplication::processEvents(); +} + +void TTDHeapQueryWidget::performQuery() +{ + if (!m_controller) + { + updateStatus("No debugger controller available"); + return; + } + + updateStatus("Querying TTD heap objects..."); + m_queryButton->setEnabled(false); + QApplication::processEvents(); + + try + { + // Execute the TTD heap query + auto events = m_controller->GetTTDHeapObjects(); + + // Populate the results table + m_resultsTable->setRowCount(events.size()); + + for (size_t i = 0; i < events.size(); ++i) + { + const auto& event = events[i]; + + // Index + m_resultsTable->setItem(i, IndexColumn, new NumericalTableWidgetItem(QString("0x%1").arg(i, 0, 16), i)); + + // Event Type + m_resultsTable->setItem(i, EventTypeColumn, new QTableWidgetItem(QString::fromStdString(event.eventType))); + + // Action + m_resultsTable->setItem(i, ActionColumn, new QTableWidgetItem(QString::fromStdString(event.action))); + + // Time Start + QString timeStartStr = QString("%1:%2") + .arg(event.timeStart.sequence, 0, 16) + .arg(event.timeStart.step, 0, 16); + m_resultsTable->setItem(i, TimeStartColumn, new QTableWidgetItem(timeStartStr)); + + // Time End + QString timeEndStr = QString("%1:%2") + .arg(event.timeEnd.sequence, 0, 16) + .arg(event.timeEnd.step, 0, 16); + m_resultsTable->setItem(i, TimeEndColumn, new QTableWidgetItem(timeEndStr)); + + // Heap + m_resultsTable->setItem(i, HeapColumn, new NumericalTableWidgetItem(QString("0x%1").arg(event.heap, 0, 16), event.heap)); + + // Address + if (event.address != 0) + m_resultsTable->setItem(i, AddressColumn, new NumericalTableWidgetItem(QString("0x%1").arg(event.address, 0, 16), event.address)); + else + m_resultsTable->setItem(i, AddressColumn, new QTableWidgetItem("")); + + // Previous Address + if (event.previousAddress != 0) + m_resultsTable->setItem(i, PreviousAddressColumn, new NumericalTableWidgetItem(QString("0x%1").arg(event.previousAddress, 0, 16), event.previousAddress)); + else + m_resultsTable->setItem(i, PreviousAddressColumn, new QTableWidgetItem("")); + + // Size + if (event.size != 0) + m_resultsTable->setItem(i, SizeColumn, new NumericalTableWidgetItem(QString("0x%1").arg(event.size, 0, 16), event.size)); + else + m_resultsTable->setItem(i, SizeColumn, new QTableWidgetItem("")); + + // Base Address + if (event.baseAddress != 0) + m_resultsTable->setItem(i, BaseAddressColumn, new NumericalTableWidgetItem(QString("0x%1").arg(event.baseAddress, 0, 16), event.baseAddress)); + else + m_resultsTable->setItem(i, BaseAddressColumn, new QTableWidgetItem("")); + + // Flags + if (event.flags != 0) + m_resultsTable->setItem(i, FlagsColumn, new NumericalTableWidgetItem(QString("0x%1").arg(event.flags, 0, 16), event.flags)); + else + m_resultsTable->setItem(i, FlagsColumn, new QTableWidgetItem("")); + + // Result + m_resultsTable->setItem(i, ResultColumn, new NumericalTableWidgetItem(QString("0x%1").arg(event.result, 0, 16), event.result)); + + // Reserve Size + if (event.reserveSize != 0) + m_resultsTable->setItem(i, ReserveSizeColumn, new NumericalTableWidgetItem(QString("0x%1").arg(event.reserveSize, 0, 16), event.reserveSize)); + else + m_resultsTable->setItem(i, ReserveSizeColumn, new QTableWidgetItem("")); + + // Commit Size + if (event.commitSize != 0) + m_resultsTable->setItem(i, CommitSizeColumn, new NumericalTableWidgetItem(QString("0x%1").arg(event.commitSize, 0, 16), event.commitSize)); + else + m_resultsTable->setItem(i, CommitSizeColumn, new QTableWidgetItem("")); + + // Make Read Only + if (event.makeReadOnly != 0) + m_resultsTable->setItem(i, MakeReadOnlyColumn, new NumericalTableWidgetItem(QString("0x%1").arg(event.makeReadOnly, 0, 16), event.makeReadOnly)); + else + m_resultsTable->setItem(i, MakeReadOnlyColumn, new QTableWidgetItem("")); + + // Thread ID + m_resultsTable->setItem(i, ThreadIdColumn, new NumericalTableWidgetItem(QString::number(event.threadId), event.threadId)); + + // Unique Thread ID + m_resultsTable->setItem(i, UniqueThreadIdColumn, new NumericalTableWidgetItem(QString::number(event.uniqueThreadId), event.uniqueThreadId)); + + // Parameters + QStringList paramStrings; + for (const auto& param : event.parameters) + { + paramStrings << QString::fromStdString(param); + } + m_resultsTable->setItem(i, ParametersColumn, new QTableWidgetItem(paramStrings.join(", "))); + } + + updateStatus(QString("Found %1 heap objects").arg(events.size())); + } + catch (const std::exception& e) + { + updateStatus(QString("Error querying heap objects: %1").arg(e.what())); + LogError("Exception in TTD heap query: %s", e.what()); + } + + m_queryButton->setEnabled(true); +} + +void TTDHeapQueryWidget::clearResults() +{ + m_resultsTable->setRowCount(0); + updateStatus("Results cleared"); +} + +void TTDHeapQueryWidget::onCellDoubleClicked(int row, int column) +{ + if (!m_controller) + return; + + // Navigate to TimeStart for the selected heap event + if (column == TimeStartColumn) + { + auto* item = m_resultsTable->item(row, TimeStartColumn); + if (!item) + return; + + QString timeStr = item->text(); + QStringList parts = timeStr.split(':'); + if (parts.size() != 2) + return; + + bool ok1, ok2; + uint64_t sequence = parts[0].toULongLong(&ok1, 16); + uint64_t step = parts[1].toULongLong(&ok2, 16); + + if (ok1 && ok2) + { + TTDPosition position(sequence, step); + if (m_controller->SetTTDPosition(position)) + { + updateStatus(QString("Navigated to position %1:%2").arg(sequence, 0, 16).arg(step, 0, 16)); + } + else + { + updateStatus("Failed to navigate to position"); + } + } + } + // Navigate to TimeEnd for the selected heap event + else if (column == TimeEndColumn) + { + auto* item = m_resultsTable->item(row, TimeEndColumn); + if (!item) + return; + + QString timeStr = item->text(); + QStringList parts = timeStr.split(':'); + if (parts.size() != 2) + return; + + bool ok1, ok2; + uint64_t sequence = parts[0].toULongLong(&ok1, 16); + uint64_t step = parts[1].toULongLong(&ok2, 16); + + if (ok1 && ok2) + { + TTDPosition position(sequence, step); + if (m_controller->SetTTDPosition(position)) + { + updateStatus(QString("Navigated to position %1:%2").arg(sequence, 0, 16).arg(step, 0, 16)); + } + else + { + updateStatus("Failed to navigate to position"); + } + } + } +} + +bool TTDHeapQueryWidget::isUnused() const +{ + return m_resultsTable->rowCount() == 0; +} + +void TTDHeapQueryWidget::setupContextMenu() +{ + m_contextMenuManager = new ContextMenuManager(this); + m_menu = new Menu(); + + m_menu->addAction(new MenuAction("Column Visibility...", [=]() { showColumnVisibilityDialog(); })); + m_menu->addAction(new MenuAction("Reset Columns to Default", [=]() { resetColumnsToDefault(); })); + + m_contextMenuManager->setMenus(QList{m_menu}); +} + +void TTDHeapQueryWidget::setupUIActions() +{ + setupContextMenu(); + + m_actionHandler.setupActionHandler(this); + m_actionHandler.setActionDisplayName("Copy", "Copy"); + m_actionHandler.bindAction("Copy", UIAction([=]() { copy(); }, [=]() { return canCopy(); })); +} + +void TTDHeapQueryWidget::updateColumnVisibility() +{ + for (int i = 0; i < m_columnVisibility.size() && i < m_resultsTable->columnCount(); ++i) + { + m_resultsTable->setColumnHidden(i, !m_columnVisibility[i]); + } +} + +bool TTDHeapQueryWidget::canCopy() +{ + return m_resultsTable->selectedItems().size() > 0; +} + +void TTDHeapQueryWidget::contextMenuEvent(QContextMenuEvent* event) +{ + if (m_contextMenuManager) + m_contextMenuManager->show(m_menu, &m_actionHandler); +} + +void TTDHeapQueryWidget::showColumnVisibilityDialog() +{ + // Create a simple dialog to toggle column visibility + QDialog dialog(this); + dialog.setWindowTitle("Column Visibility"); + dialog.setModal(true); + + auto* layout = new QVBoxLayout(&dialog); + + QList checkboxes; + for (int i = 0; i < m_columnNames.size(); ++i) + { + auto* checkbox = new QCheckBox(m_columnNames[i]); + checkbox->setChecked(m_columnVisibility[i]); + checkboxes.append(checkbox); + layout->addWidget(checkbox); + } + + auto* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(buttonBox); + + connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + if (dialog.exec() == QDialog::Accepted) + { + for (int i = 0; i < checkboxes.size() && i < m_columnVisibility.size(); ++i) + { + m_columnVisibility[i] = checkboxes[i]->isChecked(); + } + updateColumnVisibility(); + } +} + +void TTDHeapQueryWidget::resetColumnsToDefault() +{ + // Reset to default visibility + m_columnVisibility.clear(); + m_columnVisibility << true << true << true << true << true // Index, Event Type, Action, Time Start, Time End + << true << true << false << true << false // Heap, Address, Previous Address, Size, Base Address + << false << true << false << false << false // Flags, Result, Reserve Size, Commit Size, Make Read Only + << false << false << false; // Thread ID, Unique Thread ID, Parameters + updateColumnVisibility(); +} + +void TTDHeapQueryWidget::showContextMenu(const QPoint& position) +{ + if (m_contextMenuManager) + m_contextMenuManager->show(m_menu, &m_actionHandler); +} + +void TTDHeapQueryWidget::copy() +{ + copySelectedRow(); +} + +void TTDHeapQueryWidget::copySelectedCell() +{ + auto selectedItems = m_resultsTable->selectedItems(); + if (selectedItems.isEmpty()) + return; + + QApplication::clipboard()->setText(selectedItems.first()->text()); +} + +void TTDHeapQueryWidget::copySelectedRow() +{ + auto selectedItems = m_resultsTable->selectedItems(); + if (selectedItems.isEmpty()) + return; + + int row = selectedItems.first()->row(); + QStringList rowData; + + for (int col = 0; col < m_resultsTable->columnCount(); ++col) + { + auto* item = m_resultsTable->item(row, col); + rowData << (item ? item->text() : ""); + } + + QApplication::clipboard()->setText(rowData.join("\t")); +} + +void TTDHeapQueryWidget::copyEntireTable() +{ + QStringList tableData; + + // Add header + QStringList headers; + for (int col = 0; col < m_resultsTable->columnCount(); ++col) + { + headers << m_resultsTable->horizontalHeaderItem(col)->text(); + } + tableData << headers.join("\t"); + + // Add rows + for (int row = 0; row < m_resultsTable->rowCount(); ++row) + { + QStringList rowData; + for (int col = 0; col < m_resultsTable->columnCount(); ++col) + { + auto* item = m_resultsTable->item(row, col); + rowData << (item ? item->text() : ""); + } + tableData << rowData.join("\t"); + } + + QApplication::clipboard()->setText(tableData.join("\n")); +} + +// TTDHeapWidget implementation (tab container) +TTDHeapWidget::TTDHeapWidget(QWidget* parent, BinaryViewRef data) + : QWidget(parent), m_data(data) +{ + m_controller = DebuggerController::GetController(m_data); + setupUI(); +} + +TTDHeapWidget::~TTDHeapWidget() +{ +} + +void TTDHeapWidget::setupUI() +{ + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + // Tab widget with new tab button + auto* headerWidget = new QWidget(); + auto* headerLayout = new QHBoxLayout(headerWidget); + headerLayout->setContentsMargins(0, 0, 0, 0); + + m_tabWidget = new QTabWidget(); + m_tabWidget->setTabsClosable(true); + m_tabWidget->setMovable(true); + + m_newTabButton = new QToolButton(); + m_newTabButton->setText("+"); + m_newTabButton->setToolTip("New Tab"); + m_newTabButton->setAutoRaise(true); + + headerLayout->addWidget(m_tabWidget, 1); + headerLayout->addWidget(m_newTabButton); + + layout->addWidget(headerWidget); + + // Create initial tab + createNewTab(); + + // Connect signals + connect(m_newTabButton, &QToolButton::clicked, this, &TTDHeapWidget::createNewTab); + connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &TTDHeapWidget::closeTab); +} + +void TTDHeapWidget::createNewTab() +{ + auto* queryWidget = new TTDHeapQueryWidget(this, m_data); + int index = m_tabWidget->addTab(queryWidget, "Heap Query"); + m_tabWidget->setCurrentIndex(index); +} + +void TTDHeapWidget::closeTab(int index) +{ + if (m_tabWidget->count() <= 1) + return; // Keep at least one tab + + QWidget* widget = m_tabWidget->widget(index); + m_tabWidget->removeTab(index); + delete widget; +} + +TTDHeapQueryWidget* TTDHeapWidget::getCurrentOrNewQueryWidget() +{ + auto* currentWidget = qobject_cast(m_tabWidget->currentWidget()); + if (!currentWidget || !currentWidget->isUnused()) + { + createNewTab(); + currentWidget = qobject_cast(m_tabWidget->currentWidget()); + } + return currentWidget; +} + +void TTDHeapWidget::performQuery() +{ + auto* queryWidget = getCurrentOrNewQueryWidget(); + if (queryWidget) + queryWidget->performQuery(); +} + +void TTDHeapWidget::performQueryInNewTab() +{ + createNewTab(); + auto* queryWidget = qobject_cast(m_tabWidget->currentWidget()); + if (queryWidget) + queryWidget->performQuery(); +} + +// TTDHeapSidebarWidget implementation +TTDHeapSidebarWidget::TTDHeapSidebarWidget(BinaryViewRef data) + : SidebarWidget("TTD Heap"), m_data(data) +{ + m_controller = DebuggerController::GetController(m_data); + m_heapWidget = new TTDHeapWidget(this, m_data); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_heapWidget); +} + +TTDHeapSidebarWidget::~TTDHeapSidebarWidget() +{ +} + +void TTDHeapSidebarWidget::performQuery() +{ + if (m_heapWidget) + m_heapWidget->performQuery(); +} + +void TTDHeapSidebarWidget::performQueryInNewTab() +{ + if (m_heapWidget) + m_heapWidget->performQueryInNewTab(); +} + +// TTDHeapWidgetType implementation +TTDHeapWidgetType::TTDHeapWidgetType() + : SidebarWidgetType(QIcon(":/debugger/ttd-heap").pixmap(QSize(64, 64)).toImage(), "TTD Heap") +{ +} + +SidebarWidget* TTDHeapWidgetType::createWidget(ViewFrame* frame, BinaryViewRef data) +{ + return new TTDHeapSidebarWidget(data); +} + +SidebarContentClassifier* TTDHeapWidgetType::contentClassifier(ViewFrame*, BinaryViewRef data) +{ + return new ActiveDebugSessionSidebarContentClassifier(data); +} + +#include "ttdheapwidget.moc" \ No newline at end of file diff --git a/ui/ttdheapwidget.h b/ui/ttdheapwidget.h new file mode 100644 index 00000000..053eec54 --- /dev/null +++ b/ui/ttdheapwidget.h @@ -0,0 +1,188 @@ +/* +Copyright 2020-2025 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "inttypes.h" +#include "binaryninjaapi.h" +#include "debuggerapi.h" +#include "viewframe.h" +#include "expandablegroup.h" +#include "debuggeruicommon.h" +#include "menus.h" +#include "uitypes.h" + +using namespace BinaryNinja; +using namespace BinaryNinjaDebuggerAPI; + +class TTDHeapQueryWidget : public QWidget +{ + Q_OBJECT + +public: + // Enum for logical column identification + enum LogicalColumn { + IndexColumn = 0, + EventTypeColumn, + ActionColumn, + TimeStartColumn, + TimeEndColumn, + HeapColumn, + AddressColumn, + PreviousAddressColumn, + SizeColumn, + BaseAddressColumn, + FlagsColumn, + ResultColumn, + ReserveSizeColumn, + CommitSizeColumn, + MakeReadOnlyColumn, + ThreadIdColumn, + UniqueThreadIdColumn, + ParametersColumn + }; + +private: + BinaryViewRef m_data; + DbgRef m_controller; + + // Input controls + QPushButton* m_queryButton; + QPushButton* m_clearButton; + + // Results table + QTableWidget* m_resultsTable; + + // Status label + QLabel* m_statusLabel; + + // Column visibility + QStringList m_columnNames; + QList m_columnVisibility; + + // UIAction support + UIActionHandler m_actionHandler; + ContextMenuManager* m_contextMenuManager; + Menu* m_menu; + + void setupUI(); + void setupTable(); + void updateStatus(const QString& message); + void setupContextMenu(); + void setupUIActions(); + void updateColumnVisibility(); + bool canCopy(); + + virtual void contextMenuEvent(QContextMenuEvent* event) override; + +public: + TTDHeapQueryWidget(QWidget* parent, BinaryViewRef data); + virtual ~TTDHeapQueryWidget(); + + // Method to execute query from context menu + void performQuery(); + + // Method to check if this tab is unused (no results and default parameters) + bool isUnused() const; + +private Q_SLOTS: + void clearResults(); + void onCellDoubleClicked(int row, int column); + void showColumnVisibilityDialog(); + void resetColumnsToDefault(); + void showContextMenu(const QPoint& position); + void copy(); + void copySelectedCell(); + void copySelectedRow(); + void copyEntireTable(); +}; + +class TTDHeapWidget : public QWidget +{ + Q_OBJECT + +private: + BinaryViewRef m_data; + DbgRef m_controller; + QTabWidget* m_tabWidget; + QToolButton* m_newTabButton; + + void setupUI(); + +public: + TTDHeapWidget(QWidget* parent, BinaryViewRef data); + virtual ~TTDHeapWidget(); + + // Method to get current query widget or create new tab + TTDHeapQueryWidget* getCurrentOrNewQueryWidget(); + void performQuery(); + void performQueryInNewTab(); + +private Q_SLOTS: + void createNewTab(); + void closeTab(int index); +}; + + +class TTDHeapSidebarWidget : public SidebarWidget +{ + Q_OBJECT + +private: + TTDHeapWidget* m_heapWidget; + BinaryViewRef m_data; + DbgRef m_controller; + +public: + TTDHeapSidebarWidget(BinaryViewRef data); + ~TTDHeapSidebarWidget(); + + // Method to access the TTD Heap widget for context menu actions + void performQuery(); + void performQueryInNewTab(); +}; + + +class TTDHeapWidgetType : public SidebarWidgetType +{ +public: + TTDHeapWidgetType(); + SidebarWidget* createWidget(ViewFrame* frame, BinaryViewRef data) override; + SidebarWidgetLocation defaultLocation() const override { return SidebarWidgetLocation::RightContent; } + SidebarContextSensitivity contextSensitivity() const override { return PerViewTypeSidebarContext; } + SidebarIconVisibility defaultIconVisibility() const override { return HideSidebarIconIfNoContent; } + SidebarContentClassifier* contentClassifier(ViewFrame*, BinaryViewRef) override; +}; \ No newline at end of file diff --git a/ui/ui.cpp b/ui/ui.cpp index 710f15e3..14ade57d 100644 --- a/ui/ui.cpp +++ b/ui/ui.cpp @@ -44,6 +44,7 @@ limitations under the License. #include "debuggerinfowidget.h" #include "ttdmemorywidget.h" #include "ttdcallswidget.h" +#include "ttdheapwidget.h" #include "ttdanalysisdialog.h" #include "timestampnavigationdialog.h" #include "freeversion.h" @@ -1678,6 +1679,7 @@ void GlobalDebuggerUI::InitializeUI() Sidebar::addSidebarWidgetType(new DebugInfoWidgetType()); Sidebar::addSidebarWidgetType(new TTDMemoryWidgetType()); Sidebar::addSidebarWidgetType(new TTDCallsWidgetType()); + Sidebar::addSidebarWidgetType(new TTDHeapWidgetType()); }