Interactive Debugging
A GDB-like command-line debugger for OpenCL kernels — breakpoints, stepping, variable inspection, memory dumps, and work-item switching, all from a terminal prompt.
1 How It Hooks In
The InteractiveDebugger registers three Plugin callbacks:
instructionExecuted— the main hook; after every IR instruction,shouldShowPrompt()decides whether to pause and present the(oclgrind)prompt.kernelBegin— resets debugger state, captures theKernelInvocationandProgramfor source-line lookups.kernelEnd— restores signal handlers.log— onERRORmessages from other plugins, setsm_forceBreakto halt at the next instruction (like GDB'sbreak on exception).
The debugger declares isThreadSafe() = false, which forces Oclgrind to serialize execution to a single work-item at a time — essential for interactive control.
stateDiagram-v2
[*] --> KernelBegin
KernelBegin --> Running : initialize state
Running --> CheckPrompt : instructionExecuted
CheckPrompt --> Running : shouldShowPrompt = false
CheckPrompt --> Prompt : shouldShowPrompt = true
Prompt --> Running : step / next / continue
Prompt --> Prompt : print / info / backtrace / mem
Prompt --> [*] : quit
Running --> KernelEnd : all work-items done
KernelEnd --> [*]
2 shouldShowPrompt Decision Logic
This function determines whether to pause execution. The priority chain is:
- Not running — user typed
quit; never pause. - Force break — an error was logged or Ctrl+C (
SIGINT) was pressed. - Breakpoint hit — current source line matches a registered breakpoint.
- Continue mode — user typed
continue; skip until next breakpoint. - Barrier / Finished — work-item hit a barrier or completed; always pause.
- Source stepping — if on a new source line and not stepping over a call (
next), pause.
bool InteractiveDebugger::shouldShowPrompt(const WorkItem* workItem)
{
if (!m_running) return false;
if (m_forceBreak || sigintBreak) return true;
if (hasHitBreakpoint()) return true;
if (m_continue) return false;
if (workItem->getState() == WorkItem::BARRIER) return true;
if (workItem->getState() == WorkItem::FINISHED) return true;
if (!m_program->getNumSourceLines()) return true;
size_t line = getCurrentLineNumber();
// "next" skips into deeper call frames
if (m_next && workItem->getCallStack().size() > m_previousDepth)
return false;
if (!line || line == m_previousLine)
return false;
return true;
}
HAVE_READLINE, the debugger provides command history and line editing via GNU Readline. Otherwise, it falls back to raw getline(cin, ...).
3 Command Reference
| Command | Short | Description |
|---|---|---|
break [line] | b | Set breakpoint at current or specified line |
continue | c | Run until next breakpoint (registers SIGINT handler) |
step | s | Step one source line, entering function calls |
next | n | Step one source line, stepping over calls |
print <vars> | p | Print variable values (supports foo[i] indexing) |
backtrace | bt | Print call stack with function arguments |
info [break] | i | Show kernel invocation details or list breakpoints |
list [line|-] | l | List 10 source lines around current position |
gmem/lmem/pmem | gm/lm/pm | Dump global/local/private memory at address |
workitem x [y z] | wi | Switch to work-item by global ID |
delete [n] | d | Delete breakpoint N or all |
quit | q | Exit debugger |
4 Step vs Next
Both step and next record the current call-stack depth and source line. The difference is in shouldShowPrompt:
- step — pauses on any new source line, including inside called functions.
- next — sets
m_next = true, causing the prompt to be suppressed whilecallStack.size() > m_previousDepth. This effectively skips over function calls, resuming when control returns to the original depth.
graph TD
CMD[User types next] --> SAVE[Save depth=D line=L]
SAVE --> EXEC[Execute next instruction]
EXEC --> CHK{callStack.size > D?}
CHK -->|Yes: inside call| EXEC
CHK -->|No| LINE{New source line?}
LINE -->|Same line| EXEC
LINE -->|Different line| PAUSE[Show prompt]
5 Memory Dump Commands
The mem handler reads the first character of the command name to select the address space:
bool InteractiveDebugger::mem(vector<string> args)
{
Memory* memory = NULL;
if (args[0][0] == 'g')
memory = m_context->getGlobalMemory();
else if (args[0][0] == 'l')
memory = m_kernelInvocation->getCurrentWorkGroup()
->getLocalMemory();
else if (args[0][0] == 'p')
memory = m_kernelInvocation->getCurrentWorkItem()
->getPrivateMemory();
if (args.size() == 1) { memory->dump(); return false; }
// Parse hex address (must be 4-byte aligned)
size_t address;
stringstream ss(args[1]);
ss >> hex >> address;
size_t size = (args.size() == 3) ? stoul(args[2]) : 8;
unsigned char* data =
(unsigned char*)memory->getPointer(address);
for (unsigned i = 0; i < size; i++) {
if (i % 4 == 0)
cout << endl << hex << setw(16) << (address + i) << ":";
cout << " " << hex << setw(2) << setfill('0') << (int)data[i];
}
cout << endl;
return false;
}
gmem/lmem/pmem must be 4-byte aligned (address % 4 == 0). Unaligned addresses are rejected with "Invalid address." This is a debugger UI constraint, not a hardware requirement.
6 Work-Item Switching
The workitem command accepts up to 3 arguments (global ID dimensions). It performs a const_cast to call KernelInvocation::switchWorkItem(gid) — the only place where a Plugin actively modifies simulation state. If the target work-item has already finished, the switch is rejected.
wi 128 0 0, use info to see the new work-item's state, then print to inspect its local variables. The debugger shows the current source line of the newly selected work-item automatically.
7 Breakpoints & SIGINT
Breakpoints are stored per-Program in a map<size_t, size_t> (breakpoint ID → line number). The hasHitBreakpoint() method tracks m_lastBreakLine to avoid re-triggering the same breakpoint on the same line until execution moves away and returns.
When continue is active, a custom SIGINT handler sets sigintBreak = true, which causes shouldShowPrompt to return true on the next instruction — mimicking GDB's Ctrl+C behavior. The original signal handler is restored when the prompt appears or the kernel ends.
8 Backtrace & Function Display
The backtrace command walks the work-item's call stack (a std::stack<const llvm::Instruction*>), calling printFunction for each frame. printFunction extracts the LLVM function name, iterates its arguments, and uses WorkItem::printValue to display runtime values — giving a rich GDB-style backtrace with live argument data.
9 Source Line Mapping
getLineNumber extracts the source location from LLVM debug metadata (!dbg) attached to each instruction. If the kernel was compiled without -g (debug info), getLineNumber returns 0 and the debugger falls back to dumping raw LLVM IR instructions. The list command pages through source lines in increments of 10 (LIST_LENGTH), supporting forward (l), backward (l -), and absolute (l 42) navigation.
10 Error-Triggered Breakpoints
When any other plugin (e.g., MemCheck or RaceDetector) reports an error through the Context logging system, the log callback sets m_forceBreak = true. On the very next instructionExecuted, shouldShowPrompt returns true and the debugger pauses. This allows developers to inspect the state that caused the error — variables, memory contents, and call stack — immediately at the point of failure.
m_forceBreak, dropping you into the debugger prompt where you can use print to examine pointer values and gmem to inspect the surrounding memory layout.
11 Platform & Signal Handling
On POSIX systems, the continue command installs a custom SIGINT handler via sigaction. The handler sets a global flag (sigintBreak) rather than interrupting execution directly — ensuring the simulator remains in a consistent state. On Windows (non-MinGW), signal handling is omitted; the only way to break is via breakpoints or errors.
The original signal handler is carefully saved (m_oldSignalHandler) and restored when the debugger prompt reappears or the kernel ends, avoiding interference with the host application's signal handling.
isThreadSafe() returns false, forcing Oclgrind to disable its work-group parallelism. This is necessary for interactive control but means kernel execution is significantly slower. Remove -i (interactive mode) for production validation runs.
12 Print & Expression Evaluation
The print command delegates to WorkItem::printExpression, which supports:
- Simple variable names — resolved from LLVM debug info to live register values.
- Array indexing —
print data[42]computes the element address and reads from the correct memory space. - Multiple variables —
print x y zprints all three in one command.
If expression evaluation encounters a FatalError (e.g., variable not in scope), it catches the exception and prints a diagnostic rather than crashing the debugger session.
13 Info Command Details
Without arguments, info displays the full kernel invocation context: kernel name, global work size, global offset, local work size, current work-item global ID, and the current source line. With info break, it lists all active breakpoints as numbered entries with their line numbers — useful before a delete command.
oclgrind -i ./your_program to enable the interactive debugger. The (oclgrind) prompt appears at the first instruction of the first work-item. Type help to see all commands.