Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Debug Adapter Protocol (IDE debugging)

copperline-ctl --dap is a Debug Adapter Protocol server: VS Code, nvim-dap and any other DAP client can debug a program running in Copperline with source-level breakpoints and stepping, the register file, locals and globals, memory, the custom chipset, reverse execution and a Debug Console, without a GDB in between.

The adapter is a client of the control protocol, like the MCP mode. It launches an emulator with --run PROG (windowed by default, so the Amiga’s screen stays live beside the editor) or attaches to one started with --control-info, and turns each DAP request into control protocol calls. Source lines, symbols, variables and call frames come from the program’s own debug information, relocated by the hunk addresses the guest reports when it loads the program.

copperline-ctl --dap                 # on stdin/stdout, how IDEs spawn adapters
copperline-ctl --dap-listen :4711    # over TCP (a "debugServer" connection)
copperline-ctl --dap --info /tmp/ccp.json   # launch/attach both use this session

VS Code

For installation, executable paths, a complete launch configuration, and screenshots, start with the VS Code guide. Package and install tools/vscode-copperline as a VSIX; it contributes the copperline debug type and runs copperline-ctl --dap from your PATH. The settings copperline.ctlExecutable and copperline.emulatorExecutable name the two executable files when they are elsewhere (a source build’s target/release/copperline-ctl and target/release/copperline, or the copies every release package ships; see Command-line tools for where each package puts them). A launch configuration:

{
  "type": "copperline",
  "request": "launch",
  "name": "Run hello in Copperline",
  "program": "${workspaceFolder}/hello",
  "model": "A1200",
  "fast": "8M",
  "entryPoint": "main",
  "stopOnEntry": true
}

F5 opens a Copperline window, warp-boots to the program, and stops at its entry point with the source open; breakpoints, Step Over/Into/Out, Step Back, the Variables view, hovers, the Disassembly view, the memory viewer and the Debug Console all work from there. Stop closes the window.

While the program is paused, the debug toolbar’s Profile button captures one emulated frame and Profile (Multi) prompts for a frame count. The adapter sends the custom copperline/profile {"frames": N} request, derives a compact live unwind table from the session’s DWARF, captures precise instruction samples, converts them to a source-mapped .cpuprofile, and opens the result in VS Code. The profile contains optimized inline frames and a [Bus wait] child that isolates time lost to chip-DMA contention. Other DAP clients can send the same custom request and open the returned path.

The same toolbar can open Copperline’s native Debugger, Console, and Frame Analyzer windows without reimplementing them in a webview. The Debug sidebar’s Custom Registers tree is fed by the adapter’s Chipset scope; hovering a register shows its shared access, chipset, and bit-field documentation. The command palette also provides Init Amiga Project, Convert EXE to ADF, and Profile File Size. The project template detects Bartman’s installed toolchain through amiga.bin-path, or can select bebbo amiga-gcc or vbcc/vasm.

Other clients

nvim-dap, with the adapter on stdio:

local dap = require("dap")
dap.adapters.copperline = { type = "executable", command = "copperline-ctl", args = { "--dap" } }
dap.configurations.c = {
  { type = "copperline", request = "launch", name = "Run in Copperline",
    program = "${workspaceFolder}/hello", stopOnEntry = true, entryPoint = "main" },
}

Any client that connects to a TCP debug server can use --dap-listen ADDR instead. Zed loads debug adapters through extensions only; none exists yet.

Launch and attach

launch starts copperline --control-gui :0 --control-info FILE --run PROGRAM (with --control and --noaudio when headless is true) and connects to it. The emulator boots the bundled AROS ROM, or whatever the configuration says. Arguments:

ArgumentMeaning
programThe hunk executable on the host (required). Its directory is mounted in the guest.
argsCommand-line arguments for the program (a string or an array).
copperlineThe emulator binary. Default: COPPERLINE_BIN, then a copperline next to copperline-ctl, then PATH.
configA TOML configuration file (--config). If omitted, Copperline checks copperline.toml in its working directory, then the launcher’s saved default, then built-in settings.
romA Kickstart ROM supplied as Copperline’s positional ROM argument. If omitted, the selected configuration’s ROM is used, falling back to bundled AROS.
factoryIgnore the saved default configuration (--factory).
model, chipset, cpu, chip, fast, slowThe matching copperline flags.
memoryFill--ram-init: zero, random[:SEED], pattern:WORD, or 0xWORD for uninitialised-read testing.
fpuFit or omit an FPU (--fpu / --no-fpu).
stackAmigaDOS CLI stack size in bytes before launch, 2048 through 2147483644 (--run-stack).
ntscSelect NTSC timing when true, PAL when false (--video).
detachStart the guest executable asynchronously and close the boot CLI (--run-detach; Kickstart 2.0+ or AROS).
emulatorLogMirror the launched emulator’s stdout/stderr log into the Debug Console.
rtcTime--rtc-time: the guest clock’s seed. Default: the launch time, pinned, so reverse execution replays exactly (a guest reading the host clock would diverge; see Reverse debugging).
extraArgsFurther emulator flags, as an array.
headlessNo window (--control).
noAudio--noaudio for a windowed session.
stopOnEntryStop at the entry point once the program is loaded (default true).
entryPointThe symbol to stop at instead of the first instruction of the first hunk: main or entry for C programs whose first hunk begins with a startup stub.
symbolFileAn ELF with DWARF the program was converted from (elf2hunk); program.elf is tried automatically.
sourceMap{"/build/prefix": "/host/prefix"} for sources recorded under another path.
coverageAn lcov .info path: the emulator counts every instruction the program retires from its first instruction to its exit and writes line/function coverage there when it exits or the session stops (--coverage, with sourceMap applied; see Guest code coverage). Launch only.
cwd, timeoutMsThe emulator’s working directory, and how long to wait for its control endpoint (default 60 s).

attach connects to a running emulator through controlInfo (the --control-info file) or address + token, and takes program, symbolFile, sourceMap, entryPoint and stopOnEntry the same way. When the program is already running, its segments are matched against the file and the symbols relocate at once; otherwise a loadseg break waits for the guest to load it.

Behind a launch, the control server arms a one-shot loadseg stop for the program before the first frame runs (the same break-at-entry --gdb has), so the boot cannot outrun the adapter. At that stop the adapter reads the program’s segments with segments.list, relocates its debug information, binds the breakpoints the client set while the program was not loaded yet, and runs to the entry point.

Debug information

The adapter reads the executable itself and, optionally, an ELF sibling. What each toolchain provides:

ToolchainWhat the adapter gets
vasm -Fhunkexe -linedebugSource lines from the LINE debug hunks, symbols from the symbol hunks. Breakpoints and stepping by assembly source line.
bebbo amiga-gcc 6.5 (m68k-amigaos-gcc -g -O0)DWARF from the trailing debug hunk: lines, functions, parameters and locals, globals, struct/array/pointer/enum types, call-frame information for the call stack.
bebbo amiga-gcc 13/15/16 (-g)These toolchains’ linkers drop the DWARF sections from hunk output: symbols only (function breakpoints, symbolised disassembly, a scanned call stack). Build with the 6.5 toolchain for source-level debugging.
Bartman’s m68k-amiga-elf + elf2hunkThe hunk file’s symbols plus the ELF’s DWARF through symbolFile (or program.elf), including multi-file DWARF 4/5 builds linked with -r -nostdlib; ELF sections map onto hunks in section order, as elf2hunk allocates them.
Anything strippedDisassembly, registers, memory, chipset.

Keep the ELF used by elf2hunk alongside the hunk executable, or name it explicitly with symbolFile. For example, a relocatable build can use:

m68k-amiga-elf-gcc -g -O0 -r -nostdlib main.c worker.c -o program.elf
elf2hunk program.elf program

The adapter applies the ELF’s debug-section relocations before reading DWARF, including references between compilation units’ abbreviation and line tables. Source breakpoints set before LoadSeg are initially unverified; they bind and emit a breakpoint changed event when the program loads. Selecting DWARF 4 instead of 5 still requires applying these relocations.

A fully linked ELF built with -Wl,--emit-relocs is also supported. That option retains relocation records for tools such as elf2hunk; its debug contents are already resolved by the linker. The adapter preserves those contents without applying the retained relocations again. --emit-relocs is not needed for -r output, which already carries pending relocations.

Locals need -O0: the adapter evaluates the single-operation DWARF locations (DW_OP_fbreg, DW_OP_breg, DW_OP_reg, DW_OP_addr, DW_OP_call_frame_cfa) and shows anything else as unsupported rather than guessing. Values render by type: integers, characters, booleans, floats, pointers (with the string behind a char *), arrays and structs expand in the Variables view. Without DWARF the Globals scope lists the data and BSS symbols as longs.

The console line printed at launch says what was found: 19 hunk symbols; DWARF from the executable's debug hunk: 3 function(s), 43 line row(s), 5 global(s), call-frame info.

What maps to what

DAPCopperline
Source, function and instruction breakpointsbreak.add {"kind": "pc"}; a source line without code binds to the next line that has some, like GDB. Conditions are one comparison the machine evaluates itself (d0 == 5, [$DFF006] != 0, a0 >= d1, sr & $2000; a memory operand compares the 16-bit word at the address); hit conditions are ignore counts.
Data breakpointsbreak.add {"kind": "watch"} on the words of the variable, with read, write, or read/write access (eight words at most).
Exception breakpointsbreak.add {"kind": "catch"}: bus error, address error, illegal instruction, zero divide, CHK/TRAPV, privilege violation, line-A, line-F, and TRAP #7. Address error, illegal instruction, and TRAP #7 are selected by default, and launch sessions arm them only after the target program loads so the OS boot does not stop first.
Continue / Pausecontinue / pause.
Step Over / Into / Outstep_over / step / step_out, repeated until the source line changes (statement granularity) or once (instruction granularity). Step Out while the PC is in Kickstart uses run_until {"pc_outside": true} to return to program code. Stepping into code without lines runs it to its return. A line that outgrows 64 single steps (a loop) gets temporary breakpoints on the function’s other lines and the return address instead.
Step Back / Reverse Continuereverse_step / reverse_continue from the snapshot ring (see Reverse debugging); Step Back also repeats until the line changes. The adapter takes a snapshot (reverse_anchor) at every stop a run ends in, so stepping back replays from there: the boot volume --run stages is a host directory mount, whose traffic a replay from an older snapshot could not reproduce.
Call stackCall-frame information when the DWARF has it, else a scan of the stack for return addresses that follow a JSR/BSR. Frames beyond the innermost are looked up at the call site. ROM frames use live names such as [exec] AllocMem+$12, derived from the running guest rather than a per-ROM symbol file.
ScopesRegisters (D0-D7, A0-A7, PC, SR with its flags, plus FP0-FP7/FPCR/FPSR/FPIAR when an FPU is fitted), Locals, Globals, Chipset (every custom register and the beam position).
Disassemblydisasm; each instruction text ends in its theoretical cycle count or range, while a precise profile measures additional contention actually encountered.
Set variableregs.set for the innermost frame’s registers; mem.write for base-type variables and members.
Evaluate / hover / watchRegisters, variables and symbols by name, numbers ($DFF000, 0x1234, %1010), + - * /, [expr] / [expr].w / [expr].b memory reads, d0.w. In the Debug Console, !method {json} sends a raw control-protocol request and prints the reply (!status, !beam.get, !capture.screenshot {"path": "/tmp/s.png"}).
Memory viewmem.read / mem.write (base64).
Disassembly viewdisasm, with program symbols, source lines, and live ROM/LVO names; backwards disassembly anchors at the nearest program or ROM function start.
Jump to cursorregs.set {"reg": "pc"}.
Debug Console outputSerial output (which is where KPrintF goes) as stdout; uaelib function 86 (debug_log), optional emulatorLog, and the adapter’s own notes as console.
CPU profilingCustom copperline/profile {"frames": N} -> precise profile.start, bounded frame step, profile.stop, and a merged .cpuprofile path.
CoverageThe coverage launch argument -> the emulator’s --coverage run; the lcov file appears beside the program when it exits or the session ends.
Modules / loaded sourcesThe program with its first hunk’s address, and the source files its debug information names.

A stop from a breakpoint the debugger window set, or from a catch, reg watch, beam trap or Copper breakpoint set through !break.add, is reported as a breakpoint with the machine’s description. In a windowed session a pause or resume made from the window is noticed within a second and reflected in the IDE.

Limitations