Focus Deep-Dive

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:

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:

  1. Not running — user typed quit; never pause.
  2. Force break — an error was logged or Ctrl+C (SIGINT) was pressed.
  3. Breakpoint hit — current source line matches a registered breakpoint.
  4. Continue mode — user typed continue; skip until next breakpoint.
  5. Barrier / Finished — work-item hit a barrier or completed; always pause.
  6. 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;
}
readline integration: When compiled with HAVE_READLINE, the debugger provides command history and line editing via GNU Readline. Otherwise, it falls back to raw getline(cin, ...).

3  Command Reference

CommandShortDescription
break [line]bSet breakpoint at current or specified line
continuecRun until next breakpoint (registers SIGINT handler)
stepsStep one source line, entering function calls
nextnStep one source line, stepping over calls
print <vars>pPrint variable values (supports foo[i] indexing)
backtracebtPrint call stack with function arguments
info [break]iShow kernel invocation details or list breakpoints
list [line|-]lList 10 source lines around current position
gmem/lmem/pmemgm/lm/pmDump global/local/private memory at address
workitem x [y z]wiSwitch to work-item by global ID
delete [n]dDelete breakpoint N or all
quitqExit debugger

4  Step vs Next

Both step and next record the current call-stack depth and source line. The difference is in shouldShowPrompt:

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;
}
Alignment requirement: The address passed to 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.

Debugging tip: After switching work-items with 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.

Combining plugins: The Interactive Debugger is most powerful when used alongside MemCheck. An invalid memory access triggers 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.

Single-threaded only: 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:

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.

Quick start: Launch Oclgrind with 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.