mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-23 03:16:22 +08:00
178 lines
6.9 KiB
Markdown
178 lines
6.9 KiB
Markdown
<!--
|
|
Copyright (C) 2026 SharpEmu Emulator Project
|
|
SPDX-License-Identifier: GPL-2.0-or-later
|
|
-->
|
|
|
|
# Live debug server
|
|
|
|
SharpEmu can expose a **live debug server** so an external process can inspect
|
|
and control a running guest over TCP. The server lives in the emulator; the
|
|
companion `SharpEmu.DebugClient` executable is one client, and the wire protocol
|
|
is simple enough to script against directly.
|
|
|
|
This document describes the moving parts and the wire protocol. For day-to-day
|
|
client usage, see
|
|
[`src/SharpEmu.DebugClient/DEVELOPER_READ.md`](../src/SharpEmu.DebugClient/DEVELOPER_READ.md).
|
|
|
|
## Layering
|
|
|
|
| Assembly | Role |
|
|
| -------- | ---- |
|
|
| `SharpEmu.Core` | Defines the dispatcher seam `ICpuDebugHook` / `ICpuDebugFrame` (namespace `SharpEmu.Core.Cpu.Debug`) and the `CpuExecutionOptions.DebugHook` slot. Core has **no** reference to the debugger. |
|
|
| `SharpEmu.Debugger` | The debugger: `DebuggerSession` (implements the hook), `BreakpointStore`, the TCP `DebuggerServer`, the pluggable `IDebugProtocol` with a JSON-lines implementation, and the `DebuggerServerHost` one-call wiring. |
|
|
| `SharpEmu.CLI` | Parses `--debug-server`, builds a `DebuggerServerHost`, hands its `Hook` to `SharpEmuRuntimeOptions.DebugHook`, and manages its lifetime. |
|
|
| `SharpEmu.DebugClient` | A standalone client executable. Depends only on the BCL. |
|
|
|
|
The dependency direction is important: Core stays debugger-agnostic and only
|
|
publishes the seam. Anything that observes execution implements
|
|
`ICpuDebugHook` and is injected through the options, so the debugger can evolve
|
|
without touching the CPU core.
|
|
|
|
## Execution model
|
|
|
|
`CpuDispatcher` enters a fresh frame for the process entry point and for each
|
|
module initializer. When a `DebugHook` is attached it is notified at those
|
|
boundaries:
|
|
|
|
- `OnFrameEnter(frame)` — before the native backend runs the frame. The
|
|
`DebuggerSession` decides whether to stop (pause request, breakpoint on the
|
|
entry address, single-step, or stop-at-entry). To stop, it **parks the
|
|
emulation thread** inside this call on a gate; the frame stays live, so a
|
|
client can read and write registers and memory while parked. `continue` /
|
|
`step` release the gate.
|
|
- `OnFrameExit(frame, result)` — after the frame completes.
|
|
|
|
Because pausing parks the one thread that owns the guest context, register and
|
|
memory accessors are only served while the session reports `Paused`; otherwise
|
|
they return "not paused" so a client never observes torn state.
|
|
|
|
### What is and isn't live yet
|
|
|
|
- **Live:** attach/handshake, run-state tracking, register read/write, memory
|
|
read/write, breakpoint management, execution breakpoints at frame entry,
|
|
pause, frame-level step, continue, and stop/resume/terminate events.
|
|
- **Surface only (armed as the backend grows hooks):** per-instruction
|
|
stepping and data watchpoints (`readwatch` / `writewatch` / `accesswatch`).
|
|
The verbs and types exist so clients and tooling can be written now.
|
|
|
|
## Enabling the server
|
|
|
|
```bash
|
|
SharpEmu --debug-server "/path/to/eboot.bin" # 127.0.0.1:5714
|
|
SharpEmu --debug-server=0.0.0.0:5714 "/path/to/eboot.bin"
|
|
```
|
|
|
|
The bind address defaults to loopback; a routable address must be given
|
|
explicitly. With stop-at-entry (the default `DebuggerSessionOptions.StopAtEntry`),
|
|
the guest parks at its first frame until a client connects and issues
|
|
`continue`, giving you a window to set breakpoints before any guest code runs.
|
|
|
|
## Browser frontend
|
|
|
|
The dependency-free Python frontend can choose and launch an `eboot.bin`, attach
|
|
to its debugger automatically, and provides execution controls, registers,
|
|
memory inspection, breakpoint management, process output, and a live protocol
|
|
activity stream:
|
|
|
|
```bash
|
|
./tools/SharpEmu.DebuggerFrontend/run.sh
|
|
```
|
|
|
|
It connects to `127.0.0.1:5714` and opens `http://127.0.0.1:8765/` by default.
|
|
See [`tools/SharpEmu.DebuggerFrontend/README.md`](../tools/SharpEmu.DebuggerFrontend/README.md)
|
|
for configuration and testing options.
|
|
|
|
## Wire protocol (json-lines/1)
|
|
|
|
One JSON object per line, UTF-8, `\n`-terminated, in both directions.
|
|
|
|
### Requests
|
|
|
|
A `command` string plus command-specific fields. Numeric fields accept a JSON
|
|
number or a `0x`-prefixed hex string.
|
|
|
|
| `command` | Fields | Reply `data` |
|
|
| --------- | ------ | ------------ |
|
|
| `ping` | — | — |
|
|
| `status` (`info`) | — | `state`, `breakpoints`, `lastStop?` |
|
|
| `state` | — | `state` |
|
|
| `registers` (`regs`) | — | `registers` (rax..r15, rip, rflags, fs_base, gs_base) |
|
|
| `set-register` | `register`, `value` | — |
|
|
| `read-memory` | `address`, `length` (≤ 65536) | `address`, `length`, `bytes` (hex) |
|
|
| `write-memory` | `address`, `bytes` (hex) | `written` |
|
|
| `list-breakpoints` (`breakpoints`) | — | `breakpoints[]` |
|
|
| `add-breakpoint` (`break`) | `address`, `kind?`, `length?` | `breakpoint` |
|
|
| `remove-breakpoint` (`delete-breakpoint`) | `id` | — |
|
|
| `enable-breakpoint` | `id`, `enabled?` (default true) | — |
|
|
| `continue` (`cont`, `c`) | — | — |
|
|
| `step` (`s`) | — | — |
|
|
| `pause` | — | — |
|
|
|
|
### Replies
|
|
|
|
```json
|
|
{"ok":true,"command":"registers","data":{ "registers": { "rax":"0x…", … } }}
|
|
{"ok":false,"command":"read-memory","error":"Target is not paused."}
|
|
```
|
|
|
|
### Events (unsolicited)
|
|
|
|
```json
|
|
{"event":"hello","protocol":"json-lines/1","state":"Paused"}
|
|
{"event":"stopped","reason":"Breakpoint","address":"0x…","frameKind":"ProcessEntry","frameLabel":"eboot.bin","registers":{…},"breakpoint":{…}}
|
|
{"event":"resumed"}
|
|
{"event":"terminated"}
|
|
```
|
|
|
|
`reason` is one of `EntryPoint`, `Breakpoint`, `Watchpoint`, `Step`, `Pause`,
|
|
`Fault`, or `Stall`.
|
|
|
|
Stall stops include structured evidence in addition to the human-readable
|
|
detail. Import-loop evidence identifies the NID, resolved HLE export, repeating
|
|
guest return site, dispatch count, and first two ABI arguments:
|
|
|
|
```json
|
|
{
|
|
"event": "stopped",
|
|
"reason": "Stall",
|
|
"stall": {
|
|
"kind": "ImportLoop",
|
|
"nid": "9UK1vLZQft4",
|
|
"instructionPointer": "0x0000000801CE2418",
|
|
"dispatchIndex": 40667904,
|
|
"argument0": "0x0000000812345000",
|
|
"argument1": "0x0000000000000000",
|
|
"resolved": true,
|
|
"library": "libKernel",
|
|
"function": "scePthreadMutexLock"
|
|
}
|
|
}
|
|
```
|
|
|
|
The Python frontend uses this evidence to explain the likely failure class and
|
|
rank concrete checks/fixes. Its diagnosis is intentionally labelled heuristic:
|
|
it helps locate the responsible HLE/scheduler path but does not replace tracing.
|
|
|
|
## Swapping the protocol
|
|
|
|
`DebuggerServer` takes an `IDebugProtocol` factory. The default is
|
|
`JsonLineDebugProtocol`; a GDB remote serial stub (or any other framing) can be
|
|
dropped in without changing the session or command semantics, which live in
|
|
`DebugCommandDispatcher`.
|
|
|
|
## Embedding the server
|
|
|
|
```csharp
|
|
using SharpEmu.Debugger;
|
|
using SharpEmu.Core.Runtime;
|
|
|
|
await using var host = new DebuggerServerHost();
|
|
host.Start();
|
|
|
|
var options = new SharpEmuRuntimeOptions { DebugHook = host.Hook };
|
|
using var runtime = SharpEmuRuntime.CreateDefault(options);
|
|
var result = runtime.Run(ebootPath);
|
|
|
|
host.NotifyRunCompleted();
|
|
```
|