From 82f23a9ffed51ea3dc509ef83b514ee9ed24de0d Mon Sep 17 00:00:00 2001 From: hermansimensen Date: Sun, 24 May 2026 00:02:41 +0200 Subject: [PATCH] Initial remote debugging support Adds the ability to debug beef software on embedded platforms through the gdb-remote protocol --- IDE/src/Commands.bf | 1 + IDE/src/Debugger/DebugManager.bf | 15 ++ IDE/src/IDEApp.bf | 43 ++++++ IDE/src/ui/RemoteDebugDialog.bf | 96 ++++++++++++ IDEHelper/DebugManager.cpp | 18 +++ IDEHelper/Debugger.h | 2 + IDEHelper/LLDBDebugger.cpp | 251 ++++++++++++++++++++++++++++++- IDEHelper/LLDBDebugger.h | 17 +++ 8 files changed, 441 insertions(+), 2 deletions(-) create mode 100644 IDE/src/ui/RemoteDebugDialog.bf diff --git a/IDE/src/Commands.bf b/IDE/src/Commands.bf index fcd9956ff..8c2422177 100644 --- a/IDE/src/Commands.bf +++ b/IDE/src/Commands.bf @@ -361,6 +361,7 @@ namespace IDE Add("Zoom Out", new => gApp.Cmd_ZoomOut); Add("Zoom Reset", new => gApp.Cmd_ZoomReset); Add("Attach to Process", new => gApp.[Friend]DoAttach); + Add("Connect Remote Debugger", new => gApp.[Friend]DoConnectRemote); Add("Move Last Selection to Next Find Match", new => gApp.Cmd_MoveLastSelectionToNextFindMatch); Add("Test Enable Console", new => gApp.Cmd_TestEnableConsole); diff --git a/IDE/src/Debugger/DebugManager.bf b/IDE/src/Debugger/DebugManager.bf index f6765d614..e5b018919 100644 --- a/IDE/src/Debugger/DebugManager.bf +++ b/IDE/src/Debugger/DebugManager.bf @@ -173,6 +173,12 @@ namespace IDE.Debugger [CallingConvention(.Stdcall),CLink] static extern bool Debugger_Attach(int32 processId, AttachFlags attachFlags); + [CallingConvention(.Stdcall),CLink] + static extern bool Debugger_ConnectRemote(char8* host, int32 port, char8* elfPath); + + [CallingConvention(.Stdcall),CLink] + public static extern bool Debugger_SupportsRemoteConnect(); + [CallingConvention(.Stdcall),CLink] public static extern void Debugger_GetStdHandles(Platform.BfpFile** outStdIn, Platform.BfpFile** outStdOut, Platform.BfpFile** outStdErr); @@ -1206,6 +1212,15 @@ namespace IDE.Debugger return Debugger_Attach(process.Id, attachFlags); } + public bool ConnectRemote(String host, int32 port, String elfPath) + { + mIsRunningCompiled = false; + mIsComptimeDebug = false; + mIsRunningWithHotSwap = false; + mIsRunning = Debugger_ConnectRemote(host, port, elfPath); + return mIsRunning; + } + public void GetStdHandles(Platform.BfpFile** outStdIn, Platform.BfpFile** outStdOut, Platform.BfpFile** outStdErr) { Debugger_GetStdHandles(outStdIn, outStdOut, outStdErr); diff --git a/IDE/src/IDEApp.bf b/IDE/src/IDEApp.bf index 6c9124e8b..2f9c48bd2 100644 --- a/IDE/src/IDEApp.bf +++ b/IDE/src/IDEApp.bf @@ -5778,6 +5778,45 @@ namespace IDE } } + [IDECommand] + void DoConnectRemote() + { + var widgetWindow = GetCurrentWindow(); + if (widgetWindow != null) + { + var dialog = new RemoteDebugDialog(); + dialog.PopupWindow(mMainWindow); + } + } + + public void ConnectRemoteDebugger(String host, int32 port, String elfPath) + { + if (mDebugger.mIsRunning) + { + Fail("Already debugging a target"); + return; + } + + mExecutionPaused = false; + // Remote targets are already halted at connect time; treating the first + // stop as the "init break" would trigger auto-continue into ROM hardware + // breakpoints. Mark it handled so the IDE goes straight to Paused. + mTargetDidInitBreak = true; + mTargetHadFirstBreak = false; + + if (!mDebugger.ConnectRemote(host, port, elfPath)) + { + Fail("Failed to initiate remote connection"); + return; + } + + OutputLine("Connecting to remote target at {0}:{1} (ELF: {2})", host, port, elfPath); + CheckDebugVisualizers(); + mDebugger.IncrementSessionIdx(); + mDebugger.RehupBreakpoints(true); + mIsAttachPendingSourceShow = true; + } + [IDECommand] void DoLaunch() { @@ -6321,6 +6360,10 @@ namespace IDE AddMenuItem(subMenu, "Start With&out Compiling", "Start Without Compiling", new => UpdateMenuItem_DebugStopped_HasWorkspace); AddMenuItem(subMenu, "&Launch Process...", "Launch Process", new => UpdateMenuItem_DebugStopped); AddMenuItem(subMenu, "&Attach to Process...", "Attach to Process", new => UpdateMenuItem_DebugStopped); + AddMenuItem(subMenu, "Connect to &Remote Target...", "Connect Remote Debugger", new (item) => + { + item.SetDisabled(mDebugger.mIsRunning || !DebugManager.Debugger_SupportsRemoteConnect()); + }); AddMenuItem(subMenu, "&Stop Debugging", "Stop Debugging", new => UpdateMenuItem_DebugOrTestRunning); AddMenuItem(subMenu, "Break All", "Break All", new => UpdateMenuItem_DebugNotPaused); AddMenuItem(subMenu, "Remove All Breakpoints", "Remove All Breakpoints"); diff --git a/IDE/src/ui/RemoteDebugDialog.bf b/IDE/src/ui/RemoteDebugDialog.bf new file mode 100644 index 000000000..37169da1f --- /dev/null +++ b/IDE/src/ui/RemoteDebugDialog.bf @@ -0,0 +1,96 @@ +using System; +using Beefy.events; +using Beefy.widgets; +using Beefy.theme.dark; +using Beefy.gfx; +using Beefy; + +namespace IDE.ui +{ + class RemoteDebugDialog : DarkDialog + { + public static String sLastHost = new String("localhost") ~ delete _; + public static int32 sLastPort = 1234; + public static String sLastElf = new String("") ~ delete _; + + EditWidget mHostEdit; + EditWidget mPortEdit; + PathEditWidget mElfEdit; + + public this() + { + mWindowFlags = BFWindow.Flags.ClientSized | .TopMost | .Caption | + .Border | .SysMenu | .PopupPosition; + + AddOkCancelButtons(new (evt) => { Connect(); }, null, 0, 1); + Title = "Connect to Remote Target"; + + mHostEdit = AddEdit(sLastHost); + var portStr = scope String(); + sLastPort.ToString(portStr); + mPortEdit = AddEdit(portStr); + + mElfEdit = new PathEditWidget(.File); + mElfEdit.SetText(sLastElf); + AddWidget(mElfEdit); + } + + void Connect() + { + var host = scope String(); + mHostEdit.GetText(host); + host.Trim(); + + var portStr = scope String(); + mPortEdit.GetText(portStr); + portStr.Trim(); + + int32 port = sLastPort; + if (int32.Parse(portStr) case .Ok(let p)) + port = p; + + var elfPath = scope String(); + mElfEdit.GetText(elfPath); + elfPath.Trim(); + + sLastHost.Set(host); + sLastPort = port; + sLastElf.Set(elfPath); + + IDEApp.sApp.ConnectRemoteDebugger(host, port, elfPath); + } + + public override void Draw(Graphics g) + { + base.Draw(g); + using (g.PushColor(DarkTheme.COLOR_TEXT)) + { + g.SetFont(DarkTheme.sDarkTheme.mSmallFont); + g.DrawString("Host:", mHostEdit.mX, mHostEdit.mY - 18); + g.DrawString("Port:", mPortEdit.mX, mPortEdit.mY - 18); + g.DrawString("ELF Binary:", mElfEdit.mX, mElfEdit.mY - 18); + } + } + + public override void ResizeComponents() + { + base.ResizeComponents(); + float editW = mWidth - 12; + mHostEdit.Resize(6, 40, editW, 20); + mPortEdit.Resize(6, 100, editW, 20); + mElfEdit.Resize(6, 160, editW, 20); + } + + public override void CalcSize() + { + mWidth = 320; + mHeight = 240; + } + + public override void Resize(float x, float y, float width, float height) + { + base.Resize(x, y, width, height); + ResizeComponents(); + } + } +} diff --git a/IDEHelper/DebugManager.cpp b/IDEHelper/DebugManager.cpp index fc5ba3a2d..dd1b7f682 100644 --- a/IDEHelper/DebugManager.cpp +++ b/IDEHelper/DebugManager.cpp @@ -911,6 +911,24 @@ BF_EXPORT bool BF_CALLTYPE Debugger_Attach(int processId, BfDbgAttachFlags attac return false; } +BF_EXPORT bool BF_CALLTYPE Debugger_ConnectRemote(const char* host, int32 port, const char* elfPath) +{ + BF_ASSERT(gDebugger == NULL); + + if (gDebugManager->mDebugger64->ConnectRemote(host, port, elfPath != NULL ? elfPath : "")) + { + gDebugger = gDebugManager->mDebugger64; + return true; + } + + return false; +} + +BF_EXPORT bool BF_CALLTYPE Debugger_SupportsRemoteConnect() +{ + return gDebugManager->mDebugger64->SupportsRemoteConnect(); +} + BF_EXPORT void Debugger_GetStdHandles(BfpFile** outStdIn, BfpFile** outStdOut, BfpFile** outStdErr) { if (gDebugger != NULL) diff --git a/IDEHelper/Debugger.h b/IDEHelper/Debugger.h index 851bd19ac..33d9cbbaf 100644 --- a/IDEHelper/Debugger.h +++ b/IDEHelper/Debugger.h @@ -276,6 +276,8 @@ class Debugger virtual bool CanOpen(const StringImpl& fileName, DebuggerResult* outResult) = 0; virtual void OpenFile(const StringImpl& launchPath, const StringImpl& targetPath, const StringImpl& args, const StringImpl& workingDir, const Array& envBlock, bool hotSwapEnabled, DbgOpenFileFlags openFileFlags) = 0; virtual bool Attach(int processId, BfDbgAttachFlags attachFlags) = 0; + virtual bool ConnectRemote(const StringImpl& host, int port, const StringImpl& elfPath) { return false; } + virtual bool SupportsRemoteConnect() { return false; } virtual void GetStdHandles(BfpFile** outStdIn, BfpFile** outStdOut, BfpFile** outStdErr) = 0; virtual void Run() = 0; virtual bool HasLoadedTargetBinary() { return true; } diff --git a/IDEHelper/LLDBDebugger.cpp b/IDEHelper/LLDBDebugger.cpp index f11460a39..e71791ac9 100644 --- a/IDEHelper/LLDBDebugger.cpp +++ b/IDEHelper/LLDBDebugger.cpp @@ -129,7 +129,11 @@ LLDBDebugger::LLDBDebugger(DebugManager* debugManager) mExceptionCode = 0; mHotSwapEnabled = false; mOpenFileFlags = DbgOpenFileFlag_None; + mRemotePort = 0; + mIsRemoteConnect = false; mLaunchThread = NULL; + mEventThreadStop = false; + mEventThread = NULL; } LLDBDebugger::~LLDBDebugger() @@ -379,7 +383,11 @@ void LLDBDebugger::DoLaunch() void BFP_CALLTYPE LLDBDebugger::LaunchThreadProc(void* param) { - ((LLDBDebugger*)param)->DoLaunch(); + LLDBDebugger* self = (LLDBDebugger*)param; + if (self->mIsRemoteConnect) + self->DoConnectRemote(); + else + self->DoLaunch(); } bool LLDBDebugger::Attach(int processId, BfDbgAttachFlags attachFlags) @@ -388,6 +396,165 @@ bool LLDBDebugger::Attach(int processId, BfDbgAttachFlags attachFlags) return false; } +void LLDBDebugger::DoConnectRemote() +{ + lldb::SBDebugger::Initialize(); + + lldb::SBDebugger debugger = lldb::SBDebugger::Create(false); + debugger.SetAsync(false); + + debugger.HandleCommand("settings set plugin.process.gdb-remote.packet-timeout 5"); + debugger.HandleCommand("settings set thread.max-backtrace-depth 32"); + + if (mElfPath.empty()) + OutputMessage("remote-connect: WARNING: no ELF path set"); + + lldb::SBError mLastError; + lldb::SBTarget target = debugger.CreateTarget( + mElfPath.empty() ? "" : mElfPath.c_str(), + nullptr, nullptr, false, mLastError); + + if (!target.IsValid()) + { + OutputMessage("remote-connect: target creation failed"); + lldb::SBDebugger::Destroy(debugger); + mRunState = RunState_Terminated; + return; + } + + String connectTarget = StrFormat("%s:%d", mRemoteHost.c_str(), mRemotePort); + + lldb::SBCommandReturnObject connectResult; + debugger.GetCommandInterpreter().HandleCommand( + StrFormat("process connect --plugin gdb-remote connect://%s", + connectTarget.c_str()).c_str(), + connectResult); + + if (!connectResult.Succeeded()) + { + const char* errMsg = connectResult.GetError(); + OutputMessage(StrFormat("remote-connect: connect to %s failed: %s", + connectTarget.c_str(), + ((errMsg != NULL) && (errMsg[0] != '\0')) ? errMsg : "(no details)")); + lldb::SBDebugger::Destroy(debugger); + mRunState = RunState_Terminated; + return; + } + + lldb::SBProcess process = target.GetProcess(); + + if (!process.IsValid()) + { + OutputMessage("remote-connect: invalid process after connect"); + lldb::SBDebugger::Destroy(debugger); + mRunState = RunState_Terminated; + return; + } + + lldb::StateType state = process.GetState(); + + if ((state == lldb::eStateExited) || (state == lldb::eStateDetached) || (state == lldb::eStateCrashed)) + { + OutputMessage("remote-connect: process terminated during connect"); + lldb::SBDebugger::Destroy(debugger); + mRunState = RunState_Terminated; + return; + } + + lldb::SBThread stopThread; + lldb::SBThread fallbackThread; + + uint32 threadCount = process.GetNumThreads(); + + for (uint32 i = 0; i < threadCount; i++) + { + lldb::SBThread t = process.GetThreadAtIndex(i); + + if (!fallbackThread.IsValid()) + fallbackThread = t; + + if (t.IsValid() && + t.GetStopReason() != lldb::eStopReasonNone) + { + stopThread = t; + break; + } + } + + if (!stopThread.IsValid()) + stopThread = fallbackThread; + + if (!stopThread.IsValid()) + { + OutputMessage("remote-connect: no valid thread found"); + + lldb::SBDebugger::Destroy(debugger); + mRunState = RunState_Terminated; + return; + } + + process.SetSelectedThread(stopThread); + lldb::StateType finalState = state; + + { + AutoCrit autoCrit(mDebugManager->mCritSect); + + mLLDBDebugger = debugger; + mLLDBTarget = target; + mLLDBProcess = process; + mDidAttach = true; + + switch (finalState) + { + case lldb::eStateStopped: + mRunState = RunState_Paused; + break; + + case lldb::eStateExited: + case lldb::eStateDetached: + case lldb::eStateCrashed: + mRunState = RunState_Terminated; + break; + + default: + mRunState = RunState_Running; + break; + } + } + + { + AutoCrit autoCrit(mDebugManager->mCritSect); + for (auto bp : mBreakpoints) + CheckBreakpoint(bp); + } + + debugger.SetAsync(true); + + //not having this as its own thread causes IDE to freeze (which is annoying) + mEventThreadStop = false; + mEventThread = BfpThread_Create( + EventPumpThreadProc, + (void*)this, + 128 * 1024, + BfpThreadCreateFlag_StackSizeReserve); +} + +bool LLDBDebugger::ConnectRemote(const StringImpl& host, int port, const StringImpl& elfPath) +{ + mRemoteHost = host; + mRemotePort = port; + mElfPath = elfPath; + mIsRemoteConnect = true; + + { + AutoCrit autoCrit(mDebugManager->mCritSect); + mRunState = RunState_Running; + } + + mLaunchThread = BfpThread_Create(LaunchThreadProc, (void*)this, 128 * 1024, BfpThreadCreateFlag_StackSizeReserve); + return true; +} + void LLDBDebugger::GetStdHandles(BfpFile** outStdIn, BfpFile** outStdOut, BfpFile** outStdErr) { } @@ -402,6 +569,62 @@ void LLDBDebugger::WaitForLaunchThread() } } +void LLDBDebugger::WaitForEventThread() +{ + if (mEventThread != NULL) + { + mEventThreadStop = true; + BfpThread_WaitFor(mEventThread, -1); + BfpThread_Release(mEventThread); + mEventThread = NULL; + mEventThreadStop = false; + } +} + +void BFP_CALLTYPE LLDBDebugger::EventPumpThreadProc(void* param) +{ + ((LLDBDebugger*)param)->DoEventPump(); +} + +void LLDBDebugger::DoEventPump() +{ + lldb::SBListener listener = mLLDBDebugger.GetListener(); + + if ((mRunState == RunState_Running) && mLLDBProcess.IsValid()) + { + lldb::StateType initState = mLLDBProcess.GetState(); + OutputMessage(StrFormat("remote-connect: Event pump initial state check: %d", (int)initState)); + if (initState == lldb::eStateStopped) + { + AutoCrit autoCrit(mDebugManager->mCritSect); + // Re-check mRunState under the lock in case ContinueDebugEvent ran + // concurrently and already set it back to Running. + if (mRunState == RunState_Running) + { + ClearCallStack(); + mActiveBreakpoint = NULL; + mRunState = RunState_Paused; + OutputMessage("remote-connect: Event pump detected halted target — enabling Continue"); + } + } + } + + while (!mEventThreadStop) + { + lldb::SBEvent event; + + if (listener.WaitForEvent(1, event)) + { + if (lldb::SBProcess::EventIsProcessEvent(event)) + { + lldb::StateType state = lldb::SBProcess::GetStateFromEvent(event); + LLDBLog("DoEventPump got state:%d\n", (int)state); + HandleProcessEvent(state); + } + } + } +} + void LLDBDebugger::Run() { // Kick off the background launch thread if OpenFile has stored params for us. @@ -415,6 +638,13 @@ void LLDBDebugger::Run() void LLDBDebugger::HandleProcessEvent(lldb::StateType state) { + if (state == lldb::eStateRunning) + { + if ((mRunState == RunState_Paused) || (mRunState == RunState_Breakpoint)) + mRunState = RunState_Running; + return; + } + // Process exited or was detached if ((state == lldb::eStateExited) || (state == lldb::eStateDetached)) { @@ -544,6 +774,9 @@ void LLDBDebugger::HandleProcessEvent(lldb::StateType state) void LLDBDebugger::Update() { + if (mIsRemoteConnect) + return; + if (!mLLDBProcess.IsValid()) return; if ((mRunState == RunState_NotStarted) || (mRunState == RunState_Terminating) || (mRunState == RunState_Terminated)) @@ -686,7 +919,11 @@ Breakpoint* LLDBDebugger::CreateBreakpoint(const StringImpl& fileName, int lineN lldb::SBFileSpec fileSpec(fileName.c_str(), /*resolve=*/false); bp->mLLDBBreakpoint = mLLDBTarget.BreakpointCreateByLocation(fileSpec, (uint32)(lineNum + 1)); if (bp->mLLDBBreakpoint.IsValid()) + { + if (mIsRemoteConnect) + bp->mLLDBBreakpoint.SetIsHardware(true); mBreakpointIdMap.ForceAdd((int)bp->mLLDBBreakpoint.GetID(), bp); + } } return bp; @@ -712,7 +949,11 @@ Breakpoint* LLDBDebugger::CreateSymbolBreakpoint(const StringImpl& symbolName) DoCreateBreakpointByName(bp); if (bp->mLLDBBreakpoint.IsValid()) + { + if (mIsRemoteConnect) + bp->mLLDBBreakpoint.SetIsHardware(true); mBreakpointIdMap.ForceAdd((int)bp->mLLDBBreakpoint.GetID(), bp); + } } return bp; @@ -731,6 +972,8 @@ Breakpoint* LLDBDebugger::CreateAddressBreakpoint(intptr address) bp->mLLDBBreakpoint = mLLDBTarget.BreakpointCreateByAddress((lldb::addr_t)address); if (bp->mLLDBBreakpoint.IsValid()) { + if (mIsRemoteConnect) + bp->mLLDBBreakpoint.SetIsHardware(true); mBreakpointIdMap.ForceAdd((int)bp->mLLDBBreakpoint.GetID(), bp); mBreakpointAddrMap.ForceAdd((uintptr)address, bp); } @@ -756,11 +999,15 @@ void LLDBDebugger::CheckBreakpoint(Breakpoint* checkBreakpoint) } else if (!bp->mSymbolName.IsEmpty()) { - DoCreateBreakpointByName(bp); + DoCreateBreakpointByName(bp); } if (bp->mLLDBBreakpoint.IsValid()) + { + if (mIsRemoteConnect) + bp->mLLDBBreakpoint.SetIsHardware(true); mBreakpointIdMap.ForceAdd((int)bp->mLLDBBreakpoint.GetID(), bp); + } } // Try to resolve the load address so FindBreakpointAt() works. diff --git a/IDEHelper/LLDBDebugger.h b/IDEHelper/LLDBDebugger.h index d1726301e..0f3b44ea7 100644 --- a/IDEHelper/LLDBDebugger.h +++ b/IDEHelper/LLDBDebugger.h @@ -70,9 +70,20 @@ class LLDBDebugger : public Debugger DbgOpenFileFlags mOpenFileFlags; bool mHotSwapEnabled; + // Remote connection parameters — set by ConnectRemote, consumed by the launch thread + String mRemoteHost; + int mRemotePort; + bool mIsRemoteConnect; + String mElfPath; + // Background launch thread BfpThread* mLaunchThread; + // Background event pump thread — processes all LLDB events off the main + // thread so blocking GDB-remote socket calls never freeze the IDE. + bool mEventThreadStop; + BfpThread* mEventThread; + protected: void DumpSymbolAddrs(const StringImpl& sym); void DoCreateBreakpointByName(LLDBBreakpoint* bp); @@ -87,6 +98,12 @@ class LLDBDebugger : public Debugger virtual bool CanOpen(const StringImpl& fileName, DebuggerResult* outResult) override; virtual void OpenFile(const StringImpl& launchPath, const StringImpl& targetPath, const StringImpl& args, const StringImpl& workingDir, const Array& envBlock, bool hotSwapEnabled, DbgOpenFileFlags openFileFlags) override; virtual bool Attach(int processId, BfDbgAttachFlags attachFlags) override; + virtual bool ConnectRemote(const StringImpl& host, int port, const StringImpl& elfPath) override; + virtual bool SupportsRemoteConnect() override { return true; } + void DoConnectRemote(); + void DoEventPump(); + static void BFP_CALLTYPE EventPumpThreadProc(void* param); + void WaitForEventThread(); virtual void GetStdHandles(BfpFile** outStdIn, BfpFile** outStdOut, BfpFile** outStdErr) override; virtual void Run() override; virtual void HotLoad(const Array& objectFiles, int hotIdx) override;