diff --git a/core/adapters/gdbmiadapter.cpp b/core/adapters/gdbmiadapter.cpp index 32b7bed8..e099121e 100644 --- a/core/adapters/gdbmiadapter.cpp +++ b/core/adapters/gdbmiadapter.cpp @@ -2,12 +2,151 @@ #include #include #include +#include #include "../debuggercontroller.h" #include "../../cli/log.h" using namespace BinaryNinja; using namespace BinaryNinjaDebugger; +namespace +{ + // GDB/MI string arguments use C-string escaping. Always quoting paths keeps spaces and + // characters that are meaningful to the MI parser from changing the command. + std::string QuoteMiString(const std::string& value) + { + std::string result = "\""; + for (char ch : value) + { + switch (ch) + { + case '\\': result += "\\\\"; break; + case '"': result += "\\\""; break; + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + default: result += ch; break; + } + } + return result + "\""; + } + + std::string QuoteConsoleArgument(const std::string& value) + { + std::string result = "\""; + for (char ch : value) + { + if (ch == '\\' || ch == '"') + result += '\\'; + result += ch; + } + return result + "\""; + } + + std::optional> ParseCommandLineArguments(const std::string& commandLine) + { + enum class Quote { None, Single, Double }; + Quote quote = Quote::None; + bool escaped = false; + bool argumentStarted = false; + std::string argument; + std::vector arguments; + + for (char ch : commandLine) + { + if (escaped) + { + argument += ch; + argumentStarted = true; + escaped = false; + continue; + } + + if (ch == '\\' && quote != Quote::Single) + { + escaped = true; + argumentStarted = true; + continue; + } + if (ch == '\'' && quote != Quote::Double) + { + quote = quote == Quote::Single ? Quote::None : Quote::Single; + argumentStarted = true; + continue; + } + if (ch == '"' && quote != Quote::Single) + { + quote = quote == Quote::Double ? Quote::None : Quote::Double; + argumentStarted = true; + continue; + } + if (std::isspace(static_cast(ch)) && quote == Quote::None) + { + if (argumentStarted) + { + arguments.push_back(argument); + argument.clear(); + argumentStarted = false; + } + continue; + } + + argument += ch; + argumentStarted = true; + } + + if (escaped || quote != Quote::None) + return std::nullopt; + if (argumentStarted) + arguments.push_back(argument); + return arguments; + } + + DebugStopReason StopReasonFromSignalName(const std::string& signalName) + { + static const std::unordered_map signalReasons = { + {"SIGHUP", SignalHup}, + {"SIGINT", SignalInt}, + {"SIGQUIT", SignalQuit}, + {"SIGILL", IllegalInstruction}, + {"SIGTRAP", SingleStep}, + {"SIGABRT", SignalAbrt}, + {"SIGIOT", SignalAbrt}, + {"SIGEMT", SignalEmt}, + {"SIGFPE", SignalFpe}, + {"SIGKILL", SignalKill}, + {"SIGBUS", SignalBus}, + {"SIGSEGV", SignalSegv}, + {"SIGSYS", SignalSys}, + {"SIGPIPE", SignalPipe}, + {"SIGALRM", SignalAlrm}, + {"SIGTERM", SignalTerm}, + {"SIGURG", SignalUrg}, + {"SIGSTOP", SignalStop}, + {"SIGTSTP", SignalTstp}, + {"SIGCONT", SignalCont}, + {"SIGCHLD", SignalChld}, + {"SIGCLD", SignalChld}, + {"SIGTTIN", SignalTtin}, + {"SIGTTOU", SignalTtou}, + {"SIGIO", SignalIo}, + {"SIGXCPU", SignalXcpu}, + {"SIGXFSZ", SignalXfsz}, + {"SIGVTALRM", SignalVtalrm}, + {"SIGPROF", SignalProf}, + {"SIGWINCH", SignalWinch}, + {"SIGINFO", SignalInfo}, + {"SIGUSR1", SignalUsr1}, + {"SIGUSR2", SignalUsr2}, + {"SIGSTKFLT", SignalStkflt}, + {"SIGPOLL", SignalPoll}, + }; + + auto reason = signalReasons.find(signalName); + return reason == signalReasons.end() ? UnknownReason : reason->second; + } +} + GdbMiAdapter::GdbMiAdapter(BinaryView* data) : DebugAdapter(data) { m_lastStopReason = UnknownReason; m_targetRunningAtomic.store(false, std::memory_order_release); @@ -291,10 +430,33 @@ void GdbMiAdapter::AsyncRecordHandler(const MiRecord& record) dbgevt.data.exitData.exitCode = m_exitCode; PostDebuggerEvent(dbgevt); + { + std::unique_lock lock(m_eventMutex); + if (m_localLaunchBootstrap) + m_localLaunchExited = true; + } m_eventCV.notify_all(); } else { + bool bootstrapStop = false; + { + std::unique_lock lock(m_eventMutex); + if (m_localLaunchBootstrap) + { + m_localLaunchStopped = true; + bootstrapStop = true; + } + } + + // Local launch uses an internal starti stop to discover the executable's + // relocated entry point. Do not expose that loader stop to the controller. + if (bootstrapStop) + { + m_eventCV.notify_all(); + return; + } + // Normal stop - kick a background refresh so we don't block the reader ScheduleStateRefresh(); m_eventCV.notify_all(); @@ -403,7 +565,11 @@ DebugStopReason GdbMiAdapter::GetStopReason(const MiRecord& record) if (reason == "exited-normally" || reason == "exited") return ProcessExited; if (reason == "signal-received") - return SignalInt; + { + if (value.Exists("signal-name")) + return StopReasonFromSignalName(value["signal-name"].GetString()); + return UnknownReason; + } } return UnknownReason; } @@ -419,58 +585,8 @@ bool GdbMiAdapter::RunMonitorCommand(const std::string& command) const 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); - scope = SettingsResourceScope; - auto ipAddress = settings->Get("connect.ipAddress", data, &scope); - scope = SettingsResourceScope; - auto serverPort = static_cast(settings->Get("connect.port", data, &scope)); - if (ipAddress.empty() || serverPort == 0) - { - LogError("Missing connection settings for restart."); - return false; - } - - 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); - // TODO: we should offer an option on whether or not to connect in extended mode - std::string connectCmd = "-target-select remote " + ipAddress + ":" + std::to_string(serverPort); - - 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; - } - +bool GdbMiAdapter::DetectTargetArchitecture(bool remoteSession) +{ // Get architecture and register setup LogInfo("Detecting target architecture..."); @@ -551,6 +667,7 @@ bool GdbMiAdapter::Connect(const std::string& server, uint32_t port) { // (reverse-step) packet support, which was negotiated during -target-select. m_canReverseContinue = false; m_canReverseStep = false; + if (remoteSession) { std::string bcStatus = InvokeBackendCommand("show remote reverse-continue-packet"); std::string bsStatus = InvokeBackendCommand("show remote reverse-step-packet"); @@ -612,7 +729,7 @@ bool GdbMiAdapter::Connect(const std::string& server, uint32_t port) { // Set the final architecture m_remoteArch = detectedArch; - LogInfo("Final detected remote architecture: %s", m_remoteArch.c_str()); + LogInfo("Final detected target architecture: %s", m_remoteArch.c_str()); // Get register names (regListResult already fetched above) if (regListResult.command == "done") @@ -644,6 +761,66 @@ bool GdbMiAdapter::Connect(const std::string& server, uint32_t port) { } } + return true; +} + +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); + scope = SettingsResourceScope; + auto ipAddress = settings->Get("connect.ipAddress", data, &scope); + scope = SettingsResourceScope; + auto serverPort = static_cast(settings->Get("connect.port", data, &scope)); + if (ipAddress.empty() || serverPort == 0) + { + LogError("Missing connection settings for restart."); + return false; + } + + 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"); + if (!symbolFile.empty()) + m_mi->SendCommand("-interpreter-exec console " + + QuoteMiString("add-symbol-file " + QuoteConsoleArgument(symbolFile))); + + m_mi->SendCommand("-file-exec-file " + QuoteMiString(inputFile)); + // TODO: we should offer an option on whether or not to connect in extended mode + std::string connectCmd = "-target-select remote " + ipAddress + ":" + std::to_string(serverPort); + + 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; + } + + if (!DetectTargetArchitecture(true)) + return false; + // AFTER we are connected and stopped, populate the cache for the first time. LogInfo("Populating initial state cache..."); ScheduleStateRefresh(); @@ -655,24 +832,290 @@ bool GdbMiAdapter::Connect(const std::string& server, uint32_t port) { 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&) +bool GdbMiAdapter::Execute(const std::string& path, const LaunchConfigurations& configs) +{ + return ExecuteWithArgs(path, "", "", configs); +} + +bool GdbMiAdapter::ExecuteWithArgs(const std::string& path, const std::string& args, + const std::string& workingDir, const LaunchConfigurations& configs) { + (void)configs; InvalidateCache(); + auto settings = GetAdapterSettings(); BNSettingsScope scope = SettingsResourceScope; auto data = GetData(); - auto server = settings->Get("connect.ipAddress", data, &scope); + auto gdbPath = settings->Get("gdb.path", data, &scope); scope = SettingsResourceScope; - auto port = static_cast(settings->Get("connect.port", data, &scope)); - if (server.empty() || port == 0) + auto symbolFile = settings->Get("gdb.symbolFile", data, &scope); + scope = SettingsResourceScope; + auto executablePath = settings->Get("launch.executablePath", data, &scope); + scope = SettingsResourceScope; + auto workingDirectory = settings->Get("launch.workingDirectory", data, &scope); + scope = SettingsResourceScope; + auto commandLineArgs = settings->Get("launch.commandLineArguments", data, &scope); + + // Use settings values, fall back to function parameters + if (executablePath.empty()) + executablePath = path; + if (workingDirectory.empty()) + workingDirectory = workingDir; + if (commandLineArgs.empty()) + commandLineArgs = args; + + if (gdbPath.empty()) { - LogError("Missing connection settings for restart."); + LogError("GDB path is not configured"); return false; } - return Connect(server, port); + if (executablePath.empty()) + { + LogError("No executable path specified for local debugging"); + return false; + } + + m_connected = false; + m_mi = std::make_unique(gdbPath, ""); + + // 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()) + { + LogError("Failed to start GDB process"); + 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"); + + // Load the executable explicitly so launch failures can be reported before the adapter + // enters its asynchronous run/wait state. + auto fileResult = m_mi->SendCommand("-file-exec-and-symbols " + QuoteMiString(executablePath)); + if (fileResult.command != "done") + { + LogError("Failed to load executable: %s", fileResult.fullLine.c_str()); + m_mi->Stop(); + m_mi.reset(); + return false; + } + + if (!symbolFile.empty() && symbolFile != executablePath) + { + auto symbolResult = m_mi->SendCommand("-interpreter-exec console " + + QuoteMiString("add-symbol-file " + QuoteConsoleArgument(symbolFile))); + if (symbolResult.command != "done") + { + LogError("Failed to load symbol file: %s", symbolResult.fullLine.c_str()); + m_mi->Stop(); + m_mi.reset(); + return false; + } + } + + if (!workingDirectory.empty()) + { + auto cwdResult = m_mi->SendCommand("-environment-cd " + QuoteMiString(workingDirectory)); + if (cwdResult.command != "done") + { + LogError("Failed to set working directory: %s", cwdResult.fullLine.c_str()); + m_mi->Stop(); + m_mi.reset(); + return false; + } + } + + if (!commandLineArgs.empty()) + { + auto parsedArguments = ParseCommandLineArguments(commandLineArgs); + if (!parsedArguments) + { + LogError("Invalid command line arguments: unmatched quote or trailing escape"); + m_mi->Stop(); + m_mi.reset(); + return false; + } + + std::string argumentCommand = "-exec-arguments"; + for (const auto& argument : *parsedArguments) + argumentCommand += " " + QuoteMiString(argument); + auto argumentResult = m_mi->SendCommand(argumentCommand); + if (argumentResult.command != "done") + { + LogError("Failed to set command line arguments: %s", argumentResult.fullLine.c_str()); + m_mi->Stop(); + m_mi.reset(); + return false; + } + } + + // Mark the transport ready before architecture detection, since backend console + // commands and pending breakpoints require an active adapter session. + m_connected = true; + + if (!DetectTargetArchitecture(false)) + { + m_connected = false; + m_mi->Stop(); + m_mi.reset(); + return false; + } + + // Software breakpoints with an immediately resolvable address can be installed + // before execution. Module-relative and hardware breakpoints remain pending and + // are applied by the first stopped-event state refresh. + ApplyBreakpoints(); + + // GDB's MI --start option is equivalent to the CLI `start` command and therefore + // depends on a discoverable `main` symbol. Stripped executables do not have one, + // so they would run directly to completion. Start at the first machine instruction + // instead, then resolve the relocated ELF entry point while the process is stopped. + { + std::unique_lock lock(m_eventMutex); + m_localLaunchBootstrap = true; + m_localLaunchStopped = false; + m_localLaunchExited = false; + } + + auto clearLaunchBootstrap = [this]() { + std::unique_lock lock(m_eventMutex); + m_localLaunchBootstrap = false; + m_localLaunchStopped = false; + m_localLaunchExited = false; + }; + + auto waitForLaunchStop = [this](std::chrono::milliseconds timeout) { + std::unique_lock lock(m_eventMutex); + return m_eventCV.wait_for(lock, timeout, + [this]() { return m_localLaunchStopped || m_localLaunchExited; }); + }; + + auto runResult = m_mi->SendCommand("-interpreter-exec console " + QuoteMiString("starti"), 5000); + if (runResult.command != "running" && runResult.command != "done") + { + LogError("Failed to launch target: %s", runResult.fullLine.c_str()); + clearLaunchBootstrap(); + m_connected = false; + m_mi->Stop(); + m_mi.reset(); + return false; + } + + if (!waitForLaunchStop(std::chrono::seconds(15))) + { + LogError("Timed out waiting for the initial GDB stop"); + clearLaunchBootstrap(); + m_connected = false; + m_mi->Stop(); + m_mi.reset(); + return false; + } + + { + std::unique_lock lock(m_eventMutex); + if (m_localLaunchExited) + { + m_localLaunchBootstrap = false; + return true; + } + } + + const bool stopAtSystemEntry = Settings::Instance()->Get("debugger.stopAtSystemEntryPoint"); + const bool stopAtProgramEntry = Settings::Instance()->Get("debugger.stopAtEntryPoint"); + bool continueToProgramEntry = !stopAtSystemEntry && stopAtProgramEntry && m_hasEntryFunction + && (m_entryPoint >= m_start); + + uint64_t entryAddress = 0; + uint64_t currentPc = 0; + if (continueToProgramEntry) + { + uint64_t moduleBase = 0; + if (!GetModuleBase(executablePath, moduleBase)) + { + LogWarn("Could not resolve the executable load address; stopping at the system entry point"); + continueToProgramEntry = false; + } + else + { + entryAddress = moduleBase + (m_entryPoint - m_start); + auto pcResult = m_mi->SendCommand("-data-evaluate-expression $pc"); + if (pcResult.command == "done") + { + auto value = MiValue::Parse(pcResult.payload); + if (value.Exists("value")) + { + try + { + currentPc = std::stoull(value["value"].GetString(), nullptr, 0); + } + catch (...) + { + LogWarn("Failed to parse the program counter at the initial GDB stop"); + } + } + } + } + } + + // The process now exists, so module-relative user breakpoints that could not be + // resolved before starti can be installed before any program code executes. + ApplyBreakpoints(); + ApplyPendingHardwareBreakpoints(); + + if (continueToProgramEntry && currentPc != entryAddress) + { + auto breakpointResult = + m_mi->SendCommand(fmt::format("-break-insert -t *0x{:x}", entryAddress)); + if (breakpointResult.command != "done") + { + LogWarn("Failed to set temporary entry-point breakpoint: %s", breakpointResult.fullLine.c_str()); + } + else + { + { + std::unique_lock lock(m_eventMutex); + m_localLaunchStopped = false; + m_localLaunchExited = false; + } + + auto continueResult = m_mi->SendCommand("-exec-continue", 5000); + if (continueResult.command != "running" && continueResult.command != "done") + { + LogWarn("Failed to continue to the program entry point: %s", continueResult.fullLine.c_str()); + } + else if (!waitForLaunchStop(std::chrono::seconds(15))) + { + LogError("Timed out waiting for the program entry-point stop"); + clearLaunchBootstrap(); + m_connected = false; + m_mi->Stop(); + m_mi.reset(); + return false; + } + } + } + + { + std::unique_lock lock(m_eventMutex); + if (m_localLaunchExited) + { + m_localLaunchBootstrap = false; + return true; + } + m_localLaunchBootstrap = false; + m_localLaunchStopped = false; + } + + // Publish only the final stop (normally the relocated ELF entry point) to the + // controller. The internal starti stop remains invisible to the UI. + ScheduleStateRefresh(); + + return true; } + bool GdbMiAdapter::Attach(uint32_t) { InvalidateCache(); auto settings = GetAdapterSettings(); @@ -929,14 +1372,23 @@ bool GdbMiAdapter::WriteRegister(const std::string& reg, intx::uint512 value) { std::string cmd = "-gdb-set $" + reg + "=" + to_string(value); auto result = m_mi->SendCommand(cmd); - return result.command == "done"; + if (result.command != "done") + return false; + + // Register values are prefetched by the MI adapter. Refresh the entire cache + // because writing one register can also change correlated registers. + { + std::unique_lock cacheLock(m_cacheMutex); + m_cachedRegisters.clear(); + } + UpdateAllRegisters(); + return true; } DataBuffer GdbMiAdapter::ReadMemory(std::uintptr_t address, size_t size) { if (!m_mi) return {}; LogDebug("GdbMiAdapter::ReadMemory 0x%" PRIX64 "-0x%" PRIX64, (uint64_t)address, (uint64_t)(address+size)); // TODO: we can use 'info mem' to get list of memory regions available for reading. - DataBuffer zero(size); // Acquire GDB command mutex to serialize access to GDB std::unique_lock cmdLock(m_gdbCommandMutex); @@ -944,14 +1396,24 @@ DataBuffer GdbMiAdapter::ReadMemory(std::uintptr_t address, size_t size) { std::string cmd = fmt::format("-data-read-memory-bytes 0x{:x} {}", address, size); auto result = m_mi->SendCommand(cmd); if (result.command != "done") - { + { LogDebug("Failed to read memory at 0x%" PRIX64, (uint64_t)address); - - return zero; - } + return {}; + } auto value = MiValue::Parse(result.payload); + if (!value.Exists("memory") || !value["memory"].IsList() || value["memory"].size() == 0 + || !value["memory"][0].Exists("contents")) + { + LogDebug("Malformed memory read reply"); + return {}; + } std::string hex_contents = value["memory"][0]["contents"].GetString(); + if ((hex_contents.length() % 2) != 0) + { + LogDebug("Odd-length hex contents in memory read reply"); + return {}; + } DataBuffer buffer(hex_contents.length() / 2); for(size_t i = 0; i < buffer.GetLength(); i++) { // Parse with the non-throwing std::from_chars, since std::stoul throws on @@ -961,7 +1423,7 @@ DataBuffer GdbMiAdapter::ReadMemory(std::uintptr_t address, size_t size) { if (std::from_chars(first, first + 2, byte, 16).ec != std::errc()) { LogDebug("Malformed hex contents in memory read reply"); - return zero; + return {}; } buffer[i] = byte; } @@ -1544,6 +2006,20 @@ void GdbMiAdapter::GenerateDefaultAdapterSettings(BinaryView* data) if (scope != SettingsResourceScope) adapterSettings->Set("common.inputFile", data->GetFile()->GetOriginalFilename(), data, SettingsResourceScope); + scope = SettingsResourceScope; + adapterSettings->Get("launch.executablePath", data, &scope); + if (scope != SettingsResourceScope) + adapterSettings->Set("launch.executablePath", data->GetFile()->GetOriginalFilename(), data, SettingsResourceScope); +} + +bool GdbMiAdapterType::CanExecute(BinaryView* data) +{ +#ifdef __linux__ + return data && data->GetTypeName() == "ELF"; +#else + (void)data; + return false; +#endif } Ref GdbMiAdapterType::RegisterAdapterSettings() @@ -1552,7 +2028,7 @@ Ref GdbMiAdapterType::RegisterAdapterSettings() settings->SetResourceId("gdb_mi_adapter_settings"); settings->RegisterSetting("gdb.path", R"({ "title": "Full GDB Executable Path", - "type": "string", "default": "/usr/bin/gdb-multiarch", + "type": "string", "default": "/usr/bin/gdb", "description": "Path to the GDB executable e.g., gdb-multiarch, arm-none-eabi-gdb.", "uiSelectionAction": "file" })"); @@ -1566,6 +2042,33 @@ Ref GdbMiAdapterType::RegisterAdapterSettings() "uiSelectionAction" : "file" })"); + settings->RegisterSetting("launch.executablePath", + R"({ + "title" : "Executable Path", + "type" : "string", + "default" : "", + "description" : "Path of the executable to launch for local debugging.", + "readOnly" : false, + "uiSelectionAction" : "file" + })"); + settings->RegisterSetting("launch.workingDirectory", + R"({ + "title" : "Working Directory", + "type" : "string", + "default" : "", + "description" : "Working directory to launch the target in.", + "readOnly" : false, + "uiSelectionAction" : "directory" + })"); + settings->RegisterSetting("launch.commandLineArguments", + R"({ + "title" : "Command Line Arguments", + "type" : "string", + "default" : "", + "description" : "Command line arguments to pass to the target.", + "readOnly" : false + })"); + settings->RegisterSetting("connect.ipAddress", R"({ "title" : "IP Address", diff --git a/core/adapters/gdbmiadapter.h b/core/adapters/gdbmiadapter.h index afb19e0e..b3f906cc 100644 --- a/core/adapters/gdbmiadapter.h +++ b/core/adapters/gdbmiadapter.h @@ -25,6 +25,9 @@ class GdbMiAdapter : public BinaryNinjaDebugger::DebugAdapter std::mutex m_eventMutex; std::mutex m_gdbCommandMutex; std::condition_variable m_eventCV; + bool m_localLaunchBootstrap = false; + bool m_localLaunchStopped = false; + bool m_localLaunchExited = false; // Console output buffering for console commands std::mutex m_consoleBufferMutex; @@ -50,6 +53,7 @@ class GdbMiAdapter : public BinaryNinjaDebugger::DebugAdapter static intx::uint512 ParseGdbValue(const std::string& valueStr); bool RunMonitorCommand(const std::string& command) const; + bool DetectTargetArchitecture(bool remoteSession); void ApplyBreakpoints(); void ApplyPendingHardwareBreakpoints(); bool GetModuleBase(const std::string& moduleName, uint64_t& base); @@ -136,7 +140,7 @@ class GdbMiAdapterType : public BinaryNinjaDebugger::DebugAdapterType 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; } + bool CanExecute(BinaryView* data) override; static Ref GetAdapterSettings(); private: diff --git a/test/debugger_test.py b/test/debugger_test.py index 685dcee5..4ea8854a 100644 --- a/test/debugger_test.py +++ b/test/debugger_test.py @@ -6,8 +6,10 @@ import sys import time import platform +import shutil import threading import subprocess +import tempfile import unittest from binaryninja import load, Settings @@ -653,6 +655,64 @@ def test_attach(self): dbg.quit_and_wait() +@unittest.skipUnless(platform.system() == 'Linux', 'GDB MI local launch is only supported on Linux') +class GdbMiLinuxTest(DebuggerAPI): + def setUp(self) -> None: + self.arch = 'arm64' if platform.machine() in ['arm64', 'aarch64'] else platform.machine() + self.adapter_type = 'GDB MI' + + def create_debugger(self, bv): + dbg = super().create_debugger(bv) + + # Debugger settings are registered lazily when the first controller is + # constructed, so configure them here rather than in setUp. + if not hasattr(self, '_entry_settings_configured'): + settings = Settings() + previous_system_entry = settings.get_bool('debugger.stopAtSystemEntryPoint') + previous_program_entry = settings.get_bool('debugger.stopAtEntryPoint') + self.assertTrue(settings.set_bool('debugger.stopAtSystemEntryPoint', False)) + self.assertTrue(settings.set_bool('debugger.stopAtEntryPoint', True)) + self.addCleanup(settings.set_bool, 'debugger.stopAtEntryPoint', previous_program_entry) + self.addCleanup(settings.set_bool, 'debugger.stopAtSystemEntryPoint', previous_system_entry) + self._entry_settings_configured = True + + return dbg + + def test_local_launch_stops_at_stripped_pie_entry_point(self): + gdb_path = shutil.which('gdb') + strip_path = shutil.which('strip') + if gdb_path is None: + self.skipTest('gdb is not installed') + if strip_path is None: + self.skipTest('strip is not installed') + + source_path = name_to_fpath('helloworld_pie', self.arch) + if not os.path.exists(source_path): + self.skipTest('PIE test binary not built (configure with -DBUILD_DEBUGGER_TEST_BINARIES=ON)') + + with tempfile.TemporaryDirectory() as temp_dir: + stripped_path = os.path.join(temp_dir, 'helloworld_pie_stripped') + shutil.copy2(source_path, stripped_path) + subprocess.run([strip_path, '--strip-all', stripped_path], check=True) + + bv = load(stripped_path) + dbg = self.create_debugger(bv) + dbg.executable_path = stripped_path + dbg.set_adapter_property('gdb.path', gdb_path) + + try: + reason = dbg.launch_and_wait(20000) + self.assertEqual(reason, DebugStopReason.Breakpoint) + + remote_base = dbg.get_remote_base() + self.assertIsNotNone(remote_base) + expected_entry = remote_base + (bv.entry_point - bv.start) + self.assertEqual(dbg.ip, expected_entry) + finally: + if dbg.connected: + dbg.quit_and_wait() + + @unittest.skipIf(platform.machine() not in ['arm64', 'aarch64'], "Only run arm64 tests on arm Mac or Linux") class DebuggerArm64Test(DebuggerAPI): def setUp(self) -> None: