diff --git a/core/adapters/gdbmiadapter.cpp b/core/adapters/gdbmiadapter.cpp index 888989f0..d23b7a7b 100644 --- a/core/adapters/gdbmiadapter.cpp +++ b/core/adapters/gdbmiadapter.cpp @@ -418,58 +418,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::StartGdbAndDetectArch(const std::string& gdbPath, const std::string& inputFile, const std::string& symbolFile) +{ // Get architecture and register setup LogInfo("Detecting target architecture..."); @@ -643,6 +593,65 @@ 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; + m_isLocalSession = 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; + } + + if (!StartGdbAndDetectArch(gdbPath, inputFile, symbolFile)) + return false; + // AFTER we are connected and stopped, populate the cache for the first time. LogInfo("Populating initial state cache..."); ScheduleStateRefresh(); @@ -654,25 +663,167 @@ 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) { 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 inputFile = settings->Get("common.inputFile", 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_isLocalSession = true; + + if (inputFile.empty()) inputFile = executablePath; + + // Check if the executable path is a UDB recording file (.undo) + // For recordings, don't pass the file as GDB's target executable argument + bool isUndoRecording = (executablePath.size() >= 5 && + executablePath.substr(executablePath.size() - 5) == ".undo"); + + std::string gdbTargetArg = isUndoRecording ? "" : inputFile; + m_mi = std::make_unique(gdbPath, gdbTargetArg); + + // 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"); + + if (isUndoRecording) + { + // UDB recording: use "uload" to load the trace file. + // After uload, the target is already stopped at the beginning of the recording. + auto uloadResult = m_mi->SendCommand( + "-interpreter-exec console \"uload " + executablePath + "\"", 30000); + if (uloadResult.command != "done") + { + LogError("Failed to load UDB recording: %s", uloadResult.fullLine.c_str()); + m_mi->Stop(); + m_mi.reset(); + return false; + } + + // Load separate symbol file if specified + if (!symbolFile.empty()) + m_mi->SendCommand("-interpreter-exec console \"add-symbol-file " + symbolFile + "\""); + + m_connected = true; + + if (!StartGdbAndDetectArch(gdbPath, inputFile, symbolFile)) + { + m_connected = false; + m_mi->Stop(); + m_mi.reset(); + return false; + } + + // UDB recording is already stopped after uload, just populate state + LogInfo("UDB recording loaded, populating initial state..."); + ScheduleStateRefresh(); + + ApplyBreakpoints(); + ApplyPendingHardwareBreakpoints(); + } + else + { + // Normal local executable: load and run it + auto fileResult = m_mi->SendCommand("-file-exec-and-symbols " + executablePath); + if (fileResult.command != "done") + { + LogError("Failed to load executable: %s", fileResult.fullLine.c_str()); + m_mi->Stop(); + m_mi.reset(); + return false; + } + + // Load separate symbol file if specified + if (!symbolFile.empty() && symbolFile != executablePath) + m_mi->SendCommand("-interpreter-exec console \"add-symbol-file " + symbolFile + "\""); + + // Set working directory if specified + if (!workingDirectory.empty()) + m_mi->SendCommand("-environment-cd " + workingDirectory); + + // Set command line arguments if specified + if (!commandLineArgs.empty()) + m_mi->SendCommand("-exec-arguments " + commandLineArgs); + + // Apply breakpoints before running so entry breakpoints work + LogInfo("Applying breakpoints before launch..."); + m_connected = true; + + if (!StartGdbAndDetectArch(gdbPath, inputFile, symbolFile)) + { + m_connected = false; + m_mi->Stop(); + m_mi.reset(); + return false; + } + + ApplyBreakpoints(); + ApplyPendingHardwareBreakpoints(); + + // Launch the target with -exec-run, which starts the inferior and stops at the first + // breakpoint (or runs to completion if none are set). The --start flag stops at main. + auto runResult = m_mi->SendCommand("-exec-run --start", 5000); + if (runResult.command != "running" && runResult.command != "done") + { + LogError("Failed to launch target: %s", runResult.fullLine.c_str()); + m_connected = false; + m_mi->Stop(); + m_mi.reset(); + return false; + } + } + + return true; } -bool GdbMiAdapter::Attach(uint32_t) { +bool GdbMiAdapter::Attach(uint32_t pid) { InvalidateCache(); auto settings = GetAdapterSettings(); BNSettingsScope scope = SettingsResourceScope; @@ -680,13 +831,12 @@ bool GdbMiAdapter::Attach(uint32_t) { 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); + if (!server.empty() && port != 0) + return Connect(server, port); + + LogError("Missing connection settings for attach."); + return false; } std::vector GdbMiAdapter::GetProcessList() { LogWarn("GdbMiAdapter::GetProcessList not implemented"); return {}; } bool GdbMiAdapter::SuspendThread(uint32_t) { LogWarn("GdbMiAdapter::SuspendThread not implemented"); return false; } @@ -1451,6 +1601,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) +{ + // GDB MI local debugging is supported on Linux and macOS for ELF and Mach-O binaries + // For PE files, users should use DbgEng or Windows Native adapters + if (data->GetTypeName() == "PE") + return false; + + return true; } Ref GdbMiAdapterType::RegisterAdapterSettings() @@ -1473,6 +1637,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 dc9005a1..88e933fb 100644 --- a/core/adapters/gdbmiadapter.h +++ b/core/adapters/gdbmiadapter.h @@ -12,6 +12,7 @@ class GdbMiAdapter : public BinaryNinjaDebugger::DebugAdapter uint64_t m_lastStopTid = 1; uint64_t m_currentTid = 1; bool m_connected = false; + bool m_isLocalSession = false; // true when launched locally via Execute, false for remote Connect std::string m_remoteArch; std::vector m_registerNames; // In GDB's order @@ -50,6 +51,7 @@ class GdbMiAdapter : public BinaryNinjaDebugger::DebugAdapter static intx::uint512 ParseGdbValue(const std::string& valueStr); bool RunMonitorCommand(const std::string& command) const; + bool StartGdbAndDetectArch(const std::string& gdbPath, const std::string& inputFile, const std::string& symbolFile); void ApplyBreakpoints(); void ApplyPendingHardwareBreakpoints(); bool GetModuleBase(const std::string& moduleName, uint64_t& base); @@ -135,7 +137,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: