• source navigation  • diff markup  • identifier search  • freetext search  • 

Sources/ucode/docs/debugger.md

  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