How to Debug Native Code Using DAP in Oh My Pi (omp): A Complete Guide
Oh My Pi (omp) provides a built-in debug tool that speaks the Debug Adapter Protocol (DAP), allowing you to launch, attach to, and control native debuggers like gdb and lldb-dap directly from the command line or TUI.
The omp repository includes a sophisticated debugging interface that bridges the gap between your CLI environment and native debuggers. By implementing the Debug Adapter Protocol, omp enables seamless debugging of C, C++, and other compiled binaries without leaving your terminal. This guide explains how to leverage the DAP integration to debug native code using the exact source implementation found in the can1357/oh-my-pi codebase.
Architecture of the DAP Integration
The debugging system consists of three core layers that handle everything from CLI parsing to low-level DAP message exchange.
Debug Tool CLI (packages/coding-agent/src/tools/debug.ts) parses JSON parameters and routes commands to the session manager. The DebugTool.execute() method implements a switch-case covering every DAP action including launch, set_breakpoint, and continue.
DAP Session Manager (packages/coding-agent/src/dap/session.ts) maintains a single active debugging session. It handles the initialization handshake, sends requests, and listens for events like stopped or terminated. Key methods include launch(), attach(), and continue(), each coordinating with the internal event loop to track execution state.
Adapter Resolution (packages/coding-agent/src/dap/config.ts) automatically selects the appropriate native debugger. The selectLaunchAdapter() function inspects file extensions and executable markers, preferring gdb or lldb-dap for extension-less binaries while respecting user overrides via the adapter parameter.
Launching Native Binaries
To start debugging a native executable, use the launch action with the path to your binary. The tool automatically resolves the working directory and selects the correct debugger adapter based on the file type.
{
"action": "launch",
"program": "./build/myapp",
"args": ["--verbose"],
"cwd": "/home/user/project"
}
From the CLI:
omp debug '{"action":"launch","program":"./build/myapp","args":["--verbose"]}'
When you execute this command, selectLaunchAdapter() in packages/coding-agent/src/dap/config.ts (lines 27-34) detects that ./build/myapp has no file extension and selects gdb (or lldb-dap on Darwin systems). The session manager then spawns the adapter via DapClient.spawn(), sends the initialize request, and completes the configuration handshake with configurationDone before issuing the actual launch request. The launch arguments are assembled at packages/coding-agent/src/tools/debug.ts (lines 654-662), merging user-provided values with the adapter's default configuration from packages/coding-agent/src/dap/defaults.json.
Setting Breakpoints
Set source-level breakpoints using the set_breakpoint action, which supports optional conditions.
{
"action": "set_breakpoint",
"file": "src/main.c",
"line": 42,
"condition": "i > 10"
}
CLI example:
omp debug '{"action":"set_breakpoint","file":"src/main.c","line":42,"condition":"i > 10"}'
Internally, DebugTool.execute() routes this to dapSessionManager.setBreakpoint(). This method builds a DAP setBreakpoints request containing the normalized source path and breakpoint parameters. The implementation at packages/coding-agent/src/dap/session.ts (lines 44-50) deduplicates existing breakpoints for the same line, sorts them by line number, and maps the response back to internal state to track verified status.
Controlling Execution
Once stopped, control program flow using standard debugging commands. Each action maps directly to a DAP request sent through the active session.
Continue execution:
omp debug '{"action":"continue"}'
Step over:
omp debug '{"action":"step_over"}'
The continue() method in packages/coding-agent/src/dap/session.ts (lines 4-22) first resolves the current thread ID, clears stale stop state, and subscribes to a stop outcome before sending the request. This guarantees that the manager captures the next stopped event rather than racing with it. Similarly, stepOver() (lines 111-121) prepares the outcome listener, issues the next request, and awaits the corresponding stop event.
Inspecting Program State
Examine threads, stack frames, and variables using dedicated inspection actions. Each query returns formatted output suitable for terminal display.
List active threads:
omp debug '{"action":"threads"}'
Get stack trace:
omp debug '{"action":"stack_trace","levels":10}'
Inspect variables in a frame:
omp debug '{"action":"scopes","frame_id":3}'
omp debug '{"action":"variables","variable_ref":5}'
These actions delegate to dapSessionManager.threads(), stackTrace(), scopes(), and variables(), which issue the corresponding DAP requests. For low-level debugger interaction, use the evaluate action with context: "repl" to send raw commands to the underlying debugger:
omp debug '{"action":"evaluate","expression":"info registers","context":"repl"}'
Advanced Usage Scenarios
Forcing a Specific Debugger Adapter
While omp auto-selects adapters, you can force a specific debugger by providing the adapter field:
omp debug '{"action":"launch","adapter":"lldb-dap","program":"./mybinary"}'
This bypasses the automatic selection logic in selectLaunchAdapter() and uses the specified adapter configuration from defaults.json.
Attaching to Running Processes
For remote debugging scenarios (such as Delve debugging a Go process), use the attach action:
{
"action": "attach",
"port": 2345
}
The selectAttachAdapter logic recognizes the port parameter and selects the dlv adapter when appropriate.
Pausing and Terminating Sessions
Pause a hung process:
omp debug '{"action":"pause"}'
Cleanly terminate the current session:
omp debug '{"action":"terminate"}'
List all active sessions (useful when managing multiple debug targets):
omp debug '{"action":"sessions"}'
Summary
- omp debug exposes full DAP capabilities through JSON actions, wrapping complex debugger initialization into simple CLI commands.
- The system automatically selects gdb or lldb-dap for native binaries based on file heuristics, but supports explicit adapter overrides.
- All standard debugging operations—breakpoint management, stepping, stack inspection, and variable evaluation—are available as discrete actions routed through
DapSessionManager. - The implementation guarantees event coherence by preparing stop outcomes before sending continuation commands, preventing lost debugger events.
- Native debugger commands can be passed through using the
evaluateaction withcontext: "repl", enabling access to platform-specific features like register inspection.
Frequently Asked Questions
Which native debuggers does omp support out of the box?
According to the defaults.json configuration in packages/coding-agent/src/dap/defaults.json, omp ships with built-in adapter definitions for gdb, lldb-dap, dlv (Delve for Go), and debugpy. You can extend support by modifying the configuration or providing explicit command paths via the adapter selection API.
How does omp choose between gdb and lldb-dap automatically?
The selectLaunchAdapter() function in packages/coding-agent/src/dap/config.ts examines the target program's file extension and executable markers. For binaries without extensions (typical for native executables), it prefers gdb on Linux systems and lldb-dap on Darwin, returning the first match from the sorted adapter list. You can override this behavior by specifying the adapter parameter in your launch request.
Can I debug remote processes or attach to existing PIDs?
Yes. Use the attach action with either a processId for local attachment or a port for remote debugging. When a port is specified, the adapter resolution logic selects appropriate remote debugging adapters (such as dlv for Go remote debugging). The session manager handles the attach handshake identically to a launch session, setting up the event loop before returning control.
Why might my breakpoint not be verified or hit?
Breakpoint verification depends on the path matching between your source file and the debug information embedded in the binary. The setBreakpoint() implementation normalizes paths and deduplicates entries, but if the source in packages/coding-agent/src/dap/session.ts cannot reconcile the path with what the debugger expects, the breakpoint may remain unverified. Ensure your cwd and file paths match the compilation context, or use the evaluate action with "context": "repl" to send raw debugger commands for manual breakpoint verification.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →