copperline-ctl --dap is a Debug Adapter Protocol
server. VS Code, nvim-dap and other DAP clients use it to 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, with no 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 sessionVS 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). The extension passes
copperline.emulatorExecutable to the adapter as COPPERLINE_BIN. 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 includes inlined frames from optimized
code 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 opens Copperline’s native Debugger, Console, and Frame
Analyzer windows (the custom copperline/ui.show {"window"} request). The
Debug sidebar’s Custom Registers tree comes from the custom
copperline/chipset request, the data behind the 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
(copperline-ctl exe2adf), Profile File Size (copperline-ctl size-report), and Show Coverage. 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:
| Argument | Meaning |
|---|---|
program | The hunk executable on the host (required). Its directory is mounted in the guest. |
args | Command-line arguments for the program (--run-args): a string, or an array whose elements are each quoted for the AmigaDOS command line. |
copperline | The emulator binary. Default: COPPERLINE_BIN, then a copperline next to copperline-ctl, then PATH. |
config | A TOML configuration file (--config). If omitted, Copperline checks copperline.toml in its working directory, then beside its executable, then the launcher’s saved default, then built-in settings. |
rom | A Kickstart ROM supplied as Copperline’s positional ROM argument. If omitted, the selected configuration’s ROM is used, falling back to bundled AROS. |
factory | Ignore the saved default configuration and a copperline.toml beside the executable (--factory). |
model, chipset, cpu, chip, fast, slow | The matching copperline flags. |
memoryFill | --ram-init: zero, random[:SEED], pattern:WORD, or 0xWORD for uninitialized-read testing. |
fpu | Fit or omit an FPU (--fpu / --no-fpu). |
stack | AmigaDOS CLI stack size in bytes before launch, 2048 through 2147483644 (--run-stack). |
ntsc | Select NTSC timing when true, PAL when false (--video). |
detach | Start the guest executable asynchronously and close the boot CLI (--run-detach; Kickstart 2.0+ or AROS). |
emulatorLog | Mirror 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). |
extraArgs | Further emulator flags, as an array. |
headless | No window (--control). |
noAudio | --noaudio for a windowed session. |
stopOnEntry | Stop at the entry point once the program is loaded (default true). |
entryPoint | The 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. |
symbolFile | An 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. |
coverage | An 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, timeoutMs | The 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
(stopOnEntry defaults to false here). 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. An
adapter started with --info or --connect already has a session, and
both launch and attach use it instead of starting an emulator.
Stopping the session shuts down an emulator the adapter launched and leaves an attached one running, unless the client asks for it to be terminated. Restart relaunches a launched emulator with the same arguments and re-attaches to an attached one.
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:
| Toolchain | What the adapter gets |
|---|---|
vasm -Fhunkexe -linedebug | Source 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. |
AmigaPorts amiga-gcc with packed Hunk DWARF (-g -O0) | DWARF from the length-prefixed trailing debug hunk, including section-relative references: lines, functions, parameters and locals, globals, types and call-frame information. Tested with GCC 16 and DWARF 2, 4 and 5. Support depends on the linker retaining this container, rather than the GCC version alone. |
Bartman’s m68k-amiga-elf + elf2hunk | The 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 stripped | Disassembly, 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 programIf a linked Hunk executable contains only symbols and no DWARF carrier, it supports function breakpoints, symbolized disassembly and a scanned call stack, but not source lines or locals. Keep debug information through the link and do not strip the executable. A matching ELF sibling is another supported way to supply DWARF.
Prefer DWARF 4 or 5 for packed Hunk builds. GCC’s DWARF 2 frame-base
location lists use .debug_loc, which this container does not retain;
frame-relative variables then report an unsupported frame base. Direct
register-relative locals, source lines and call-frame information still work.
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¶
| DAP | Copperline |
|---|---|
| Source, function and instruction breakpoints | break.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 breakpoints | break.add {"kind": "watch"} on the words of the variable, with read, write, or read/write access (eight words at most). |
| Exception breakpoints | break.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 / Pause | continue / pause. |
| Step Over / Into / Out | step_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 Continue | reverse_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 stack | Call-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. |
| Scopes | Registers (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). |
| Disassembly | disasm; each instruction text ends in its theoretical cycle count or range, while a precise profile measures additional contention actually encountered. |
| Set variable | regs.set for the innermost frame’s registers; mem.write for base-type variables and members. |
| Evaluate / hover / watch | Registers, 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 view | mem.read / mem.write (base64). |
| Disassembly view | disasm, with program symbols, source lines, and live ROM/LVO names; backwards disassembly anchors at the nearest program or ROM function start. |
| Jump to cursor | regs.set {"reg": "pc"}. |
| Debug Console output | Paula serial output as stdout; uaelib function 86 (debug_log, which the project template’s KPrintF() calls), the optional emulatorLog, and the adapter’s own notes as console. |
| CPU profiling | Custom copperline/profile {"frames": N} -> precise profile.start, step_frame quanta until profile.status reports the N frames written (bounded; a quantum resumed after a breakpoint can end inside the same video frame), profile.stop, and a merged .cpuprofile path. |
| Coverage | The coverage launch argument -> the emulator’s --coverage run; the emulator writes the lcov file to that path when the program exits or the session ends. |
| Modules / loaded sources | The 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 the IDE picks up a pause or resume made from the window within a
second.
Limitations¶
Locals are shown for simple location expressions and one level of struct/array nesting; optimized code, location lists and DWARF expressions render as unsupported.
One thread (the CPU) is presented; Exec tasks are not separate threads.
The
sourceMapis a prefix replacement; when a recorded path does not exist on the host, the adapter also tries the path’s tail under the program’s directory.