From e925cc480f7f3de0690a02f64e025e4a0dca509c Mon Sep 17 00:00:00 2001 From: AV <3045629+anvol@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:07:20 +0300 Subject: [PATCH 1/5] initial implementation of GDB MI with basic setup for stm32f4 GDB MI now implements most basic functionality Notify UI that we're connected and ready to serve data small fixes to handle quit cleanup feat(gdb): Refactor event handling and command execution - An event loop (`EventLoop`) running in a dedicated thread (`m_eventThread`) now handles all asynchronous output from GDB. This replaces the previous callback-based approach (`AsyncRecordHandler`). - A mutex (`m_gdbCommandMutex`) is now used exclusively within the `SendCommand` method, centralizing locking and preventing race conditions. Higher-level functions no longer manage the lock themselves. - The target's running state is now managed by a `std::atomic` (`m_targetRunningAtomic`) for safe access across threads. - Asynchronous commands (`-exec-run`, `-exec-step`, etc.) are now sent via a new `SendCommandAsync` method, which does not wait for a result, allowing the UI to remain responsive while the target is running. - Logging has been adjusted, changing many `LogInfo` calls to `LogDebug` to reduce default output verbosity. - The GDB-MI value parser (`MiValue::parse`) has been rewritten to be more robust and correctly handle complex GDB output formats. Add watch widget implementation for basic types MIParser fixes and cleanup Added unit test for MI parser Implemented StepOut function --- core/CMakeLists.txt | 10 + core/adapters/gdbmiadapter.cpp | 835 +++++++++++++++++++++++++++++++ core/adapters/gdbmiadapter.h | 120 +++++ core/adapters/gdbmiconnector.cpp | 575 +++++++++++++++++++++ core/adapters/gdbmiconnector.h | 92 ++++ core/debugger.cpp | 2 + core/tests/CMakeLists.txt | 46 ++ core/tests/miparser_test.cpp | 295 +++++++++++ 8 files changed, 1975 insertions(+) create mode 100644 core/adapters/gdbmiadapter.cpp create mode 100644 core/adapters/gdbmiadapter.h create mode 100644 core/adapters/gdbmiconnector.cpp create mode 100644 core/adapters/gdbmiconnector.h create mode 100644 core/tests/CMakeLists.txt create mode 100644 core/tests/miparser_test.cpp diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 51d60620..af1a3069 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -26,6 +26,10 @@ file(GLOB ADAPTER_SOURCES CONFIGURE_DEPENDS adapters/lldbadapter.h adapters/gdbadapter.cpp adapters/gdbadapter.h + adapters/gdbmiconnector.cpp + adapters/gdbmiconnector.h + adapters/gdbmiadapter.cpp + adapters/gdbmiadapter.h adapters/rspconnector.cpp adapters/rspconnector.h adapters/corelliumadapter.cpp @@ -221,3 +225,9 @@ if(APPLE) COMMAND codesign --deep --options runtime --entitlements ${PROJECT_SOURCE_DIR}/../test/entitlements.plist -s - ${PROJECT_SOURCE_DIR}/../test/binaries/Darwin-x86_64-signed/* ) endif() + +# Add core unit tests if requested +option(BUILD_CORE_TESTS "Build core unit tests" OFF) +if(BUILD_CORE_TESTS) + add_subdirectory(tests) +endif() diff --git a/core/adapters/gdbmiadapter.cpp b/core/adapters/gdbmiadapter.cpp new file mode 100644 index 00000000..2ea76ed8 --- /dev/null +++ b/core/adapters/gdbmiadapter.cpp @@ -0,0 +1,835 @@ +#include "gdbmiadapter.h" +#include +#include "../debuggercontroller.h" // Assuming this path is correct for your project structure +#include "../../cli/log.h" // For Log::print + +using namespace BinaryNinja; +using namespace BinaryNinjaDebugger; + +GdbMiAdapter::GdbMiAdapter(BinaryView* data) : DebugAdapter(data) { + m_lastStopReason = UnknownReason; + m_targetRunningAtomic.store(false, std::memory_order_release); +} + +GdbMiAdapter::~GdbMiAdapter() { + Stop(); +} + +intx::uint512 GdbMiAdapter::ParseGdbValue(const std::string& valueStr) +{ + if (valueStr.empty()) return 0; + try { + return intx::from_string(valueStr); + } catch(...) { + LogError("Failed to parse GDB value: \"%s\"", valueStr.c_str()); + return 0; + } +} + +// --- Helper to clear cache when target is resumed --- +void GdbMiAdapter::InvalidateCache() { + std::unique_lock lock(m_cacheMutex); + m_cachedThreads.clear(); + m_cachedRegisters.clear(); + m_cachedFrames.clear(); + // Do not clear m_watchList here, as we want to preserve the watch expressions +} + +// --- Internal methods to query GDB and fill the cache --- +void GdbMiAdapter::UpdateThreadList() { + auto result = m_mi->SendCommand("-thread-info"); + if (result.command != "done") { + LogError("Failed to get thread info: %s", result.fullLine.c_str()); + return; + } + + std::vector threads; + LogDebug("Thread info response: %s", result.payload.c_str()); + + // Try to parse the thread info - the response format varies by GDB version + auto value = MiValue::Parse(result.payload); + + // Check if we have a threads list in the response (newer GDB versions) + if (value.Exists("threads")) { + const auto& threadsList = value["threads"].GetList(); + LogDebug("Found %zu threads in thread-info response", threadsList.size()); + + for(const auto& threadVal : threadsList) { + try { + if (!threadVal.Exists("id")) { + LogError("Thread entry missing 'id' field"); + continue; + } + uint32_t tid = std::stoi(threadVal["id"].GetString()); + uint64_t pc = 0; + if (threadVal.Exists("frame.addr")) + { + try + { + pc = std::stoull(threadVal["frame.addr"].GetString(), nullptr, 16); + } + catch (...) + { + } + } + threads.emplace_back(tid, pc); + LogDebug("Added thread with id: %u", tid); + } catch(const std::exception& e) { + LogError("Failed to parse thread entry: %s", e.what()); + } catch(...) { + LogError("Unknown error parsing thread entry"); + } + } + } + // Fallback: Check if we have thread info directly in the payload (older GDB versions) + else if (result.payload.find("threads") != std::string::npos) { + threads.emplace_back(1); + LogDebug("Added fallback thread with id: 1"); + } + else { + LogError("No threads list in thread-info response. Payload: %s", result.payload.c_str()); + return; + } + + std::unique_lock cacheLock(m_cacheMutex); + m_cachedThreads = threads; + LogDebug("Updated thread list cache with %zu threads", threads.size()); +} + +void GdbMiAdapter::UpdateAllRegisters() { + if (m_registerNames.empty()) { + LogError("Cannot update registers: register names list is empty"); + return; + } + + auto result = m_mi->SendCommand("-data-list-register-values r"); + if (result.command != "done") { + LogError("Failed to get register values: %s", result.fullLine.c_str()); + return; + } + + std::unordered_map regs; + + auto gdbmiregisters = MiValue::Parse(result.payload); + if (!gdbmiregisters.IsDict() || !gdbmiregisters["register-values"].IsList()) + { + LogError("No register-values in response. Payload: %s", result.payload.c_str()); + return; + } + + for (int i; i(regs["pc"].m_value); + m_stackPointer = static_cast(regs["sp"].m_value); + } + + std::unique_lock cacheLock(m_cacheMutex); + m_cachedRegisters = regs; + LogInfo("Updated register cache with %zu registers", regs.size()); +} + +void GdbMiAdapter::UpdateStackFrames(uint32_t tid) { + if (GetActiveThreadId() != tid) { + SetActiveThreadId(tid); + } + auto result = m_mi->SendCommand("-stack-list-frames"); + if (result.command != "done") { + LogError("Failed to get stack frames: %s", result.fullLine.c_str()); + return; + } + + std::vector frames; + auto gdbmi_frames = MiValue::Parse(result.payload); + for (int i = 0; i < gdbmi_frames["stack"].size(); ++i) + { + auto parsed_frame = gdbmi_frames["stack"][i]; + auto debug_frame = DebugFrame(i, + std::stoull(parsed_frame["frame"]["addr"].GetString(), 0, 16), + 0, + 0, + parsed_frame["frame"]["func"].GetString(), + 0, + "n/a"); + frames.push_back(debug_frame); + } + + std::unique_lock cacheLock(m_cacheMutex); + m_cachedFrames[tid] = frames; + LogInfo("Updated stack frames cache with %zu frames for thread %u", frames.size(), tid); +} + +void GdbMiAdapter::AsyncRecordHandler(const MiRecord& record) +{ + if (record.command == "stopped") + { + // LogDebug(" stopped event"); + m_lastStopReason = GetStopReason(record); + + // Update TID (optional, not holding the event mutex) + auto value = MiValue::Parse(record.payload); + if (value.Exists("thread-id")) + { + try { + m_lastStopTid = std::stoi(value["thread-id"].GetString()); + m_currentTid = m_lastStopTid; + } catch(...) { LogError("GDBMI: error parsing thread-id"); } + } + + // Update target state BEFORE posting events + m_targetRunningAtomic.store(false, std::memory_order_release); + + // Kick a background refresh so we don’t block the reader + ScheduleStateRefresh(); + + m_eventCV.notify_all(); + } + else if (record.command == "running") + { + // LogDebug(" running event"); + InvalidateCache(); + + m_targetRunningAtomic.store(true, std::memory_order_release); + + DebuggerEvent event; + event.type = ResumeEventType; + PostDebuggerEvent(event); + + m_eventCV.notify_all(); + } + else if (record.type == '~' || record.type == '@' || record.type == '&' || record.type == '=') + { // Console stream output + std::string message; + std::string raw = record.command; + if (!record.payload.empty()) + raw += "," + record.payload; + + if (raw.length() > 2 && raw.front() == '"' && raw.back() == '"') + raw = raw.substr(1, raw.length() - 2); + + for (size_t i = 0; i < raw.length(); ++i) { + if (raw[i] == '\\' && i + 1 < raw.length()) { + switch (raw[i+1]) { + case 'n': message += '\n'; break; + case 'r': message += '\r'; break; + case 't': message += '\t'; break; + case '"': message += '"'; break; + case '\\': message += '\\'; break; + default: message += raw[i+1]; break; + } + i++; + } else { + message += raw[i]; + } + } + + DebuggerEvent event; + event.type = StdoutMessageEventType; + event.data.messageData.message = message; + PostDebuggerEvent(event); + } +} + +void GdbMiAdapter::ScheduleStateRefresh() +{ + // dispatch off-thread to avoid reader blocking + if (!m_connected || m_targetRunningAtomic) return; + std::thread([this]{ + // Serialize MI traffic with m_gdbCommandMutex (not the reader/event mutex)? + { + std::unique_lock lock(m_gdbCommandMutex); + UpdateThreadList(); + UpdateAllRegisters(); + UpdateStackFrames(m_currentTid); + } + + DebuggerEvent ev; + ev.type = AdapterStoppedEventType; + ev.data.targetStoppedData.reason = m_lastStopReason; + PostDebuggerEvent(ev); + }).detach(); +} + +DebugStopReason GdbMiAdapter::GetStopReason(const MiRecord& record) { + auto value = MiValue::Parse(record.payload); + if (value.Exists("reason")) { + const std::string& reason = value["reason"].GetString(); + if (reason == "breakpoint-hit") return Breakpoint; + if (reason == "end-stepping-range") return SingleStep; + if (reason == "exited-normally" || reason == "exited") return ProcessExited; + if (reason == "signal-received") return SignalInt; + } + return UnknownReason; +} + +std::string GdbMiAdapter::RunMonitorCommand(const std::string& command) { + if (!m_mi) return ""; + // Monitor commands don't use MI syntax, they use the console interpreter + auto result = m_mi->SendCommand("-interpreter-exec console \"monitor " + command + "\""); + // The result is usually printed to the console stream ('~' records), which is hard to + // capture synchronously. For now, we assume it worked if we get a 'done' back. + // A better implementation would buffer console output between commands. + return (result.command == "done") ? "success" : "error"; +} + +bool GdbMiAdapter::Connect(const std::string& server, uint32_t port) { + auto settings = GetAdapterSettings(); + BNSettingsScope scope = SettingsResourceScope; + auto data = GetData(); + auto gdbPath = settings->Get("gdb.path", data, &scope); + scope = SettingsResourceScope; + auto symbolFile = settings->Get("gdb.symbolFile", data, &scope); + scope = SettingsResourceScope; + auto inputFile = settings->Get("common.inputFile", data, &scope); + m_connected = false; + + if (gdbPath.empty()) return false; + + if (inputFile.empty()) inputFile = symbolFile; + + m_mi = std::make_unique(gdbPath, inputFile); + + // Set up async callback BEFORE starting GDB to avoid race conditions + m_mi->SetAsyncCallback([this](const MiRecord& record){ this->AsyncRecordHandler(record); }); + + if (!m_mi->Start()) return false; + + m_mi->SendCommand("-gdb-set mi-async on"); + m_mi->SendCommand("-gdb-set pagination off"); + m_mi->SendCommand("-gdb-set confirm off"); + m_mi->SendCommand("-enable-frame-filters"); + m_mi->SendCommand("-interpreter-exec console \"add-symbol-file "+symbolFile+"\""); + + m_mi->SendCommand("-file-exec-file " + inputFile); + std::string connectCmd = "-target-select extended-remote " + server + ":" + std::to_string(port); + + auto result = m_mi->SendCommand(connectCmd, 1000); + m_connected = (result.command == "connected"); + if (!m_connected) + { + LogError("Failed to connect to target"); + m_mi->Stop(); + m_mi.reset(); + return false; + } + + // Get architecture and register setup + LogInfo("Detecting target architecture..."); + + // Try multiple methods to detect architecture since $arch may return "void" on embedded targets + std::string detectedArch; + auto regListResult = m_mi->SendCommand("-data-list-register-names"); // we might need it for arch detection and then again later + // Method 1: Try $arch (may return "void" on some targets) + auto archResult = m_mi->SendCommand("-data-evaluate-expression $arch"); + + if (archResult.command == "done") + { + auto value = MiValue::Parse(archResult.payload); + std::string archStr = value["value"].GetString(); + + // Remove quotes if present + if (archStr.length() >= 2 && archStr.front() == '"' && archStr.back() == '"') + { + archStr = archStr.substr(1, archStr.length() - 2); + } + + LogInfo("Raw architecture string from $arch: %s", archStr.c_str()); + + if (archStr != "void" && !archStr.empty()) + { + detectedArch = archStr; + } + } + + // Method 2: If $arch failed or returned "void", try to detect from register names + if (detectedArch.empty()) + { + LogInfo("$arch returned void or empty, trying register-based detection..."); + + // Get register names first to help with architecture detection + if (regListResult.command == "done") + { + LogDebug("Register names for architecture detection: %s", regListResult.payload.c_str()); + + // Check for ARM Cortex-M registers (common in embedded) + if (regListResult.payload.find("r0") != std::string::npos && regListResult.payload.find("sp") != std::string::npos && regListResult.payload.find("lr") != std::string::npos && regListResult.payload.find("pc") != std::string::npos) + { + // Check for specific ARM Cortex-M registers + if (regListResult.payload.find("xpsr") != std::string::npos || regListResult.payload.find("primask") != std::string::npos || regListResult.payload.find("faultmask") != std::string::npos) + { + detectedArch = "armv7-m"; // Cortex-M + LogInfo("Detected ARM Cortex-M architecture from registers"); + } + else + { + detectedArch = "arm"; // Generic ARM + LogInfo("Detected generic ARM architecture from registers"); + } + } + // Check for x86 registers + else if (regListResult.payload.find("eax") != std::string::npos || regListResult.payload.find("rax") != std::string::npos) + { + detectedArch = "x86"; + LogInfo("Detected x86 architecture from registers"); + } + } + } + + // Method 3: If still not detected, use fallback based on common patterns + if (detectedArch.empty()) + { + LogInfo("Using fallback architecture detection..."); + + // Check the initial stop event for architecture hints + // From logs: arch="armv3m" appears in the stop event + if (m_lastStopReason != UnknownReason) + { + // We know we're dealing with an ARM target from the logs + detectedArch = "armv7-m"; // Default to Cortex-M for embedded + LogInfo("Using fallback ARM Cortex-M architecture"); + } + else + { + detectedArch = "arm"; // Final fallback + LogInfo("Using generic ARM architecture as final fallback"); + } + } + + // Set the final architecture + m_remoteArch = detectedArch; + LogInfo("Final detected remote architecture: %s", m_remoteArch.c_str()); + + // Get register names + if (regListResult.command == "done") + { + LogDebug("Register names response: %s", regListResult.payload.c_str()); + auto value = MiValue::Parse(regListResult.payload); + + // Check for register-names in different possible locations + if (value.Exists("register-names")) + { + for (const auto& regVal : value["register-names"].GetList()) + { + m_registerNames.push_back(regVal.GetString()); + } + LogInfo("Found %zu registers in register-names list", m_registerNames.size()); + } + else + { + LogError("No register-names in response. Payload: %s", regListResult.payload.c_str()); + } + } + else + { + LogError("Failed to get register names: %s", regListResult.fullLine.c_str()); + // Fallback for common embedded architectures + if (m_remoteArch.find("arm") != std::string::npos) + { + LogInfo("Using ARM register fallback"); + m_registerNames = { "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "sp", "lr", "pc", "xpsr" }; + } + } + + // AFTER we are connected and stopped, populate the cache for the first time. + LogInfo("Populating initial state cache..."); + ScheduleStateRefresh(); + + LogInfo("Applying breakpoints..."); + ApplyBreakpoints(); + + return true; +} + +// --- Empty implementations for unsupported actions --- +bool GdbMiAdapter::Execute(const std::string&, const LaunchConfigurations&) { LogWarn("GdbMiAdapter::Execute not implemented"); return false; } +bool GdbMiAdapter::ExecuteWithArgs(const std::string&, const std::string&, const std::string&, const LaunchConfigurations&) +{ + auto settings = GetAdapterSettings(); + BNSettingsScope scope = SettingsResourceScope; + auto data = GetData(); + auto server = settings->Get("connect.ipAddress", data, &scope); + scope = SettingsResourceScope; + auto port = static_cast(settings->Get("connect.port", data, &scope)); + if (server.empty() || port == 0) + { + LogError("Missing connection settings for restart."); + return false; + } + return Connect(server, port); +} +bool GdbMiAdapter::Attach(uint32_t) { + auto settings = GetAdapterSettings(); + BNSettingsScope scope = SettingsResourceScope; + auto data = GetData(); + auto server = settings->Get("connect.ipAddress", data, &scope); + scope = SettingsResourceScope; + auto port = static_cast(settings->Get("connect.port", data, &scope)); + if (server.empty() || port == 0) + { + LogError("Missing connection settings for restart."); + return false; + } + + return Connect(server, port); +} +std::vector GdbMiAdapter::GetProcessList() { LogWarn("GdbMiAdapter::GetProcessList not implemented"); return {}; } +bool GdbMiAdapter::SuspendThread(uint32_t) { LogWarn("GdbMiAdapter::SuspendThread not implemented"); return false; } +bool GdbMiAdapter::ResumeThread(uint32_t) { LogWarn("GdbMiAdapter::ResumeThread not implemented"); return false; } + +void GdbMiAdapter::Stop() +{ + if (m_mi && m_mi->IsRunning()) + { + LogDebug("GDB MI connector stopping..."); + m_mi->SetAsyncCallback(nullptr); + m_mi->Stop(); + m_mi.reset(); + LogDebug("GDB MI connector stopped."); + } + + // Clear all cached data + { + std::unique_lock lock(m_cacheMutex); + m_cachedThreads.clear(); + m_cachedRegisters.clear(); + m_cachedFrames.clear(); + } + + // Reset target state + m_targetRunningAtomic.store(false, std::memory_order_release); + +} + +bool GdbMiAdapter::Quit() +{ + Detach(); + Stop(); + m_connected = false; + + LogInfo("GDB MI adapter quit completed successfully"); + return true; +} + +bool GdbMiAdapter::Detach() { + if (m_mi && m_connected) m_mi->SendCommand("-target-detach"); + m_connected = false; + m_targetRunningAtomic.store(false); + + DebuggerEvent dbgevt; + dbgevt.type = DetachedEventType; + PostDebuggerEvent(dbgevt); + + return true; +} + +std::vector GdbMiAdapter::GetThreadList() { + std::unique_lock lock(m_cacheMutex); + return m_cachedThreads; +} + +std::unordered_map GdbMiAdapter::ReadAllRegisters() { + std::unique_lock lock(m_cacheMutex); + return m_cachedRegisters; +} + +DebugRegister GdbMiAdapter::ReadRegister(const std::string& reg) { + std::unique_lock lock(m_cacheMutex); + if (m_cachedRegisters.contains(reg)) return m_cachedRegisters[reg]; + + LogWarn("GdbMiAdapter::ReadRegister failed to retrieve '%s'", reg.c_str()); + return {}; +} + +std::vector GdbMiAdapter::GetFramesOfThread(uint32_t tid) { + std::unique_lock lock(m_cacheMutex); + if (m_cachedFrames.contains(tid)) + { + return m_cachedFrames[tid]; + } + + // If not cached, return an empty list for now and trigger a background refresh. + // The UI will be updated once the data is available via an event. + if (!m_targetRunningAtomic) + { + ScheduleStateRefresh(); + } + + return {}; +} + +uint32_t GdbMiAdapter::GetActiveThreadId() const { return m_currentTid; } + +DebugThread GdbMiAdapter::GetActiveThread() const { + GdbMiAdapter* self = const_cast(this); + uint64_t pc = self->GetInstructionOffset(); + return DebugThread(m_currentTid, pc); +} + +bool GdbMiAdapter::SetActiveThreadId(uint32_t tid) { + if (!m_mi) return false; + auto result = m_mi->SendCommand("-thread-select " + std::to_string(tid)); + if (result.command == "done") { + m_currentTid = tid; + return true; + } + return false; +} + +bool GdbMiAdapter::SetActiveThread(const DebugThread& thread) { return SetActiveThreadId(thread.m_tid); } + +DebugBreakpoint GdbMiAdapter::AddBreakpoint(std::uintptr_t address, unsigned long breakpoint_type) { + if (!m_mi) { return {}; } + + LogDebug("-break-insert -h *0x%lx", address); + auto result = m_mi->SendCommand(fmt::format("-break-insert -h *0x{:x}", address)); + if (result.command == "done") { + DebuggerEvent evt; + evt.type = BackendMessageEventType; + evt.data.messageData.message = result.command; + PostDebuggerEvent(evt); + + return DebugBreakpoint{address, 0, true}; + } + + LogWarn("Failed to set BP at %lu", address); + return {}; +} + +DebugBreakpoint GdbMiAdapter::AddBreakpoint(const ModuleNameAndOffset& address, unsigned long breakpoint_type) { + if (!m_mi) + { + if (std::ranges::find(m_pendingBreakpoints, address) == m_pendingBreakpoints.end()) + m_pendingBreakpoints.push_back(address); + } + else + { + uint64_t addr = address.offset + m_originalImageBase; + + AddBreakpoint(addr, breakpoint_type); + } + + return {}; +} + +bool GdbMiAdapter::RemoveBreakpoint(const DebugBreakpoint& breakpoint) { + if (!m_mi) return false; + m_mi->SendCommand(fmt::format("-break-delete *0x{:x}", breakpoint.m_address)); + return true; +} + +std::vector GdbMiAdapter::GetBreakpointList() const { LogWarn("GdbMiAdapter::GetBreakpointList not implemented"); return {}; } + +bool GdbMiAdapter::WriteRegister(const std::string& reg, intx::uint512 value) { + if (!m_mi) return false; + std::string cmd = "-gdb-set $" + reg + "=" + to_string(value); + auto result = m_mi->SendCommand(cmd); + return result.command == "done"; +} + +DataBuffer GdbMiAdapter::ReadMemory(std::uintptr_t address, size_t size) { + if (!m_mi) return {}; + + // embedded specifics: we can use 'info mem' to get list of memory regions available for reading. + // it's safe to assume 0x08000000 - 0x60000000 is good enough for most arm-cortex targets + if (address > 0x60000000) return {}; + if (address < 0x08000000) return {}; + + std::string cmd = fmt::format("-data-read-memory-bytes 0x{:x} {}", address, size); + auto result = m_mi->SendCommand(cmd); + if (result.command != "done") return {}; + + auto value = MiValue::Parse(result.payload); + std::string hex_contents = value["memory"][0]["contents"].GetString(); + DataBuffer buffer(hex_contents.length() / 2); + for(size_t i = 0; i < buffer.GetLength(); i++) { + buffer[i] = std::stoul(hex_contents.substr(i*2, 2), nullptr, 16); + } + return buffer; +} + +bool GdbMiAdapter::WriteMemory(std::uintptr_t address, const DataBuffer& buffer) { + if (!m_mi) return false; + std::string hex; + for(size_t i = 0; i < buffer.GetLength(); i++) { + hex += fmt::format("{:02x}", buffer[i]); + } + std::string cmd = fmt::format("-data-write-memory-bytes 0x{:x} \"{}\"", address, hex); + auto result = m_mi->SendCommand(cmd); + return result.command == "done"; +} + +std::vector GdbMiAdapter::GetModuleList() +{ + if (!m_mi) + return {}; + + std::vector modules; + Ref data = GetData(); + if (!data) + return {}; + + std::string name = data->GetFile()->GetOriginalFilename(); + modules.push_back(DebugModule("SRAM", "SRAM", 0x20000000, 0x00040000, true)); + modules.push_back(DebugModule(name, name, 0x08000000, 0x00100000, true)); + return modules; +} + +bool GdbMiAdapter::Go() +{ + if (!m_mi || m_targetRunningAtomic) return false; + + return (m_mi->SendCommand("-exec-continue").command == "running"); +} + +bool GdbMiAdapter::BreakInto() { + if (!m_mi || !m_targetRunningAtomic) return false; + + return (m_mi->SendCommand("-exec-interrupt").command == "done"); +} + +bool GdbMiAdapter::StepInto() { + if (!m_mi || m_targetRunningAtomic) return false; + + return (m_mi->SendCommand("-exec-step-instruction").command == "running"); +} + +bool GdbMiAdapter::StepOver() { + if (!m_mi || m_targetRunningAtomic) return false; + + return (m_mi->SendCommand("-exec-next-instruction").command != "running"); +} + +bool GdbMiAdapter::StepReturn() { + if (!m_mi || m_targetRunningAtomic) return false; + + return (m_mi->SendCommand("-exec-finish").command == "running"); +} + +uint64_t GdbMiAdapter::GetInstructionOffset() { + LogDebug("GdbMiAdapter::GetInstructionOffset = 0x%llX", m_instructionOffset); + return m_instructionOffset; +} + +uint64_t GdbMiAdapter::GetStackPointer() { + LogDebug("GdbMiAdapter::GetStackPointer = 0x%llX", m_stackPointer); + return m_stackPointer; +} + +std::string GdbMiAdapter::InvokeBackendCommand(const std::string& command) { + if (!m_mi) return "error, transport not ready"; + auto result = m_mi->SendCommand("-interpreter-exec console \"" + command + "\""); + return (result.command == "done") ? result.payload : "Error sending command."; +} + +uint64_t GdbMiAdapter::ExitCode() { return 0; } + +DebugStopReason GdbMiAdapter::StopReason() { return m_lastStopReason; } + +std::string GdbMiAdapter::GetTargetArchitecture() { return m_remoteArch; } + +bool GdbMiAdapter::SupportFeature(DebugAdapterCapacity feature) { + switch (feature) { + case DebugAdapterSupportStepOver: return true; + case DebugAdapterSupportModules: return true; + case DebugAdapterSupportThreads: return true; + case DebugAdapterSupportTTD: return false; + default: return false; + } +} + +// --- Adapter Type Registration --- +GdbMiAdapterType::GdbMiAdapterType() : DebugAdapterType("GDB MI") {} + +DebugAdapter* GdbMiAdapterType::Create(BinaryView* data) +{ + return new GdbMiAdapter(data); +} + +Ref GdbMiAdapterType::GetAdapterSettings() +{ + static Ref settings = RegisterAdapterSettings(); + return settings; +} + +Ref GdbMiAdapter::GetAdapterSettings() +{ + return GdbMiAdapterType::GetAdapterSettings(); +} + +Ref GdbMiAdapterType::RegisterAdapterSettings() +{ + Ref settings = Settings::Instance("GdbMiAdapterSettings"); + settings->SetResourceId("gdb_mi_adapter_settings"); + settings->RegisterSetting("gdb.path", R"({ + "title": "GDB Executable Path", + "type": "string", "default": "gdb-multiarch", + "description": "Path to the GDB executable e.g., gdb-multiarch, arm-none-eabi-gdb.", + "uiSelectionAction": "file" + })"); + settings->RegisterSetting("common.inputFile", + R"({ + "title" : "Input File", + "type" : "string", + "default" : "", + "description" : "Input file to use to find the base address of the binary view", + "readOnly" : false, + "uiSelectionAction" : "file" + })"); + + settings->RegisterSetting("connect.ipAddress", + R"({ + "title" : "IP Address", + "type" : "string", + "default" : "127.0.0.1", + "description" : "IP address of the debug stub to connect to", + "readOnly" : false + })"); + settings->RegisterSetting("connect.port", + R"({ + "title" : "Port", + "type" : "number", + "default" : 3333, + "minValue" : 0, + "maxValue" : 65535, + "description" : "Port of the debug stub to connect to", + "readOnly" : false + })"); + settings->RegisterSetting("gdb.symbolFile", R"({ + "title": "Symbol File, optional", + "type": "string", "default": "", + "description": "Path to the ELF file with DWARF debug info for the target.", + "uiSelectionAction": "file" + })"); + + return settings; +} + +void GdbMiAdapter::ApplyBreakpoints() +{ + for (const auto& bp : m_pendingBreakpoints) + { + AddBreakpoint(bp, 0); + } + m_pendingBreakpoints.clear(); +} + + +void BinaryNinjaDebugger::InitGdbMiAdapterType() +{ + static GdbMiAdapterType miType; + DebugAdapterType::Register(&miType); +} diff --git a/core/adapters/gdbmiadapter.h b/core/adapters/gdbmiadapter.h new file mode 100644 index 00000000..9d182448 --- /dev/null +++ b/core/adapters/gdbmiadapter.h @@ -0,0 +1,120 @@ +#pragma once +#include "gdbmiconnector.h" +#include "../debugadapter.h" +#include "../debugadaptertype.h" +#include "../../vendor/intx/intx.hpp" + +class GdbMiAdapter : public BinaryNinjaDebugger::DebugAdapter +{ +private: + std::unique_ptr m_mi; + uint64_t m_lastStopTid = 1; + uint64_t m_currentTid = 1; + bool m_connected = false; + std::string m_remoteArch; + std::vector m_registerNames; // In GDB's order + BinaryNinjaDebugger::DebugStopReason m_lastStopReason; + std::atomic m_targetRunningAtomic{false}; + + std::mutex m_eventMutex; + std::mutex m_gdbCommandMutex; + std::condition_variable m_eventCV; + + // --- Cached Target State --- + std::mutex m_cacheMutex; // To protect access to cached data + std::vector m_cachedThreads; + std::unordered_map m_cachedRegisters; + std::map> m_cachedFrames; // tid -> frames + + void UpdateThreadList(); + void UpdateAllRegisters(); + void UpdateStackFrames(uint32_t tid); + + void InvalidateCache(); // Helper to clear the cache when the target runs + + void AsyncRecordHandler(const MiRecord& record); + void ScheduleStateRefresh(); + BinaryNinjaDebugger::DebugStopReason GetStopReason(const MiRecord& record); + static intx::uint512 ParseGdbValue(const std::string& valueStr); + + std::string RunMonitorCommand(const std::string& command); + void ApplyBreakpoints(); + std::vector m_pendingBreakpoints {}; + +public: + GdbMiAdapter(BinaryView* data); + ~GdbMiAdapter() override; + + uint64_t m_instructionOffset; + uint64_t m_stackPointer; + // --- Overridden Virtual Functions --- + // All functions that override a virtual function in DebugAdapter should have `override`. + + bool Execute(const std::string& path, const BinaryNinjaDebugger::LaunchConfigurations& configs) override; + bool ExecuteWithArgs(const std::string& path, const std::string& args, const std::string& workingDir, const BinaryNinjaDebugger::LaunchConfigurations& configs) override; + bool Attach(uint32_t pid) override; + bool Connect(const std::string& server, uint32_t port) override; + bool Detach() override; + bool Quit() override; + void Stop(); + + std::vector GetProcessList() override; + std::vector GetThreadList() override; + BinaryNinjaDebugger::DebugThread GetActiveThread() const override; + uint32_t GetActiveThreadId() const override; + bool SetActiveThread(const BinaryNinjaDebugger::DebugThread& thread) override; + bool SetActiveThreadId(uint32_t tid) override; + bool SuspendThread(uint32_t tid) override; + bool ResumeThread(uint32_t tid) override; + + std::vector GetFramesOfThread(uint32_t tid) override; + BinaryNinjaDebugger::DebugBreakpoint AddBreakpoint(std::uintptr_t address, unsigned long breakpoint_type) override; + BinaryNinjaDebugger::DebugBreakpoint AddBreakpoint(const BinaryNinjaDebugger::ModuleNameAndOffset& address, unsigned long breakpoint_type) override; + bool RemoveBreakpoint(const BinaryNinjaDebugger::DebugBreakpoint& breakpoint) override; + std::vector GetBreakpointList() const override; + + std::unordered_map ReadAllRegisters() override; + BinaryNinjaDebugger::DebugRegister ReadRegister(const std::string& reg) override; + bool WriteRegister(const std::string& reg, intx::uint512 value) override; + + BinaryNinja::DataBuffer ReadMemory(std::uintptr_t address, size_t size) override; + bool WriteMemory(std::uintptr_t address, const BinaryNinja::DataBuffer& buffer) override; + + std::vector GetModuleList() override; + std::string GetTargetArchitecture() override; + BinaryNinjaDebugger::DebugStopReason StopReason() override; + uint64_t ExitCode() override; + + bool BreakInto() override; + bool Go() override; + bool StepInto() override; + bool StepOver() override; + bool StepReturn() override; + + std::string InvokeBackendCommand(const std::string& command) override; + uint64_t GetInstructionOffset() override; + uint64_t GetStackPointer() override; + bool SupportFeature(BinaryNinjaDebugger::DebugAdapterCapacity feature) override; + + Ref GetAdapterSettings() override; +}; + + +// --- GdbMiAdapterType Declaration --- +class GdbMiAdapterType : public BinaryNinjaDebugger::DebugAdapterType + { + public: + GdbMiAdapterType(); + BinaryNinjaDebugger::DebugAdapter* Create(BinaryView* data) override; + bool IsValidForData(BinaryView* data) override { return true; } + bool CanConnect(BinaryView* data) override { return true; } + bool CanExecute(BinaryView* data) override { return false; } + static Ref GetAdapterSettings(); + + private: + static Ref RegisterAdapterSettings(); + }; + +namespace BinaryNinjaDebugger { + void InitGdbMiAdapterType(); +} diff --git a/core/adapters/gdbmiconnector.cpp b/core/adapters/gdbmiconnector.cpp new file mode 100644 index 00000000..9ce4bc26 --- /dev/null +++ b/core/adapters/gdbmiconnector.cpp @@ -0,0 +1,575 @@ +#include "gdbmiconnector.h" +#include +#include +#include +#include +#ifdef WIN32 +#else +#include +#include +// For Linux/macOS, we need the environment variables +extern char** environ; +#endif + +using namespace BinaryNinja; + +// MiValue implementation (a simple recursive-descent parser for GDB MI results) +MiValue::MiValue(const std::string& str) : m_string(str) {} +const MiValue& MiValue::operator[](const std::string& key) const +{ + static MiValue empty; + auto it = m_dict.find(key); + return (it != m_dict.end()) ? it->second : empty; +} +const MiValue& MiValue::operator[](size_t index) const +{ + static MiValue empty; + return (index < m_list.size()) ? m_list[index] : empty; +} +const std::string& MiValue::GetString() const { return m_string; } +const std::vector& MiValue::GetList() const { return m_list; } +const std::map& MiValue::GetDict() const { return m_dict; } +size_t MiValue::size() const { return m_isList ? m_list.size() : m_dict.size(); } +bool MiValue::Exists(const std::string& key) const { return m_dict.contains(key); } +MiValue MiValue::Parse(const std::string& data) +{ + MiValue result; + size_t pos = 0; + + auto skip_ws = [&]() { + while (pos < data.size() && isspace(data[pos])) + pos++; + }; + + auto parse_string = [&]() -> std::string { + if (data[pos] != '"') + return ""; + pos++; + std::string str; + while (pos < data.size() && data[pos] != '"') + { + if (data[pos] == '\\' && pos + 1 < data.size()) + { + str += data[pos + 1]; + pos += 2; + } + else + { + str += data[pos]; + pos++; + } + } + if (pos < data.size()) + pos++; // Skip closing quote + return str; + }; + + std::function parse_value = [&]() -> MiValue { + skip_ws(); + if (pos >= data.size()) + return MiValue(""); + if (data[pos] == '"') + return MiValue(parse_string()); + if (data[pos] == '{') + { + pos++; + MiValue dict; + dict.m_isDict = true; + while (pos < data.size() && data[pos] != '}') + { + skip_ws(); + size_t eq = data.find('=', pos); + if (eq != std::string::npos) + { + auto key = data.substr(pos, eq - pos); + pos = eq + 1; + dict.m_dict[key] = parse_value(); + skip_ws(); + if (pos < data.size() && data[pos] == ',') + pos++; + } + else + { + break; + } + } + if (pos < data.size()) + pos++; // Skip '}' + return dict; + } + if (data[pos] == '[') + { + pos++; + MiValue list; + list.m_isList = true; + while (pos < data.size() && data[pos] != ']') + { + skip_ws(); + + // Check if this is a child=value pattern (common in GDB MI arrays) + size_t child_end = data.find('=', pos); + size_t next_comma = data.find(',', pos); + size_t next_bracket = data.find(']', pos); + + if (child_end != std::string::npos && + child_end < next_comma && child_end < next_bracket && child_end - pos > 0 && + data[pos] != '{' && data[pos] != '[') // Don't apply to dicts or arrays + { + // Extract the key (e.g., "child") + std::string key = data.substr(pos, child_end - pos); + pos = child_end + 1; // Skip '=' + + // Parse the value + MiValue value = parse_value(); + + // Create a dictionary for this key-value pair + MiValue dict; + dict.m_isDict = true; + dict.m_dict[key] = value; + list.m_list.push_back(dict); + } + else + { + // Normal array element + list.m_list.push_back(parse_value()); + } + + skip_ws(); + if (pos < data.size() && data[pos] == ',') + pos++; + } + if (pos < data.size()) + pos++; // Skip ']' + return list; + } + // This is not a valid MI value. It might be a raw string. + // For now, we just log it and return an empty value. + size_t end = data.find_first_of(",}]", pos); + if (end == std::string::npos) + end = data.size(); + LogDebug("Raw MI value: %s", data.substr(pos, end - pos).c_str()); + result = MiValue(data.substr(pos, end - pos)); + pos = end; + return result; + }; + + // Main parsing logic starts here + result.m_isDict = true; + while (pos < data.size()) + { + skip_ws(); + size_t eq = data.find('=', pos); + if (eq != std::string::npos) + { + auto key = data.substr(pos, eq - pos); + pos = eq + 1; + result.m_dict[key] = parse_value(); + skip_ws(); + if (pos < data.size() && data[pos] == ',') + pos++; + } + else + { + if (data[pos] == '"') + return MiValue(parse_string()); + // Fallback, just give it as is + return MiValue(data.substr(pos, data.size())); + } + } + return result; +} + +GdbMiConnector::GdbMiConnector(const std::string& gdbPath, const std::string& targetExecutable) + : m_gdbPath(gdbPath), m_targetExecutable(targetExecutable) +{ +} + +GdbMiConnector::~GdbMiConnector() +{ + Stop(); +} + +void GdbMiConnector::Stop() +{ + if (!m_running) + return; + + LogInfo("GDB MI connector: Starting graceful shutdown..."); + + // Send a quit command to GDB and wait for a response + SendCommand("-gdb-exit", 100); + + m_running = false; + + // Give GDB a moment to shut down + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Wait for the reader thread to finish, which should happen naturally now + if (m_readerThread.joinable()) + { + LogInfo("GDB MI connector: Waiting for reader thread to finish..."); + m_readerThread.join(); + LogInfo("GDB MI connector: Reader thread finished"); + } + +#ifdef WIN32 + CloseHandle(m_gdb_stdin_write); + CloseHandle(m_gdb_stdout_read); + CloseHandle(m_pi.hProcess); + CloseHandle(m_pi.hThread); +#else + close(m_gdb_stdin_write); + close(m_gdb_stdout_read); + if (m_pid > 0) + { + int status; + // Check if the process is still around, but don't hang waiting for it + if (waitpid(m_pid, &status, WNOHANG) == 0) + { + // If it's still there, it's likely a zombie, so we can kill it. + // But a graceful shutdown should prevent this. + LogWarn("GDB process (PID: %d) did not exit gracefully, forcing termination.", m_pid); + kill(m_pid, SIGKILL); + waitpid(m_pid, &status, 0); + } + else + { + LogInfo("GDB process (PID: %d) exited gracefully.", m_pid); + } + } +#endif + + LogInfo("GDB MI connector: Shutdown completed"); +} + +bool GdbMiConnector::Start() +{ + if (m_running) + return true; + +#ifdef WIN32 + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + HANDLE gdb_stdout_write = NULL; + HANDLE gdb_stdin_read = NULL; + + if (!CreatePipe(&m_gdb_stdout_read, &gdb_stdout_write, &saAttr, 0)) return false; + if (!SetHandleInformation(m_gdb_stdout_read, HANDLE_FLAG_INHERIT, 0)) return false; + if (!CreatePipe(&gdb_stdin_read, &m_gdb_stdin_write, &saAttr, 0)) return false; + if (!SetHandleInformation(m_gdb_stdin_write, HANDLE_FLAG_INHERIT, 0)) return false; + + STARTUPINFOA si; + ZeroMemory(&si, sizeof(STARTUPINFOA)); + si.cb = sizeof(STARTUPINFOA); + si.hStdError = gdb_stdout_write; + si.hStdOutput = gdb_stdout_write; + si.hStdInput = gdb_stdin_read; + si.dwFlags |= STARTF_USESTDHANDLES; + + std::string cmd = m_gdbPath + " -q --interpreter=mi2"; + if (!m_targetExecutable.empty()) { + cmd += " \"" + m_targetExecutable + "\""; + } + + if (!CreateProcessA(NULL, (LPSTR)cmd.c_str(), NULL, NULL, TRUE, 0, NULL, NULL, &si, &m_pi)) + { + CloseHandle(m_gdb_stdout_read); + CloseHandle(gdb_stdout_write); + CloseHandle(gdb_stdin_read); + CloseHandle(m_gdb_stdin_write); + return false; + } + CloseHandle(gdb_stdout_write); + CloseHandle(gdb_stdin_read); +#else + int gdb_stdin_pipe[2]; + int gdb_stdout_pipe[2]; + + if (pipe(gdb_stdin_pipe) != 0 || pipe(gdb_stdout_pipe) != 0) return false; + + std::vector argv; + std::string gdbPathCopy = m_gdbPath; + std::string miArg = "-q"; + std::string miArg2 = "--interpreter=mi2"; + std::string targetCopy = m_targetExecutable; + + argv.push_back(const_cast(gdbPathCopy.c_str())); + argv.push_back(const_cast(miArg.c_str())); + argv.push_back(const_cast(miArg2.c_str())); + if (!m_targetExecutable.empty()) { + argv.push_back(const_cast(targetCopy.c_str())); + } + argv.push_back(nullptr); + + posix_spawn_file_actions_t file_actions; + posix_spawn_file_actions_init(&file_actions); + posix_spawn_file_actions_addclose(&file_actions, gdb_stdin_pipe[1]); + posix_spawn_file_actions_addclose(&file_actions, gdb_stdout_pipe[0]); + posix_spawn_file_actions_adddup2(&file_actions, gdb_stdin_pipe[0], STDIN_FILENO); + posix_spawn_file_actions_adddup2(&file_actions, gdb_stdout_pipe[1], STDOUT_FILENO); + posix_spawn_file_actions_adddup2(&file_actions, gdb_stdout_pipe[1], STDERR_FILENO); + + int result = posix_spawn(&m_pid, m_gdbPath.c_str(), &file_actions, nullptr, argv.data(), environ); + + close(gdb_stdin_pipe[0]); + close(gdb_stdout_pipe[1]); + + if (result != 0) { + m_pid = -1; + close(gdb_stdin_pipe[1]); + close(gdb_stdout_pipe[0]); + return false; + } + + m_gdb_stdin_write = gdb_stdin_pipe[1]; + m_gdb_stdout_read = gdb_stdout_pipe[0]; +#endif + + m_running = true; + m_readerThread = std::thread(&GdbMiConnector::ReaderThread, this); + return true; +} + +MiRecord GdbMiConnector::SendCommand(const std::string& command, int timeout_ms) +{ + if (!m_running) return {}; + + if (std::this_thread::get_id() == m_readerThread.get_id()) { + LogError("SendCommand called from reader thread; would deadlock"); + return {}; + } + + long token = m_nextToken++; + std::string fullCommand = std::to_string(token) + command + "\n"; + + + #ifdef WIN32 + DWORD bytesWritten; + if (!WriteFile(m_gdb_stdin_write, fullCommand.c_str(), fullCommand.length(), &bytesWritten, NULL)) + return {}; + #else + if (write(m_gdb_stdin_write, fullCommand.c_str(), fullCommand.length()) < 0) + return {}; + #endif + + std::unique_lock lock(m_mutex); + LogDebug("GDB->: %s", fullCommand.c_str()); + if (m_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), [&] { return m_responses.count(token); })) + { + MiRecord record = m_responses[token]; + m_responses.erase(token); + return record; + } + + + + return {}; +} + +void GdbMiConnector::ReaderThread() +{ + std::string currentLine; + char buffer[4096]; + +#ifndef WIN32 + // Make the read fd non-blocking so we can drain per select wakeup + int flags = fcntl(m_gdb_stdout_read, F_GETFL, 0); + fcntl(m_gdb_stdout_read, F_SETFL, flags | O_NONBLOCK); + + while (m_running) + { + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(m_gdb_stdout_read, &rfds); + + struct timeval tv = {0}; + tv.tv_sec = 1; + tv.tv_usec = 0; + + int retval = select(m_gdb_stdout_read + 1, &rfds, nullptr, nullptr, &tv); + if (retval == -1) + { + LogWarn("Lost connection to GDB"); + m_running = false; + continue; + } + if (retval == 0) + { + continue; + } + + for (;;) + { + ssize_t n = read(m_gdb_stdout_read, buffer, sizeof(buffer)); + buffer[n] = 0; + if (n > 0) + { + // Frame by CR or LF; strip CRs + for (ssize_t i = 0; i < n; ++i) + { + char c = buffer[i]; + + if (c == '\r' || c == '\n') + { + if (!currentLine.empty()) + { + // Trim any leftover CRs or spaces from ends + size_t start = 0; + while (start < currentLine.size() && + (currentLine[start] == '\r' || currentLine[start] == ' ' || currentLine[start] == '\t')) + ++start; + size_t end = currentLine.size(); + while (end > start && + (currentLine[end - 1] == '\r' || currentLine[end - 1] == ' ' || currentLine[end - 1] == '\t')) + --end; + + std::string line = currentLine.substr(start, end - start); + + if (!line.empty()) + { + MiRecord record = ParseLine(line); + if (record.token.has_value() && record.type == '^') + { + std::unique_lock lock(m_mutex); + // LogDebug("Notify about response %ld", *record.token); + m_responses[*record.token] = record; + m_cv.notify_all(); + } + else if (m_asyncCallback && record.type != '^') + { + // Do not block the reader; just forward + m_asyncCallback(record); + } + } + } + currentLine.clear(); + } + else + { + if (c != '\0') // ignore NULs just in case + currentLine += c; + } + } + continue; // try reading more (drain) + } + + if (n == -1 && errno == EAGAIN) + break; // no more data for now + + if (n == 0) + { + // EOF + m_running = false; + break; + } + + // n == -1 and not EAGAIN + LogError("read error from GDB: %d", errno); + m_running = false; + break; + } + } +#else + DWORD bytesRead; + while (m_running && ReadFile(m_gdb_stdout_read, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead > 0) + { + // Similar CR/LF framing on Windows + for (DWORD i = 0; i < bytesRead; ++i) + { + char c = buffer[i]; + if (c == '\r' || c == '\n') + { + if (!currentLine.empty()) + { + // Trim CR/space + size_t start = 0; + while (start < currentLine.size() && + (currentLine[start] == '\r' || currentLine[start] == ' ' || currentLine[start] == '\t')) + ++start; + size_t end = currentLine.size(); + while (end > start && + (currentLine[end - 1] == '\r' || currentLine[end - 1] == ' ' || currentLine[end - 1] == '\t')) + --end; + std::string line = currentLine.substr(start, end - start); + + if (!line.empty()) + { + MiRecord record = ParseLine(line); + if (record.token.has_value() && record.type == '^') + { + std::unique_lock lock(m_mutex); + LogDebug("Notify about response %ld", *record.token); + m_responses[*record.token] = record; + m_cv.notify_all(); + } + else if (m_asyncCallback) + { + m_asyncCallback(record); + } + } + } + currentLine.clear(); + } + else + { + if (c != '\0') + currentLine += c; + } + } + } +#endif +} + +MiRecord GdbMiConnector::ParseLine(const std::string &line) { + LogDebug("GDB<-: %s", line.c_str()); + + MiRecord record; + record.fullLine = line; + + // Normalize CRLF + std::string s = line; + if (!s.empty() && s.back() == '\r') + s.pop_back(); + + if (s == "(gdb)") { + m_gdbReady = true; + return record; + } + + size_t pos = 0; + + // Parse optional token + if (pos < s.size() && isdigit(static_cast(s[pos]))) { + try { + record.token = std::stol(s, &pos); + // LogDebug("Got response for %ld", *record.token); + } catch (...) { + LogWarn("Can't parse token in line: %s", s.c_str()); + pos = 0; + record.token.reset(); + } + } + + if (pos >= s.size()) + return record; + + // Parse record type (^, *, +, ~, @, &, =) + record.type = s[pos++]; + + // Result-record: token? '^' result-class [',' results] + // Stream/async output doesn't have a result-class, but we still parse the remainder consistently + if (pos <= s.size()) { + size_t comma = s.find(',', pos); + if (comma != std::string::npos) { + record.command = s.substr(pos, comma - pos); // result-class or stream text prefix + record.payload = s.substr(comma + 1); // results or rest + } else { + record.command = s.substr(pos); + } + } + + return record; +} diff --git a/core/adapters/gdbmiconnector.h b/core/adapters/gdbmiconnector.h new file mode 100644 index 00000000..7dff3c46 --- /dev/null +++ b/core/adapters/gdbmiconnector.h @@ -0,0 +1,92 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef WIN32 +#include +#else +#include +#include +#endif + +// Represents a parsed GDB MI record +struct MiRecord +{ + std::string fullLine; // 1^done,threads=[{...}] + std::optional token; // 1,2,3,4,5... (autoincremented correlation counter) + char type; // '^', '*', '+', '~', '@', '&' + std::string command; // "done", "error", "stopped", "running" + std::string payload; // threads=[{...}] +}; +// Helper class to parse MI key-value pairs +class MiValue +{ + std::map m_dict; + std::vector m_list; + std::string m_string; + bool m_isList = false; + bool m_isDict = false; + +public: + MiValue() = default; + MiValue(const std::string& str); + static MiValue Parse(const std::string& data); + + const MiValue& operator[](const std::string& key) const; + const MiValue& operator[](size_t index) const; + const std::string& GetString() const; + const std::vector& GetList() const; + const std::map& GetDict() const; + size_t size() const; + bool IsList() const { return m_isList; } + bool IsDict() const { return m_isDict; } + bool IsString() const { return !m_isList && !m_isDict; } + bool Exists(const std::string& key) const; +}; + +class GdbMiConnector +{ + std::string m_gdbPath; + std::string m_targetExecutable; + std::thread m_readerThread; + std::mutex m_mutex; + std::condition_variable m_cv; + std::map m_responses; + std::queue m_asyncRecords; + long m_nextToken = 1; + bool m_running = false; + bool m_gdbReady = false; + +#ifdef WIN32 + PROCESS_INFORMATION m_pi; + HANDLE m_gdb_stdin_write = NULL; + HANDLE m_gdb_stdout_read = NULL; +#else + pid_t m_pid = -1; + int m_gdb_stdin_write = -1; + int m_gdb_stdout_read = -1; +#endif + std::function m_asyncCallback; // ADDED: Callback for async records + + void ReaderThread(); + MiRecord ParseLine(const std::string& line); + +public: + GdbMiConnector(const std::string& gdbPath, const std::string& targetExecutable); + ~GdbMiConnector(); + + void SetAsyncCallback(std::function cb) { m_asyncCallback = cb; } + + bool Start(); + void Stop(); + bool IsRunning() const { return m_running; } + + // Synchronously send a command and wait for its result record (^done, ^error, etc.) + MiRecord SendCommand(const std::string& command, int timeout_ms = 1000); +}; \ No newline at end of file diff --git a/core/debugger.cpp b/core/debugger.cpp index 47ce5e27..159da5c3 100644 --- a/core/debugger.cpp +++ b/core/debugger.cpp @@ -16,6 +16,7 @@ limitations under the License. #include #include "adapters/gdbadapter.h" +#include "adapters/gdbmiadapter.h" #include "adapters/lldbrspadapter.h" #include "adapters/lldbadapter.h" #include "adapters/corelliumadapter.h" @@ -50,6 +51,7 @@ void InitDebugAdapterTypes() InitCorelliumAdapterType(); InitGdbAdapterType(); + InitGdbMiAdapterType(); InitLldbAdapterType(); InitEsrevenAdapterType(); InitLldbCoreDumpAdapterType(); diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt new file mode 100644 index 00000000..97b1538a --- /dev/null +++ b/core/tests/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.13 FATAL_ERROR) + +# Core unit tests +project(core-tests) + +include(FetchContent) + +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip + DOWNLOAD_EXTRACT_TIMESTAMP TRUE +) + +FetchContent_MakeAvailable(googletest) +# Find required packages +#find_package(GTest REQUIRED) + +# Include directories +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_SOURCE_DIR}/../.. +) + +enable_testing() + +# Create test executable +add_executable(miparser_test miparser_test.cpp) +target_compile_features(miparser_test PRIVATE cxx_std_20) +set_target_properties(miparser_test PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON +) + +# Link dependencies +target_link_libraries(miparser_test + GTest::gtest + debuggercore # Link against the core library +) + +target_sources(miparser_test PRIVATE ../adapters/gdbmiconnector.cpp) + +# Add test to CTest +include(GoogleTest) +gtest_discover_tests(miparser_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) diff --git a/core/tests/miparser_test.cpp b/core/tests/miparser_test.cpp new file mode 100644 index 00000000..5435c1f9 --- /dev/null +++ b/core/tests/miparser_test.cpp @@ -0,0 +1,295 @@ +#include +#include "../adapters/gdbmiconnector.h" + +// Remove Binary Ninja dependencies for standalone test +namespace BinaryNinja { + static void LogDebug(const char* format, ...) { + // Mock implementation - do nothing for tests + } + static void LogInfo(const char* format, ...) { + // Mock implementation - do nothing for tests + } + static void LogWarn(const char* format, ...) { + // Mock implementation - do nothing for tests + } + static void LogError(const char* format, ...) { + // Mock implementation - do nothing for tests + } +} + +class MiParserTest : public ::testing::Test { +protected: + void SetUp() override { + // Initialize Binary Ninja core if needed + } + + void TearDown() override { + // Clean up if needed + } +}; + +// Test basic string parsing +TEST_F(MiParserTest, BasicString) { + MiValue result = MiValue::Parse("\"hello world\""); + EXPECT_TRUE(result.IsString()); + EXPECT_EQ(result.GetString(), "hello world"); +} + +// Test basic dictionary parsing +TEST_F(MiParserTest, BasicDictionary) { + MiValue result = MiValue::Parse("id=\"i1\",pid=\"42000\""); + EXPECT_TRUE(result.IsDict()); + EXPECT_EQ(result["id"].GetString(), "i1"); + EXPECT_EQ(result["pid"].GetString(), "42000"); +} + +// Test array parsing +TEST_F(MiParserTest, BasicArray) { + MiValue result = MiValue::Parse("result=[\"one\",\"two\",\"three\"]"); + EXPECT_TRUE(result["result"].IsList()); + EXPECT_EQ(result["result"].size(), 3); + EXPECT_EQ(result["result"][0].GetString(), "one"); + EXPECT_EQ(result["result"][1].GetString(), "two"); + EXPECT_EQ(result["result"][2].GetString(), "three"); +} + +// Test nested dictionary +TEST_F(MiParserTest, NestedDictionary) { + MiValue result = MiValue::Parse("frame={addr=\"0x08072c52\",func=\"OS_TaskIdle\",args=[{name=\"p_arg\",value=\"\"}],arch=\"armv3m\"},thread-id=\"1\",stopped-threads=\"all\""); + EXPECT_TRUE(result.IsDict()); + EXPECT_TRUE(result["frame"].IsDict()); + EXPECT_EQ(result["frame"]["addr"].GetString(), "0x08072c52"); + EXPECT_EQ(result["frame"]["func"].GetString(), "OS_TaskIdle"); + EXPECT_EQ(result["frame"]["args"][0]["name"].GetString(), "p_arg"); + EXPECT_EQ(result["frame"]["args"][0]["value"].GetString(), ""); +} + +// Test the specific problematic case that was causing infinite loops +TEST_F(MiParserTest, VarListChildrenWithChildPattern) { + std::string input = "numchild=\"2\",children=[child={name=\"var2.0\",exp=\"0\",numchild=\"31\",value=\"{...}\",type=\"struct custom_cmplx_t\"},child={name=\"var2.1\",exp=\"1\",numchild=\"31\",value=\"{...}\",type=\"struct custom_cmplx_t\"}],has_more=\"0\""; + + MiValue result = MiValue::Parse(input); + EXPECT_TRUE(result.IsDict()); + + // Check top-level fields + EXPECT_EQ(result["numchild"].GetString(), "2"); + EXPECT_EQ(result["has_more"].GetString(), "0"); + + // Check children array + EXPECT_TRUE(result["children"].IsList()); + EXPECT_EQ(result["children"].size(), 2); + + // Check first child + MiValue child0 = result["children"][0]; + EXPECT_TRUE(child0.IsDict()); + EXPECT_TRUE(child0["child"].IsDict()); + EXPECT_EQ(child0["child"]["name"].GetString(), "var2.0"); + EXPECT_EQ(child0["child"]["exp"].GetString(), "0"); + EXPECT_EQ(child0["child"]["numchild"].GetString(), "31"); + EXPECT_EQ(child0["child"]["value"].GetString(), "{...}"); + EXPECT_EQ(child0["child"]["type"].GetString(), "struct custom_cmplx_t"); + + // Check second child + MiValue child1 = result["children"][1]; + EXPECT_TRUE(child1.IsDict()); + EXPECT_TRUE(child1["child"].IsDict()); + EXPECT_EQ(child1["child"]["name"].GetString(), "var2.1"); + EXPECT_EQ(child1["child"]["exp"].GetString(), "1"); + EXPECT_EQ(child1["child"]["numchild"].GetString(), "31"); + EXPECT_EQ(child1["child"]["value"].GetString(), "{...}"); + EXPECT_EQ(child1["child"]["type"].GetString(), "struct custom_cmplx_t"); +} + +// Test thread information response +TEST_F(MiParserTest, ThreadInfoResponse) { + std::string input = "threads=[{id=\"1\",target-id=\"Remote target\",frame={level=\"0\",addr=\"0x08072c52\",func=\"OS_TaskIdle\",args=[{name=\"p_arg\",value=\"\"}],arch=\"armv3m\"},state=\"stopped\"}],current-thread-id=\"1\""; + + MiValue result = MiValue::Parse(input); + EXPECT_TRUE(result.IsDict()); + + EXPECT_TRUE(result["threads"].IsList()); + EXPECT_EQ(result["threads"].size(), 1); + + MiValue thread = result["threads"][0]; + EXPECT_TRUE(thread.IsDict()); + EXPECT_EQ(thread["id"].GetString(), "1"); + EXPECT_EQ(thread["target-id"].GetString(), "Remote target"); + EXPECT_EQ(thread["state"].GetString(), "stopped"); + + EXPECT_TRUE(thread["frame"].IsDict()); + EXPECT_EQ(thread["frame"]["level"].GetString(), "0"); + EXPECT_EQ(thread["frame"]["addr"].GetString(), "0x08072c52"); + EXPECT_EQ(thread["frame"]["func"].GetString(), "OS_TaskIdle"); + EXPECT_EQ(thread["frame"]["arch"].GetString(), "armv3m"); + + EXPECT_TRUE(thread["frame"]["args"].IsList()); + EXPECT_EQ(thread["frame"]["args"].size(), 1); + EXPECT_EQ(thread["frame"]["args"][0]["name"].GetString(), "p_arg"); + EXPECT_EQ(thread["frame"]["args"][0]["value"].GetString(), ""); +} + +// Test register values response +TEST_F(MiParserTest, RegisterValuesResponse) { + std::string input = "register-values=[{number=\"0\",value=\"0x07070707\"},{number=\"1\",value=\"0x2001eea8\"},{number=\"2\",value=\"0x02020202\"}]"; + + MiValue result = MiValue::Parse(input); + EXPECT_TRUE(result.IsDict()); + + EXPECT_TRUE(result["register-values"].IsList()); + EXPECT_EQ(result["register-values"].size(), 3); + + EXPECT_EQ(result["register-values"][0]["number"].GetString(), "0"); + EXPECT_EQ(result["register-values"][0]["value"].GetString(), "0x07070707"); + + EXPECT_EQ(result["register-values"][1]["number"].GetString(), "1"); + EXPECT_EQ(result["register-values"][1]["value"].GetString(), "0x2001eea8"); + + EXPECT_EQ(result["register-values"][2]["number"].GetString(), "2"); + EXPECT_EQ(result["register-values"][2]["value"].GetString(), "0x02020202"); +} + +// Test breakpoint information +TEST_F(MiParserTest, BreakpointInfo) { + std::string input = "bkpt={number=\"1\",type=\"hw breakpoint\",disp=\"keep\",enabled=\"y\",addr=\"0x080fc186\",at=\"\",thread-groups=[\"i1\"],times=\"0\",original-location=\"*0x80fc186\"}"; + + MiValue result = MiValue::Parse(input); + EXPECT_TRUE(result.IsDict()); + + EXPECT_TRUE(result["bkpt"].IsDict()); + EXPECT_EQ(result["bkpt"]["number"].GetString(), "1"); + EXPECT_EQ(result["bkpt"]["type"].GetString(), "hw breakpoint"); + EXPECT_EQ(result["bkpt"]["disp"].GetString(), "keep"); + EXPECT_EQ(result["bkpt"]["enabled"].GetString(), "y"); + EXPECT_EQ(result["bkpt"]["addr"].GetString(), "0x080fc186"); + EXPECT_EQ(result["bkpt"]["at"].GetString(), ""); + EXPECT_EQ(result["bkpt"]["times"].GetString(), "0"); + EXPECT_EQ(result["bkpt"]["original-location"].GetString(), "*0x80fc186"); + + EXPECT_TRUE(result["bkpt"]["thread-groups"].IsList()); + EXPECT_EQ(result["bkpt"]["thread-groups"].size(), 1); + EXPECT_EQ(result["bkpt"]["thread-groups"][0].GetString(), "i1"); +} + +// Test memory response +TEST_F(MiParserTest, MemoryResponse) { + std::string input = "memory=[{begin=\"0x2001ee00\",offset=\"0x00000000\",end=\"0x2001ef00\",contents=\"c584070016480b0000000000\"}]"; + + MiValue result = MiValue::Parse(input); + EXPECT_TRUE(result.IsDict()); + + EXPECT_TRUE(result["memory"].IsList()); + EXPECT_EQ(result["memory"].size(), 1); + + MiValue memory = result["memory"][0]; + EXPECT_TRUE(memory.IsDict()); + EXPECT_EQ(memory["begin"].GetString(), "0x2001ee00"); + EXPECT_EQ(memory["offset"].GetString(), "0x00000000"); + EXPECT_EQ(memory["end"].GetString(), "0x2001ef00"); + EXPECT_EQ(memory["contents"].GetString(), "c584070016480b0000000000"); +} + +// Test stack frames +TEST_F(MiParserTest, StackFrames) { + std::string input = "stack=[frame={level=\"0\",addr=\"0x08072c52\",func=\"OS_TaskIdle\",arch=\"armv3m\"},frame={level=\"1\",addr=\"0xfffffffe\",func=\"\"}]"; + + MiValue result = MiValue::Parse(input); + EXPECT_TRUE(result.IsDict()); + + EXPECT_TRUE(result["stack"].IsList()); + EXPECT_EQ(result["stack"].size(), 2); + + EXPECT_EQ(result["stack"][0]["frame"]["level"].GetString(), "0"); + EXPECT_EQ(result["stack"][0]["frame"]["addr"].GetString(), "0x08072c52"); + EXPECT_EQ(result["stack"][0]["frame"]["func"].GetString(), "OS_TaskIdle"); + EXPECT_EQ(result["stack"][0]["frame"]["arch"].GetString(), "armv3m"); + + EXPECT_EQ(result["stack"][1]["frame"]["level"].GetString(), "1"); + EXPECT_EQ(result["stack"][1]["frame"]["addr"].GetString(), "0xfffffffe"); + EXPECT_EQ(result["stack"][1]["frame"]["func"].GetString(), ""); +} + +// Test empty array +TEST_F(MiParserTest, EmptyArray) { + MiValue result = MiValue::Parse("empty_array=[]"); + EXPECT_TRUE(result.IsDict()); + EXPECT_TRUE(result["empty_array"].IsList()); + EXPECT_EQ(result["empty_array"].size(), 0); +} + +// Test empty dictionary +TEST_F(MiParserTest, EmptyDictionary) { + MiValue result = MiValue::Parse("empty_dict={}"); + EXPECT_TRUE(result.IsDict()); + EXPECT_TRUE(result["empty_dict"].IsDict()); + EXPECT_EQ(result["empty_dict"].size(), 0); +} + +// Test complex nested structure +TEST_F(MiParserTest, ComplexNestedStructure) { + std::string input = "response={result=\"done\",data={items=[{id=1,values=[1,2,3]},{id=2,values=[4,5,6]}],count=2}}"; + + MiValue result = MiValue::Parse(input); + EXPECT_TRUE(result.IsDict()); + EXPECT_TRUE(result["response"].IsDict()); + EXPECT_EQ(result["response"]["result"].GetString(), "done"); + + EXPECT_TRUE(result["response"]["data"].IsDict()); + EXPECT_EQ(result["response"]["data"]["count"].GetString(), "2"); + + EXPECT_TRUE(result["response"]["data"]["items"].IsList()); + EXPECT_EQ(result["response"]["data"]["items"].size(), 2); + + EXPECT_EQ(result["response"]["data"]["items"][0]["id"].GetString(), "1"); + EXPECT_TRUE(result["response"]["data"]["items"][0]["values"].IsList()); + EXPECT_EQ(result["response"]["data"]["items"][0]["values"].size(), 3); +} + +// Test that no infinite loops occur with malformed input +TEST_F(MiParserTest, MalformedInputNoInfiniteLoop) { + // This should not cause an infinite loop + std::string input = "malformed=[unclosed array"; + EXPECT_NO_THROW({ + MiValue result = MiValue::Parse(input); + // We don't care about the result, just that it doesn't hang + }); + + // Test with unclosed string + input = "key=\"unclosed string"; + EXPECT_NO_THROW({ + MiValue result = MiValue::Parse(input); + }); + + // Test with random garbage + input = "asdf1234!@#$%^&*()"; + EXPECT_NO_THROW({ + MiValue result = MiValue::Parse(input); + }); +} + +// Test escaped characters in strings +TEST_F(MiParserTest, EscapedCharacters) { + MiValue result = MiValue::Parse("key=\"value with \\\"quotes\\\" and \\\\ backslash\""); + EXPECT_TRUE(result.IsDict()); + EXPECT_EQ(result["key"].GetString(), "value with \"quotes\" and \\ backslash"); +} + +// Test mixed array types (should handle gracefully) +TEST_F(MiParserTest, MixedArrayTypes) { + std::string input = "mixed=[\"string\",123,true,{nested=\"value\"}]"; + MiValue result = MiValue::Parse(input); + EXPECT_TRUE(result.IsDict()); + EXPECT_TRUE(result["mixed"].IsList()); + EXPECT_EQ(result["mixed"].size(), 4); + + // Should all be treated as strings in the current implementation + EXPECT_EQ(result["mixed"][0].GetString(), "string"); + EXPECT_EQ(result["mixed"][1].GetString(), "123"); + EXPECT_EQ(result["mixed"][2].GetString(), "true"); + EXPECT_TRUE(result["mixed"][3].IsDict()); + EXPECT_EQ(result["mixed"][3]["nested"].GetString(), "value"); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From a4b426f27ffa3124a705d7364fe086cd962d200f Mon Sep 17 00:00:00 2001 From: AV <3045629+anvol@users.noreply.github.com> Date: Sat, 11 Oct 2025 16:28:03 +0300 Subject: [PATCH 2/5] - clear register names before populating - fix missing default init values --- core/adapters/gdbmiadapter.cpp | 3 ++- core/adapters/gdbmiadapter.h | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/adapters/gdbmiadapter.cpp b/core/adapters/gdbmiadapter.cpp index 2ea76ed8..2eb5d2b3 100644 --- a/core/adapters/gdbmiadapter.cpp +++ b/core/adapters/gdbmiadapter.cpp @@ -117,7 +117,7 @@ void GdbMiAdapter::UpdateAllRegisters() { return; } - for (int i; i Date: Sat, 11 Oct 2025 19:21:03 +0300 Subject: [PATCH 3/5] fix bp log message --- core/adapters/gdbmiadapter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/adapters/gdbmiadapter.cpp b/core/adapters/gdbmiadapter.cpp index 2eb5d2b3..460b60f5 100644 --- a/core/adapters/gdbmiadapter.cpp +++ b/core/adapters/gdbmiadapter.cpp @@ -606,7 +606,7 @@ DebugBreakpoint GdbMiAdapter::AddBreakpoint(std::uintptr_t address, unsigned lon return DebugBreakpoint{address, 0, true}; } - LogWarn("Failed to set BP at %lu", address); + LogWarn("Failed to set BP at 0x%lux", address); return {}; } From de0e0cda8baffb8a762adb9215dd2f49b111c044 Mon Sep 17 00:00:00 2001 From: AV <3045629+anvol@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:35:50 +0300 Subject: [PATCH 4/5] Improve ReaderThread exit handling Clear cache on stop/quit/restart --- core/adapters/gdbmiadapter.cpp | 108 ++++--- core/adapters/gdbmiadapter.h | 2 +- core/adapters/gdbmiconnector.cpp | 475 ++++++++++++++++++++----------- core/adapters/gdbmiconnector.h | 4 + 4 files changed, 389 insertions(+), 200 deletions(-) diff --git a/core/adapters/gdbmiadapter.cpp b/core/adapters/gdbmiadapter.cpp index 460b60f5..9dedb36b 100644 --- a/core/adapters/gdbmiadapter.cpp +++ b/core/adapters/gdbmiadapter.cpp @@ -211,6 +211,10 @@ void GdbMiAdapter::AsyncRecordHandler(const MiRecord& record) m_eventCV.notify_all(); } + else if (record.command == "error") + { + LogError("GDBMI: %s", record.payload.c_str()); + } else if (record.type == '~' || record.type == '@' || record.type == '&' || record.type == '=') { // Console stream output std::string message; @@ -264,26 +268,33 @@ void GdbMiAdapter::ScheduleStateRefresh() }).detach(); } -DebugStopReason GdbMiAdapter::GetStopReason(const MiRecord& record) { - auto value = MiValue::Parse(record.payload); - if (value.Exists("reason")) { - const std::string& reason = value["reason"].GetString(); - if (reason == "breakpoint-hit") return Breakpoint; - if (reason == "end-stepping-range") return SingleStep; - if (reason == "exited-normally" || reason == "exited") return ProcessExited; - if (reason == "signal-received") return SignalInt; - } - return UnknownReason; +DebugStopReason GdbMiAdapter::GetStopReason(const MiRecord& record) +{ + auto value = MiValue::Parse(record.payload); + if (value.Exists("reason")) + { + const std::string& reason = value["reason"].GetString(); + if (reason == "breakpoint-hit") + return Breakpoint; + if (reason == "end-stepping-range") + return SingleStep; + if (reason == "exited-normally" || reason == "exited") + return ProcessExited; + if (reason == "signal-received") + return SignalInt; + } + return UnknownReason; } -std::string GdbMiAdapter::RunMonitorCommand(const std::string& command) { - if (!m_mi) return ""; +bool GdbMiAdapter::RunMonitorCommand(const std::string& command) const +{ + if (!m_mi) return false; // Monitor commands don't use MI syntax, they use the console interpreter auto result = m_mi->SendCommand("-interpreter-exec console \"monitor " + command + "\""); // The result is usually printed to the console stream ('~' records), which is hard to // capture synchronously. For now, we assume it worked if we get a 'done' back. // A better implementation would buffer console output between commands. - return (result.command == "done") ? "success" : "error"; + return (result.command == "done"); } bool GdbMiAdapter::Connect(const std::string& server, uint32_t port) { @@ -459,6 +470,7 @@ bool GdbMiAdapter::Connect(const std::string& server, uint32_t port) { bool GdbMiAdapter::Execute(const std::string&, const LaunchConfigurations&) { LogWarn("GdbMiAdapter::Execute not implemented"); return false; } bool GdbMiAdapter::ExecuteWithArgs(const std::string&, const std::string&, const std::string&, const LaunchConfigurations&) { + InvalidateCache(); auto settings = GetAdapterSettings(); BNSettingsScope scope = SettingsResourceScope; auto data = GetData(); @@ -473,6 +485,7 @@ bool GdbMiAdapter::ExecuteWithArgs(const std::string&, const std::string&, const return Connect(server, port); } bool GdbMiAdapter::Attach(uint32_t) { + InvalidateCache(); auto settings = GetAdapterSettings(); BNSettingsScope scope = SettingsResourceScope; auto data = GetData(); @@ -493,26 +506,32 @@ bool GdbMiAdapter::ResumeThread(uint32_t) { LogWarn("GdbMiAdapter::ResumeThread void GdbMiAdapter::Stop() { - if (m_mi && m_mi->IsRunning()) + try { - LogDebug("GDB MI connector stopping..."); - m_mi->SetAsyncCallback(nullptr); - m_mi->Stop(); - m_mi.reset(); - LogDebug("GDB MI connector stopped."); + if (m_mi && m_mi->IsRunning()) + { + LogDebug("GDB MI connector stopping..."); + m_mi->SetAsyncCallback(nullptr); + m_mi->Stop(); + m_mi.reset(); + LogDebug("GDB MI connector stopped."); + } } - - // Clear all cached data + catch (const std::exception& e) + { + LogError("Exception during GDB MI adapter stop: %s", e.what()); + } + catch (...) { - std::unique_lock lock(m_cacheMutex); - m_cachedThreads.clear(); - m_cachedRegisters.clear(); - m_cachedFrames.clear(); + LogError("Unknown exception during GDB MI adapter stop"); } + // Clear all cached data + InvalidateCache(); + // Reset target state m_targetRunningAtomic.store(false, std::memory_order_release); - + m_connected = false; } bool GdbMiAdapter::Quit() @@ -520,6 +539,7 @@ bool GdbMiAdapter::Quit() Detach(); Stop(); m_connected = false; + m_targetRunningAtomic.store(false); LogInfo("GDB MI adapter quit completed successfully"); return true; @@ -575,7 +595,7 @@ std::vector GdbMiAdapter::GetFramesOfThread(uint32_t tid) { uint32_t GdbMiAdapter::GetActiveThreadId() const { return m_currentTid; } DebugThread GdbMiAdapter::GetActiveThread() const { - GdbMiAdapter* self = const_cast(this); + auto self = const_cast(this); uint64_t pc = self->GetInstructionOffset(); return DebugThread(m_currentTid, pc); } @@ -600,7 +620,7 @@ DebugBreakpoint GdbMiAdapter::AddBreakpoint(std::uintptr_t address, unsigned lon if (result.command == "done") { DebuggerEvent evt; evt.type = BackendMessageEventType; - evt.data.messageData.message = result.command; + evt.data.messageData.message = result.payload; PostDebuggerEvent(evt); return DebugBreakpoint{address, 0, true}; @@ -628,8 +648,18 @@ DebugBreakpoint GdbMiAdapter::AddBreakpoint(const ModuleNameAndOffset& address, bool GdbMiAdapter::RemoveBreakpoint(const DebugBreakpoint& breakpoint) { if (!m_mi) return false; - m_mi->SendCommand(fmt::format("-break-delete *0x{:x}", breakpoint.m_address)); - return true; + auto result = m_mi->SendCommand(fmt::format("-break-delete *0x{:x}", breakpoint.m_address)); + + if (result.command == "done") { + DebuggerEvent evt; + evt.type = BackendMessageEventType; + evt.data.messageData.message = result.payload; + PostDebuggerEvent(evt); + + return true; + } + + return false; } std::vector GdbMiAdapter::GetBreakpointList() const { LogWarn("GdbMiAdapter::GetBreakpointList not implemented"); return {}; } @@ -643,15 +673,21 @@ bool GdbMiAdapter::WriteRegister(const std::string& reg, intx::uint512 value) { DataBuffer GdbMiAdapter::ReadMemory(std::uintptr_t address, size_t size) { if (!m_mi) return {}; - + LogDebug("GdbMiAdapter::ReadMemory 0x%lX-0x%lX", address, address+size); // embedded specifics: we can use 'info mem' to get list of memory regions available for reading. // it's safe to assume 0x08000000 - 0x60000000 is good enough for most arm-cortex targets - if (address > 0x60000000) return {}; - if (address < 0x08000000) return {}; + DataBuffer zero(size); + if (address > 0x60000000) return zero; + if (address < 0x08000000) return zero; std::string cmd = fmt::format("-data-read-memory-bytes 0x{:x} {}", address, size); auto result = m_mi->SendCommand(cmd); - if (result.command != "done") return {}; + if (result.command != "done") + { + LogWarn("Failed to read memory at 0x%lX", address); + + return zero; + } auto value = MiValue::Parse(result.payload); std::string hex_contents = value["memory"][0]["contents"].GetString(); @@ -684,8 +720,8 @@ std::vector GdbMiAdapter::GetModuleList() return {}; std::string name = data->GetFile()->GetOriginalFilename(); - modules.push_back(DebugModule("SRAM", "SRAM", 0x20000000, 0x00040000, true)); - modules.push_back(DebugModule(name, name, 0x08000000, 0x00100000, true)); + modules.emplace_back("SRAM", "SRAM", 0x20000000, 0x00040000, true); + modules.emplace_back(name, name, 0x08000000, 0x00100000, true); return modules; } diff --git a/core/adapters/gdbmiadapter.h b/core/adapters/gdbmiadapter.h index 1f84ccc0..8a31617e 100644 --- a/core/adapters/gdbmiadapter.h +++ b/core/adapters/gdbmiadapter.h @@ -37,7 +37,7 @@ class GdbMiAdapter : public BinaryNinjaDebugger::DebugAdapter BinaryNinjaDebugger::DebugStopReason GetStopReason(const MiRecord& record); static intx::uint512 ParseGdbValue(const std::string& valueStr); - std::string RunMonitorCommand(const std::string& command); + bool RunMonitorCommand(const std::string& command) const; void ApplyBreakpoints(); std::vector m_pendingBreakpoints {}; diff --git a/core/adapters/gdbmiconnector.cpp b/core/adapters/gdbmiconnector.cpp index 9ce4bc26..ae0ede17 100644 --- a/core/adapters/gdbmiconnector.cpp +++ b/core/adapters/gdbmiconnector.cpp @@ -7,6 +7,7 @@ #else #include #include +#include // for strerror // For Linux/macOS, we need the environment variables extern char** environ; #endif @@ -195,50 +196,22 @@ void GdbMiConnector::Stop() return; LogInfo("GDB MI connector: Starting graceful shutdown..."); - - // Send a quit command to GDB and wait for a response - SendCommand("-gdb-exit", 100); - + SendCommand("-gdb-exit", 200); + // Set running flag to false first to signal reader thread to exit m_running = false; - // Give GDB a moment to shut down - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + // Close file handles to wake up reader thread blocked on I/O + CloseFileHandles(); - // Wait for the reader thread to finish, which should happen naturally now + // Wait for the reader thread to finish with a timeout if (m_readerThread.joinable()) { LogInfo("GDB MI connector: Waiting for reader thread to finish..."); m_readerThread.join(); - LogInfo("GDB MI connector: Reader thread finished"); - } - -#ifdef WIN32 - CloseHandle(m_gdb_stdin_write); - CloseHandle(m_gdb_stdout_read); - CloseHandle(m_pi.hProcess); - CloseHandle(m_pi.hThread); -#else - close(m_gdb_stdin_write); - close(m_gdb_stdout_read); - if (m_pid > 0) - { - int status; - // Check if the process is still around, but don't hang waiting for it - if (waitpid(m_pid, &status, WNOHANG) == 0) - { - // If it's still there, it's likely a zombie, so we can kill it. - // But a graceful shutdown should prevent this. - LogWarn("GDB process (PID: %d) did not exit gracefully, forcing termination.", m_pid); - kill(m_pid, SIGKILL); - waitpid(m_pid, &status, 0); - } - else - { - LogInfo("GDB process (PID: %d) exited gracefully.", m_pid); - } } -#endif + // Try to terminate GDB process if still running + TerminateGdbProcess(); LogInfo("GDB MI connector: Shutdown completed"); } @@ -345,182 +318,258 @@ MiRecord GdbMiConnector::SendCommand(const std::string& command, int timeout_ms) long token = m_nextToken++; std::string fullCommand = std::to_string(token) + command + "\n"; - - #ifdef WIN32 - DWORD bytesWritten; - if (!WriteFile(m_gdb_stdin_write, fullCommand.c_str(), fullCommand.length(), &bytesWritten, NULL)) - return {}; - #else - if (write(m_gdb_stdin_write, fullCommand.c_str(), fullCommand.length()) < 0) - return {}; - #endif + try { +#ifdef WIN32 + DWORD bytesWritten; + if (!WriteFile(m_gdb_stdin_write, fullCommand.c_str(), static_cast(fullCommand.length()), &bytesWritten, NULL)) { + DWORD error = GetLastError(); + LogError("Failed to write to GDB stdin, error: %lu", error); + m_running = false; + return {}; + } +#else + ssize_t bytesWritten = write(m_gdb_stdin_write, fullCommand.c_str(), fullCommand.length()); + if (bytesWritten < 0) { + int error = errno; + LogError("Failed to write to GDB stdin, error: %d (%s)", error, strerror(error)); + + // Handle specific pipe errors + if (error == EPIPE || error == ECONNRESET) { + LogError("GDB process pipe broken - process likely terminated"); + m_running = false; + } else if (error == EBADF) { + LogError("Invalid file descriptor for GDB stdin"); + m_running = false; + } + return {}; + } else if (static_cast(bytesWritten) != fullCommand.length()) { + LogWarn("Partial write to GDB stdin: %zd of %zu bytes", bytesWritten, fullCommand.length()); + } +#endif + } catch (const std::exception& e) { + LogError("Exception while writing to GDB: %s", e.what()); + m_running = false; + return {}; + } catch (...) { + LogError("Unknown exception while writing to GDB"); + m_running = false; + return {}; + } std::unique_lock lock(m_mutex); LogDebug("GDB->: %s", fullCommand.c_str()); - if (m_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), [&] { return m_responses.count(token); })) + + // Wait for response with timeout + if (m_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), [&] { return m_responses.count(token) || !m_running; })) { MiRecord record = m_responses[token]; m_responses.erase(token); return record; + } else { + LogWarn("Timeout waiting for GDB response to command: %s", command.c_str()); + return {}; } - - - - return {}; } void GdbMiConnector::ReaderThread() { std::string currentLine; - char buffer[4096]; + char buffer[8192] = {0}; + try { #ifndef WIN32 - // Make the read fd non-blocking so we can drain per select wakeup - int flags = fcntl(m_gdb_stdout_read, F_GETFL, 0); - fcntl(m_gdb_stdout_read, F_SETFL, flags | O_NONBLOCK); + // Make the read fd non-blocking so we can drain per select wakeup + int flags = fcntl(m_gdb_stdout_read, F_GETFL, 0); + fcntl(m_gdb_stdout_read, F_SETFL, flags | O_NONBLOCK); - while (m_running) - { - fd_set rfds; - FD_ZERO(&rfds); - FD_SET(m_gdb_stdout_read, &rfds); + while (m_running) + { + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(m_gdb_stdout_read, &rfds); - struct timeval tv = {0}; - tv.tv_sec = 1; - tv.tv_usec = 0; + struct timeval tv = {0}; + tv.tv_sec = 0; + tv.tv_usec = 200000; // 200ms - int retval = select(m_gdb_stdout_read + 1, &rfds, nullptr, nullptr, &tv); - if (retval == -1) - { - LogWarn("Lost connection to GDB"); - m_running = false; - continue; - } - if (retval == 0) - { - continue; - } + int retval = select(m_gdb_stdout_read + 1, &rfds, nullptr, nullptr, &tv); + if (retval == -1) + { + int selectError = errno; + if (selectError != EINTR) { // Ignore interrupted system calls + LogError("Select error from GDB: %d (%s)", selectError, strerror(selectError)); + m_running = false; + break; + } + continue; + } + if (retval == 0) + { + continue; // Timeout, check if we should continue + } - for (;;) - { - ssize_t n = read(m_gdb_stdout_read, buffer, sizeof(buffer)); - buffer[n] = 0; - if (n > 0) + while (m_running) { - // Frame by CR or LF; strip CRs - for (ssize_t i = 0; i < n; ++i) + ssize_t n = read(m_gdb_stdout_read, buffer, sizeof(buffer)-1); + if (n > 0) { - char c = buffer[i]; - - if (c == '\r' || c == '\n') + buffer[n] = 0; + // Frame by CR or LF; strip CRs + for (ssize_t i = 0; i < n; ++i) { - if (!currentLine.empty()) + char c = buffer[i]; + + if (c == '\r' || c == '\n') { - // Trim any leftover CRs or spaces from ends - size_t start = 0; - while (start < currentLine.size() && - (currentLine[start] == '\r' || currentLine[start] == ' ' || currentLine[start] == '\t')) - ++start; - size_t end = currentLine.size(); - while (end > start && - (currentLine[end - 1] == '\r' || currentLine[end - 1] == ' ' || currentLine[end - 1] == '\t')) - --end; - - std::string line = currentLine.substr(start, end - start); - - if (!line.empty()) + if (!currentLine.empty()) { - MiRecord record = ParseLine(line); - if (record.token.has_value() && record.type == '^') + // Trim any leftover CRs or spaces from ends + size_t start = 0; + while (start < currentLine.size() && + (currentLine[start] == '\r' || currentLine[start] == ' ' || currentLine[start] == '\t')) + ++start; + size_t end = currentLine.size(); + while (end > start && + (currentLine[end - 1] == '\r' || currentLine[end - 1] == ' ' || currentLine[end - 1] == '\t')) + --end; + + std::string line = currentLine.substr(start, end - start); + + if (!line.empty()) { - std::unique_lock lock(m_mutex); - // LogDebug("Notify about response %ld", *record.token); - m_responses[*record.token] = record; - m_cv.notify_all(); - } - else if (m_asyncCallback && record.type != '^') - { - // Do not block the reader; just forward - m_asyncCallback(record); + MiRecord record = ParseLine(line); + if (record.token.has_value() && record.type == '^') + { + std::unique_lock lock(m_mutex); + // LogDebug("Notify about response %ld", *record.token); + m_responses[*record.token] = record; + m_cv.notify_all(); + } + else if (m_asyncCallback && record.type != '^') + { + // Do not block the reader; just forward + m_asyncCallback(record); + } } } + currentLine.clear(); + } + else + { + if (c != '\0') // ignore NULs just in case + currentLine += c; } - currentLine.clear(); - } - else - { - if (c != '\0') // ignore NULs just in case - currentLine += c; } + continue; // try reading more (drain) } - continue; // try reading more (drain) - } - if (n == -1 && errno == EAGAIN) - break; // no more data for now + if (n == -1 && errno == EAGAIN) + break; // no more data for now - if (n == 0) - { - // EOF + if (n == 0) + { + // EOF - GDB process has terminated + LogInfo("GDB process EOF - connection closed"); + m_running = false; + break; + } + + // n == -1 and not EAGAIN + int readError = errno; + LogError("Read error from GDB: %d (%s)", readError, strerror(readError)); + + // Handle specific pipe errors + if (readError == EPIPE || readError == ECONNRESET) { + LogError("GDB process pipe broken - process terminated"); + } else if (readError == EBADF) { + LogError("Invalid file descriptor for GDB stdout"); + } + m_running = false; break; } - - // n == -1 and not EAGAIN - LogError("read error from GDB: %d", errno); - m_running = false; - break; } - } #else - DWORD bytesRead; - while (m_running && ReadFile(m_gdb_stdout_read, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead > 0) - { - // Similar CR/LF framing on Windows - for (DWORD i = 0; i < bytesRead; ++i) + DWORD bytesRead; + while (m_running) { - char c = buffer[i]; - if (c == '\r' || c == '\n') + if (!ReadFile(m_gdb_stdout_read, buffer, sizeof(buffer), &bytesRead, NULL)) + { + DWORD error = GetLastError(); + if (error == ERROR_BROKEN_PIPE) + { + LogInfo("GDB process pipe broken - connection closed"); + } + else + { + LogError("ReadFile error from GDB: %lu", error); + } + m_running = false; + break; + } + + if (bytesRead == 0) + { + // EOF + LogInfo("GDB process EOF - connection closed"); + m_running = false; + break; + } + + // Similar CR/LF framing on Windows + for (DWORD i = 0; i < bytesRead; ++i) { - if (!currentLine.empty()) + char c = buffer[i]; + if (c == '\r' || c == '\n') { - // Trim CR/space - size_t start = 0; - while (start < currentLine.size() && - (currentLine[start] == '\r' || currentLine[start] == ' ' || currentLine[start] == '\t')) - ++start; - size_t end = currentLine.size(); - while (end > start && - (currentLine[end - 1] == '\r' || currentLine[end - 1] == ' ' || currentLine[end - 1] == '\t')) - --end; - std::string line = currentLine.substr(start, end - start); - - if (!line.empty()) + if (!currentLine.empty()) { - MiRecord record = ParseLine(line); - if (record.token.has_value() && record.type == '^') + // Trim CR/space + size_t start = 0; + while (start < currentLine.size() && + (currentLine[start] == '\r' || currentLine[start] == ' ' || currentLine[start] == '\t')) + ++start; + size_t end = currentLine.size(); + while (end > start && + (currentLine[end - 1] == '\r' || currentLine[end - 1] == ' ' || currentLine[end - 1] == '\t')) + --end; + std::string line = currentLine.substr(start, end - start); + + if (!line.empty()) { - std::unique_lock lock(m_mutex); - LogDebug("Notify about response %ld", *record.token); - m_responses[*record.token] = record; - m_cv.notify_all(); - } - else if (m_asyncCallback) - { - m_asyncCallback(record); + MiRecord record = ParseLine(line); + if (record.token.has_value() && record.type == '^') + { + std::unique_lock lock(m_mutex); + LogDebug("Notify about response %ld", *record.token); + m_responses[*record.token] = record; + m_cv.notify_all(); + } + else if (m_asyncCallback) + { + m_asyncCallback(record); + } } } + currentLine.clear(); + } + else + { + if (c != '\0') + currentLine += c; } - currentLine.clear(); - } - else - { - if (c != '\0') - currentLine += c; } } - } #endif + } catch (const std::exception& e) { + LogError("Exception in GDB reader thread: %s", e.what()); + m_running = false; + } catch (...) { + LogError("Unknown exception in GDB reader thread"); + m_running = false; + } + + LogInfo("GDB reader thread exiting"); } MiRecord GdbMiConnector::ParseLine(const std::string &line) { @@ -573,3 +622,103 @@ MiRecord GdbMiConnector::ParseLine(const std::string &line) { return record; } + +// Helper method to terminate GDB process gracefully +bool GdbMiConnector::TerminateGdbProcess() +{ +#ifdef WIN32 + if (m_pi.hProcess) + { + // Try graceful termination first + if (TerminateProcess(m_pi.hProcess, 0)) + { + LogInfo("GDB process terminated gracefully"); + return true; + } + else + { + DWORD error = GetLastError(); + LogError("Failed to terminate GDB process, error: %lu", error); + return false; + } + } +#else + if (m_pid > 0) + { + int status; + + // Check if process is still running + if (waitpid(m_pid, &status, WNOHANG) == 0) + { + // Process is still running, try graceful shutdown first + LogInfo("Attempting graceful shutdown of GDB process (PID: %d)", m_pid); + kill(m_pid, SIGTERM); + + // Wait up to 2 seconds for graceful shutdown + auto start = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() - start < std::chrono::seconds(2)) + { + if (waitpid(m_pid, &status, WNOHANG) != 0) + { + LogInfo("GDB process (PID: %d) exited gracefully", m_pid); + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // If still running, force termination + if (waitpid(m_pid, &status, WNOHANG) == 0) + { + LogWarn("GDB process (PID: %d) did not exit gracefully, forcing termination", m_pid); + kill(m_pid, SIGKILL); + waitpid(m_pid, &status, 0); + LogInfo("GDB process (PID: %d) terminated with SIGKILL", m_pid); + } + } + else + { + LogInfo("GDB process (PID: %d) already exited", m_pid); + } + return true; + } +#endif + return true; +} + +// Helper method to close all file handles +void GdbMiConnector::CloseFileHandles() +{ +#ifdef WIN32 + if (m_gdb_stdin_write && m_gdb_stdin_write != INVALID_HANDLE_VALUE) + { + CloseHandle(m_gdb_stdin_write); + m_gdb_stdin_write = NULL; + } + if (m_gdb_stdout_read && m_gdb_stdout_read != INVALID_HANDLE_VALUE) + { + CloseHandle(m_gdb_stdout_read); + m_gdb_stdout_read = NULL; + } + if (m_pi.hProcess) + { + CloseHandle(m_pi.hProcess); + m_pi.hProcess = NULL; + } + if (m_pi.hThread) + { + CloseHandle(m_pi.hThread); + m_pi.hThread = NULL; + } +#else + if (m_gdb_stdin_write >= 0) + { + close(m_gdb_stdin_write); + m_gdb_stdin_write = -1; + } + if (m_gdb_stdout_read >= 0) + { + close(m_gdb_stdout_read); + m_gdb_stdout_read = -1; + } +#endif +} diff --git a/core/adapters/gdbmiconnector.h b/core/adapters/gdbmiconnector.h index 7dff3c46..b20de4c6 100644 --- a/core/adapters/gdbmiconnector.h +++ b/core/adapters/gdbmiconnector.h @@ -76,6 +76,10 @@ class GdbMiConnector void ReaderThread(); MiRecord ParseLine(const std::string& line); + + // Helper methods for robust process management + bool TerminateGdbProcess(); + void CloseFileHandles(); public: GdbMiConnector(const std::string& gdbPath, const std::string& targetExecutable); From eb68959cead1f67b68b216d96548b928d531ff98 Mon Sep 17 00:00:00 2001 From: AV <3045629+anvol@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:23:59 +0300 Subject: [PATCH 5/5] Fix StepOver Remove all breakpoints at address Use BackendMessageEventType as output for gdb messages --- core/adapters/gdbmiadapter.cpp | 68 ++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/core/adapters/gdbmiadapter.cpp b/core/adapters/gdbmiadapter.cpp index 9dedb36b..c05ac88d 100644 --- a/core/adapters/gdbmiadapter.cpp +++ b/core/adapters/gdbmiadapter.cpp @@ -242,7 +242,7 @@ void GdbMiAdapter::AsyncRecordHandler(const MiRecord& record) } DebuggerEvent event; - event.type = StdoutMessageEventType; + event.type = BackendMessageEventType; event.data.messageData.message = message; PostDebuggerEvent(event); } @@ -648,21 +648,67 @@ DebugBreakpoint GdbMiAdapter::AddBreakpoint(const ModuleNameAndOffset& address, bool GdbMiAdapter::RemoveBreakpoint(const DebugBreakpoint& breakpoint) { if (!m_mi) return false; - auto result = m_mi->SendCommand(fmt::format("-break-delete *0x{:x}", breakpoint.m_address)); - if (result.command == "done") { - DebuggerEvent evt; - evt.type = BackendMessageEventType; - evt.data.messageData.message = result.payload; - PostDebuggerEvent(evt); + auto breakpoints = GetBreakpointList(); + uint64_t id_to_remove = 0; + int removed = 0; + for (const auto& bp: breakpoints) + { + if (bp.m_address == breakpoint.m_address) + { + id_to_remove = bp.m_id; + auto result = m_mi->SendCommand(fmt::format("-break-delete {}", id_to_remove)); + + if (result.command == "done") { + DebuggerEvent evt; + evt.type = BackendMessageEventType; + evt.data.messageData.message = result.payload; + PostDebuggerEvent(evt); - return true; + removed++; + } + } + } + + if (removed == 0) + { + LogWarn("Failed to remove breakpoint at 0x%lX", breakpoint.m_address); + return false; } return false; } -std::vector GdbMiAdapter::GetBreakpointList() const { LogWarn("GdbMiAdapter::GetBreakpointList not implemented"); return {}; } +std::vector GdbMiAdapter::GetBreakpointList() const { + if (!m_mi) + return {}; + + auto result = m_mi->SendCommand("-break-list"); + if (result.command != "done") + { + LogWarn("Failed to get breakpoint list"); + return {}; + } + + std::vector breakpoints; + auto table = MiValue::Parse(result.payload); + if (table.Exists("BreakpointTable")) + { + auto bp_table = table["BreakpointTable"]; + if (bp_table.Exists("body")) + { + for (const auto& item: bp_table["body"].GetList()) + { + auto bp = item["bkpt"]; + uint64_t addr = std::stoull(bp["addr"].GetString(), 0, 16); + uint64_t id = std::stoull(bp["number"].GetString(), 0, 10); + LogDebug("Parsed breakpoint %llu at 0x%llx", id, addr); + breakpoints.emplace_back(addr, id, true); + } + } + } + return breakpoints; +} bool GdbMiAdapter::WriteRegister(const std::string& reg, intx::uint512 value) { if (!m_mi) return false; @@ -747,7 +793,7 @@ bool GdbMiAdapter::StepInto() { bool GdbMiAdapter::StepOver() { if (!m_mi || m_targetRunningAtomic) return false; - return (m_mi->SendCommand("-exec-next-instruction").command != "running"); + return (m_mi->SendCommand("-exec-next-instruction").command == "running"); } bool GdbMiAdapter::StepReturn() { @@ -769,7 +815,7 @@ uint64_t GdbMiAdapter::GetStackPointer() { std::string GdbMiAdapter::InvokeBackendCommand(const std::string& command) { if (!m_mi) return "error, transport not ready"; auto result = m_mi->SendCommand("-interpreter-exec console \"" + command + "\""); - return (result.command == "done") ? result.payload : "Error sending command."; + return (result.command == "done") ? result.payload : result.command; } uint64_t GdbMiAdapter::ExitCode() { return 0; }