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..c05ac88d --- /dev/null +++ b/core/adapters/gdbmiadapter.cpp @@ -0,0 +1,918 @@ +#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 = 0; i < gdbmiregisters["register-values"].size(); i++) + { + auto gdbmi_reg = gdbmiregisters["register-values"][i]; + auto reg_idx = std::stoul(gdbmi_reg["number"].GetString(), 0, 10); + auto reg_value = ParseGdbValue(gdbmi_reg["value"].GetString()); + if (reg_idx < m_registerNames.size()) + { + std::string name = m_registerNames[reg_idx]; + if (!name.empty()) + { + regs[name] = DebugRegister(name, reg_value, 0, reg_idx); + } + } + } + + if (m_remoteArch == "armv7-m" && regs.contains("pc") && regs.contains("sp")) + { + m_instructionOffset = static_cast(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.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; + 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 = BackendMessageEventType; + 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; +} + +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"); +} + +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")) + { + m_registerNames.clear(); + 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&) +{ + InvalidateCache(); + 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) { + InvalidateCache(); + 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() +{ + try + { + 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."); + } + } + catch (const std::exception& e) + { + LogError("Exception during GDB MI adapter stop: %s", e.what()); + } + catch (...) + { + 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() +{ + Detach(); + Stop(); + m_connected = false; + m_targetRunningAtomic.store(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 { + auto 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.payload; + PostDebuggerEvent(evt); + + return DebugBreakpoint{address, 0, true}; + } + + LogWarn("Failed to set BP at 0x%lux", 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; + + 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); + + removed++; + } + } + } + + if (removed == 0) + { + LogWarn("Failed to remove breakpoint at 0x%lX", breakpoint.m_address); + return false; + } + + return false; +} + +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; + 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 {}; + 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 + 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") + { + 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(); + 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.emplace_back("SRAM", "SRAM", 0x20000000, 0x00040000, true); + modules.emplace_back(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 : result.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..8a31617e --- /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); + + bool RunMonitorCommand(const std::string& command) const; + void ApplyBreakpoints(); + std::vector m_pendingBreakpoints {}; + +public: + GdbMiAdapter(BinaryView* data); + ~GdbMiAdapter() override; + + uint64_t m_instructionOffset = 0; + uint64_t m_stackPointer = 0; + // --- 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..ae0ede17 --- /dev/null +++ b/core/adapters/gdbmiconnector.cpp @@ -0,0 +1,724 @@ +#include "gdbmiconnector.h" +#include +#include +#include +#include +#ifdef WIN32 +#else +#include +#include +#include // for strerror +// 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..."); + SendCommand("-gdb-exit", 200); + // Set running flag to false first to signal reader thread to exit + m_running = false; + + // Close file handles to wake up reader thread blocked on I/O + CloseFileHandles(); + + // 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(); + } + + // Try to terminate GDB process if still running + TerminateGdbProcess(); + 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"; + + 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()); + + // 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 {}; + } +} + +void GdbMiConnector::ReaderThread() +{ + std::string currentLine; + 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); + + while (m_running) + { + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(m_gdb_stdout_read, &rfds); + + 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) + { + 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 + } + + while (m_running) + { + ssize_t n = read(m_gdb_stdout_read, buffer, sizeof(buffer)-1); + if (n > 0) + { + buffer[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 - 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; + } + } +#else + DWORD bytesRead; + while (m_running) + { + 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) + { + 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 + } 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) { + 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; +} + +// 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 new file mode 100644 index 00000000..b20de4c6 --- /dev/null +++ b/core/adapters/gdbmiconnector.h @@ -0,0 +1,96 @@ +#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); + + // Helper methods for robust process management + bool TerminateGdbProcess(); + void CloseFileHandles(); + +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(); +}