1 # UCode Debugger 2 3 ## Overview 4 5 The ucode interpreter includes source-level debugging support: breakpoints, 6 stepping, stack inspection, and runtime expression evaluation. The 7 implementation is split into a **server** (the debug core, `lib/debug.c` + 8 `lib/debug_remote.c` + `lib/debug_proto.c`, loaded as the `debug` module) and 9 a **client** (`udbg`, at the repository root) that talks to it over a simple, 10 line-based text protocol. 11 12 The server never renders anything - no ANSI escapes, no syntax highlighting, 13 no formatted columns. It only emits and consumes structured protocol 14 messages (see "Wire Protocol" below). All rendering, source buffer handling 15 and interactive line editing live in the client. This split exists so that 16 alternative clients - IDE integrations, editor plugins, other tooling - can 17 drive the exact same debug core without reimplementing any of its logic, and 18 so the client can be tested and evolved independently of the VM-side 19 breakpoint machinery. 20 21 There is exactly one way a session is driven, regardless of how it was 22 reached: a connected file descriptor is handed to `bk_enter_session()` 23 (`lib/debug.c`), which writes a `PAUSED` message and then reads and dispatches 24 protocol commands until the client tells it to resume or quit. Three things 25 differ only in *how that fd is obtained*: 26 27 - **Local (`ucode -x script.uc`)** - `uc_debugger()` creates a `socketpair()`, 28 forks, and execs `udbg --fd 3` in the child with one end of the pair on fd 29 3; the parent (running the script) keeps the other end as the session fd. 30 The child owns the real controlling terminal and is the interactive 31 client; the parent never touches its own stdin/stdout for protocol 32 traffic. 33 - **Remote, explicit path (`debug.listen(path)`)** - accepts a single 34 connection on an arbitrary, caller-chosen Unix domain socket path and hands 35 it to the same session driver. 36 - **Remote, SIGUSR1 attach (`ucode -X`, `debug.attach()`, `debug.listen()` 37 with no path)** - arms a breakpoint and/or a `SIGUSR1` handler; once 38 triggered, waits (up to 30s) for a client to connect to the PID-derived 39 attach socket `/tmp/ucode-debug-<pid>.sock`, then hands off the accepted fd 40 the same way. `udbg <pid>` automates sending the signal and connecting. 41 42 --- 43 44 ## Wire Protocol 45 46 One message per line, `\n`-terminated: an uppercase **VERB**, optionally 47 followed by a single space and a JSON object payload. 48 49 ``` 50 PAUSED {"reason":"breakpoint","file":"script.uc","line":12,"col":3,"function":"main","breakpoint_id":1} 51 BREAK {"spec":"script.uc:12"} 52 BREAKPOINT_ADDED {"id":1} 53 ``` 54 55 A payload, when present, is always a JSON *object* (never a bare 56 array/string/number), so new fields can be added without breaking existing 57 clients. `file` fields are the source's display path exactly as the server 58 resolves it (repository-relative when the source lives under the current 59 working directory, absolute otherwise) - clients should treat it as an 60 opaque key for the `SOURCE` verb, not derive anything from its shape. 61 62 ### Client → server commands 63 64 | Verb | Payload | Response | 65 |---|---|---| 66 | `BREAK` | `{"spec":"path[:line[:col]]"\|"expr"}` | `BREAKPOINT_ADDED {"id"}` or `ERROR` | 67 | `DELETE` | `{"id":N}` (omit for the current breakpoint) | `OK` or `ERROR` | 68 | `LIST_BREAKPOINTS` | none | `BREAKPOINTS {"items":[{"id"?,"kind","file"?,"line"?,"col"?,"function"?}]}` | 69 | `NEXT` | none | none synchronously - see "No synchronous step acks" below | 70 | `STEP` | none | none synchronously | 71 | `CONTINUE` | none | none synchronously | 72 | `RETURN` | none | none synchronously | 73 | `BACKTRACE` | `{"full":bool}` | `BACKTRACE {"frames":[...]}` (see below) | 74 | `VARIABLES` | none | `VARIABLES {"vars":[...]}` (see below) | 75 | `SOURCES` | none | `SOURCES {"items":[{"index","file"}]}` | 76 | `PRINT` | `{"expr":"..."}` | `VALUE {"repr"}` or `ERROR` | 77 | `LINES` | `{"spec"?,"before"?,"after"?}` | `SOURCE_RANGE {"file","from","to","cursor"?}` - no source text, see "Source Resolution" | 78 | `THROW` | `{"type"?,"message"}` | raises the exception; no direct response | 79 | `DISASSEMBLE` | `{"spec"?}` | `DISASSEMBLY {"function","instructions":[...]}` | 80 | `SOURCE` | `{"file"}` | `SOURCE {"file","text"\|null,"error"?}` | 81 | `HELP` | `{"command"?}` | `HELP {"commands":[{"verb","help"}]}` | 82 | `QUIT` | none | terminates the debugged program (like `exit()`); no confirmation prompt - a client that wants one must ask the user itself before sending this | 83 84 `BACKTRACE` frame shape: `{"kind":"script"|"native","index","file"?,"line"?, 85 "col"?,"insn"?,"function"?,"module"?,"variables"?}` - `variables` is only 86 present when `full:true` was requested, and has the same shape as 87 `VARIABLES`'s `vars` array. 88 89 `VARIABLES`/backtrace-`variables` entry shape: `{"name","kind":"this"| 90 "local"|"internal"|"upvalue","value_repr"}` - `value_repr` is a pre-rendered 91 string (via the same formatter `print()`/`printf()` use) since ucode values 92 include closures, resources and regexes that don't round-trip through JSON; 93 there is no separate machine-typed `value` field. 94 95 ### Server → client events 96 97 | Verb | Payload | 98 |---|---| 99 | `PAUSED` | `{"reason":"entry"\|"breakpoint"\|"step"\|"exception"\|"uncaught","file"?,"line"?,"col"?,"function"?,"breakpoint_id"?,"exception_type"?,"exception_message"?}` | 100 | `EVENT` | `{"event":"exception"\|"exit"\|"signal", ...}` - unsolicited, can arrive at any time (e.g. right before the process exits) | 101 | `ERROR` | `{"message"}` - the uniform failure shape for every command above | 102 103 ### No synchronous step acks 104 105 `NEXT`/`STEP`/`CONTINUE`/`RETURN` do not get an immediate acknowledgement. 106 The next thing a client sees is whatever actually happens next: a new 107 `PAUSED` if execution hits another breakpoint/step boundary, an `EVENT` 108 carrying `"event":"exit"` if the program ends, or nothing further for a 109 while if it just keeps running. This mirrors the real control flow exactly 110 - there is no "done stepping" moment to report before that. 111 112 ### Source resolution 113 114 The server resolves `{file, line, col}` locations from the running program's 115 debug info, but **never sends rendered or highlighted source text** for a 116 `PAUSED`/`SOURCE_RANGE`/backtrace frame - only the coordinates. A client 117 that wants to display source has two options: 118 119 - **It already has the file** - the common case either way debugging is 120 actually done: fully locally (client and target share a filesystem, e.g. 121 `-x`/`udbg <pid>` on the same box) or from a development checkout against 122 a remote target (the *client*, not the target, has the real/better 123 source access - think a stripped production device). Either way the 124 client should try reading the file itself first, keyed by the `file` 125 string from any location payload, and never needs a round-trip to the 126 server for it. `udbg` does this (see `-s`/`--srcdir` below for path 127 mapping when the reported path doesn't exist as-is locally). 128 - **It doesn't** (no local access at all) - send `SOURCE {"file":"..."}` 129 and use the returned raw `text`. If the server itself has no source 130 available either (running precompiled bytecode with no embedded source 131 and no matching local file), `text` is `null` and `error` explains why. 132 133 `udbg` implements this as: try the exact reported path; if that fails and 134 `-s DIR`/`--srcdir DIR` was given, try `DIR/<basename of the reported 135 path>`; only then fall back to asking the server. 136 137 --- 138 139 ## Debugger API (`module:debug`) 140 141 | Function | Description | 142 |----------|-------------| 143 | `debug.memdump(path)` | Dump VM heap state to file for analysis | 144 | `debug.traceback([level])` | Get current call stack trace (structured data, not the CLI's `BACKTRACE` output) | 145 | `debug.sourcepos()` | Get current source position (filename, line, byte) | 146 | `debug.getinfo(value)` | Query internal value information | 147 | `debug.getlocal(level, var)` / `debug.setlocal(level, var, value)` | Get/set a local variable | 148 | `debug.getupval(target, var)` / `debug.setupval(target, var, value)` | Get/set an upvalue | 149 | `debug.debugger([target])` | Local interactive session: forks and execs `udbg --fd N` over a socketpair, then pauses (immediately, or at entry to `target` if given) | 150 | `debug.attach(mainfn)` | Arm `SIGUSR1`-triggered attach and break on entry to `mainfn` | 151 | `debug.break()` | Pause execution right here, waiting for an attach-socket client | 152 | `debug.breakpoint(spec[, mainfn])` | Install a breakpoint from a location spec, usable before the program starts running | 153 | `debug.listen([wait\|path])` | Enable remote debugging - explicit path, `SIGUSR1`-armed, or block-until-attached (see below) | 154 155 `debug.listen()` usage: 156 157 ```ucode 158 import { listen } from 'debug'; 159 160 // Arm SIGUSR1-triggered remote debugging on /tmp/ucode-debug-<pid>.sock, 161 // matching what -X and `udbg <pid>` expect, and keep running. 162 listen(); 163 164 // ...or pause right here, synchronously, until a debugger attaches (or a 165 // 30s timeout elapses) - also arms SIGUSR1 for later, same as above. 166 listen(true); 167 168 // ...or bind an arbitrary, caller-chosen socket path and block 169 // indefinitely until a client connects on it, independent of SIGUSR1. 170 listen("/tmp/ucode-debug.sock"); 171 ``` 172 173 This is the primary way to enable remote debugging in a host application 174 that embeds the ucode VM directly (uhttpd, uwsd, ...) and therefore has no 175 `-X` flag of its own. `debug.listen()`'s `SIGUSR1` handling is dispatched 176 through ucode's own `signal()` builtin (`uc_vm_signal_dispatch()`, itself 177 only ever called from *within* the VM's per-instruction loop) rather than 178 the `-X` flag's `uc_vm_break_request()`/`STATUS_BREAK` mechanism, since the 179 latter unwinds the *entire* C call stack back to whoever called 180 `uc_vm_execute()` - fine for `main.c`'s own `-X` loop, but not safe for a 181 host calling `uc_vm_call()` from its own request-handling code, which would 182 have no way to handle an unexpected `STATUS_BREAK` bubbling out of what it 183 thought was a normal call. 184 185 --- 186 187 ## `udbg` Client 188 189 `udbg` is a typed-command protocol client with ANSI source rendering (the 190 original interactive debugger's exact ucode/utpl syntax highlighter and 191 statement/header-bar styling, ported into `debug_highlight.c` - see below) 192 but no line-editing or history yet; that's follow-up work that can be built 193 against this same protocol without touching the server again. 194 195 ``` 196 udbg [-s DIR] <pid> # SIGUSR1-attach to a running `-X` process, gdb -p style 197 udbg [-s DIR] <socket-path> # connect to an explicit debug.listen(path) socket 198 udbg [-s DIR] --fd <n> # use an inherited, already-connected fd (internal, used by `-x`) 199 ``` 200 201 `-s DIR`/`--srcdir DIR` gives a local directory to also look for source 202 files under (by basename) when the server-reported path doesn't exist 203 as-is on this machine - see "Source resolution" above. 204 205 Typed commands at the `dbg >` prompt map directly onto the protocol verbs 206 above (`break <spec>`, `delete [id]`, `list`, `next`, `step`, `continue`, 207 `return`, `backtrace [full]`, `variables`, `sources`, `print <expr>`, 208 `lines [spec] [before] [after]`, `throw [type] <message>`, `disassemble 209 [spec]`, `source <file>`, `help [verb]`, `quit`). 210 211 --- 212 213 ## Breakpoint Location Syntax 214 215 Used by `BREAK`'s `spec` field, `debug.breakpoint()`, and the `-x`/`-X` 216 command-line breakpoint argument: 217 218 ``` 219 path[:line[:col]] # File and line number (path optional if a frame is active) 220 line[:col] # Line in the current file (requires an active frame) 221 expression # ucode expression evaluating to a function (e.g. obj.method) 222 (expression) # Parens to disambiguate an expression from a bare path 223 ``` 224 225 A `path`/`line` spec resolves to the next real bytecode statement at or 226 after that position - breaking on a comment-only or blank line lands on the 227 next actual statement, not an error. 228 229 --- 230 231 ## Building on the Protocol: Local `-x` Wiring 232 233 `uc_debugger()` (`lib/debug.c`) does the following once, on first call: 234 235 1. `socketpair(AF_UNIX, SOCK_STREAM, 0, sv)`. 236 2. `fork()`; the child `dup2(sv[1], 3)` and `execlp("udbg", "udbg", "--fd", 237 "3", NULL)`. 238 3. The parent closes its copy of `sv[1]`, keeps `sv[0]` as the session fd 239 (`debug_remote_set_active_fd()`), and proceeds exactly like the 240 remote-attach case from here on. 241 242 Neither process ever manipulates the *debuggee's* own stdin/stdout for 243 protocol traffic - the child (client) inherits the real controlling 244 terminal for its own I/O, and the parent (VM) only ever reads/writes the 245 socketpair fd. If the client process dies or disconnects, this is treated 246 like a remote client dropping the connection: the script is resumed 247 unattended rather than left hanging. 248 249 --- 250 251 ## Testing 252 253 `tests/custom/99_debugger/run_debugger_tests.uc` is a standalone (non-cram) 254 integration suite that starts real target scripts via `ucode -X<file>:1` 255 (the same attach-socket mechanism `-X`/`udbg` use), connects to the 256 resulting PID-derived Unix domain socket with the `socket` module, sends 257 batches of protocol messages, and asserts on the *parsed* JSON responses 258 and/or the target script's own stdout - never on rendered text, since 259 nothing is rendered server-side. Run it directly with: 260 261 ```bash 262 UCODE_BIN=/path/to/build/ucode ./build/ucode -L build tests/custom/99_debugger/run_debugger_tests.uc 263 ``` 264 265 Note for anyone writing new cases: a bare line-number `BREAK`/`-X` spec 266 against a script that is *only* variable declarations (no function calls or 267 other statements) is a narrow, pre-existing edge case in 268 `resolve_breakpoint()`/`lookup_stmt_boundary()` that doesn't always resolve 269 reliably - prefer `STEP` to advance past declarations, or target a function 270 name instead, both of which are unaffected.
This page was automatically generated by LXR 0.3.1. • OpenWrt