From 8448d0547bb6f426f8cd4b84cb761f53bcad4a93 Mon Sep 17 00:00:00 2001 From: Mark Burton Date: Mon, 27 Jul 2026 11:27:23 +0200 Subject: [PATCH 1/4] gdb: move gdb_port from the CPU to the QEMU instance QEMU's gdbstub state is global to a QEMU instance, so there is one stub per instance and each of its vCPUs is a gdb thread. Starting it per CPU therefore only ever worked for a single-CPU instance: a two-CPU platform aborted at startup with "gdbstub: couldn't create chardev". Give QemuInstance its own gdb_port and start the stub in start_of_simulation, which is the first point where a CPU is realized. QemuCpu::p_gdb_port is kept as a deprecated forwarder so existing platforms keep working; conflicting values now warn and keep the first instead of aborting. Signed-off-by: Mark Burton --- docs/libqbox.md | 18 +++++-- examples/hello-qbox/README.md | 13 +++-- platforms/cortex-m55-remote/src/remote_cpu.h | 2 +- qemu-components/common/include/cpu.h | 37 +++++++++++--- .../common/include/qemu-instance.h | 51 ++++++++++++++++--- 5 files changed, 97 insertions(+), 24 deletions(-) diff --git a/docs/libqbox.md b/docs/libqbox.md index 959c6f43..66cda827 100644 --- a/docs/libqbox.md +++ b/docs/libqbox.md @@ -138,15 +138,23 @@ port parameter instead. ## GDB Support -To attach GDB to a CPU, set the `gdb_port` CCI parameter to a +To attach GDB, set the `gdb_port` CCI parameter on the **QEMU instance** to a non-zero value: ```bash -./build/platforms/platforms-vp --gs_luafile conf.lua -p platform.cpu_1.gdb_port=1234 +./build/platforms/platforms-vp --gs_luafile conf.lua -p platform.qemu_inst.gdb_port=1234 ``` -This opens a GDB server on port 1234 for `cpu_1`. The virtual -platform will wait for GDB to connect before proceeding. +This opens a GDB server on port 1234 for that instance. Every CPU of the instance +appears as a GDB thread (`info threads` in GDB), so one port debugs all of them. +The virtual platform will wait for GDB to connect before proceeding. + +`gdb_port` belongs to the instance rather than to a CPU because QEMU's GDB stub +state is global to an instance -- there can only be one stub per instance. +Setting it on a CPU still works but is **deprecated**: the value is forwarded to +the owning instance and a warning is logged. Setting it on two CPUs of the same +instance used to abort the process (`gdbstub: couldn't create chardev`); it now +warns and keeps the first value. ## Supported Components @@ -179,7 +187,7 @@ The following parameters are shared by most ARM A-profile CPUs | psci_conduit | string | "disabled" | PSCI conduit: "disabled", "hvc", or "smc" | | rvbar | uint64_t | 0 | Reset vector base address register | | cntfrq_hz | uint64_t | 0 | Generic Timer CNTFRQ in Hz | -| gdb_port | (from base) | 0 | GDB server port (non-zero to enable) | +| gdb_port | (from base) | 0 | **Deprecated** -- set `gdb_port` on the QEMU instance instead. Forwarded there with a warning. | Note: Cortex-M and Cortex-R CPUs have different parameter sets. diff --git a/examples/hello-qbox/README.md b/examples/hello-qbox/README.md index 2b27234e..7919512f 100644 --- a/examples/hello-qbox/README.md +++ b/examples/hello-qbox/README.md @@ -589,13 +589,14 @@ To list all available parameters for a platform: ## Debugging with GDB -Qbox exposes a GDB server for each CPU. Set `gdb_port` on a CPU -component, then connect with your GDB client. +Qbox exposes one GDB server per QEMU instance. Set `gdb_port` on the instance, +then connect with your GDB client. Every CPU of that instance appears as a GDB +thread, so a single port debugs all of them (`info threads`). In `platform.lua`: ```lua -platform.cpu_0 = { +platform.qemu_inst = { -- ... existing config ... gdb_port = 4321, } @@ -605,9 +606,13 @@ Or override it at runtime without editing the file: ```bash ./build/hello-qbox-vp --gs_luafile platform.lua \ - --param platform.cpu_0.gdb_port=4321 + --param platform.qemu_inst.gdb_port=4321 ``` +> Setting `gdb_port` on a CPU is deprecated. It still works — the value is +> forwarded to that CPU's instance and a warning is logged — because QEMU's GDB +> stub state is global to an instance, so only one stub can exist per instance. + Connect from another terminal: ```bash diff --git a/platforms/cortex-m55-remote/src/remote_cpu.h b/platforms/cortex-m55-remote/src/remote_cpu.h index 9d4196b7..43087ee1 100644 --- a/platforms/cortex-m55-remote/src/remote_cpu.h +++ b/platforms/cortex-m55-remote/src/remote_cpu.h @@ -43,7 +43,7 @@ class RemoteCPU : public sc_core::sc_module .get_cci_value() .get_uint(); - if (!m_gdb_port.is_default_value()) m_cpu.p_gdb_port = m_gdb_port; + if (!m_gdb_port.is_default_value()) m_qemu_inst.p_gdb_port = m_gdb_port; SCP_INFO(()) << "number of irqs = " << m_irq_num; diff --git a/qemu-components/common/include/cpu.h b/qemu-components/common/include/cpu.h index 0c9418e2..55e89c95 100644 --- a/qemu-components/common/include/cpu.h +++ b/qemu-components/common/include/cpu.h @@ -533,6 +533,8 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface auto resetcb = std::bind(&QemuCpu::reset_cb, this, _1); reset.register_value_changed_cb(resetcb); + forward_deprecated_gdb_port(); + m_time_sync->on_construct(); m_inst.add_dev(this); @@ -639,6 +641,35 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface m_cpu_hint_ext.set_cpu(m_cpu); } + /* Called from the constructor, so elaboration-time readers of the instance + * parameter see the forwarded value. */ + void forward_deprecated_gdb_port() + { + if (p_gdb_port.is_default_value()) return; + + std::ostringstream msg; + msg << name() + << ": gdb_port is deprecated on a CPU and should be set on the QEMU instance instead (there is one gdb " + "stub per instance, and each of its CPUs is a gdb thread). "; + + cci::cci_param& inst_port = m_inst.p_gdb_port; + if (inst_port.is_default_value()) { + msg << "Forwarding " << p_gdb_port.get_value() << " to " << m_inst.name() << ".gdb_port"; + inst_port.set_value(p_gdb_port.get_value()); + } else if (inst_port.get_value() == 0) { + /* gdb explicitly disabled on the instance: the non-deprecated + * setting wins. */ + msg << "Ignoring " << p_gdb_port.get_value() << ", as gdb is explicitly disabled by " << m_inst.name() + << ".gdb_port = 0"; + } else if (inst_port.get_value() != p_gdb_port.get_value()) { + /* Only one stub can exist, so keep what the instance already has. */ + msg << "Keeping " << m_inst.name() << ".gdb_port = " << inst_port.get_value() << ", ignoring " + << p_gdb_port.get_value(); + } + + SC_REPORT_WARNING("/qbox/cpu/gdb_port", msg.str().c_str()); + } + void halt_cb(const bool& val) { SCP_TRACE(())("Halt : {}", val); @@ -697,12 +728,6 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface { QemuDevice::end_of_elaboration(); m_time_sync->on_end_of_elaboration(); - if (!p_gdb_port.is_default_value()) { - std::stringstream ss; - SCP_INFO(()) << "Starting gdb server on TCP port " << p_gdb_port; - ss << "tcp::" << p_gdb_port; - m_inst.get().start_gdb_server(ss.str()); - } } virtual void start_of_simulation() override diff --git a/qemu-components/common/include/qemu-instance.h b/qemu-components/common/include/qemu-instance.h index ed006a27..d1032593 100644 --- a/qemu-components/common/include/qemu-instance.h +++ b/qemu-components/common/include/qemu-instance.h @@ -97,6 +97,10 @@ class QemuInstance : public sc_core::sc_module public: TargetSignalSocket reset; + /* TCP port for QEMU's GDB stub, 0 to disable. There is one stub per + * instance, presenting each of its vCPUs as a gdb thread. */ + cci::cci_param p_gdb_port; + // these will be used by wait_for_work when it needs a global lock std::mutex g_signaled_lock; std::condition_variable g_signaled_cond; @@ -186,10 +190,14 @@ class QemuInstance : public sc_core::sc_module m_inst.push_qemu_arg("libqbox"); /* argv[0] */ m_inst.push_qemu_arg({ - "-M", "none", /* no machine */ - "-m", "0", /* Guest memory is managed by gs_memory module */ - "-monitor", "null", /* no monitor */ - "-serial", "null", /* no serial backend */ + "-M", + "none", /* no machine */ + "-m", + "0", /* Guest memory is managed by gs_memory module */ + "-monitor", + "null", /* no monitor */ + "-serial", + "null", /* no serial backend */ }); const char* args = "qemu_args."; /* Update documentations because it's not anymore 'args.' it's 'qemu_args.' */ @@ -327,6 +335,9 @@ class QemuInstance : public sc_core::sc_module , p_args("qemu_args", "", "additional space separated arguments") , p_accel("accel", "tcg", "Virtualization accelerator") , p_whpx_args("whpx_args", "", "Additional WHPX accelerator properties (e.g. gicd-base-address=0x17000000)") + , p_gdb_port("gdb_port", 0, + "Wait for gdb connection on TCP port (0 = disabled). One stub per QEMU " + "instance; each vCPU of the instance appears as a gdb thread.") , p_time_sync_strategy("time_sync_strategy", "quantum_keeper", "QEMU<->SystemC time synchronization strategy: \"quantum_keeper\" (default) uses the " "traditional quantum keeper; \"mcips\" syncs time based on the number of instructions " @@ -408,8 +419,8 @@ class QemuInstance : public sc_core::sc_module */ void set_vnc_args(const std::string& vnc_options) { - m_inst.push_qemu_arg("-vnc"); - m_inst.push_qemu_arg(vnc_options.c_str()); + m_inst.push_qemu_arg("-vnc"); + m_inst.push_qemu_arg(vnc_options.c_str()); } /** @@ -486,7 +497,8 @@ class QemuInstance : public sc_core::sc_module if (m_display_argument.empty()) { m_inst.push_qemu_arg({ - "-display", "none", /* no GUI */ + "-display", + "none", /* no GUI */ }); } @@ -563,7 +575,30 @@ class QemuInstance : public sc_core::sc_module init(); // dlsyms libqemu_init; plugin functions are part of the returned LibQemuExports table. } } - void start_of_simulation(void) override { get().finish_qemu_init(); } + /* Start QEMU's gdbstub, if a port was configured. + * + * start_of_simulation, not end_of_elaboration: gdbserver_start() needs a + * *realized* CPU, and CPUs realize in QemuDevice::end_of_elaboration. It + * must also precede QemuCpu::start_of_simulation, which releases the vCPU, + * or "wait for gdb to connect" would not hold - the same instance-before-CPU + * order that finish_qemu_init() already relies on. */ + void start_gdbstub_if_configured() + { + if (p_gdb_port.get_value() == 0) return; + + /* Loopback only: a gdb stub is unauthenticated full control of the + * simulation. Tunnel in (ssh -L) to debug from another host. */ + std::stringstream ss; + ss << "tcp:127.0.0.1:" << p_gdb_port.get_value(); + SCP_INFO(()) << "Starting gdb server for this QEMU instance on TCP port " << p_gdb_port.get_value(); + get().start_gdb_server(ss.str()); + } + + void start_of_simulation(void) override + { + start_gdbstub_if_configured(); + get().finish_qemu_init(); + } void reset_cb(const bool val) { From fe6692b4057d754421db14918d7cd16ae884457f Mon Sep 17 00:00:00 2001 From: Mark Burton Date: Mon, 27 Jul 2026 13:17:35 +0200 Subject: [PATCH 2/4] mcd: add an MCD debug server and an MCP bridge mcd_server is a SystemC module exposing an MCD server: object model, memory and register access, run/stop/step, and breakpoints and watchpoints. Memory goes through a router, run-control through QEMU's gdb stub, with each vCPU of an instance addressed as a gdb thread. mcd_mcp bridges that to MCP, so an agent can drive the platform as a set of tools. Tests cover the JSON-RPC layer, and single- and multi-core platforms driven through the bridge. Signed-off-by: Mark Burton --- systemc-components/CMakeLists.txt | 2 + systemc-components/mcd_mcp/CMakeLists.txt | 15 + systemc-components/mcd_mcp/README.md | 119 ++ .../mcd_mcp/include/mcd_client.h | 108 + .../mcd_mcp/include/mcd_debug.h | 141 ++ systemc-components/mcd_mcp/src/mcd_client.c | 563 +++++ systemc-components/mcd_mcp/src/mcd_debug.cc | 189 ++ systemc-components/mcd_mcp/src/mcd_mcp.cc | 811 ++++++++ systemc-components/mcd_server/CMakeLists.txt | 8 + .../mcd_server/include/mcd_server.h | 287 +++ .../mcd_server/src/mcd_server.cc | 1812 +++++++++++++++++ tests/qbox/CMakeLists.txt | 6 + tests/qbox/mcd/CMakeLists.txt | 107 + tests/qbox/mcd/hello-debug.c | 39 + tests/qbox/mcd/mcd-platform.lua | 96 + tests/qbox/mcd/test_mcd_mcp.py | 306 +++ tests/qbox/mcd/test_mcd_smp.py | 168 ++ tests/qbox/mcd/test_mcd_unit.py | 162 ++ 18 files changed, 4939 insertions(+) create mode 100644 systemc-components/mcd_mcp/CMakeLists.txt create mode 100644 systemc-components/mcd_mcp/README.md create mode 100644 systemc-components/mcd_mcp/include/mcd_client.h create mode 100644 systemc-components/mcd_mcp/include/mcd_debug.h create mode 100644 systemc-components/mcd_mcp/src/mcd_client.c create mode 100644 systemc-components/mcd_mcp/src/mcd_debug.cc create mode 100644 systemc-components/mcd_mcp/src/mcd_mcp.cc create mode 100644 systemc-components/mcd_server/CMakeLists.txt create mode 100644 systemc-components/mcd_server/include/mcd_server.h create mode 100644 systemc-components/mcd_server/src/mcd_server.cc create mode 100644 tests/qbox/mcd/CMakeLists.txt create mode 100644 tests/qbox/mcd/hello-debug.c create mode 100644 tests/qbox/mcd/mcd-platform.lua create mode 100644 tests/qbox/mcd/test_mcd_mcp.py create mode 100644 tests/qbox/mcd/test_mcd_smp.py create mode 100644 tests/qbox/mcd/test_mcd_unit.py diff --git a/systemc-components/CMakeLists.txt b/systemc-components/CMakeLists.txt index 8b75b411..42bef4c8 100644 --- a/systemc-components/CMakeLists.txt +++ b/systemc-components/CMakeLists.txt @@ -24,6 +24,8 @@ add_subdirectory(generic_lua_model) add_subdirectory(container_builder) add_subdirectory(smmu500) add_subdirectory(smmuv3) +add_subdirectory(mcd_server) +add_subdirectory(mcd_mcp) if(NOT WIN32) add_subdirectory(macs) add_subdirectory(component_constructor) diff --git a/systemc-components/mcd_mcp/CMakeLists.txt b/systemc-components/mcd_mcp/CMakeLists.txt new file mode 100644 index 00000000..0b6866c8 --- /dev/null +++ b/systemc-components/mcd_mcp/CMakeLists.txt @@ -0,0 +1,15 @@ +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. +# SPDX-License-Identifier: BSD-3-Clause + +add_library(mcd_client STATIC src/mcd_client.c) +target_include_directories(mcd_client PUBLIC include) +if(WIN32) + target_link_libraries(mcd_client PUBLIC ws2_32) +endif() + +add_library(mcd_debug STATIC src/mcd_debug.cc) +target_include_directories(mcd_debug PUBLIC include) +target_link_libraries(mcd_debug PUBLIC mcd_client) + +add_executable(mcd_mcp src/mcd_mcp.cc) +target_link_libraries(mcd_mcp PRIVATE mcd_debug) diff --git a/systemc-components/mcd_mcp/README.md b/systemc-components/mcd_mcp/README.md new file mode 100644 index 00000000..4dcbf4a9 --- /dev/null +++ b/systemc-components/mcd_mcp/README.md @@ -0,0 +1,119 @@ + + +# MCD debugging for QBox, with an MCP bridge for AI agents + +This directory lets a tool — a script, a debugger front-end, or an AI agent such +as Claude — inspect and control a running QBox virtual platform through the +**MCD** (Multi-Core Debug) object model. + +| Component | Location | Role | +|-----------|----------|------| +| `mcd_server` | `systemc-components/mcd_server` | SystemC dynamic module linked into the platform. Exposes an MCD object model over a length-prefixed binary TCP protocol. | +| `mcd_mcp` | `systemc-components/mcd_mcp` | Standalone process speaking the [Model Context Protocol](https://modelcontextprotocol.io) (JSON-RPC 2.0 over stdio). Connects to `mcd_server` and re-exposes debugging as MCP *tools*. | + +``` + AI agent / MCP client mcd_mcp mcd_server (in the VP) + ─────────────────────── stdio ───────── TCP (binary) ────────────────────── + tools/call mcd_read_mem ─────▶ mcd_client ───────────▶ transport_dbg ─▶ router/RAM + tools/call mcd_read_reg ─────▶ ───────────▶ GDB-RSP ───────▶ QEMU gdbstub +``` + +## Capabilities + +Memory accesses are TLM `transport_dbg` transactions, so they work whether or not +the CPU is running and never perturb timing. Registers, run control and +breakpoints/watchpoints go to QEMU's GDB stub (`Z`/`z` packets), so register +numbering follows the target's GDB layout (AArch64: `x0..x30` = 0..30, `sp` = 31, +`pc` = 32) and watchpoints fire only on CPU accesses, not on `mcd_write_mem`. +`mcd_server` enables the stub automatically: any QEMU instance whose `gdb_port` +CCI parameter is still 0 gets a free port assigned. + +QEMU's stub state is global to an instance, so cores are per-instance GDB +threads: registers and stepping are per core, but run, stop and breakpoints act +on the whole instance. A breakpoint fires on whichever core of the instance +reaches the address first, and `mcd_wait_stop` reports the stop only on the core +QEMU attributed it to — poll each core when you do not know which will hit. + +`mcd_server` holds the SystemC kernel open only while it is itself the reason the +CPUs are idle (a CPU it halted, or a breakpoint armed), releasing that hold on +resume, on clearing the last breakpoint, and on disconnect — which also resumes +the target and removes installed breakpoints. Natural quiescence and a firmware +power-off still end the simulation; platforms wanting it pinned open +unconditionally can instantiate `keep_alive`. + +The MCD port grants unauthenticated read/write access to all of the simulated +machine's memory and registers, so the server binds to **loopback only**; reach +it from another host by tunnelling, not by rebinding. + +## Building and running + +Both components build as part of the normal QBox build, added from +`systemc-components/CMakeLists.txt` as `mcd_server` (`mcd_server.dylib`, loaded +by the platform) and `mcd_mcp` (which statically links the `mcd_client` and +`mcd_debug` helper libraries). + +Instantiate `mcd_server` in your Lua platform description; see +`tests/qbox/mcd/mcd-platform.lua` for a complete example: + +```lua +mcd_server = { + moduletype = "mcd_server", + dylib_path = "mcd_server", + mcd_port = 1235, -- TCP port for the MCD wire protocol +} +``` + +During elaboration the server discovers routers (as memory spaces) and QEMU +instances (as debug endpoints, identified by the `tcg_mode` CCI parameter rather +than by C++ type), assigning each instance a `gdb_port`. Cores are discovered +from the GDB stub on first use. + +## Registering the MCP server with an agent + +`mcd_mcp` speaks MCP on stdio, so any MCP client can launch it. For Claude Code, +add it to your `.mcp.json` (or run `claude mcp add`): + +```json +{ + "mcpServers": { + "mcd": { + "command": "/path/to/qbox/build/mcd_mcp" + } + } +} +``` + +Then, from the agent: `mcd_connect` with the `host`/`port` the platform is +listening on (default `127.0.0.1:1235`), `mcd_describe` to see the topology, +then `mcd_stop`, `mcd_read_reg`, `mcd_step`, `mcd_read_mem`, … to debug. + +## Tools + +| Tool | Purpose | +|------|---------| +| `mcd_connect` / `mcd_disconnect` | Open / close the connection to `mcd_server`. | +| `mcd_status` | JSON summary of the connection and object model. | +| `mcd_describe` | Human-readable topology table. | +| `mcd_refresh` | Re-query the target's object model. | +| `mcd_read_mem` / `mcd_write_mem` | Memory access (hex dump / hex byte string), optional `space_id`. | +| `mcd_read_reg` / `mcd_write_reg` | Register access by GDB register number, optional `cpu_idx`. | +| `mcd_regs_dump` | Dump a range of registers for a core. | +| `mcd_run` / `mcd_stop` / `mcd_step` | Run control. | +| `mcd_set_bp` / `mcd_clear_bp` / `mcd_list_bp` / `mcd_wait_stop` | Breakpoints and watchpoints. | +| `mcd_snapshot` / `mcd_diff` | Save a named register snapshot and report which registers changed since. | + +## Tests + +`tests/qbox/mcd` holds `test_mcd_unit.py` (MCP framing and tool schema, no +platform needed), `test_mcd_smp.py` (two CPUs on one `QemuInstance` via +`-p cores=2`) and `test_mcd_mcp.py` (end-to-end against the `mcd-vp` +platform). All are registered with CTest (`ctest -R mcd_mcp`); the integration +tests are skipped without an AArch64 toolchain (`aarch64-linux-gnu-gcc`, or +`clang` + `ld.lld`) to build their firmware. + +The platform reads two CCI presets from the command line (via the lua `GET()` +global), which must appear before `--gs_luafile`: `-p mcd.port=` and +`-p 'fw=""'` (string values are JSON, so the path is quoted). diff --git a/systemc-components/mcd_mcp/include/mcd_client.h b/systemc-components/mcd_mcp/include/mcd_client.h new file mode 100644 index 00000000..983c7357 --- /dev/null +++ b/systemc-components/mcd_mcp/include/mcd_client.h @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef _QBOX_MCD_CLIENT_H +#define _QBOX_MCD_CLIENT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Wire-compatible mirrors of the mcd_server object model (mcd_server.h). */ +typedef struct { + uint32_t num_cores; +} mcd_server_st; + +typedef struct { + char system_name[256]; +} mcd_system_st; + +typedef struct { + char device_name[256]; +} mcd_device_st; + +typedef struct { + uint32_t core_id; + uint32_t device_id; +} mcd_core_st; + +typedef struct { + uint32_t mem_space_id; + char mem_space_name[64]; +} mcd_mem_space_st; + +/* Values match the mcd_server MCD_BP_* wire constants (and the GDB Z/z type + * digit). */ +typedef enum { + MCD_BP_TYPE_SW_BREAK = 0, + MCD_BP_TYPE_HW_BREAK = 1, + MCD_BP_TYPE_WATCH_WRITE = 2, + MCD_BP_TYPE_WATCH_READ = 3, + MCD_BP_TYPE_WATCH_ACCESS = 4, +} mcd_bp_type_et; + +typedef struct { + uint32_t cpu; + uint32_t type; /* mcd_bp_type_et */ + uint64_t addr; + uint32_t kind; /* length in bytes */ +} mcd_bp_st; + +/* Values match the mcd_server MCD_STOP_* wire constants. */ +typedef enum { + MCD_STOP_REASON_RUNNING = 0, + MCD_STOP_REASON_HALTED = 1, + MCD_STOP_REASON_BREAK = 2, + MCD_STOP_REASON_WATCH_WRITE = 3, + MCD_STOP_REASON_WATCH_READ = 4, + MCD_STOP_REASON_WATCH_ACCESS = 5, + MCD_STOP_REASON_SIGNAL = 6, +} mcd_stop_reason_et; + +typedef struct { + uint8_t stopped; /* 0 => still running (timeout), 1 => stopped */ + uint32_t reason; /* mcd_stop_reason_et */ + uint64_t watch_addr; /* triggering address for watchpoint reasons, else 0 */ +} mcd_stop_st; + +typedef struct mcd_client_s mcd_client_t; + +mcd_client_t* mcd_client_connect(const char* host, uint16_t port); +void mcd_client_disconnect(mcd_client_t* c); + +/* *num is in/out: on entry the capacity of *out, on return the number of records + * written, never more than that capacity (excess records are dropped silently, + * so the count is not the server's total). Return 0 on success, -1 on error. */ +int mcd_client_qry_servers(mcd_client_t* c, uint32_t* num, mcd_server_st* out); +int mcd_client_qry_systems(mcd_client_t* c, uint32_t* num, mcd_system_st* out); +int mcd_client_qry_devices(mcd_client_t* c, uint32_t* num, mcd_device_st* out); +int mcd_client_qry_cores(mcd_client_t* c, uint32_t* num, mcd_core_st* out); +int mcd_client_qry_mem_spaces(mcd_client_t* c, uint32_t* num, mcd_mem_space_st* out); + +int mcd_client_run(mcd_client_t* c); +int mcd_client_stop(mcd_client_t* c); +int mcd_client_step(mcd_client_t* c, uint32_t cpu_idx); +int mcd_client_read_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, uint8_t* buf); +int mcd_client_write_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, const uint8_t* buf); +int mcd_client_read_reg(mcd_client_t* c, uint32_t cpu_idx, uint32_t regno, uint64_t* val); +int mcd_client_write_reg(mcd_client_t* c, uint32_t cpu_idx, uint32_t regno, uint64_t val); + +/* kind is the length in bytes (0 => server default, one AArch64 instruction + * word). Return 0 on success, -1 on error. */ +int mcd_client_set_bp(mcd_client_t* c, uint32_t cpu_idx, uint32_t type, uint64_t addr, uint32_t kind); +int mcd_client_clr_bp(mcd_client_t* c, uint32_t cpu_idx, uint32_t type, uint64_t addr, uint32_t kind); +int mcd_client_list_bp(mcd_client_t* c, uint32_t* num, mcd_bp_st* out); + +/* Wait up to timeout_ms for the core to stop. out->stopped is 0 if the timeout + * elapsed with the target still running. Return 0 on success, -1 on error. */ +int mcd_client_wait_stop(mcd_client_t* c, uint32_t cpu_idx, uint32_t timeout_ms, mcd_stop_st* out); + +#ifdef __cplusplus +} +#endif + +#endif /* _QBOX_MCD_CLIENT_H */ diff --git a/systemc-components/mcd_mcp/include/mcd_debug.h b/systemc-components/mcd_mcp/include/mcd_debug.h new file mode 100644 index 00000000..f35613ee --- /dev/null +++ b/systemc-components/mcd_mcp/include/mcd_debug.h @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef _QBOX_MCD_DEBUG_H +#define _QBOX_MCD_DEBUG_H + +#include +#include +#include +#include +#include + +#include + +namespace mcd { + +struct MemSpace { + uint32_t id; + std::string name; +}; + +struct CoreInfo { + uint32_t core_id; + uint32_t device_id; +}; + +// Mirrors mcd_bp_type_et on the wire. +enum class BpType : uint32_t { + SwBreak = 0, + HwBreak = 1, + WatchWrite = 2, + WatchRead = 3, + WatchAccess = 4, +}; + +struct BpInfo { + uint32_t cpu; + BpType type; + uint64_t addr; + uint32_t kind; +}; + +struct StopEvent { + bool stopped; // false => timed out with the target still running + uint32_t reason; // mcd_stop_reason_et + uint64_t watch_addr; +}; + +// RAII connection to one mcd_server instance. All operations throw +// std::runtime_error on failure. +class Connection +{ +public: + explicit Connection(const std::string& host, uint16_t port = 1235); + ~Connection(); + + Connection(const Connection&) = delete; + Connection& operator=(const Connection&) = delete; + + const std::string& host() const { return m_host; } + uint16_t port() const { return m_port; } + + // Cached; re-query with refresh(). + const std::vector& systems() const { return m_systems; } + const std::vector& devices() const { return m_devices; } + const std::vector& cores() const { return m_cores; } + const std::vector& mem_spaces() const { return m_mem_spaces; } + void refresh(); + + // Live query, not cached. + std::vector list_breakpoints(); + + mcd_client_t* handle() const { return m_client; } + +private: + mcd_client_t* m_client; + std::string m_host; + uint16_t m_port; + std::vector m_systems; + std::vector m_devices; + std::vector m_cores; + std::vector m_mem_spaces; + void populate(); +}; + +// Handle to one logical debug core: a value holding a Connection reference and +// the core index, so it must not outlive the Connection. +class Core +{ +public: + Core(Connection& conn, uint32_t idx): m_conn(conn), m_idx(idx) {} + + uint32_t index() const { return m_idx; } + const CoreInfo& info() const; + + void run(); + void stop(); + void step(); + + // regno is the GDB register number. + uint64_t read_reg(uint32_t regno); + void write_reg(uint32_t regno, uint64_t val); + + // kind is the length in bytes (0 => server default). + void set_breakpoint(uint64_t addr, BpType type = BpType::SwBreak, uint32_t kind = 0); + void clear_breakpoint(uint64_t addr, BpType type = BpType::SwBreak, uint32_t kind = 0); + + StopEvent wait_stop(uint32_t timeout_ms); + + // space_id 0 = physical. + std::vector read_mem(uint64_t addr, uint32_t len, uint32_t space_id = 0); + void write_mem(uint64_t addr, const std::vector& data, uint32_t space_id = 0); + + // Single T at addr, little-endian. + template + T read_pod(uint64_t addr, uint32_t space_id = 0) + { + auto buf = read_mem(addr, sizeof(T), space_id); + T v{}; + std::memcpy(&v, buf.data(), sizeof(T)); + return v; + } + + template + void write_pod(uint64_t addr, T val, uint32_t space_id = 0) + { + std::vector buf(sizeof(T)); + std::memcpy(buf.data(), &val, sizeof(T)); + write_mem(addr, buf, space_id); + } + +private: + Connection& m_conn; + uint32_t m_idx; +}; + +} // namespace mcd + +#endif /* _QBOX_MCD_DEBUG_H */ diff --git a/systemc-components/mcd_mcp/src/mcd_client.c b/systemc-components/mcd_mcp/src/mcd_client.c new file mode 100644 index 00000000..020e3f5a --- /dev/null +++ b/systemc-components/mcd_mcp/src/mcd_client.c @@ -0,0 +1,563 @@ +/* + * Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "mcd_client.h" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#ifdef _WIN32 +typedef SOCKET socket_t; +#define INVALID_SOCK INVALID_SOCKET +#define CLOSE_SOCKET closesocket +#define SOCK_EINTR WSAEINTR +#define sock_errno() WSAGetLastError() +#else +typedef int socket_t; +#define INVALID_SOCK (-1) +#define CLOSE_SOCKET close +#define SOCK_EINTR EINTR +#define sock_errno() errno +#endif + +struct mcd_client_s { + socket_t fd; +}; + +/* Upper bound on the length a peer may announce for a reply frame: the protocol + * is length-prefixed, so a cap is needed to avoid reading an arbitrary frame. */ +#define MCD_MAX_FRAME (1u * 1024u * 1024u) + +/* Suppress SIGPIPE on write to a closed socket, which would otherwise kill the + * process. Linux has per-call MSG_NOSIGNAL, macOS/BSD per-socket SO_NOSIGPIPE; + * Windows has neither, and no SIGPIPE either. */ +#ifdef MSG_NOSIGNAL +#define MCD_SEND_FLAGS MSG_NOSIGNAL +#else +#define MCD_SEND_FLAGS 0 +#endif + +/* SO_RCVTIMEO takes a DWORD of milliseconds on winsock, a struct timeval on + * POSIX, so the option value is not portable and is wrapped here. */ +#ifdef _WIN32 +typedef DWORD sock_timeout_t; +typedef int sock_optlen_t; +#else +typedef struct timeval sock_timeout_t; +typedef socklen_t sock_optlen_t; +#endif + +static void sock_timeout_from_ms(sock_timeout_t* t, uint32_t ms) +{ +#ifdef _WIN32 + *t = (DWORD)ms; +#else + t->tv_sec = (time_t)(ms / 1000); + t->tv_usec = (long)(ms % 1000) * 1000; +#endif +} + +static void sock_set_recv_timeout(socket_t fd, const sock_timeout_t* t) +{ + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)t, (int)sizeof(*t)); +} + +static void put_u32_le(uint8_t* p, uint32_t x) +{ + p[0] = (uint8_t)(x & 0xff); + p[1] = (uint8_t)((x >> 8) & 0xff); + p[2] = (uint8_t)((x >> 16) & 0xff); + p[3] = (uint8_t)((x >> 24) & 0xff); +} + +static void put_u64_le(uint8_t* p, uint64_t x) +{ + int i; + for (i = 0; i < 8; ++i) { + p[i] = (uint8_t)((x >> (8 * i)) & 0xff); + } +} + +static uint32_t get_u32_le(const uint8_t* p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static uint64_t get_u64_le(const uint8_t* p) +{ + uint64_t x = 0; + int i; + for (i = 0; i < 8; ++i) { + x |= (uint64_t)p[i] << (8 * i); + } + return x; +} + +/* winsock's send/recv take a char* and an int length; every transfer here is + * bounded by MCD_MAX_FRAME. */ +static int send_all(socket_t fd, const uint8_t* buf, uint32_t len) +{ + uint32_t sent = 0; + while (sent < len) { + int r = (int)send(fd, (const char*)(buf + sent), (int)(len - sent), MCD_SEND_FLAGS); + if (r < 0) { + if (sock_errno() == SOCK_EINTR) continue; + return -1; + } + sent += (uint32_t)r; + } + return 0; +} + +static int recv_all(socket_t fd, uint8_t* buf, uint32_t len) +{ + uint32_t got = 0; + while (got < len) { + int r = (int)recv(fd, (char*)(buf + got), (int)(len - got), 0); + if (r == 0) return -1; /* peer closed */ + if (r < 0) { + if (sock_errno() == SOCK_EINTR) continue; + return -1; + } + got += (uint32_t)r; + } + return 0; +} + +/* Build [u32 LE (1+plen)][opcode][payload] and send it. */ +static int send_frame(socket_t fd, uint8_t opcode, const uint8_t* payload, uint32_t plen) +{ + uint8_t hdr[5]; + put_u32_le(hdr, 1 + plen); + hdr[4] = opcode; + if (send_all(fd, hdr, sizeof(hdr)) != 0) return -1; + if (plen && send_all(fd, payload, plen) != 0) return -1; + return 0; +} + +/* Discard @p len bytes so a reply we cannot store leaves the stream framed. + * Returns 0 if all of it was drained. */ +static int drain(socket_t fd, uint32_t len) +{ + uint8_t scratch[512]; + while (len) { + uint32_t chunk = len < sizeof(scratch) ? len : (uint32_t)sizeof(scratch); + if (recv_all(fd, scratch, chunk) != 0) return -1; + len -= chunk; + } + return 0; +} + +/* Read [u32 LE len][retcode][payload]; *blen gets the payload byte count. At + * most @p cap payload bytes are stored in @p buf (NULL allowed when cap is 0); + * a longer payload is drained and reported as an error, since callers pass + * small fixed-size stack buffers. */ +static int recv_frame(socket_t fd, uint8_t* retcode, uint8_t* buf, uint32_t cap, uint32_t* blen) +{ + uint8_t lenbuf[4]; + if (recv_all(fd, lenbuf, sizeof(lenbuf)) != 0) return -1; + uint32_t len = get_u32_le(lenbuf); + if (len < 1) return -1; /* must contain at least the return code */ + if (len > MCD_MAX_FRAME) return -1; /* implausible: do not try to read it */ + + if (recv_all(fd, retcode, 1) != 0) return -1; + + uint32_t plen = len - 1; + if (plen > cap) { + (void)drain(fd, plen); + return -1; + } + if (plen && recv_all(fd, buf, plen) != 0) return -1; + *blen = plen; + return 0; +} + +/* No library init hook, so winsock is started per connection and stopped on + * disconnect; the calls are reference counted. */ +mcd_client_t* mcd_client_connect(const char* host, uint16_t port) +{ + char portstr[16]; + snprintf(portstr, sizeof(portstr), "%u", (unsigned)port); + +#ifdef _WIN32 + { + WSADATA wsa_data; + if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) return NULL; + } +#endif + + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo* res = NULL; + if (getaddrinfo(host, portstr, &hints, &res) != 0 || res == NULL) { +#ifdef _WIN32 + WSACleanup(); +#endif + return NULL; + } + + socket_t fd = INVALID_SOCK; + struct addrinfo* ai; + for (ai = res; ai != NULL; ai = ai->ai_next) { + fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (fd == INVALID_SOCK) continue; + if (connect(fd, ai->ai_addr, (int)ai->ai_addrlen) == 0) break; + CLOSE_SOCKET(fd); + fd = INVALID_SOCK; + } + freeaddrinfo(res); + if (fd == INVALID_SOCK) { +#ifdef _WIN32 + WSACleanup(); +#endif + return NULL; + } + + { + sock_timeout_t tv; + sock_timeout_from_ms(&tv, 5000); + sock_set_recv_timeout(fd, &tv); + } + +#ifdef SO_NOSIGPIPE + { + int on = 1; + setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, (const char*)&on, (int)sizeof(on)); + } +#endif + + mcd_client_t* c = (mcd_client_t*)malloc(sizeof(*c)); + if (!c) { + CLOSE_SOCKET(fd); +#ifdef _WIN32 + WSACleanup(); +#endif + return NULL; + } + c->fd = fd; + return c; +} + +void mcd_client_disconnect(mcd_client_t* c) +{ + if (!c) return; + if (c->fd != INVALID_SOCK) CLOSE_SOCKET(c->fd); + free(c); +#ifdef _WIN32 + WSACleanup(); +#endif +} + +/* Close a connection whose framing can no longer be trusted: fd becomes + * INVALID_SOCK and every later call fails fast, rather than parsing a + * desynchronised stream into plausible-looking garbage replies. */ +static void invalidate(mcd_client_t* c) +{ + if (c && c->fd != INVALID_SOCK) { + CLOSE_SOCKET(c->fd); + c->fd = INVALID_SOCK; + } +} + +/* Send a framed request and read the reply, storing at most @p cap payload bytes + * in @p resp. Returns 0 only when the transport succeeds and the server return + * code is MCD_RET_ACT_NONE (0). A transport or framing failure invalidates the + * connection; a non-zero return code does not, that frame arrived intact. */ +static int transact(mcd_client_t* c, uint8_t opcode, const uint8_t* payload, uint32_t plen, uint8_t* resp, uint32_t cap, + uint32_t* rlen) +{ + if (!c || c->fd == INVALID_SOCK) return -1; + if (send_frame(c->fd, opcode, payload, plen) != 0) { + invalidate(c); + return -1; + } + + uint8_t rc = 0; + uint32_t blen = 0; + if (recv_frame(c->fd, &rc, resp, cap, &blen) != 0) { + invalidate(c); + return -1; + } + if (rc != 0) return -1; + if (rlen) *rlen = blen; + return 0; +} + +/* Scratch buffer for a query reply (4-byte count + records); the largest record + * is 256 bytes. */ +#define MCD_CLIENT_SCRATCH 65536 + +int mcd_client_qry_servers(mcd_client_t* c, uint32_t* num, mcd_server_st* out) +{ + uint8_t resp[MCD_CLIENT_SCRATCH]; + uint32_t blen = 0; + if (transact(c, 0x01, NULL, 0, resp, sizeof(resp), &blen) != 0) return -1; + if (blen < 4) return -1; + + uint32_t count = get_u32_le(&resp[0]); + uint32_t cap = (num && out) ? *num : 0; + uint32_t i; + for (i = 0; i < count && i < cap; ++i) { + uint32_t off = 4 + i * 4; + if (off + 4 > blen) return -1; + out[i].num_cores = get_u32_le(&resp[off]); + } + /* Report what was written, not what the server claimed. */ + if (num) *num = i; + return 0; +} + +int mcd_client_qry_systems(mcd_client_t* c, uint32_t* num, mcd_system_st* out) +{ + uint8_t resp[MCD_CLIENT_SCRATCH]; + uint32_t blen = 0; + if (transact(c, 0x02, NULL, 0, resp, sizeof(resp), &blen) != 0) return -1; + if (blen < 4) return -1; + + uint32_t count = get_u32_le(&resp[0]); + uint32_t cap = (num && out) ? *num : 0; + uint32_t i; + for (i = 0; i < count && i < cap; ++i) { + uint32_t off = 4 + i * 256; + if (off + 256 > blen) return -1; + memcpy(out[i].system_name, &resp[off], 256); + /* Wire strings are fixed-width and not guaranteed terminated. */ + out[i].system_name[sizeof(out[i].system_name) - 1] = '\0'; + } + if (num) *num = i; + return 0; +} + +int mcd_client_qry_devices(mcd_client_t* c, uint32_t* num, mcd_device_st* out) +{ + uint8_t resp[MCD_CLIENT_SCRATCH]; + uint32_t blen = 0; + if (transact(c, 0x03, NULL, 0, resp, sizeof(resp), &blen) != 0) return -1; + if (blen < 4) return -1; + + uint32_t count = get_u32_le(&resp[0]); + uint32_t cap = (num && out) ? *num : 0; + uint32_t i; + for (i = 0; i < count && i < cap; ++i) { + uint32_t off = 4 + i * 256; + if (off + 256 > blen) return -1; + memcpy(out[i].device_name, &resp[off], 256); + out[i].device_name[sizeof(out[i].device_name) - 1] = '\0'; + } + if (num) *num = i; + return 0; +} + +int mcd_client_qry_cores(mcd_client_t* c, uint32_t* num, mcd_core_st* out) +{ + uint8_t resp[MCD_CLIENT_SCRATCH]; + uint32_t blen = 0; + if (transact(c, 0x04, NULL, 0, resp, sizeof(resp), &blen) != 0) return -1; + if (blen < 4) return -1; + + uint32_t count = get_u32_le(&resp[0]); + uint32_t cap = (num && out) ? *num : 0; + uint32_t i; + for (i = 0; i < count && i < cap; ++i) { + uint32_t off = 4 + i * 8; + if (off + 8 > blen) return -1; + out[i].core_id = get_u32_le(&resp[off]); + out[i].device_id = get_u32_le(&resp[off + 4]); + } + if (num) *num = i; + return 0; +} + +int mcd_client_qry_mem_spaces(mcd_client_t* c, uint32_t* num, mcd_mem_space_st* out) +{ + uint8_t resp[MCD_CLIENT_SCRATCH]; + uint32_t blen = 0; + if (transact(c, 0x05, NULL, 0, resp, sizeof(resp), &blen) != 0) return -1; + if (blen < 4) return -1; + + uint32_t count = get_u32_le(&resp[0]); + uint32_t cap = (num && out) ? *num : 0; + uint32_t i; + for (i = 0; i < count && i < cap; ++i) { + uint32_t off = 4 + i * (4 + 64); + if (off + 4 + 64 > blen) return -1; + out[i].mem_space_id = get_u32_le(&resp[off]); + memcpy(out[i].mem_space_name, &resp[off + 4], 64); + out[i].mem_space_name[sizeof(out[i].mem_space_name) - 1] = '\0'; + } + if (num) *num = i; + return 0; +} + +/* Run control returns no payload, so capacity 0: any payload the server does + * send is a protocol error. */ +int mcd_client_run(mcd_client_t* c) { return transact(c, 0x10, NULL, 0, NULL, 0, NULL); } + +int mcd_client_stop(mcd_client_t* c) { return transact(c, 0x11, NULL, 0, NULL, 0, NULL); } + +int mcd_client_step(mcd_client_t* c, uint32_t cpu_idx) +{ + uint8_t req[4]; + put_u32_le(req, cpu_idx); + return transact(c, 0x12, req, sizeof(req), NULL, 0, NULL); +} + +int mcd_client_read_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, uint8_t* buf) +{ + if (len > MCD_MAX_FRAME) return -1; + if (len && !buf) return -1; + + uint8_t req[16]; + put_u64_le(&req[0], addr); + put_u32_le(&req[8], len); + put_u32_le(&req[12], space_id); + + /* The reply payload is exactly @p len bytes, written into the caller's + * buffer, which is therefore also the capacity bound. */ + uint32_t blen = 0; + if (transact(c, 0x20, req, sizeof(req), buf, len, &blen) != 0) return -1; + if (blen != len) return -1; + return 0; +} + +int mcd_client_write_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, const uint8_t* buf) +{ + /* Bound len so the 16 + len payload length cannot wrap. */ + if (len > MCD_MAX_FRAME) return -1; + if (len && !buf) return -1; + + uint32_t plen = 16 + len; + uint8_t* req = (uint8_t*)malloc(plen); + if (!req) return -1; + put_u64_le(&req[0], addr); + put_u32_le(&req[8], len); + put_u32_le(&req[12], space_id); + if (len) memcpy(&req[16], buf, len); + + int rc = transact(c, 0x21, req, plen, NULL, 0, NULL); + free(req); + return rc; +} + +int mcd_client_read_reg(mcd_client_t* c, uint32_t cpu_idx, uint32_t regno, uint64_t* val) +{ + uint8_t req[8]; + put_u32_le(&req[0], cpu_idx); + put_u32_le(&req[4], regno); + + uint8_t resp[8]; + uint32_t blen = 0; + if (transact(c, 0x30, req, sizeof(req), resp, sizeof(resp), &blen) != 0) return -1; + if (blen != 8) return -1; + if (val) *val = get_u64_le(resp); + return 0; +} + +int mcd_client_write_reg(mcd_client_t* c, uint32_t cpu_idx, uint32_t regno, uint64_t val) +{ + uint8_t req[16]; + put_u32_le(&req[0], cpu_idx); + put_u32_le(&req[4], regno); + put_u64_le(&req[8], val); + return transact(c, 0x31, req, sizeof(req), NULL, 0, NULL); +} + +/* Encode the [cpu][type][addr][kind] request shared by SET_BP and CLR_BP. */ +static void put_bp_req(uint8_t req[20], uint32_t cpu, uint32_t type, uint64_t addr, uint32_t kind) +{ + put_u32_le(&req[0], cpu); + put_u32_le(&req[4], type); + put_u64_le(&req[8], addr); + put_u32_le(&req[16], kind); +} + +int mcd_client_set_bp(mcd_client_t* c, uint32_t cpu_idx, uint32_t type, uint64_t addr, uint32_t kind) +{ + uint8_t req[20]; + put_bp_req(req, cpu_idx, type, addr, kind); + return transact(c, 0x40, req, sizeof(req), NULL, 0, NULL); +} + +int mcd_client_clr_bp(mcd_client_t* c, uint32_t cpu_idx, uint32_t type, uint64_t addr, uint32_t kind) +{ + uint8_t req[20]; + put_bp_req(req, cpu_idx, type, addr, kind); + return transact(c, 0x41, req, sizeof(req), NULL, 0, NULL); +} + +int mcd_client_list_bp(mcd_client_t* c, uint32_t* num, mcd_bp_st* out) +{ + uint8_t resp[MCD_CLIENT_SCRATCH]; + uint32_t blen = 0; + if (transact(c, 0x42, NULL, 0, resp, sizeof(resp), &blen) != 0) return -1; + if (blen < 4) return -1; + + uint32_t count = get_u32_le(&resp[0]); + uint32_t cap = (num && out) ? *num : 0; + uint32_t i; + for (i = 0; i < count && i < cap; ++i) { + uint32_t off = 4 + i * 20; + if (off + 20 > blen) return -1; + out[i].cpu = get_u32_le(&resp[off]); + out[i].type = get_u32_le(&resp[off + 4]); + out[i].addr = (uint64_t)get_u32_le(&resp[off + 8]) | ((uint64_t)get_u32_le(&resp[off + 12]) << 32); + out[i].kind = get_u32_le(&resp[off + 16]); + } + if (num) *num = i; + return 0; +} + +int mcd_client_wait_stop(mcd_client_t* c, uint32_t cpu_idx, uint32_t timeout_ms, mcd_stop_st* out) +{ + if (!c || c->fd == INVALID_SOCK) return -1; + + uint8_t req[8]; + put_u32_le(&req[0], cpu_idx); + put_u32_le(&req[4], timeout_ms); + + /* The server holds the reply for up to timeout_ms, so raise SO_RCVTIMEO + * above it for this call and restore it afterwards. */ + sock_timeout_t saved; + sock_timeout_t tv; + sock_optlen_t slen = (sock_optlen_t)sizeof(saved); + sock_timeout_from_ms(&saved, 5000); + getsockopt(c->fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&saved, &slen); + /* Clamped so the +2 s margin cannot wrap. */ + sock_timeout_from_ms(&tv, (timeout_ms > 0xfffff000u) ? 0xffffffffu : timeout_ms + 2000u); + sock_set_recv_timeout(c->fd, &tv); + + uint8_t resp[16]; + uint32_t blen = 0; + int rc = transact(c, 0x43, req, sizeof(req), resp, sizeof(resp), &blen); + + /* transact() invalidates the fd on any transport failure. */ + if (c->fd != INVALID_SOCK) sock_set_recv_timeout(c->fd, &saved); + + if (rc != 0) return -1; + if (blen < 13) return -1; + if (out) { + out->stopped = resp[0]; + out->reason = get_u32_le(&resp[1]); + out->watch_addr = (uint64_t)get_u32_le(&resp[5]) | ((uint64_t)get_u32_le(&resp[9]) << 32); + } + return 0; +} diff --git a/systemc-components/mcd_mcp/src/mcd_debug.cc b/systemc-components/mcd_mcp/src/mcd_debug.cc new file mode 100644 index 00000000..7583b9f6 --- /dev/null +++ b/systemc-components/mcd_mcp/src/mcd_debug.cc @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include + +#include + +namespace mcd { + +namespace { +constexpr uint32_t k_max_records = 256; +} + +Connection::Connection(const std::string& host, uint16_t port): m_client(nullptr), m_host(host), m_port(port) +{ + m_client = mcd_client_connect(host.c_str(), port); + if (!m_client) { + throw std::runtime_error("mcd: failed to connect to " + host + ":" + std::to_string(port)); + } + populate(); +} + +Connection::~Connection() +{ + if (m_client) { + mcd_client_disconnect(m_client); + } +} + +void Connection::refresh() { populate(); } + +/* Each block passes a fixed-capacity array; mcd_client_qry_* returns a count + * bounded by that capacity, and the `i < tmp.size()` guard keeps the bound + * visible at the point of use. */ +void Connection::populate() +{ + m_systems.clear(); + m_devices.clear(); + m_cores.clear(); + m_mem_spaces.clear(); + + { + std::array tmp{}; + uint32_t num = tmp.size(); + if (mcd_client_qry_systems(m_client, &num, tmp.data()) != 0) { + throw std::runtime_error("mcd: qry_systems failed"); + } + for (uint32_t i = 0; i < num && i < tmp.size(); ++i) { + m_systems.emplace_back(tmp[i].system_name); + } + } + + { + std::array tmp{}; + uint32_t num = tmp.size(); + if (mcd_client_qry_devices(m_client, &num, tmp.data()) != 0) { + throw std::runtime_error("mcd: qry_devices failed"); + } + for (uint32_t i = 0; i < num && i < tmp.size(); ++i) { + m_devices.emplace_back(tmp[i].device_name); + } + } + + { + std::array tmp{}; + uint32_t num = tmp.size(); + if (mcd_client_qry_cores(m_client, &num, tmp.data()) != 0) { + throw std::runtime_error("mcd: qry_cores failed"); + } + for (uint32_t i = 0; i < num && i < tmp.size(); ++i) { + m_cores.push_back(CoreInfo{ tmp[i].core_id, tmp[i].device_id }); + } + } + + { + std::array tmp{}; + uint32_t num = tmp.size(); + if (mcd_client_qry_mem_spaces(m_client, &num, tmp.data()) != 0) { + throw std::runtime_error("mcd: qry_mem_spaces failed"); + } + for (uint32_t i = 0; i < num && i < tmp.size(); ++i) { + m_mem_spaces.push_back(MemSpace{ tmp[i].mem_space_id, tmp[i].mem_space_name }); + } + } +} + +std::vector Connection::list_breakpoints() +{ + std::array tmp{}; + uint32_t num = tmp.size(); + if (mcd_client_list_bp(m_client, &num, tmp.data()) != 0) { + throw std::runtime_error("mcd: list_bp failed"); + } + std::vector out; + for (uint32_t i = 0; i < num && i < tmp.size(); ++i) { + out.push_back(BpInfo{ tmp[i].cpu, static_cast(tmp[i].type), tmp[i].addr, tmp[i].kind }); + } + return out; +} + +const CoreInfo& Core::info() const +{ + const auto& cores = m_conn.cores(); + if (m_idx >= cores.size()) { + throw std::runtime_error("mcd: core index " + std::to_string(m_idx) + " out of range"); + } + return cores[m_idx]; +} + +void Core::run() +{ + if (mcd_client_run(m_conn.handle()) != 0) { + throw std::runtime_error("mcd: run failed"); + } +} + +void Core::stop() +{ + if (mcd_client_stop(m_conn.handle()) != 0) { + throw std::runtime_error("mcd: stop failed"); + } +} + +void Core::step() +{ + if (mcd_client_step(m_conn.handle(), m_idx) != 0) { + throw std::runtime_error("mcd: step failed"); + } +} + +uint64_t Core::read_reg(uint32_t regno) +{ + uint64_t val = 0; + if (mcd_client_read_reg(m_conn.handle(), m_idx, regno, &val) != 0) { + throw std::runtime_error("mcd: read_reg " + std::to_string(regno) + " failed"); + } + return val; +} + +void Core::write_reg(uint32_t regno, uint64_t val) +{ + if (mcd_client_write_reg(m_conn.handle(), m_idx, regno, val) != 0) { + throw std::runtime_error("mcd: write_reg " + std::to_string(regno) + " failed"); + } +} + +void Core::set_breakpoint(uint64_t addr, BpType type, uint32_t kind) +{ + if (mcd_client_set_bp(m_conn.handle(), m_idx, static_cast(type), addr, kind) != 0) { + throw std::runtime_error("mcd: set_breakpoint failed"); + } +} + +void Core::clear_breakpoint(uint64_t addr, BpType type, uint32_t kind) +{ + if (mcd_client_clr_bp(m_conn.handle(), m_idx, static_cast(type), addr, kind) != 0) { + throw std::runtime_error("mcd: clear_breakpoint failed"); + } +} + +StopEvent Core::wait_stop(uint32_t timeout_ms) +{ + mcd_stop_st st{}; + if (mcd_client_wait_stop(m_conn.handle(), m_idx, timeout_ms, &st) != 0) { + throw std::runtime_error("mcd: wait_stop failed"); + } + return StopEvent{ st.stopped != 0, st.reason, st.watch_addr }; +} + +std::vector Core::read_mem(uint64_t addr, uint32_t len, uint32_t space_id) +{ + std::vector buf(len); + if (len && mcd_client_read_mem(m_conn.handle(), addr, len, space_id, buf.data()) != 0) { + throw std::runtime_error("mcd: read_mem failed"); + } + return buf; +} + +void Core::write_mem(uint64_t addr, const std::vector& data, uint32_t space_id) +{ + if (!data.empty() && + mcd_client_write_mem(m_conn.handle(), addr, static_cast(data.size()), space_id, data.data()) != 0) { + throw std::runtime_error("mcd: write_mem failed"); + } +} + +} // namespace mcd diff --git a/systemc-components/mcd_mcp/src/mcd_mcp.cc b/systemc-components/mcd_mcp/src/mcd_mcp.cc new file mode 100644 index 00000000..3dc3d523 --- /dev/null +++ b/systemc-components/mcd_mcp/src/mcd_mcp.cc @@ -0,0 +1,811 @@ +/* + * Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#endif + +#include + +namespace { + +// Minimal JSON value, parser and emitter: only the shapes an MCP client sends +// are supported, this is not a general library. + +struct JsonValue { + enum Type { Null, Bool, Num, Str, Arr, Obj } type = Null; + bool b = false; + double num = 0; + std::string str; + std::vector arr; + std::vector> obj; + + const JsonValue* find(const std::string& k) const + { + if (type != Obj) return nullptr; + for (const auto& p : obj) + if (p.first == k) return &p.second; + return nullptr; + } +}; + +std::string json_escape(const std::string& s) +{ + std::string o; + o.reserve(s.size() + 8); + for (char c : s) { + switch (c) { + case '"': + o += "\\\""; + break; + case '\\': + o += "\\\\"; + break; + case '\n': + o += "\\n"; + break; + case '\r': + o += "\\r"; + break; + case '\t': + o += "\\t"; + break; + case '\b': + o += "\\b"; + break; + case '\f': + o += "\\f"; + break; + default: + if (static_cast(c) < 0x20) { + char buf[8]; + std::snprintf(buf, sizeof(buf), "\\u%04x", c & 0xff); + o += buf; + } else { + o += c; + } + } + } + return o; +} + +std::string json_emit(const JsonValue& v) +{ + switch (v.type) { + case JsonValue::Null: + return "null"; + case JsonValue::Bool: + return v.b ? "true" : "false"; + case JsonValue::Str: + return "\"" + json_escape(v.str) + "\""; + case JsonValue::Num: { + double d = v.num; + if (d == static_cast(static_cast(d))) return std::to_string(static_cast(d)); + char buf[32]; + std::snprintf(buf, sizeof(buf), "%g", d); + return buf; + } + case JsonValue::Arr: { + std::string o = "["; + for (size_t i = 0; i < v.arr.size(); ++i) { + if (i) o += ","; + o += json_emit(v.arr[i]); + } + return o + "]"; + } + case JsonValue::Obj: { + std::string o = "{"; + for (size_t i = 0; i < v.obj.size(); ++i) { + if (i) o += ","; + o += "\"" + json_escape(v.obj[i].first) + "\":" + json_emit(v.obj[i].second); + } + return o + "}"; + } + } + return "null"; +} + +struct Parser { + const std::string& s; + size_t i = 0; + explicit Parser(const std::string& in): s(in) {} + + void ws() + { + while (i < s.size() && std::isspace(static_cast(s[i]))) ++i; + } + + JsonValue parse() + { + ws(); + return value(); + } + + JsonValue value() + { + ws(); + if (i >= s.size()) throw std::runtime_error("json: unexpected end"); + char c = s[i]; + if (c == '{') return object(); + if (c == '[') return array(); + if (c == '"') { + JsonValue v; + v.type = JsonValue::Str; + v.str = string_(); + return v; + } + if (c == 't' || c == 'f') return boolean(); + if (c == 'n') { + expect_lit("null"); + return JsonValue{}; + } + return number(); + } + + void expect_lit(const char* lit) + { + for (const char* p = lit; *p; ++p, ++i) + if (i >= s.size() || s[i] != *p) throw std::runtime_error("json: bad literal"); + } + + JsonValue boolean() + { + JsonValue v; + v.type = JsonValue::Bool; + if (s[i] == 't') { + expect_lit("true"); + v.b = true; + } else { + expect_lit("false"); + v.b = false; + } + return v; + } + + JsonValue number() + { + size_t start = i; + while (i < s.size() && (std::isdigit(static_cast(s[i])) || s[i] == '-' || s[i] == '+' || + s[i] == '.' || s[i] == 'e' || s[i] == 'E')) + ++i; + if (i == start) throw std::runtime_error("json: bad number"); + JsonValue v; + v.type = JsonValue::Num; + v.num = std::strtod(s.substr(start, i - start).c_str(), nullptr); + return v; + } + + std::string string_() + { + ++i; + std::string out; + while (i < s.size()) { + char c = s[i++]; + if (c == '"') return out; + if (c == '\\') { + if (i >= s.size()) break; + char e = s[i++]; + switch (e) { + case '"': + out += '"'; + break; + case '\\': + out += '\\'; + break; + case '/': + out += '/'; + break; + case 'n': + out += '\n'; + break; + case 'r': + out += '\r'; + break; + case 't': + out += '\t'; + break; + case 'b': + out += '\b'; + break; + case 'f': + out += '\f'; + break; + case 'u': + // \uXXXX is unused by this protocol: skip the 4 hex digits. + i += 4; + out += '?'; + break; + default: + out += e; + break; + } + } else { + out += c; + } + } + throw std::runtime_error("json: unterminated string"); + } + + JsonValue array() + { + JsonValue v; + v.type = JsonValue::Arr; + ++i; + ws(); + if (i < s.size() && s[i] == ']') { + ++i; + return v; + } + while (true) { + v.arr.push_back(value()); + ws(); + if (i < s.size() && s[i] == ',') { + ++i; + continue; + } + if (i < s.size() && s[i] == ']') { + ++i; + break; + } + throw std::runtime_error("json: bad array"); + } + return v; + } + + JsonValue object() + { + JsonValue v; + v.type = JsonValue::Obj; + ++i; + ws(); + if (i < s.size() && s[i] == '}') { + ++i; + return v; + } + while (true) { + ws(); + if (i >= s.size() || s[i] != '"') throw std::runtime_error("json: bad key"); + std::string key = string_(); + ws(); + if (i >= s.size() || s[i] != ':') throw std::runtime_error("json: expected ':'"); + ++i; + v.obj.emplace_back(std::move(key), value()); + ws(); + if (i < s.size() && s[i] == ',') { + ++i; + continue; + } + if (i < s.size() && s[i] == '}') { + ++i; + break; + } + throw std::runtime_error("json: bad object"); + } + return v; + } +}; + +std::string req_str(const JsonValue* args, const char* key) +{ + const JsonValue* v = args ? args->find(key) : nullptr; + if (!v || v->type != JsonValue::Str) throw std::runtime_error(std::string("missing string arg: ") + key); + return v->str; +} + +int64_t req_int(const JsonValue* args, const char* key) +{ + const JsonValue* v = args ? args->find(key) : nullptr; + if (!v || v->type != JsonValue::Num) throw std::runtime_error(std::string("missing integer arg: ") + key); + return static_cast(v->num); +} + +int64_t opt_int(const JsonValue* args, const char* key, int64_t def) +{ + const JsonValue* v = args ? args->find(key) : nullptr; + if (!v || v->type != JsonValue::Num) return def; + return static_cast(v->num); +} + +uint64_t parse_u64(const std::string& s, int base) { return std::stoull(s, nullptr, base); } + +std::vector parse_hex_bytes(const std::string& in) +{ + std::string s = in; + if (s.size() >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s = s.substr(2); + if (s.size() % 2 != 0) throw std::runtime_error("hex data must have an even number of digits"); + std::vector out; + out.reserve(s.size() / 2); + for (size_t j = 0; j < s.size(); j += 2) + out.push_back(static_cast(std::stoul(s.substr(j, 2), nullptr, 16))); + return out; +} + +std::unique_ptr g_conn; +// snapshot name -> {first_regno, values} +std::map>> g_snapshots; + +mcd::Core core(uint32_t idx = 0) +{ + if (!g_conn) throw std::runtime_error("not connected (call mcd_connect first)"); + return mcd::Core(*g_conn, idx); +} + +std::string status_json() +{ + if (!g_conn) return "{\"connected\":false}"; + std::string o = "{\"connected\":true,\"host\":\"" + json_escape(g_conn->host()) + + "\",\"port\":" + std::to_string(g_conn->port()); + o += ",\"systems\":["; + for (size_t k = 0; k < g_conn->systems().size(); ++k) { + if (k) o += ","; + o += "\"" + json_escape(g_conn->systems()[k]) + "\""; + } + o += "],\"devices\":["; + for (size_t k = 0; k < g_conn->devices().size(); ++k) { + if (k) o += ","; + o += "\"" + json_escape(g_conn->devices()[k]) + "\""; + } + o += "],\"cores\":["; + for (size_t k = 0; k < g_conn->cores().size(); ++k) { + if (k) o += ","; + o += "{\"core_id\":" + std::to_string(g_conn->cores()[k].core_id) + + ",\"device_id\":" + std::to_string(g_conn->cores()[k].device_id) + "}"; + } + o += "],\"mem_spaces\":["; + for (size_t k = 0; k < g_conn->mem_spaces().size(); ++k) { + if (k) o += ","; + o += "{\"id\":" + std::to_string(g_conn->mem_spaces()[k].id) + ",\"name\":\"" + + json_escape(g_conn->mem_spaces()[k].name) + "\"}"; + } + o += "]}"; + return o; +} + +std::string hex_dump(uint64_t addr, const std::vector& buf) +{ + std::string o; + char line[32]; + for (size_t off = 0; off < buf.size(); off += 16) { + std::snprintf(line, sizeof(line), "%016llx: ", static_cast(addr + off)); + o += line; + std::string ascii; + for (size_t j = 0; j < 16; ++j) { + if (off + j < buf.size()) { + char hx[4]; + std::snprintf(hx, sizeof(hx), "%02x ", buf[off + j]); + o += hx; + char c = static_cast(buf[off + j]); + ascii += (c >= 0x20 && c < 0x7f) ? c : '.'; + } else { + o += " "; + } + } + o += " " + ascii + "\n"; + } + return o; +} + +std::string describe_text() +{ + if (!g_conn) return "not connected"; + std::string o; + o += "host: " + g_conn->host() + ":" + std::to_string(g_conn->port()) + "\n"; + o += "systems (" + std::to_string(g_conn->systems().size()) + "):\n"; + for (size_t i = 0; i < g_conn->systems().size(); ++i) + o += " [" + std::to_string(i) + "] " + g_conn->systems()[i] + "\n"; + o += "devices (" + std::to_string(g_conn->devices().size()) + "):\n"; + for (size_t i = 0; i < g_conn->devices().size(); ++i) + o += " [" + std::to_string(i) + "] " + g_conn->devices()[i] + "\n"; + o += "cores (" + std::to_string(g_conn->cores().size()) + "):\n"; + for (size_t i = 0; i < g_conn->cores().size(); ++i) { + char line[64]; + std::snprintf(line, sizeof(line), " cpu%zu core_id=%u device_id=%u\n", i, g_conn->cores()[i].core_id, + g_conn->cores()[i].device_id); + o += line; + } + o += "mem_spaces (" + std::to_string(g_conn->mem_spaces().size()) + "):\n"; + for (size_t i = 0; i < g_conn->mem_spaces().size(); ++i) { + o += " id=" + std::to_string(g_conn->mem_spaces()[i].id) + " name=" + g_conn->mem_spaces()[i].name + "\n"; + } + return o; +} + +std::string regs_dump_text(uint32_t cpu_idx, uint32_t start_regno, uint32_t count) +{ + mcd::Core c = core(cpu_idx); + std::string o; + char line[48]; + for (uint32_t r = start_regno; r < start_regno + count; ++r) { + uint64_t v = c.read_reg(r); + std::snprintf(line, sizeof(line), " r%-3u 0x%016llx\n", r, static_cast(v)); + o += line; + } + return o; +} + +mcd::BpType parse_bp_type(const std::string& s) +{ + if (s == "sw" || s == "swbreak" || s == "break" || s == "breakpoint") return mcd::BpType::SwBreak; + if (s == "hw" || s == "hwbreak") return mcd::BpType::HwBreak; + if (s == "write" || s == "wwatch" || s == "watch") return mcd::BpType::WatchWrite; + if (s == "read" || s == "rwatch") return mcd::BpType::WatchRead; + if (s == "access" || s == "awatch" || s == "rw") return mcd::BpType::WatchAccess; + throw std::runtime_error("unknown breakpoint type '" + s + "' (use sw|hw|write|read|access)"); +} + +const char* bp_type_name(mcd::BpType t) +{ + switch (t) { + case mcd::BpType::SwBreak: + return "sw"; + case mcd::BpType::HwBreak: + return "hw"; + case mcd::BpType::WatchWrite: + return "write-watch"; + case mcd::BpType::WatchRead: + return "read-watch"; + case mcd::BpType::WatchAccess: + return "access-watch"; + } + return "?"; +} + +const char* stop_reason_name(uint32_t reason) +{ + switch (reason) { + case 0: + return "running"; + case 1: + return "halted"; + case 2: + return "breakpoint"; + case 3: + return "write-watchpoint"; + case 4: + return "read-watchpoint"; + case 5: + return "access-watchpoint"; + case 6: + return "signal"; + } + return "unknown"; +} + +// Returns the text body of the tool result; throws on error. +std::string call_tool(const std::string& name, const JsonValue* args) +{ + if (name == "mcd_connect") { + std::string host = req_str(args, "host"); + uint16_t port = static_cast(opt_int(args, "port", 1235)); + g_conn = std::make_unique(host, port); + return "connected to " + host + ":" + std::to_string(port) + ", " + std::to_string(g_conn->cores().size()) + + " cores, " + std::to_string(g_conn->mem_spaces().size()) + " mem_spaces"; + } + if (name == "mcd_disconnect") { + g_conn.reset(); + return "disconnected"; + } + if (name == "mcd_status") { + return status_json(); + } + if (name == "mcd_run") { + core().run(); + return "ok"; + } + if (name == "mcd_stop") { + core().stop(); + return "ok"; + } + if (name == "mcd_step") { + core(static_cast(opt_int(args, "cpu_idx", 0))).step(); + return "ok"; + } + if (name == "mcd_read_reg") { + uint32_t cpu_idx = static_cast(opt_int(args, "cpu_idx", 0)); + uint64_t v = core(cpu_idx).read_reg(static_cast(req_int(args, "regno"))); + char buf[32]; + std::snprintf(buf, sizeof(buf), "0x%016llx", static_cast(v)); + return buf; + } + if (name == "mcd_write_reg") { + uint32_t cpu_idx = static_cast(opt_int(args, "cpu_idx", 0)); + uint32_t regno = static_cast(req_int(args, "regno")); + uint64_t val = parse_u64(req_str(args, "value"), 0); // auto-detect hex/dec + core(cpu_idx).write_reg(regno, val); + return "ok"; + } + if (name == "mcd_read_mem") { + uint64_t addr = parse_u64(req_str(args, "addr"), 16); + uint32_t len = static_cast(req_int(args, "len")); + uint32_t space = static_cast(opt_int(args, "space_id", 0)); + return hex_dump(addr, core().read_mem(addr, len, space)); + } + if (name == "mcd_write_mem") { + uint64_t addr = parse_u64(req_str(args, "addr"), 16); + std::vector data = parse_hex_bytes(req_str(args, "data")); + uint32_t space = static_cast(opt_int(args, "space_id", 0)); + core().write_mem(addr, data, space); + return "ok"; + } + if (name == "mcd_refresh") { + if (!g_conn) throw std::runtime_error("not connected (call mcd_connect first)"); + g_conn->refresh(); + return status_json(); + } + if (name == "mcd_describe") { + return describe_text(); + } + if (name == "mcd_regs_dump") { + uint32_t cpu_idx = static_cast(opt_int(args, "cpu_idx", 0)); + uint32_t start = static_cast(opt_int(args, "start_regno", 0)); + uint32_t count = static_cast(opt_int(args, "count", 32)); + return "registers cpu" + std::to_string(cpu_idx) + " r" + std::to_string(start) + ".." + + std::to_string(start + count - 1) + ":\n" + regs_dump_text(cpu_idx, start, count); + } + if (name == "mcd_snapshot") { + std::string snap_name = req_str(args, "name"); + uint32_t cpu_idx = static_cast(opt_int(args, "cpu_idx", 0)); + uint32_t start = static_cast(opt_int(args, "start_regno", 0)); + uint32_t count = static_cast(opt_int(args, "count", 32)); + mcd::Core c = core(cpu_idx); + std::vector vals; + vals.reserve(count); + for (uint32_t r = start; r < start + count; ++r) vals.push_back(c.read_reg(r)); + g_snapshots[snap_name] = { start, std::move(vals) }; + return "snapshot '" + snap_name + "' saved: " + std::to_string(count) + " regs from r" + std::to_string(start); + } + if (name == "mcd_diff") { + std::string snap_name = req_str(args, "name"); + auto it = g_snapshots.find(snap_name); + if (it == g_snapshots.end()) throw std::runtime_error("no snapshot named '" + snap_name + "'"); + uint32_t cpu_idx = static_cast(opt_int(args, "cpu_idx", 0)); + uint32_t start = it->second.first; + const std::vector& saved = it->second.second; + mcd::Core c = core(cpu_idx); + std::string o; + bool any = false; + char line[80]; + for (uint32_t i = 0; i < static_cast(saved.size()); ++i) { + uint64_t now = c.read_reg(start + i); + if (now != saved[i]) { + std::snprintf(line, sizeof(line), " r%-3u was=0x%016llx now=0x%016llx\n", start + i, + static_cast(saved[i]), static_cast(now)); + o += line; + any = true; + } + } + return any ? ("changed registers (cpu" + std::to_string(cpu_idx) + " vs '" + snap_name + "'):\n" + o) + : "no changes (cpu" + std::to_string(cpu_idx) + " vs '" + snap_name + "')"; + } + if (name == "mcd_set_bp") { + uint32_t cpu_idx = static_cast(opt_int(args, "cpu_idx", 0)); + uint64_t addr = parse_u64(req_str(args, "addr"), 16); + mcd::BpType type = parse_bp_type(args && args->find("type") ? req_str(args, "type") : "sw"); + uint32_t kind = static_cast(opt_int(args, "len", 0)); + core(cpu_idx).set_breakpoint(addr, type, kind); + char buf[96]; + std::snprintf(buf, sizeof(buf), "set %s @0x%llx on cpu%u", bp_type_name(type), + static_cast(addr), cpu_idx); + return buf; + } + if (name == "mcd_clear_bp") { + uint32_t cpu_idx = static_cast(opt_int(args, "cpu_idx", 0)); + uint64_t addr = parse_u64(req_str(args, "addr"), 16); + mcd::BpType type = parse_bp_type(args && args->find("type") ? req_str(args, "type") : "sw"); + uint32_t kind = static_cast(opt_int(args, "len", 0)); + core(cpu_idx).clear_breakpoint(addr, type, kind); + char buf[96]; + std::snprintf(buf, sizeof(buf), "cleared %s @0x%llx on cpu%u", bp_type_name(type), + static_cast(addr), cpu_idx); + return buf; + } + if (name == "mcd_list_bp") { + if (!g_conn) throw std::runtime_error("not connected (call mcd_connect first)"); + auto bps = g_conn->list_breakpoints(); + if (bps.empty()) return "no breakpoints or watchpoints set"; + std::string o = "breakpoints/watchpoints (" + std::to_string(bps.size()) + "):\n"; + char line[96]; + for (const auto& bp : bps) { + std::snprintf(line, sizeof(line), " cpu%u %-13s @0x%llx len=%u\n", bp.cpu, bp_type_name(bp.type), + static_cast(bp.addr), bp.kind); + o += line; + } + return o; + } + if (name == "mcd_wait_stop") { + uint32_t cpu_idx = static_cast(opt_int(args, "cpu_idx", 0)); + uint32_t timeout_ms = static_cast(opt_int(args, "timeout_ms", 2000)); + mcd::Core c = core(cpu_idx); + mcd::StopEvent ev = c.wait_stop(timeout_ms); + if (!ev.stopped) { + return "still running (no stop within " + std::to_string(timeout_ms) + " ms)"; + } + /* Halted: PC is AArch64 GDB reg 32. */ + std::string o = std::string("stopped: ") + stop_reason_name(ev.reason); + char buf[64]; + try { + uint64_t pc = c.read_reg(32); + std::snprintf(buf, sizeof(buf), " pc=0x%llx", static_cast(pc)); + o += buf; + } catch (const std::exception&) { + /* PC read is best-effort. */ + } + if (ev.reason >= 3 && ev.reason <= 5) { + std::snprintf(buf, sizeof(buf), " watch_addr=0x%llx", static_cast(ev.watch_addr)); + o += buf; + } + return o; + } + throw std::runtime_error("unknown tool: " + name); +} + +const char* k_tools_list = + "[{\"name\":\"mcd_connect\",\"description\":\"Connect to an mcd_server instance.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"host\":{\"type\":\"string\"}," + "\"port\":{\"type\":\"integer\"}},\"required\":[\"host\"]}}," + "{\"name\":\"mcd_disconnect\",\"description\":\"Close the active connection.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," + "{\"name\":\"mcd_status\",\"description\":\"JSON summary of connection, systems, devices, cores, mem_spaces.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," + "{\"name\":\"mcd_run\",\"description\":\"Resume the target.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," + "{\"name\":\"mcd_stop\",\"description\":\"Halt the target.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," + "{\"name\":\"mcd_step\",\"description\":\"Single-step one core.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"cpu_idx\":{\"type\":\"integer\"}}}}," + "{\"name\":\"mcd_read_reg\",\"description\":\"Read a GDB register by number for a core (default cpu0); returns " + "0x-prefixed hex. Register numbering follows the target's GDB layout (AArch64: x0..x30=0..30, sp=31, pc=32).\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"cpu_idx\":{\"type\":\"integer\"},\"regno\":{\"type\":" + "\"integer\"}}," + "\"required\":[\"regno\"]}}," + "{\"name\":\"mcd_write_reg\",\"description\":\"Write a GDB register for a core (default cpu0); value is hex " + "(0x...) or decimal.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"cpu_idx\":{\"type\":\"integer\"},\"regno\":{\"type\":" + "\"integer\"}," + "\"value\":{\"type\":\"string\"}},\"required\":[\"regno\",\"value\"]}}," + "{\"name\":\"mcd_read_mem\",\"description\":\"Read memory; returns a hex dump.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"addr\":{\"type\":\"string\"}," + "\"len\":{\"type\":\"integer\"},\"space_id\":{\"type\":\"integer\"}}," + "\"required\":[\"addr\",\"len\"]}}," + "{\"name\":\"mcd_write_mem\",\"description\":\"Write memory from a hex byte string.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"addr\":{\"type\":\"string\"}," + "\"data\":{\"type\":\"string\"},\"space_id\":{\"type\":\"integer\"}}," + "\"required\":[\"addr\",\"data\"]}}," + "{\"name\":\"mcd_refresh\",\"description\":\"Re-query the target and return the updated status.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," + "{\"name\":\"mcd_describe\",\"description\":\"Human-readable topology table: host, systems, devices, cores, " + "mem_spaces.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," + "{\"name\":\"mcd_regs_dump\",\"description\":\"Dump a range of GDB registers for a core (default: r0..r31 of " + "cpu0).\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"cpu_idx\":{\"type\":\"integer\"}," + "\"start_regno\":{\"type\":\"integer\"},\"count\":{\"type\":\"integer\"}}}}," + "{\"name\":\"mcd_snapshot\",\"description\":\"Save a named register snapshot for later diff.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}," + "\"cpu_idx\":{\"type\":\"integer\"},\"start_regno\":{\"type\":\"integer\"}," + "\"count\":{\"type\":\"integer\"}},\"required\":[\"name\"]}}," + "{\"name\":\"mcd_diff\",\"description\":\"Show which registers changed since the named snapshot.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}," + "\"cpu_idx\":{\"type\":\"integer\"}},\"required\":[\"name\"]}}," + "{\"name\":\"mcd_set_bp\",\"description\":\"Set a breakpoint or watchpoint at a hex address. type: " + "sw|hw|write|read|access (default sw). len is the watch length in bytes (default 4). Combine with mcd_run + " + "mcd_wait_stop to run to it.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"addr\":{\"type\":\"string\"}," + "\"type\":{\"type\":\"string\"},\"len\":{\"type\":\"integer\"},\"cpu_idx\":{\"type\":\"integer\"}}," + "\"required\":[\"addr\"]}}," + "{\"name\":\"mcd_clear_bp\",\"description\":\"Remove a breakpoint or watchpoint previously set at a hex address " + "(same type as when set).\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"addr\":{\"type\":\"string\"}," + "\"type\":{\"type\":\"string\"},\"len\":{\"type\":\"integer\"},\"cpu_idx\":{\"type\":\"integer\"}}," + "\"required\":[\"addr\"]}}," + "{\"name\":\"mcd_list_bp\",\"description\":\"List the breakpoints and watchpoints currently installed on the " + "target.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," + "{\"name\":\"mcd_wait_stop\",\"description\":\"Wait for a core to stop (breakpoint/watchpoint/signal). Returns the " + "stop reason and PC once halted, or reports it is still running if timeout_ms (default 2000) elapses first.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"cpu_idx\":{\"type\":\"integer\"}," + "\"timeout_ms\":{\"type\":\"integer\"}}}}]"; + +void send(const std::string& msg) +{ + std::cout << msg << "\n"; + std::cout.flush(); +} + +std::string reply_result(const std::string& id_raw, const std::string& result_json) +{ + return "{\"jsonrpc\":\"2.0\",\"id\":" + id_raw + ",\"result\":" + result_json + "}"; +} + +std::string reply_error(const std::string& id_raw, int code, const std::string& message) +{ + return "{\"jsonrpc\":\"2.0\",\"id\":" + id_raw + ",\"error\":{\"code\":" + std::to_string(code) + + ",\"message\":\"" + json_escape(message) + "\"}}"; +} + +std::string tool_content(const std::string& text, bool is_error) +{ + return "{\"content\":[{\"type\":\"text\",\"text\":\"" + json_escape(text) + + "\"}],\"isError\":" + (is_error ? "true" : "false") + "}"; +} + +void dispatch(const JsonValue& req) +{ + const JsonValue* method_v = req.find("method"); + const JsonValue* id_v = req.find("id"); + std::string method = (method_v && method_v->type == JsonValue::Str) ? method_v->str : ""; + + // JSON-RPC: a request without an id is a notification and must get no reply. + bool is_notification = (id_v == nullptr); + std::string id_raw = id_v ? json_emit(*id_v) : "null"; + + if (method == "initialize") { + send(reply_result(id_raw, + "{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{}}," + "\"serverInfo\":{\"name\":\"mcd-mcp\",\"version\":\"0.1\"}}")); + return; + } + if (method == "notifications/initialized") { + return; + } + if (method == "tools/list") { + send(reply_result(id_raw, std::string("{\"tools\":") + k_tools_list + "}")); + return; + } + if (method == "tools/call") { + const JsonValue* params = req.find("params"); + const JsonValue* name_v = params ? params->find("name") : nullptr; + const JsonValue* args = params ? params->find("arguments") : nullptr; + std::string name = (name_v && name_v->type == JsonValue::Str) ? name_v->str : ""; + try { + std::string text = call_tool(name, args); + send(reply_result(id_raw, tool_content(text, false))); + } catch (const std::exception& e) { + send(reply_result(id_raw, tool_content(e.what(), true))); + } + return; + } + + if (is_notification) return; + send(reply_error(id_raw, -32601, "Method not found")); +} + +} // namespace + +int main() +{ + std::ios::sync_with_stdio(false); +#ifdef _WIN32 + /* MCP frames are newline-delimited JSON: text mode would rewrite the + * delimiters in both directions. */ + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +#endif + std::string line; + while (std::getline(std::cin, line)) { + if (!line.empty() && line.back() == '\r') line.pop_back(); /* CRLF-terminated host */ + if (line.empty()) continue; + try { + Parser p(line); + JsonValue req = p.parse(); + dispatch(req); + } catch (const std::exception& e) { + send(reply_error("null", -32700, std::string("Parse error: ") + e.what())); + } + } + return 0; +} diff --git a/systemc-components/mcd_server/CMakeLists.txt b/systemc-components/mcd_server/CMakeLists.txt new file mode 100644 index 00000000..f4e836a4 --- /dev/null +++ b/systemc-components/mcd_server/CMakeLists.txt @@ -0,0 +1,8 @@ +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. +# SPDX-License-Identifier: BSD-3-Clause + +gs_create_dymod(mcd_server) +target_link_libraries(mcd_server PUBLIC router) +if(WIN32) + target_link_libraries(mcd_server PRIVATE ws2_32) +endif() diff --git a/systemc-components/mcd_server/include/mcd_server.h b/systemc-components/mcd_server/include/mcd_server.h new file mode 100644 index 00000000..a9e025df --- /dev/null +++ b/systemc-components/mcd_server/include/mcd_server.h @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef _QBOX_MCD_SERVER_H +#define _QBOX_MCD_SERVER_H + +/* Winsock must be included before anything can pull in windows.h, which would + * otherwise bring the incompatible winsock.h in first. */ +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +/* runonsysc.h uses gs::async_event without including it: pull in the + * aggregate header, not runonsysc.h directly. */ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qbox { + +#ifdef _WIN32 +typedef SOCKET socket_t; +static constexpr socket_t INVALID_SOCK = INVALID_SOCKET; +#define CLOSE_SOCKET closesocket +#else +typedef int socket_t; +static constexpr socket_t INVALID_SOCK = -1; +#define CLOSE_SOCKET close +#endif + +/* MCD object model and opcodes, mirroring the public SPRINT MCD API + * (mcd_api.h, BSD-3-Clause), so no MCD SDK dependency is needed. */ +typedef struct mcd_server_st { + uint32_t num_cores; +} mcd_server_st; + +typedef struct mcd_system_st { + char system_name[256]; +} mcd_system_st; + +typedef struct mcd_device_st { + char device_name[256]; +} mcd_device_st; + +typedef struct mcd_core_st { + uint32_t core_id; + uint32_t device_id; +} mcd_core_st; + +struct mcd_mem_space_st { + uint32_t mem_space_id; + char mem_space_name[64]; +}; + +typedef enum { + MCD_RET_ACT_NONE = 0, + MCD_RET_ERR_GENERAL = 1, +} mcd_return_et; + +#define MCD_OP_QRY_SERVERS 0x01 +#define MCD_OP_QRY_SYSTEMS 0x02 +#define MCD_OP_QRY_DEVICES 0x03 +#define MCD_OP_QRY_CORES 0x04 +#define MCD_OP_QRY_MEM_SPACES 0x05 +#define MCD_OP_RUN 0x10 +#define MCD_OP_STOP 0x11 +#define MCD_OP_STEP 0x12 +#define MCD_OP_READ_MEM 0x20 +#define MCD_OP_WRITE_MEM 0x21 +#define MCD_OP_READ_REG 0x30 +#define MCD_OP_WRITE_REG 0x31 +#define MCD_OP_SET_BP 0x40 +#define MCD_OP_CLR_BP 0x41 +#define MCD_OP_LIST_BP 0x42 +#define MCD_OP_WAIT_STOP 0x43 + +/* Breakpoint/watchpoint types; values equal the GDB Z/z packet type digit. */ +#define MCD_BP_SW_BREAK 0 /* software breakpoint (Z0) */ +#define MCD_BP_HW_BREAK 1 /* hardware breakpoint (Z1) */ +#define MCD_BP_WATCH_WRITE 2 /* write watchpoint (Z2) */ +#define MCD_BP_WATCH_READ 3 /* read watchpoint (Z3) */ +#define MCD_BP_WATCH_ACCESS 4 /* access watchpoint (Z4) */ + +/* Stop reasons reported by MCD_OP_WAIT_STOP. */ +#define MCD_STOP_RUNNING 0 /* timed out; target still running */ +#define MCD_STOP_HALTED 1 /* already halted (nothing was running) */ +#define MCD_STOP_BREAK 2 /* SIGTRAP: breakpoint or single step */ +#define MCD_STOP_WATCH_WRITE 3 +#define MCD_STOP_WATCH_READ 4 +#define MCD_STOP_WATCH_ACCESS 5 +#define MCD_STOP_SIGNAL 6 /* some other signal */ + +/** + * @brief Multi-Core Debug (MCD) TCP server component. + * + * Length-prefixed binary protocol over TCP; the accept loop is a std::thread, + * not a SystemC thread. Memory access uses transport_dbg on a bound TLM socket; + * CPU control goes to QEMU's GDB-RSP stub. + */ +class mcd_server : public sc_core::sc_module +{ + SCP_LOGGER(); + +public: + cci::cci_param p_mcd_port; + + mcd_server(const sc_core::sc_module_name& name); + ~mcd_server(); + + /** + * @brief Register a QEMU instance and the GDB-RSP endpoint reaching it. + * @param gdb_hostport GDB-RSP "host:port"; one endpoint serves every core + */ + void bind_instance(sc_core::sc_object* inst, const std::string& gdb_hostport); + + /** + * @brief Register a TLM initiator socket for debug memory access. + * @param space_id identifier the debugger uses to select this space + */ + void bind_target(tlm::tlm_initiator_socket<>* socket, uint32_t space_id = 0, const std::string& name = "physical"); + + void before_end_of_elaboration() override; + void end_of_elaboration() override; + +private: + mcd_return_et mcd_qry_servers(uint32_t* num_servers, mcd_server_st* servers); + mcd_return_et mcd_qry_systems(uint32_t* num_systems, mcd_system_st* systems); + /* @p cap is the output array capacity; *num is the number written. A null + * array queries the count only. */ + mcd_return_et mcd_qry_devices(uint32_t* num_devices, mcd_device_st* devices, uint32_t cap); + mcd_return_et mcd_qry_cores(uint32_t* num_cores, mcd_core_st* cores, uint32_t cap); + + void start_server(); + void server_thread(); + void handle_client(socket_t fd); + + mcd_return_et dispatch(uint8_t opcode, const std::vector& req, std::vector& resp); + + mcd_return_et op_qry_servers(const std::vector& req, std::vector& resp); + mcd_return_et op_qry_systems(const std::vector& req, std::vector& resp); + mcd_return_et op_qry_devices(const std::vector& req, std::vector& resp); + mcd_return_et op_qry_cores(const std::vector& req, std::vector& resp); + mcd_return_et op_qry_mem_spaces(const std::vector& req, std::vector& resp); + mcd_return_et op_read_mem(const std::vector& req, std::vector& resp); + mcd_return_et op_write_mem(const std::vector& req, std::vector& resp); + mcd_return_et op_run(const std::vector& req, std::vector& resp); + mcd_return_et op_stop(const std::vector& req, std::vector& resp); + mcd_return_et op_step(const std::vector& req, std::vector& resp); + mcd_return_et op_read_reg(const std::vector& req, std::vector& resp); + mcd_return_et op_write_reg(const std::vector& req, std::vector& resp); + mcd_return_et op_set_bp(const std::vector& req, std::vector& resp); + mcd_return_et op_clr_bp(const std::vector& req, std::vector& resp); + mcd_return_et op_list_bp(const std::vector& req, std::vector& resp); + mcd_return_et op_wait_stop(const std::vector& req, std::vector& resp); + + socket_t m_listen_fd; + std::thread m_thread; + std::atomic m_running; + + /* Connection being serviced, or INVALID_SOCK. Published so the destructor can + * shutdown() it: the worker blocks in recv() here, which closing the listener + * does not interrupt. */ + std::atomic m_client_fd; + std::atomic m_thread_done; + + /* TLM (transport_dbg included) is not thread safe: every call into the model + * runs on the SystemC kernel thread, never on the socket worker. */ + gs::runonsysc m_sc; + + /* Suspending channel attached only while the debugger is the reason the CPUs + * are halted: a gdbstub halt removes the quantum keeper's suspending channel + * (QemuCpu::wait_for_work -> m_qk->stop()), starving the kernel. The attach is + * applied on the SystemC thread in update(), so it must precede the halt. */ + gs::async_event m_debug_hold; + bool m_debug_hold_active; /* mirrors m_debug_hold; guarded by m_gdb_mutex */ + + /* Both idempotent; caller must hold m_gdb_mutex. */ + void debug_hold(bool hold); + void update_debug_hold(); + + /* True once simulated time has advanced, i.e. a resume really took effect. */ + bool resume_observed(); + + /* One debug endpoint per QEMU instance: run/stop are global + * vm_stop()/vm_start(); only register access and stepping are per core. */ + std::vector m_insts; + std::vector m_gdb_ports; /* GDB-RSP host:port per instance */ + + /* Persistent RSP session per instance, opened lazily; INVALID_SOCK = not + * connected. The 0x03 interrupt and c/s run-control only work within one + * long-lived connection. m_gdb_running = last told to continue. Guarded by + * m_gdb_mutex. */ + std::vector m_gdb_fds; + std::vector m_gdb_running; + std::vector m_threads_known; /* cores enumerated for this session yet */ + std::mutex m_gdb_mutex; + + /* One entry per core, in MCD core-id order, from the gdbstub itself + * (qfThreadInfo / qThreadExtraInfo). Guarded by m_gdb_mutex. */ + struct core_t { + uint32_t inst; /* index into m_insts / m_gdb_fds */ + uint32_t tid; /* gdb thread id (= QEMU cpu_index + 1) */ + std::string name; + }; + std::vector m_cores; + + /* Last stop reported by an instance, cached until the core it belongs to asks: + * a stop arrives once per session, but WAIT_STOP is per core and the stopping + * core need not be the one waited on. Guarded by m_gdb_mutex. */ + struct last_stop_t { + bool valid = false; + uint32_t reason = 0; + uint64_t watch_addr = 0; + uint32_t tid = 0; /* gdb thread id that stopped, 0 if unreported */ + }; + std::vector m_last_stop; + + void note_stop(uint32_t idx, uint32_t reason, uint64_t watch_addr, uint32_t tid); + + /* note_stop and these three require m_gdb_mutex. ensure_cores_known() opens a + * session per instance; gdb_select_core() sends "Hg" for register access. */ + void gdb_enumerate_cores(uint32_t idx); + void ensure_cores_known(); + bool gdb_select_core(uint32_t core); + + /* Breakpoints installed via MCD_OP_SET_BP. Guarded by m_gdb_mutex. `core` is + * only what the debugger asked for: QEMU installs Z/z breakpoints per address + * space, so a breakpoint fires on any core of that instance. */ + struct breakpoint_t { + uint32_t core; /* MCD core id the request came in on */ + uint32_t type; /* MCD_BP_* */ + uint64_t addr; + uint32_t kind; /* length in bytes (Z-packet "kind") */ + }; + std::vector m_breakpoints; + + /* Wait up to @p timeout_ms for a stop-reply. Returns an MCD_STOP_* reason, + * the watchpoint address, and the reply's gdb thread id (0 if absent). + * Caller holds m_gdb_mutex, as for drain_pending_stop() and gdb_cmd(). */ + uint32_t gdb_wait_stop(uint32_t idx, uint32_t timeout_ms, uint64_t& watch_addr, uint32_t& stopped_tid); + + /* Non-blocking consume of an unsolicited stop-reply, so m_gdb_running tracks + * what the instance is doing. */ + void drain_pending_stop(uint32_t idx); + + /* Connected, handshaken RSP session fd for instance @p idx, or INVALID_SOCK. */ + socket_t gdb_session(uint32_t idx); + void gdb_close_all(); + /* Invalidates the session on transport failure. */ + bool gdb_cmd(uint32_t idx, const std::string& cmd, std::string& reply, bool wait_reply = true); + + /* m_mem_spaces keeps registration order for query; m_transactors maps space_id + * to a callable issuing a transport_dbg and returning the bytes handled. */ + std::vector m_mem_spaces; + std::map> m_transactors; + + void add_mem_space(uint32_t space_id, const std::string& name, + std::function fn); +}; + +} // namespace qbox + +#endif // _QBOX_MCD_SERVER_H diff --git a/systemc-components/mcd_server/src/mcd_server.cc b/systemc-components/mcd_server/src/mcd_server.cc new file mode 100644 index 00000000..726b0bec --- /dev/null +++ b/systemc-components/mcd_server/src/mcd_server.cc @@ -0,0 +1,1812 @@ +/* + * Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "mcd_server.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Socket headers (and the socket_t/CLOSE_SOCKET portability block) come from + * mcd_server.h, which must be included before anything pulls in windows.h. */ + +namespace qbox { + +/* winsock reports through WSAGetLastError(), not errno. */ +#ifdef _WIN32 +#define SOCK_ERR_IS_INTR() (WSAGetLastError() == WSAEINTR) +#else +#define SOCK_ERR_IS_INTR() (errno == EINTR) +#endif + +/* winsock ignores select()'s nfds argument. */ +#ifdef _WIN32 +#define SOCK_NFDS(fd) 0 +#else +#define SOCK_NFDS(fd) (static_cast(fd) + 1) +#endif + +#ifndef SHUT_RDWR +#define SHUT_RDWR SD_BOTH +#endif + +static std::string sock_err() +{ +#ifdef _WIN32 + return "winsock error " + std::to_string(WSAGetLastError()); +#else + return std::strerror(errno); +#endif +} + +/* SO_RCVTIMEO takes a DWORD of milliseconds on winsock, a struct timeval on POSIX. */ +static void set_recv_timeout(socket_t fd, uint32_t ms) +{ +#ifdef _WIN32 + DWORD tv = ms; +#else + struct timeval tv; + tv.tv_sec = ms / 1000; + tv.tv_usec = (ms % 1000) * 1000; +#endif + ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&tv), sizeof(tv)); +} + +/* Frame and single-access size cap. The protocol is length-prefixed, so an + * unchecked length lets an unauthenticated peer allocate that much inside the + * simulator; 1 MiB exceeds any real debugger operation. */ +static const uint32_t MCD_MAX_FRAME = 1u * 1024u * 1024u; + +/* Cap on the client-supplied WAIT_STOP timeout: the wait holds m_gdb_mutex, so + * an unbounded value blocks every other debug operation. */ +static const uint32_t MCD_MAX_WAIT_MS = 60u * 1000u; + +/* SIGPIPE would kill the simulation when a debugger goes away mid-reply. Linux + * has the per-call flag, macOS/BSD only the per-socket option below. */ +#ifdef MSG_NOSIGNAL +#define MCD_SEND_FLAGS MSG_NOSIGNAL +#else +#define MCD_SEND_FLAGS 0 +#endif + +static void set_nosigpipe(socket_t fd) +{ +#ifdef SO_NOSIGPIPE + int on = 1; + ::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, reinterpret_cast(&on), sizeof(on)); +#else + /* Windows has neither the option nor SIGPIPE. */ + (void)fd; +#endif +} + +static void put_u32(std::vector& v, uint32_t x) +{ + v.push_back(static_cast(x & 0xff)); + v.push_back(static_cast((x >> 8) & 0xff)); + v.push_back(static_cast((x >> 16) & 0xff)); + v.push_back(static_cast((x >> 24) & 0xff)); +} + +static uint32_t get_u32(const uint8_t* p) +{ + return static_cast(p[0]) | (static_cast(p[1]) << 8) | (static_cast(p[2]) << 16) | + (static_cast(p[3]) << 24); +} + +static uint64_t get_u64(const uint8_t* p) +{ + uint64_t x = 0; + for (int i = 0; i < 8; ++i) { + x |= static_cast(p[i]) << (8 * i); + } + return x; +} + +/* winsock's recv/send take a char* and an int length, so the buffer and count are + * cast; every transfer here is bounded by MCD_MAX_FRAME. */ +static bool recv_all(socket_t fd, void* buf, size_t n) +{ + uint8_t* p = static_cast(buf); + size_t got = 0; + while (got < n) { + int r = static_cast(::recv(fd, reinterpret_cast(p + got), static_cast(n - got), 0)); + if (r == 0) return false; /* peer closed */ + if (r < 0) { + if (SOCK_ERR_IS_INTR()) continue; + return false; + } + got += static_cast(r); + } + return true; +} + +static bool send_all(socket_t fd, const void* buf, size_t n) +{ + const uint8_t* p = static_cast(buf); + size_t sent = 0; + while (sent < n) { + int r = static_cast( + ::send(fd, reinterpret_cast(p + sent), static_cast(n - sent), MCD_SEND_FLAGS)); + if (r < 0) { + if (SOCK_ERR_IS_INTR()) continue; + return false; + } + sent += static_cast(r); + } + return true; +} + +/* GDB remote-serial-protocol (RSP) client helpers: CPU control and register + * access are forwarded to QEMU's gdbstub as framed packets. */ + +static int hex_nibble(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +/* RSP checksum: sum of all data bytes, modulo 256. */ +static uint8_t gdb_checksum(const std::string& data) +{ + unsigned sum = 0; + for (unsigned char c : data) sum += c; + return static_cast(sum & 0xff); +} + +/* Split "host:port" (host may be empty, an IPv4 literal, or a name). */ +static bool gdb_parse_hostport(const std::string& hostport, std::string& host, std::string& port) +{ + std::string::size_type colon = hostport.rfind(':'); + if (colon == std::string::npos) return false; + host = hostport.substr(0, colon); + port = hostport.substr(colon + 1); + if (host.empty()) host = "127.0.0.1"; + return !port.empty(); +} + +/* Open a TCP connection to the gdbstub, with a receive timeout so a background + * worker never blocks forever. Returns the fd or INVALID_SOCK. */ +static socket_t gdb_open_once(const std::string& hostport) +{ + std::string host, port; + if (!gdb_parse_hostport(hostport, host, port)) return INVALID_SOCK; + + struct addrinfo hints; + std::memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo* res = nullptr; + if (::getaddrinfo(host.c_str(), port.c_str(), &hints, &res) != 0 || res == nullptr) { + return INVALID_SOCK; + } + + socket_t fd = INVALID_SOCK; + for (struct addrinfo* ai = res; ai != nullptr; ai = ai->ai_next) { + fd = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (fd == INVALID_SOCK) continue; + if (::connect(fd, ai->ai_addr, static_cast(ai->ai_addrlen)) == 0) break; + CLOSE_SOCKET(fd); + fd = INVALID_SOCK; + } + ::freeaddrinfo(res); + if (fd == INVALID_SOCK) return INVALID_SOCK; + + set_recv_timeout(fd, 5000); + set_nosigpipe(fd); + return fd; +} + +static socket_t gdb_open(const std::string& hostport) +{ + /* Retry ~1 s: the stub starts in start_of_simulation, after this module + * publishes its port, and callback order between modules is unspecified. + * Short enough that an absent stub is an error, not a hang. */ + for (int attempt = 0; attempt < 20; ++attempt) { + socket_t fd = gdb_open_once(hostport); + if (fd != INVALID_SOCK) return fd; + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + return INVALID_SOCK; +} + +/* Frame @p data as "$#", send it, read the '+' ack. An async + * stop-reply can land between send and ack; skip it, or its '$' is taken for the + * ack and the session desynchronises. @p stray gets the last such packet. */ +static bool gdb_write_packet(socket_t fd, const std::string& data, std::string* stray = nullptr) +{ + char csbuf[3]; + std::snprintf(csbuf, sizeof(csbuf), "%02x", gdb_checksum(data)); + + std::string pkt; + pkt.reserve(data.size() + 4); + pkt.push_back('$'); + pkt += data; + pkt.push_back('#'); + pkt += csbuf; + + if (!send_all(fd, pkt.data(), pkt.size())) return false; + + /* Bound the loop so a chatty or broken peer cannot hold us here forever. */ + for (int skipped = 0; skipped < 16; ++skipped) { + char ack = 0; + if (!recv_all(fd, &ack, 1)) return false; + + if (ack == '+') return true; + if (ack == '-') return false; /* retransmit requested; we do not retransmit */ + + if (ack == '$') { + /* Unsolicited packet: consume body, '#' and 2 checksum digits, ack + * it, keep waiting for ours. */ + std::string body; + char c = 0; + bool ok = true; + while (ok) { + if (!recv_all(fd, &c, 1)) return false; + if (c == '#') break; + body.push_back(c); + } + char cs[2]; + if (!recv_all(fd, cs, 2)) return false; + const char plus = '+'; + (void)send_all(fd, &plus, 1); + if (stray) *stray = body; + continue; + } + /* Anything else is noise (e.g. a stray interrupt byte); ignore it. */ + } + return false; +} + +/* Read a "$#" packet, validate the checksum, strip framing into + * @p out, and ack. Replies used here are printable ASCII, so RSP binary + * escaping never occurs and is not decoded. */ +static bool gdb_read_packet(socket_t fd, std::string& out) +{ + out.clear(); + + char c = 0; + do { + if (!recv_all(fd, &c, 1)) return false; + } while (c != '$'); + + uint8_t sum = 0; + while (true) { + if (!recv_all(fd, &c, 1)) return false; + if (c == '#') break; + out.push_back(c); + sum += static_cast(c); + } + + char cs[2]; + if (!recv_all(fd, cs, 2)) return false; + int hi = hex_nibble(cs[0]); + int lo = hex_nibble(cs[1]); + if (hi < 0 || lo < 0) return false; + + char ack = '+'; + (void)send_all(fd, &ack, 1); + + return static_cast((hi << 4) | lo) == sum; +} + +/* Send @p cmd on an open session and, when @p wait_reply, read the reply. + * Continue ('c') passes wait_reply = false: its reply only arrives on stop. */ +static bool gdb_txn(socket_t fd, const std::string& cmd, std::string& reply, bool wait_reply = true, + std::string* stray = nullptr) +{ + reply.clear(); + if (fd == INVALID_SOCK) return false; + bool ok = gdb_write_packet(fd, cmd, stray); + if (ok && wait_reply) { + ok = gdb_read_packet(fd, reply); + } + return ok; +} + +/* Map an MCD_BP_* type onto the GDB Z/z packet type digit. */ +static bool bp_type_to_gdb(uint32_t type, char& digit) +{ + switch (type) { + case MCD_BP_SW_BREAK: + digit = '0'; + return true; + case MCD_BP_HW_BREAK: + digit = '1'; + return true; + case MCD_BP_WATCH_WRITE: + digit = '2'; + return true; + case MCD_BP_WATCH_READ: + digit = '3'; + return true; + case MCD_BP_WATCH_ACCESS: + digit = '4'; + return true; + default: + return false; + } +} + +/* Extract the hex value following @p key ("watch:", "rwatch:", "awatch:") in a + * 'T' stop-reply packet. Returns true and sets @p out when present. */ +static bool gdb_stop_field(const std::string& pkt, const char* key, uint64_t& out) +{ + std::string::size_type pos = pkt.find(key); + if (pos == std::string::npos) return false; + pos += std::strlen(key); + std::string hexv; + while (pos < pkt.size() && pkt[pos] != ';') hexv.push_back(pkt[pos++]); + if (hexv.empty()) return false; + out = std::strtoull(hexv.c_str(), nullptr, 16); + return true; +} + +/* Classify a GDB stop-reply packet ('T aa ...' or 'S aa'). Returns an + * MCD_STOP_* reason and, for watchpoints, the triggering address. */ +static uint32_t gdb_classify_stop(const std::string& pkt, uint64_t& watch_addr) +{ + watch_addr = 0; + if (pkt.empty()) return MCD_STOP_HALTED; + + /* Signal number is the two hex digits after the leading 'T'/'S'. */ + int sig = 0; + if (pkt.size() >= 3) { + int hi = hex_nibble(pkt[1]); + int lo = hex_nibble(pkt[2]); + if (hi >= 0 && lo >= 0) sig = (hi << 4) | lo; + } + + if (pkt[0] == 'T') { + /* "watch:" is a substring of "awatch:"/"rwatch:", so test it last. */ + if (gdb_stop_field(pkt, "awatch:", watch_addr)) return MCD_STOP_WATCH_ACCESS; + if (gdb_stop_field(pkt, "rwatch:", watch_addr)) return MCD_STOP_WATCH_READ; + if (gdb_stop_field(pkt, "watch:", watch_addr)) return MCD_STOP_WATCH_WRITE; + } + + return (sig == 5) ? MCD_STOP_BREAK : MCD_STOP_SIGNAL; /* 5 = SIGTRAP */ +} + +mcd_server::mcd_server(const sc_core::sc_module_name& name) + : sc_core::sc_module(name) + , p_mcd_port("mcd_port", 1235, "MCD server TCP port") + , m_listen_fd(INVALID_SOCK) + , m_running(false) + , m_client_fd(INVALID_SOCK) + , m_thread_done(false) + , m_debug_hold(false) /* start detached: no debugger, no behaviour change */ + , m_debug_hold_active(false) +{ +#ifdef _WIN32 + WSADATA wsa_data; + if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) { + SCP_FATAL(()) << "mcd_server: WSAStartup failed"; + } +#endif + SCP_DEBUG(()) << "mcd_server constructor"; +} + +mcd_server::~mcd_server() +{ + m_running = false; + if (m_listen_fd != INVALID_SOCK) { + ::shutdown(m_listen_fd, SHUT_RDWR); + CLOSE_SOCKET(m_listen_fd); + m_listen_fd = INVALID_SOCK; + } + + /* A worker servicing a client is blocked in recv() on the accepted socket, so + * join() alone would hang: shut that socket down too. The retry loop covers + * the window between accept() returning and the fd being published. */ + if (m_thread.joinable()) { + for (int attempt = 0; attempt < 100; ++attempt) { + socket_t client = m_client_fd; + if (client != INVALID_SOCK) ::shutdown(client, SHUT_RDWR); + if (m_thread_done) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + m_thread.join(); + } + gdb_close_all(); +#ifdef _WIN32 + WSACleanup(); +#endif +} + +void mcd_server::bind_instance(sc_core::sc_object* inst, const std::string& gdb_hostport) +{ + m_insts.push_back(inst); + m_gdb_ports.push_back(gdb_hostport); + m_gdb_fds.push_back(INVALID_SOCK); + m_gdb_running.push_back(false); + m_threads_known.push_back(false); + m_last_stop.push_back(last_stop_t{}); + SCP_INFO(()) << "bind_instance[" << (m_insts.size() - 1) << "]: " << (inst ? inst->name() : "") + << " gdb-rsp=" << gdb_hostport; +} + +/* Lazily open and cache a persistent RSP session. The qSupported exchange leaves + * the stub connected, as op_stop's 0x03 interrupt requires. "multiprocess+" is + * not advertised, so thread ids stay plain hex, not p.. Needs + * m_gdb_mutex. */ +socket_t mcd_server::gdb_session(uint32_t idx) +{ + if (idx >= m_gdb_ports.size()) return INVALID_SOCK; + if (m_gdb_fds[idx] != INVALID_SOCK) return m_gdb_fds[idx]; + + /* Connecting pauses the whole instance, so this call is itself a halt: the + * hold must be taken first, since the attach is deferred to the SystemC + * thread and the kernel could starve in the interim. */ + debug_hold(true); + + socket_t fd = gdb_open(m_gdb_ports[idx]); + if (fd == INVALID_SOCK) { + SCP_WARN(()) << "mcd_server: could not open GDB-RSP session to " << m_gdb_ports[idx] << ": " << sock_err(); + return INVALID_SOCK; + } + + std::string reply; + if (!gdb_txn(fd, "qSupported", reply, /*wait_reply=*/true)) { + SCP_WARN(()) << "mcd_server: GDB-RSP handshake failed on " << m_gdb_ports[idx]; + CLOSE_SOCKET(fd); + return INVALID_SOCK; + } + + m_gdb_fds[idx] = fd; + m_gdb_running[idx] = false; + SCP_INFO(()) << "mcd_server: opened GDB-RSP session to " << m_gdb_ports[idx]; + + gdb_enumerate_cores(idx); + return fd; +} + +/* Discover the cores of instance @p idx: qfThreadInfo/qsThreadInfo list the gdb + * thread ids (QEMU uses cpu_index + 1), qThreadExtraInfo names each one + * hex-encoded ("CPU#0 [running]"). Caller must hold m_gdb_mutex. */ +void mcd_server::gdb_enumerate_cores(uint32_t idx) +{ + if (idx >= m_threads_known.size() || m_threads_known[idx]) return; + + socket_t fd = m_gdb_fds[idx]; + if (fd == INVALID_SOCK) return; + + std::vector tids; + std::string reply; + std::string cmd = "qfThreadInfo"; + /* The list arrives as one or more "m[,...]" replies terminated by + * "l". The loop is bounded: a stub that never says "l" must not wedge us. */ + for (int round = 0; round < 256; ++round) { + if (!gdb_txn(fd, cmd, reply, /*wait_reply=*/true)) break; + if (reply.empty() || reply[0] == 'l') break; + if (reply[0] != 'm') break; + + /* Comma-separated hex ids follow the leading 'm'. */ + std::string::size_type pos = 1; + while (pos < reply.size()) { + std::string::size_type comma = reply.find(',', pos); + std::string tok = reply.substr(pos, comma == std::string::npos ? std::string::npos : comma - pos); + if (!tok.empty()) { + tids.push_back(static_cast(std::strtoul(tok.c_str(), nullptr, 16))); + } + if (comma == std::string::npos) break; + pos = comma + 1; + } + cmd = "qsThreadInfo"; + } + + if (tids.empty()) { + /* No thread list: fall back to the single implicit thread, which is what + * a bare 'p'/'c' addresses. */ + SCP_WARN(()) << "mcd_server: " << m_gdb_ports[idx] << " returned no thread list; assuming a single core"; + tids.push_back(1); + } + + for (uint32_t tid : tids) { + std::string name; + char q[32]; + std::snprintf(q, sizeof(q), "qThreadExtraInfo,%x", tid); + if (gdb_txn(fd, q, reply, /*wait_reply=*/true) && !reply.empty() && reply.size() % 2 == 0) { + for (std::string::size_type i = 0; i + 1 < reply.size(); i += 2) { + int hi = hex_nibble(reply[i]); + int lo = hex_nibble(reply[i + 1]); + if (hi < 0 || lo < 0) { + name.clear(); + break; + } + name.push_back(static_cast((hi << 4) | lo)); + } + } + if (name.empty()) name = "core"; + + m_cores.push_back(core_t{ idx, tid, name }); + SCP_INFO(()) << "mcd_server: core " << (m_cores.size() - 1) << " = instance " << idx << " tid " << tid << " '" + << name << "'"; + } + + m_threads_known[idx] = true; +} + +/* Open a session to every instance lacking one. Doing so pauses that instance, + * and an object-model query must not change run state, so resume unless the + * debugger asked for the halt. Caller must hold m_gdb_mutex. */ +void mcd_server::ensure_cores_known() +{ + for (uint32_t idx = 0; idx < m_gdb_ports.size(); ++idx) { + if (m_threads_known[idx]) continue; + + bool was_open = (m_gdb_fds[idx] != INVALID_SOCK); + if (gdb_session(idx) == INVALID_SOCK) continue; + if (was_open) continue; /* already the debugger's session; leave its state alone */ + + std::string reply; + if (gdb_cmd(idx, "vCont;c", reply, /*wait_reply=*/false)) { + m_gdb_running[idx] = true; + SCP_DEBUG(()) << "mcd_server: resumed instance " << idx << " after core enumeration"; + } + } + + /* Deliberately NOT update_debug_hold(): 'vCont;c' is acked long before QEMU + * restarts the vCPUs, so releasing the hold here can starve the kernel. The + * hold stays until a request with an observed outcome re-evaluates it. */ +} + +/* Point register access at @p core with "Hg". QEMU tracks this as + * gdbserver_state.g_cpu, so it must be re-sent whenever the target core changes. + * Caller must hold m_gdb_mutex. */ +bool mcd_server::gdb_select_core(uint32_t core) +{ + if (core >= m_cores.size()) return false; + + char cmd[32]; + std::snprintf(cmd, sizeof(cmd), "Hg%x", m_cores[core].tid); + std::string reply; + if (!gdb_cmd(m_cores[core].inst, cmd, reply)) return false; + if (reply != "OK") { + SCP_WARN(()) << "mcd_server: '" << cmd << "' rejected: '" << reply << "'"; + return false; + } + return true; +} + +void mcd_server::gdb_close_all() +{ + std::lock_guard lock(m_gdb_mutex); + + /* Disarm everything: a leftover breakpoint would halt the target with no + * debugger listening, and the hold is released below, so that halt would + * starve the kernel. */ + for (const breakpoint_t& bp : m_breakpoints) { + char digit; + if (bp.core >= m_cores.size() || !bp_type_to_gdb(bp.type, digit)) continue; + char cmd[48]; + std::snprintf(cmd, sizeof(cmd), "z%c,%llx,%x", digit, static_cast(bp.addr), bp.kind); + std::string reply; + if (!gdb_cmd(m_cores[bp.core].inst, cmd, reply)) { + SCP_WARN(()) << "mcd_server: could not remove '" << cmd << "' on detach"; + } + } + if (!m_breakpoints.empty()) { + SCP_DEBUG(()) << "mcd_server: removed " << m_breakpoints.size() << " breakpoint(s) on detach"; + m_breakpoints.clear(); + } + + /* Resume anything left halted before dropping the sessions: closing the socket + * does not restart the instance, and the hold is released below. Detaching + * hands the platform back as if no debugger had attached. */ + bool resumed_any = false; + for (uint32_t idx = 0; idx < m_gdb_fds.size(); ++idx) { + if (m_gdb_fds[idx] == INVALID_SOCK) continue; + if (m_gdb_running[idx]) continue; + + std::string reply; + if (gdb_cmd(idx, "vCont;c", reply, /*wait_reply=*/false)) { + m_gdb_running[idx] = true; + resumed_any = true; + SCP_DEBUG(()) << "mcd_server: resumed instance " << idx << " on debugger detach"; + } else { + SCP_WARN(()) << "mcd_server: could not resume instance " << idx << " on detach; it stays halted"; + } + } + + /* Wait for that resume to take effect before the hold is dropped below: the + * ack precedes QEMU restarting the vCPUs, and releasing in that window + * starves the kernel - the very exit detaching must not cause. */ + if (resumed_any && !resume_observed()) { + SCP_WARN(()) << "mcd_server: target not observed running after the detach resume"; + } + + for (socket_t& fd : m_gdb_fds) { + if (fd != INVALID_SOCK) { + CLOSE_SOCKET(fd); + fd = INVALID_SOCK; + } + } + /* Forget the discovered cores so a later client re-enumerates. Only a full + * close resets this; a session dropped mid-use keeps its entry. */ + m_cores.clear(); + std::fill(m_threads_known.begin(), m_threads_known.end(), false); + std::fill(m_last_stop.begin(), m_last_stop.end(), last_stop_t{}); + + /* No sessions left: release the hold so the simulation can end normally. */ + debug_hold(false); +} + +/* Run one RSP command. Any transport failure closes and invalidates the session + * so the next call reconnects clean; otherwise one wedged command breaks every + * later query on that connection. Caller must hold m_gdb_mutex. */ +bool mcd_server::gdb_cmd(uint32_t idx, const std::string& cmd, std::string& reply, bool wait_reply) +{ + socket_t fd = gdb_session(idx); + if (fd == INVALID_SOCK) return false; + + /* A stop-reply may arrive mid-command; gdb_write_packet skips it to keep the + * stream framed, so record here that the instance is no longer running. */ + std::string stray; + bool ok = gdb_txn(fd, cmd, reply, wait_reply, &stray); + if (!stray.empty()) { + m_gdb_running[idx] = false; + SCP_INFO(()) << "mcd_server: instance " << idx << " reported a stop while '" << cmd << "' was in flight: '" + << stray << "'"; + } + if (ok) return true; + + CLOSE_SOCKET(fd); + m_gdb_fds[idx] = INVALID_SOCK; + m_gdb_running[idx] = false; + return false; +} + +void mcd_server::note_stop(uint32_t idx, uint32_t reason, uint64_t watch_addr, uint32_t tid) +{ + if (idx >= m_last_stop.size()) return; + m_last_stop[idx] = last_stop_t{ true, reason, watch_addr, tid }; +} + +/* Non-blocking check for an unsolicited stop-reply: a breakpoint hit becomes a + * global vm_stop() with no request from us, so without this m_gdb_running is only + * what we last told the instance. Caller must hold m_gdb_mutex. */ +void mcd_server::drain_pending_stop(uint32_t idx) +{ + if (idx >= m_gdb_fds.size()) return; + if (!m_gdb_running[idx]) return; /* we already believe it is stopped */ + + socket_t fd = m_gdb_fds[idx]; + if (fd == INVALID_SOCK) return; + + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(fd, &rfds); + struct timeval tv = { 0, 0 }; /* poll: do not wait */ + + if (::select(SOCK_NFDS(fd), &rfds, nullptr, nullptr, &tv) <= 0) return; + + std::string reply; + if (!gdb_read_packet(fd, reply)) { + CLOSE_SOCKET(fd); + m_gdb_fds[idx] = INVALID_SOCK; + m_gdb_running[idx] = false; + return; + } + + m_gdb_running[idx] = false; + + /* Cache it: the core it belongs to must still be able to learn why it + * stopped, even though the packet is consumed here. */ + uint64_t watch_addr = 0, tid = 0; + uint32_t reason = gdb_classify_stop(reply, watch_addr); + gdb_stop_field(reply, "thread:", tid); + note_stop(idx, reason, watch_addr, static_cast(tid)); + + SCP_INFO(()) << "mcd_server: instance " << idx << " had already stopped on its own: '" << reply << "'"; +} + +/* Wait up to @p timeout_ms for the stop-reply owed after a 'c' sent with + * wait_reply=false. Returns an MCD_STOP_* reason. Caller holds m_gdb_mutex. */ +uint32_t mcd_server::gdb_wait_stop(uint32_t idx, uint32_t timeout_ms, uint64_t& watch_addr, uint32_t& stopped_tid) +{ + watch_addr = 0; + stopped_tid = 0; + if (idx >= m_gdb_fds.size()) return MCD_STOP_HALTED; + + /* Not running -> already halted; nothing will arrive on the socket. */ + if (!m_gdb_running[idx]) return MCD_STOP_HALTED; + + socket_t fd = m_gdb_fds[idx]; + if (fd == INVALID_SOCK) return MCD_STOP_HALTED; + + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(fd, &rfds); + struct timeval tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + + int sel = ::select(SOCK_NFDS(fd), &rfds, nullptr, nullptr, &tv); + if (sel <= 0) { + /* 0 = timeout (still running); <0 = error (leave the session as is). */ + return MCD_STOP_RUNNING; + } + + std::string reply; + if (!gdb_read_packet(fd, reply)) { + CLOSE_SOCKET(fd); + m_gdb_fds[idx] = INVALID_SOCK; + m_gdb_running[idx] = false; + return MCD_STOP_HALTED; + } + + m_gdb_running[idx] = false; + uint32_t reason = gdb_classify_stop(reply, watch_addr); + /* 'T' replies carry "thread:;": the instance halts as a whole, but this + * names the core that hit the breakpoint. */ + uint64_t tid = 0; + if (gdb_stop_field(reply, "thread:", tid)) stopped_tid = static_cast(tid); + note_stop(idx, reason, watch_addr, stopped_tid); + SCP_INFO(()) << "mcd_server: WAIT_STOP inst=" << idx << " reply='" << reply << "' reason=" << reason + << " tid=" << stopped_tid; + return reason; +} + +void mcd_server::add_mem_space(uint32_t space_id, const std::string& name, + std::function fn) +{ + mcd_mem_space_st space; + space.mem_space_id = space_id; + std::memset(space.mem_space_name, 0, sizeof(space.mem_space_name)); + std::strncpy(space.mem_space_name, name.c_str(), sizeof(space.mem_space_name) - 1); + m_mem_spaces.push_back(space); + m_transactors[space_id] = std::move(fn); +} + +void mcd_server::bind_target(tlm::tlm_initiator_socket<>* socket, uint32_t space_id, const std::string& name) +{ + add_mem_space(space_id, name, + [socket](tlm::tlm_generic_payload& txn) -> unsigned int { return (*socket)->transport_dbg(txn); }); + SCP_INFO(()) << "bind_target: initiator socket registered for space id=" << space_id << " name='" << name << "'"; +} + +/* Auto-discover routers and QEMU instances, and wire up the memory spaces. */ +void mcd_server::before_end_of_elaboration() +{ + auto routers = gs::find_sc_objects>(); + for (auto* ri : routers) { + auto* sc_obj = dynamic_cast(ri); + if (!sc_obj) continue; + + /* The router's target socket is the child that casts to + * tlm_base_target_socket_b; its initiator socket is a different base + * type and will not match. */ + tlm::tlm_base_target_socket_b<>* ts = nullptr; + for (auto* child : sc_obj->get_child_objects()) { + ts = dynamic_cast*>(child); + if (ts) break; + } + if (!ts) continue; + + std::string space_name = sc_obj->name(); + uint32_t space_id = static_cast(m_mem_spaces.size()); + + /* Issue transport_dbg directly on the target socket's export: no extra + * initiator socket, and no elaboration-time bind. */ + add_mem_space(space_id, space_name, [ts](tlm::tlm_generic_payload& txn) -> unsigned int { + return ts->get_base_export()->transport_dbg(txn); + }); + + SCP_INFO(()) << "mcd_server: auto-discovered router '" << space_name << "' as mem_space id=" << space_id; + } + + /* One gdbstub port per QEMU instance, not per CPU. mcd_server links only + * `router`, so an instance is recognised by owning both a "tcg_mode" and a + * "gdb_port" CCI param; a CPU has only a deprecated "gdb_port". */ + { + /* Inside the hierarchy an originator must not be explicitly named, so use + * the current-object broker. */ + cci::cci_broker_handle broker = sc_core::sc_get_current_object() + ? cci::cci_get_broker() + : cci::cci_get_global_broker(cci::cci_originator("mcd_server")); + cci::cci_param_predicate all_params([](const cci::cci_param_untyped_handle&) { return true; }); + auto handles = broker.get_param_handles(all_params); + + /* Modules owning a "tcg_mode" param — i.e. QEMU instances. */ + std::vector inst_modules; + for (auto& ph : handles) { + const std::string pname = ph.name(); + auto dot = pname.rfind('.'); + if (dot == std::string::npos) continue; + if (pname.substr(dot + 1) != "tcg_mode") continue; + inst_modules.push_back(pname.substr(0, dot)); + } + + for (const std::string& modname : inst_modules) { + const std::string pname = modname + ".gdb_port"; + cci::cci_param_typed_handle typed(broker.get_param_handle(pname)); + if (!typed.is_valid()) { + SCP_WARN(()) << "mcd_server: QEMU instance '" << modname + << "' has no gdb_port parameter; run-control unavailable for its cores"; + continue; + } + unsigned port = typed.get_value(); + + /* QEMU serves one gdb session per instance, so a non-zero gdb_port + * is left for the user's gdb: taking it would leave their session + * connected but unanswered. An explicit 0 is not a request for gdb. */ + if (port != 0) { + auto prev = sc_core::sc_report_handler::set_actions(sc_core::SC_ERROR, + sc_core::SC_LOG | sc_core::SC_DISPLAY); + SC_REPORT_ERROR("mcd_server", (pname + " is set explicitly (" + std::to_string(port) + + "), so it is left for an external gdb: Leave it unset, or set it to 0 " + "for MCD to debug this qemu instance.") + .c_str()); + sc_core::sc_report_handler::set_actions(sc_core::SC_ERROR, prev); + continue; + } + + /* Take a private port: bind to 0 on loopback, read it back, close. */ + socket_t tmp = ::socket(AF_INET, SOCK_STREAM, 0); + if (tmp != INVALID_SOCK) { + struct sockaddr_in sa = {}; + sa.sin_family = AF_INET; + sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + sa.sin_port = 0; + if (::bind(tmp, reinterpret_cast(&sa), sizeof(sa)) == 0) { + socklen_t len = sizeof(sa); + if (::getsockname(tmp, reinterpret_cast(&sa), &len) == 0) { + port = ntohs(sa.sin_port); + typed.set_value(port); + SCP_INFO(()) << "mcd_server: auto-assigned gdb_port=" << port << " for '" << pname << "'"; + } + } + CLOSE_SOCKET(tmp); + } + if (port == 0) { + SCP_WARN(()) << "mcd_server: could not find a free port for '" << pname + << "'; run-control unavailable for this instance's cores"; + continue; + } + + sc_core::sc_object* sc_obj = sc_core::sc_find_object(modname.c_str()); + if (!sc_obj) { + SCP_WARN(()) << "mcd_server: found gdb_port for '" << modname << "' but no sc_object with that name"; + continue; + } + bind_instance(sc_obj, "127.0.0.1:" + std::to_string(port)); + } + } +} + +void mcd_server::end_of_elaboration() +{ + if (p_mcd_port.get_value()) { + SCP_INFO(()) << "Starting MCD server on TCP port " << p_mcd_port.get_value(); + start_server(); + } +} + +void mcd_server::start_server() +{ + m_listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); + if (m_listen_fd == INVALID_SOCK) { + SCP_ERR(()) << "mcd_server: socket() failed: " << sock_err(); + return; + } + + int one = 1; + ::setsockopt(m_listen_fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&one), sizeof(one)); + + set_nosigpipe(m_listen_fd); + + /* Loopback only, never INADDR_ANY: this port grants unauthenticated read/write + * access to all simulated memory and registers. Remote debuggers tunnel in. */ + struct sockaddr_in addr; + std::memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(static_cast(p_mcd_port.get_value())); + + if (::bind(m_listen_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + SCP_ERR(()) << "mcd_server: bind() failed on port " << p_mcd_port.get_value() << ": " << sock_err(); + CLOSE_SOCKET(m_listen_fd); + m_listen_fd = INVALID_SOCK; + return; + } + + if (::listen(m_listen_fd, 1) < 0) { + SCP_ERR(()) << "mcd_server: listen() failed: " << sock_err(); + CLOSE_SOCKET(m_listen_fd); + m_listen_fd = INVALID_SOCK; + return; + } + + /* No suspending channel here: the debug server does not change when the + * simulation ends. Platforms needing to stay open across debugger pauses + * instantiate the `keep_alive` component. */ + + m_running = true; + m_thread = std::thread(&mcd_server::server_thread, this); +} + +/* Accept loop: a POSIX thread, NOT a SystemC thread. */ +void mcd_server::server_thread() +{ + while (m_running) { + /* Snapshot the listening fd: the destructor stores INVALID_SOCK to break us + * out, and FD_SET on an invalid fd is undefined behaviour. */ + socket_t listen_fd = m_listen_fd; + if (listen_fd == INVALID_SOCK) break; + + /* select() with a timeout, so m_running is re-checked periodically. */ + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(listen_fd, &rfds); + struct timeval tv; + tv.tv_sec = 1; + tv.tv_usec = 0; + + int sel = ::select(SOCK_NFDS(listen_fd), &rfds, nullptr, nullptr, &tv); + if (sel < 0) { + if (SOCK_ERR_IS_INTR()) continue; + break; + } + if (sel == 0) continue; /* timeout: re-check m_running */ + + socket_t client = ::accept(listen_fd, nullptr, nullptr); + if (client == INVALID_SOCK) { + if (SOCK_ERR_IS_INTR()) continue; + if (!m_running) break; + continue; + } + set_nosigpipe(client); + if (!m_running) { + CLOSE_SOCKET(client); + break; + } + + SCP_INFO(()) << "mcd_server: client connected"; + /* Publish the fd so the destructor can shut it down: handle_client blocks + * in recv() on it, which closing the listening socket does not interrupt. */ + m_client_fd = client; + handle_client(client); + m_client_fd = INVALID_SOCK; + CLOSE_SOCKET(client); + /* Debugger gone: drop the gdb sessions, releasing any simulation hold. */ + gdb_close_all(); + SCP_INFO(()) << "mcd_server: client disconnected"; + } + m_thread_done = true; +} + +void mcd_server::handle_client(socket_t fd) +{ + while (m_running) { + /* Frame in: 4-byte LE length, then bytes = 1 opcode + payload. */ + uint8_t lenbuf[4]; + if (!recv_all(fd, lenbuf, sizeof(lenbuf))) return; + uint32_t len = get_u32(lenbuf); + if (len == 0) { + SCP_WARN(()) << "mcd_server: zero-length frame, dropping client"; + return; + } + /* Refuse before allocating: the length is attacker-controlled, and a + * bad_alloc thrown out of this worker thread terminates the process. */ + if (len > MCD_MAX_FRAME) { + SCP_WARN(()) << "mcd_server: frame length " << len << " exceeds the " << MCD_MAX_FRAME + << "-byte limit, dropping client"; + return; + } + + std::vector frame(len); + if (!recv_all(fd, frame.data(), len)) return; + + uint8_t opcode = frame[0]; + std::vector req(frame.begin() + 1, frame.end()); + std::vector resp; + + mcd_return_et rc = dispatch(opcode, req, resp); + + /* Frame out: 4-byte LE length (return code byte + payload), return + * code, payload. */ + std::vector out; + uint32_t outlen = static_cast(1 + resp.size()); + put_u32(out, outlen); + out.push_back(static_cast(rc)); + out.insert(out.end(), resp.begin(), resp.end()); + + if (!send_all(fd, out.data(), out.size())) return; + } +} + +mcd_return_et mcd_server::dispatch(uint8_t opcode, const std::vector& req, std::vector& resp) +{ + switch (opcode) { + case MCD_OP_QRY_SERVERS: + return op_qry_servers(req, resp); + case MCD_OP_QRY_SYSTEMS: + return op_qry_systems(req, resp); + case MCD_OP_QRY_DEVICES: + return op_qry_devices(req, resp); + case MCD_OP_QRY_CORES: + return op_qry_cores(req, resp); + case MCD_OP_QRY_MEM_SPACES: + return op_qry_mem_spaces(req, resp); + case MCD_OP_READ_MEM: + return op_read_mem(req, resp); + case MCD_OP_WRITE_MEM: + return op_write_mem(req, resp); + case MCD_OP_RUN: + return op_run(req, resp); + case MCD_OP_STOP: + return op_stop(req, resp); + case MCD_OP_STEP: + return op_step(req, resp); + case MCD_OP_READ_REG: + return op_read_reg(req, resp); + case MCD_OP_WRITE_REG: + return op_write_reg(req, resp); + case MCD_OP_SET_BP: + return op_set_bp(req, resp); + case MCD_OP_CLR_BP: + return op_clr_bp(req, resp); + case MCD_OP_LIST_BP: + return op_list_bp(req, resp); + case MCD_OP_WAIT_STOP: + return op_wait_stop(req, resp); + default: + SCP_WARN(()) << "mcd_server: unknown opcode 0x" << std::hex << static_cast(opcode); + return MCD_RET_ERR_GENERAL; + } +} + +mcd_return_et mcd_server::mcd_qry_servers(uint32_t* num_servers, mcd_server_st* servers) +{ + if (!num_servers) return MCD_RET_ERR_GENERAL; + /* A single QBox process presents exactly one MCD server. */ + if (servers) { + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + servers[0].num_cores = static_cast(m_cores.size()); + } + *num_servers = 1; + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::mcd_qry_systems(uint32_t* num_systems, mcd_system_st* systems) +{ + if (!num_systems) return MCD_RET_ERR_GENERAL; + if (systems) { + std::memset(systems[0].system_name, 0, sizeof(systems[0].system_name)); + std::strncpy(systems[0].system_name, "qbox", sizeof(systems[0].system_name) - 1); + } + *num_systems = 1; + return MCD_RET_ACT_NONE; +} + +/* One device per QEMU instance. @p cap is the capacity of @p devices. */ +mcd_return_et mcd_server::mcd_qry_devices(uint32_t* num_devices, mcd_device_st* devices, uint32_t cap) +{ + if (!num_devices) return MCD_RET_ERR_GENERAL; + + if (m_insts.empty()) { + /* Present one device anyway, so a client has something to hang the + * memory spaces off. */ + if (devices && cap) { + std::memset(devices[0].device_name, 0, sizeof(devices[0].device_name)); + std::strncpy(devices[0].device_name, "qbox_device", sizeof(devices[0].device_name) - 1); + *num_devices = 1; + } else { + *num_devices = 0; + } + return MCD_RET_ACT_NONE; + } + + uint32_t n = 0; + for (uint32_t i = 0; devices && i < m_insts.size() && i < cap; ++i) { + std::memset(devices[i].device_name, 0, sizeof(devices[i].device_name)); + const char* dev = (m_insts[i] && m_insts[i]->name()) ? m_insts[i]->name() : "qbox_device"; + std::strncpy(devices[i].device_name, dev, sizeof(devices[i].device_name) - 1); + n = i + 1; + } + *num_devices = devices ? n : static_cast(m_insts.size()); + return MCD_RET_ACT_NONE; +} + +/* Every core, in MCD core-id order; device_id is the owning instance's index. */ +mcd_return_et mcd_server::mcd_qry_cores(uint32_t* num_cores, mcd_core_st* cores, uint32_t cap) +{ + if (!num_cores) return MCD_RET_ERR_GENERAL; + + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + uint32_t n = 0; + for (uint32_t i = 0; cores && i < m_cores.size() && i < cap; ++i) { + cores[i].core_id = i; + cores[i].device_id = m_cores[i].inst; + n = i + 1; + } + *num_cores = cores ? n : static_cast(m_cores.size()); + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_qry_servers(const std::vector&, std::vector& resp) +{ + uint32_t n = 0; + mcd_server_st srv[1]; + mcd_return_et rc = mcd_qry_servers(&n, srv); + if (rc != MCD_RET_ACT_NONE) return rc; + + put_u32(resp, n); + for (uint32_t i = 0; i < n && i < 1; ++i) { + put_u32(resp, srv[i].num_cores); + } + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_qry_systems(const std::vector&, std::vector& resp) +{ + uint32_t n = 0; + mcd_system_st sys[1]; + mcd_return_et rc = mcd_qry_systems(&n, sys); + if (rc != MCD_RET_ACT_NONE) return rc; + + put_u32(resp, n); + for (uint32_t i = 0; i < n && i < 1; ++i) { + resp.insert(resp.end(), sys[i].system_name, sys[i].system_name + sizeof(sys[i].system_name)); + } + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_qry_devices(const std::vector&, std::vector& resp) +{ + uint32_t n = 0; + std::vector dev(m_insts.empty() ? 1 : m_insts.size()); + mcd_return_et rc = mcd_qry_devices(&n, dev.data(), static_cast(dev.size())); + if (rc != MCD_RET_ACT_NONE) return rc; + + put_u32(resp, n); + for (uint32_t i = 0; i < n; ++i) { + resp.insert(resp.end(), dev[i].device_name, dev[i].device_name + sizeof(dev[i].device_name)); + } + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_qry_cores(const std::vector&, std::vector& resp) +{ + /* Count first (null array), then size the buffer: the core table is only + * populated once the gdbstub has been asked. */ + uint32_t count = 0; + mcd_return_et rc = mcd_qry_cores(&count, nullptr, 0); + if (rc != MCD_RET_ACT_NONE) return rc; + + uint32_t n = 0; + std::vector cores(count); + if (count) { + rc = mcd_qry_cores(&n, cores.data(), count); + if (rc != MCD_RET_ACT_NONE) return rc; + } + + put_u32(resp, n); + for (uint32_t i = 0; i < n; ++i) { + put_u32(resp, cores[i].core_id); + put_u32(resp, cores[i].device_id); + } + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_qry_mem_spaces(const std::vector&, std::vector& resp) +{ + /* Response payload: 4-byte LE count, then for each space a 4-byte LE + * mem_space_id followed by the fixed 64-byte mem_space_name. */ + put_u32(resp, static_cast(m_mem_spaces.size())); + for (const mcd_mem_space_st& space : m_mem_spaces) { + put_u32(resp, space.mem_space_id); + resp.insert(resp.end(), space.mem_space_name, space.mem_space_name + sizeof(space.mem_space_name)); + } + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_read_mem(const std::vector& req, std::vector& resp) +{ + /* Request: addr u64, length u32, optional space_id u32. A 12-byte payload + * (no space_id) means space 0, "physical". */ + if (req.size() < 12) return MCD_RET_ERR_GENERAL; + + uint64_t addr = get_u64(&req[0]); + uint32_t length = get_u32(&req[8]); + uint32_t space_id = (req.size() >= 16) ? get_u32(&req[12]) : 0u; + + if (length > MCD_MAX_FRAME) { + SCP_WARN(()) << "mcd_server: read_mem length " << length << " exceeds the " << MCD_MAX_FRAME << "-byte limit"; + return MCD_RET_ERR_GENERAL; + } + + auto it = m_transactors.find(space_id); + if (it == m_transactors.end() || !it->second) { + SCP_WARN(()) << "mcd_server: read_mem with no bound target socket for space id=" << space_id; + return MCD_RET_ERR_GENERAL; + } + + if (length == 0) return MCD_RET_ACT_NONE; + + std::vector data(length, 0); + + tlm::tlm_generic_payload txn; + txn.set_command(tlm::TLM_READ_COMMAND); + txn.set_address(addr); + txn.set_data_ptr(data.data()); + txn.set_data_length(length); + txn.set_streaming_width(length); + txn.set_byte_enable_length(0); + txn.set_dmi_allowed(false); + txn.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE); + + /* transport_dbg must run on the SystemC kernel thread: TLM is not thread safe. + * run_on_sysc() blocks until the job has run, keeping `txn`/`data` valid, and + * returns false if the simulation already ended (transaction never ran). */ + unsigned int done = 0; + if (!m_sc.run_on_sysc([&] { done = it->second(txn); })) { + SCP_WARN(()) << "mcd_server: read_mem abandoned, simulation has ended"; + return MCD_RET_ERR_GENERAL; + } + + if (done != length) { + SCP_WARN(()) << "mcd_server: read_mem short read " << done << "/" << length << " @0x" << std::hex << addr; + return MCD_RET_ERR_GENERAL; + } + + resp.insert(resp.end(), data.begin(), data.end()); + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_write_mem(const std::vector& req, std::vector& resp) +{ + /* Request: addr u64, length u32, optional space_id u32, then data + * bytes. Header size (16 or 12 bytes, the latter meaning space 0) is inferred + * from the total payload. */ + (void)resp; + if (req.size() < 12) return MCD_RET_ERR_GENERAL; + + uint64_t addr = get_u64(&req[0]); + uint32_t length = get_u32(&req[8]); + + if (length > MCD_MAX_FRAME) { + SCP_WARN(()) << "mcd_server: write_mem length " << length << " exceeds the " << MCD_MAX_FRAME << "-byte limit"; + return MCD_RET_ERR_GENERAL; + } + + /* 64-bit arithmetic is required: `16u + length` wraps in 32 bits and a wrapped + * sum compares as satisfied, selecting a data offset past the buffer end. */ + const uint64_t len64 = length; + uint32_t space_id; + std::vector::size_type data_off; + if (static_cast(req.size()) >= 16u + len64) { + space_id = get_u32(&req[12]); + data_off = 16; + } else if (static_cast(req.size()) >= 12u + len64) { + space_id = 0; + data_off = 12; + } else { + return MCD_RET_ERR_GENERAL; + } + + auto it = m_transactors.find(space_id); + if (it == m_transactors.end() || !it->second) { + SCP_WARN(()) << "mcd_server: write_mem with no bound target socket for space id=" << space_id; + return MCD_RET_ERR_GENERAL; + } + + if (length == 0) return MCD_RET_ACT_NONE; + + std::vector data(req.begin() + data_off, req.begin() + data_off + length); + + tlm::tlm_generic_payload txn; + txn.set_command(tlm::TLM_WRITE_COMMAND); + txn.set_address(addr); + txn.set_data_ptr(data.data()); + txn.set_data_length(length); + txn.set_streaming_width(length); + txn.set_byte_enable_length(0); + txn.set_dmi_allowed(false); + txn.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE); + + /* See op_read_mem: executed by the SystemC kernel thread. */ + unsigned int done = 0; + if (!m_sc.run_on_sysc([&] { done = it->second(txn); })) { + SCP_WARN(()) << "mcd_server: write_mem abandoned, simulation has ended"; + return MCD_RET_ERR_GENERAL; + } + + if (done != length) { + SCP_WARN(()) << "mcd_server: write_mem short write " << done << "/" << length << " @0x" << std::hex << addr; + return MCD_RET_ERR_GENERAL; + } + return MCD_RET_ACT_NONE; +} + +/* Hold the simulation open only while the debugger is the reason the CPUs are + * stopped; see m_debug_hold in the header. */ +void mcd_server::debug_hold(bool hold) +{ + if (hold == m_debug_hold_active) return; /* attach/detach are not counted */ + m_debug_hold_active = hold; + if (hold) { + SCP_DEBUG(()) << "mcd_server: holding simulation open (debugger owns a halted CPU)"; + m_debug_hold.async_attach_suspending(); + } else { + SCP_DEBUG(()) << "mcd_server: releasing simulation hold (all CPUs resumed)"; + m_debug_hold.async_detach_suspending(); + } +} + +void mcd_server::update_debug_hold() +{ + bool hold = false; + for (size_t idx = 0; idx < m_gdb_fds.size(); ++idx) { + if (m_gdb_fds[idx] == INVALID_SOCK) continue; /* debugger does not own this instance */ + + if (!m_gdb_running[idx]) { + /* Explicitly halted by us. */ + hold = true; + break; + } + /* Running, but an armed breakpoint can halt it at any moment and stop the + * quantum keeper, so hold until the client resumes with nothing armed. */ + if (!m_breakpoints.empty()) { + hold = true; + break; + } + } + debug_hold(hold); +} + +/* Wait, bounded, for simulated time to advance. Marshalled onto the SystemC + * thread, which also fails once the simulation has ended. */ +bool mcd_server::resume_observed() +{ + sc_core::sc_time t0; + if (!m_sc.run_on_sysc([&] { t0 = sc_core::sc_time_stamp(); })) return false; + + for (int attempt = 0; attempt < 200; ++attempt) { /* ~2 s */ + sc_core::sc_time now; + if (!m_sc.run_on_sysc([&] { now = sc_core::sc_time_stamp(); })) return false; + if (now > t0) return true; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + return false; +} + +mcd_return_et mcd_server::op_run(const std::vector&, std::vector&) +{ + if (m_gdb_ports.empty()) { + SCP_WARN(()) << "mcd_server: RUN with no GDB-RSP endpoint"; + return MCD_RET_ERR_GENERAL; + } + + std::lock_guard lock(m_gdb_mutex); + bool all_ok = true; + bool resumed_any = false; + /* Run is per instance: continue is a global vm_start(), so one 'c' resumes + * every core of the instance. */ + for (uint32_t idx = 0; idx < m_gdb_ports.size(); ++idx) { + /* m_gdb_running is only what we last told the instance, so drain any + * pending stop-reply before deciding it is already running. */ + drain_pending_stop(idx); + + if (m_gdb_running[idx]) continue; /* already running */ + + /* Read only the '+' ack: the stop-reply arrives only on the next stop. */ + std::string reply; + if (!gdb_cmd(idx, "c", reply, /*wait_reply=*/false)) { + SCP_WARN(()) << "mcd_server: RUN forward to " << m_gdb_ports[idx] << " failed"; + all_ok = false; + continue; + } + m_gdb_running[idx] = true; + resumed_any = true; + SCP_INFO(()) << "mcd_server: RUN -> gdb 'c' @" << m_gdb_ports[idx]; + } + + /* Deciding whether to drop the hold needs the resume to have taken effect: + * 'c' is acked long before QEMU restarts the vCPUs, and detaching inside + * that window leaves the kernel with no suspending channel, so the + * simulation exits by starvation and the *next* request fails on a dead + * socket. Simulated time advancing is the proof, since only a running vCPU + * advances it. Skipped when a breakpoint is armed, as the hold then stays + * regardless; an unconfirmed resume also just keeps it, and is never an + * error - the 'c' itself was accepted. */ + if (resumed_any && m_breakpoints.empty()) resume_observed(); + update_debug_hold(); + return all_ok ? MCD_RET_ACT_NONE : MCD_RET_ERR_GENERAL; +} + +mcd_return_et mcd_server::op_stop(const std::vector&, std::vector&) +{ + if (m_gdb_ports.empty()) { + SCP_WARN(()) << "mcd_server: STOP with no GDB-RSP endpoint"; + return MCD_RET_ERR_GENERAL; + } + + std::lock_guard lock(m_gdb_mutex); + + /* Take the hold before any interrupt: the attach is deferred to the SystemC + * thread, so it must precede the halt that stops the quantum keeper. */ + debug_hold(true); + + bool all_ok = true; + /* Per instance: a 0x03 interrupt makes QEMU vm_stop() every core of it. */ + for (uint32_t idx = 0; idx < m_gdb_ports.size(); ++idx) { + socket_t fd = gdb_session(idx); + if (fd == INVALID_SOCK) { + all_ok = false; + continue; + } + drain_pending_stop(idx); + + /* Connecting the session already halted it: nothing to interrupt. */ + if (!m_gdb_running[idx]) continue; + + /* The 0x03 break byte is sent raw, not as a framed packet; QEMU answers + * with a stop-reply, which is consumed here. */ + const uint8_t brk = 0x03; + bool ok = send_all(fd, &brk, 1); + std::string reply; + if (ok) ok = gdb_read_packet(fd, reply); + if (!ok) { + SCP_WARN(()) << "mcd_server: STOP forward to " << m_gdb_ports[idx] << " failed"; + all_ok = false; + continue; + } + m_gdb_running[idx] = false; + SCP_INFO(()) << "mcd_server: STOP -> gdb 0x03 @" << m_gdb_ports[idx] << " reply='" << reply << "'"; + } + return all_ok ? MCD_RET_ACT_NONE : MCD_RET_ERR_GENERAL; +} + +mcd_return_et mcd_server::op_step(const std::vector& req, std::vector&) +{ + if (m_gdb_ports.empty()) { + SCP_WARN(()) << "mcd_server: STEP with no GDB-RSP endpoint"; + return MCD_RET_ERR_GENERAL; + } + + /* Request: optional core id u32; empty payload means core 0. */ + uint32_t core = (req.size() >= 4) ? get_u32(&req[0]) : 0u; + + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + if (core >= m_cores.size()) { + SCP_WARN(()) << "mcd_server: STEP core id " << core << " out of range"; + return MCD_RET_ERR_GENERAL; + } + uint32_t inst = m_cores[core].inst; + + /* "vCont;s:" steps exactly one core, leaving the others halted: + * gdb_continue_partial resumes only the CPUs named. A bare 's' would step + * whichever core the stub has selected. */ + char cmd[32]; + std::snprintf(cmd, sizeof(cmd), "vCont;s:%x", m_cores[core].tid); + + std::string reply; + if (!gdb_cmd(inst, cmd, reply)) { + SCP_WARN(()) << "mcd_server: STEP forward to " << m_gdb_ports[inst] << " failed"; + return MCD_RET_ERR_GENERAL; + } + if (reply.empty()) { + /* An empty packet means the stub does not support vCont at all. */ + SCP_WARN(()) << "mcd_server: STEP '" << cmd << "' unsupported by " << m_gdb_ports[inst]; + return MCD_RET_ERR_GENERAL; + } + /* Stepping briefly resumes the instance (vm_prepare_start); it is halted + * again once the step completes. */ + m_gdb_running[inst] = false; + update_debug_hold(); + SCP_INFO(()) << "mcd_server: STEP core=" << core << " -> '" << cmd << "' @" << m_gdb_ports[inst] << " reply='" + << reply << "'"; + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_read_reg(const std::vector& req, std::vector& resp) +{ + /* Request: core id u32, register number u32. */ + if (req.size() < 8) return MCD_RET_ERR_GENERAL; + uint32_t core = get_u32(&req[0]); + uint32_t regno = get_u32(&req[4]); + + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + if (core >= m_cores.size()) { + SCP_WARN(()) << "mcd_server: READ_REG core id " << core << " out of range"; + return MCD_RET_ERR_GENERAL; + } + uint32_t inst = m_cores[core].inst; + + /* 'p' reads from the currently selected core (gdbserver_state.g_cpu), so + * without this every core of an instance aliases the same registers. */ + if (!gdb_select_core(core)) { + SCP_WARN(()) << "mcd_server: READ_REG could not select core " << core; + return MCD_RET_ERR_GENERAL; + } + + char cmd[32]; + std::snprintf(cmd, sizeof(cmd), "p%x", regno); + + std::string reply; + if (!gdb_cmd(inst, cmd, reply)) { + SCP_WARN(()) << "mcd_server: READ_REG forward to " << m_gdb_ports[inst] << " failed"; + return MCD_RET_ERR_GENERAL; + } + /* Empty reply => 'p' unsupported; "E xx" => error. */ + if (reply.empty() || reply[0] == 'E') { + SCP_WARN(()) << "mcd_server: READ_REG regno=" << regno << " gdb error '" << reply << "'"; + return MCD_RET_ERR_GENERAL; + } + if (reply.size() % 2 != 0) { + SCP_WARN(()) << "mcd_server: READ_REG odd-length hex reply '" << reply << "'"; + return MCD_RET_ERR_GENERAL; + } + + /* Reply is the value hex-encoded in target byte order; decode preserving + * that order. */ + for (std::string::size_type i = 0; i + 1 < reply.size(); i += 2) { + int hi = hex_nibble(reply[i]); + int lo = hex_nibble(reply[i + 1]); + if (hi < 0 || lo < 0) { + SCP_WARN(()) << "mcd_server: READ_REG bad hex reply '" << reply << "'"; + return MCD_RET_ERR_GENERAL; + } + resp.push_back(static_cast((hi << 4) | lo)); + } + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_write_reg(const std::vector& req, std::vector&) +{ + /* Request: core id u32, register number u32, raw value in target byte order. */ + if (req.size() < 8) return MCD_RET_ERR_GENERAL; + uint32_t core = get_u32(&req[0]); + uint32_t regno = get_u32(&req[4]); + + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + if (core >= m_cores.size()) { + SCP_WARN(()) << "mcd_server: WRITE_REG core id " << core << " out of range"; + return MCD_RET_ERR_GENERAL; + } + uint32_t inst = m_cores[core].inst; + + /* 'P' writes to the currently selected core, so select it first. */ + if (!gdb_select_core(core)) { + SCP_WARN(()) << "mcd_server: WRITE_REG could not select core " << core; + return MCD_RET_ERR_GENERAL; + } + + /* "P=", hex-encoded in the given byte order. */ + std::string cmd = "P"; + { + char rn[16]; + std::snprintf(rn, sizeof(rn), "%x", regno); + cmd += rn; + } + cmd += '='; + for (std::vector::size_type i = 8; i < req.size(); ++i) { + char b[3]; + std::snprintf(b, sizeof(b), "%02x", req[i]); + cmd += b; + } + + std::string reply; + if (!gdb_cmd(inst, cmd, reply)) { + SCP_WARN(()) << "mcd_server: WRITE_REG forward to " << m_gdb_ports[inst] << " failed"; + return MCD_RET_ERR_GENERAL; + } + if (reply == "OK") { + SCP_INFO(()) << "mcd_server: WRITE_REG regno=" << regno << " OK"; + return MCD_RET_ACT_NONE; + } + SCP_WARN(()) << "mcd_server: WRITE_REG regno=" << regno << " gdb reply '" << reply << "'"; + return MCD_RET_ERR_GENERAL; +} + +/* Breakpoints use RSP Z/z packets: "Z,," sets, "z..." removes; + * QEMU replies "OK", "E xx", or empty if unsupported. SET_BP/CLR_BP request is + * [core u32][type u32][addr u64][kind u32] (20 bytes). */ +static bool decode_bp_req(const std::vector& req, uint32_t& core, uint32_t& type, uint64_t& addr, + uint32_t& kind) +{ + if (req.size() < 20) return false; + core = get_u32(&req[0]); + type = get_u32(&req[4]); + addr = get_u64(&req[8]); + kind = get_u32(&req[16]); + return true; +} + +mcd_return_et mcd_server::op_set_bp(const std::vector& req, std::vector&) +{ + uint32_t core, type, kind; + uint64_t addr; + if (!decode_bp_req(req, core, type, addr, kind)) return MCD_RET_ERR_GENERAL; + char digit; + if (!bp_type_to_gdb(type, digit)) { + SCP_WARN(()) << "mcd_server: SET_BP unknown type " << type; + return MCD_RET_ERR_GENERAL; + } + if (kind == 0) kind = 4; /* sensible default: one AArch64 instruction word */ + + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + if (core >= m_cores.size()) { + SCP_WARN(()) << "mcd_server: SET_BP core id " << core << " out of range"; + return MCD_RET_ERR_GENERAL; + } + uint32_t inst = m_cores[core].inst; + + /* QEMU installs Z/z breakpoints per address space, not per vCPU: this arms + * every core of the instance. `core` is recorded only for LIST_BP. */ + char cmd[48]; + std::snprintf(cmd, sizeof(cmd), "Z%c,%llx,%x", digit, static_cast(addr), kind); + + std::string reply; + if (!gdb_cmd(inst, cmd, reply)) { + SCP_WARN(()) << "mcd_server: SET_BP forward to " << m_gdb_ports[inst] << " failed"; + return MCD_RET_ERR_GENERAL; + } + if (reply != "OK") { + SCP_WARN(()) << "mcd_server: SET_BP '" << cmd << "' gdb reply '" << reply << "'"; + return MCD_RET_ERR_GENERAL; + } + m_breakpoints.push_back(breakpoint_t{ core, type, addr, kind }); + /* An armed breakpoint can halt a running target at any moment, stopping the + * quantum keeper before the client's next WAIT_STOP. */ + update_debug_hold(); + SCP_INFO(()) << "mcd_server: SET_BP core=" << core << " type=" << type << " @0x" << std::hex << addr; + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_clr_bp(const std::vector& req, std::vector&) +{ + uint32_t core, type, kind; + uint64_t addr; + if (!decode_bp_req(req, core, type, addr, kind)) return MCD_RET_ERR_GENERAL; + char digit; + if (!bp_type_to_gdb(type, digit)) { + SCP_WARN(()) << "mcd_server: CLR_BP unknown type " << type; + return MCD_RET_ERR_GENERAL; + } + if (kind == 0) kind = 4; + + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + if (core >= m_cores.size()) { + SCP_WARN(()) << "mcd_server: CLR_BP core id " << core << " out of range"; + return MCD_RET_ERR_GENERAL; + } + uint32_t inst = m_cores[core].inst; + + char cmd[48]; + std::snprintf(cmd, sizeof(cmd), "z%c,%llx,%x", digit, static_cast(addr), kind); + + std::string reply; + if (!gdb_cmd(inst, cmd, reply)) { + SCP_WARN(()) << "mcd_server: CLR_BP forward to " << m_gdb_ports[inst] << " failed"; + return MCD_RET_ERR_GENERAL; + } + if (reply != "OK") { + SCP_WARN(()) << "mcd_server: CLR_BP '" << cmd << "' gdb reply '" << reply << "'"; + return MCD_RET_ERR_GENERAL; + } + /* Drop the first matching record (core+type+addr). */ + for (auto it = m_breakpoints.begin(); it != m_breakpoints.end(); ++it) { + if (it->core == core && it->type == type && it->addr == addr) { + m_breakpoints.erase(it); + break; + } + } + /* Disarming the last breakpoint on a running target releases the hold. */ + update_debug_hold(); + SCP_INFO(()) << "mcd_server: CLR_BP core=" << core << " type=" << type << " @0x" << std::hex << addr; + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_list_bp(const std::vector&, std::vector& resp) +{ + /* Response: 4-byte LE count, then each breakpoint as + * [core u32][type u32][addr u64][kind u32] (20 bytes). */ + std::lock_guard lock(m_gdb_mutex); + put_u32(resp, static_cast(m_breakpoints.size())); + for (const breakpoint_t& bp : m_breakpoints) { + put_u32(resp, bp.core); + put_u32(resp, bp.type); + put_u32(resp, static_cast(bp.addr & 0xffffffffu)); + put_u32(resp, static_cast((bp.addr >> 32) & 0xffffffffu)); + put_u32(resp, bp.kind); + } + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_wait_stop(const std::vector& req, std::vector& resp) +{ + /* Request: [core u32][timeout_ms u32]; a short payload means core 0, 1 s. */ + uint32_t core = (req.size() >= 4) ? get_u32(&req[0]) : 0u; + uint32_t timeout_ms = (req.size() >= 8) ? get_u32(&req[4]) : 1000u; + /* The wait holds m_gdb_mutex, so an uncapped timeout locks out every other + * debug operation. Clamp rather than reject; the client can wait again. */ + if (timeout_ms > MCD_MAX_WAIT_MS) { + SCP_DEBUG(()) << "mcd_server: WAIT_STOP timeout " << timeout_ms << "ms clamped to " << MCD_MAX_WAIT_MS << "ms"; + timeout_ms = MCD_MAX_WAIT_MS; + } + uint64_t watch_addr = 0; + uint32_t reason; + { + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + if (core >= m_cores.size()) { + SCP_WARN(()) << "mcd_server: WAIT_STOP core id " << core << " out of range"; + return MCD_RET_ERR_GENERAL; + } + + uint32_t inst = m_cores[core].inst; + uint32_t tid = m_cores[core].tid; + + /* A stop already recorded for this core is delivered now. */ + if (m_last_stop[inst].valid && (m_last_stop[inst].tid == 0 || m_last_stop[inst].tid == tid)) { + reason = m_last_stop[inst].reason; + watch_addr = m_last_stop[inst].watch_addr; + m_last_stop[inst] = last_stop_t{}; /* delivered once */ + SCP_INFO(()) << "mcd_server: WAIT_STOP core=" << core << " delivering recorded stop reason=" << reason; + } else { + /* The instance halts as a whole and reports on its own session, so + * wait there and then see which core it was attributed to. */ + uint32_t stopped_tid = 0; + reason = gdb_wait_stop(inst, timeout_ms, watch_addr, stopped_tid); + + if (reason != MCD_STOP_RUNNING && reason != MCD_STOP_HALTED && stopped_tid != 0 && stopped_tid != tid) { + /* Another core stopped: report this one as still running. The + * stop stays recorded so its own core can collect it. */ + SCP_INFO(()) << "mcd_server: WAIT_STOP core=" << core << " (tid " << tid + << "): instance stopped on tid " << stopped_tid << " instead; held for that core"; + reason = MCD_STOP_RUNNING; + watch_addr = 0; + } else { + m_last_stop[inst] = last_stop_t{}; /* consumed by this wait */ + } + } + + /* The target may now be halted, or the session dropped; re-evaluate. */ + update_debug_hold(); + } + + /* Response: [stopped u8][reason u32][watch_addr u64] (13 bytes); stopped is 0 + * only for MCD_STOP_RUNNING. */ + resp.push_back(reason == MCD_STOP_RUNNING ? 0u : 1u); + put_u32(resp, reason); + put_u32(resp, static_cast(watch_addr & 0xffffffffu)); + put_u32(resp, static_cast((watch_addr >> 32) & 0xffffffffu)); + return MCD_RET_ACT_NONE; +} + +} // namespace qbox + +using qbox::mcd_server; + +/* The loader looks up "module_register" by unmangled symbol name: C linkage. */ +extern "C" void module_register() { GSC_MODULE_REGISTER_C(mcd_server); } diff --git a/tests/qbox/CMakeLists.txt b/tests/qbox/CMakeLists.txt index c76329af..2c612fe6 100644 --- a/tests/qbox/CMakeLists.txt +++ b/tests/qbox/CMakeLists.txt @@ -346,3 +346,9 @@ function(qbox_extra_add_test target) endfunction(qbox_extra_add_test) add_subdirectory(display) + +# MCD debug server + MCP bridge tests. The mcd_server / mcd_mcp targets are only +# built on non-Windows platforms (see systemc-components/CMakeLists.txt). +if(NOT WIN32) + add_subdirectory(mcd) +endif() diff --git a/tests/qbox/mcd/CMakeLists.txt b/tests/qbox/mcd/CMakeLists.txt new file mode 100644 index 00000000..fad8a05e --- /dev/null +++ b/tests/qbox/mcd/CMakeLists.txt @@ -0,0 +1,107 @@ +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. +# SPDX-License-Identifier: BSD-3-Clause +# +# In-tree tests for the MCD debug server and its MCP bridge: built and run as +# part of the normal `ctest` run. + +find_package(Python3 COMPONENTS Interpreter REQUIRED) + +# The VP driver is the example's main.cc: a generic module-factory container. +set(MCD_EXAMPLE_DIR ${PROJECT_SOURCE_DIR}/examples/hello-qbox) +set(MCD_LUA ${CMAKE_CURRENT_SOURCE_DIR}/mcd-platform.lua) + +# The topology comes entirely from the lua file at runtime. +add_executable(mcd-vp ${MCD_EXAMPLE_DIR}/main.cc) + +if(APPLE) + target_link_options(mcd-vp PRIVATE "LINKER:-dead_strip_dylibs") +else() + target_link_options(mcd-vp PRIVATE "LINKER:-as-needed") +endif() + +# The lua loads its components as dynamic modules by dylib_path, so mcd-vp links +# only the core library plus the AArch64 CPU support, and depends on the +# dynamically-loaded modules so they exist before the test runs. +target_link_libraries(mcd-vp PRIVATE ${TARGET_LIBS} cpu_arm_cortexA53) +add_dependencies(mcd-vp + router gs_memory uart-pl011 char_backend_stdio loader mcd_server) + +# Debug firmware (spins forever, so the target stays live for the debugger). +# Prefer the AArch64 cross-gcc, else clang with an AArch64 target plus ld.lld; +# with neither toolchain the integration tests are skipped. +set(DEBUG_FW_SRC ${CMAKE_CURRENT_SOURCE_DIR}/hello-debug.c) +set(DEBUG_FW ${CMAKE_CURRENT_BINARY_DIR}/hello-debug.elf) + +find_program(AARCH64_GCC aarch64-linux-gnu-gcc) +find_program(CLANG_BIN clang) +find_program(LLD_BIN ld.lld) + +set(HAVE_DEBUG_FW FALSE) +if(AARCH64_GCC) + add_custom_command( + OUTPUT ${DEBUG_FW} + COMMAND ${AARCH64_GCC} -nostdlib -static -ffreestanding + -Ttext=0x80000000 -e _start -Wl,--build-id=none -Wl,-n + -o ${DEBUG_FW} ${DEBUG_FW_SRC} + DEPENDS ${DEBUG_FW_SRC} + COMMENT "Cross-compiling hello-debug.elf (gcc)") + set(HAVE_DEBUG_FW TRUE) +elseif(CLANG_BIN AND LLD_BIN) + add_custom_command( + OUTPUT ${DEBUG_FW} + COMMAND ${CLANG_BIN} --target=aarch64-elf -ffreestanding -nostdlib + -c ${DEBUG_FW_SRC} -o ${DEBUG_FW}.o + COMMAND ${LLD_BIN} ${DEBUG_FW}.o -o ${DEBUG_FW} + -Ttext=0x80000000 -e _start --build-id=none -n + DEPENDS ${DEBUG_FW_SRC} + COMMENT "Cross-compiling hello-debug.elf (clang)") + set(HAVE_DEBUG_FW TRUE) +endif() + +# Pure-software unit tests (JSON + wire framing) need no firmware or VP. +add_test( + NAME mcd_mcp_unit + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/test_mcd_unit.py + -m $ +) +set_tests_properties(mcd_mcp_unit PROPERTIES TIMEOUT 60) + +# End-to-end: boots mcd-vp with the debug firmware and drives it through mcd_mcp. +if(HAVE_DEBUG_FW) + add_custom_target(hello-debug-firmware ALL DEPENDS ${DEBUG_FW}) + add_dependencies(mcd-vp hello-debug-firmware) + + add_test( + NAME mcd_mcp_hello + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/test_mcd_mcp.py + -e $ + -l ${MCD_LUA} + -m $ + -f ${DEBUG_FW} + ) + # Component dylibs are looked up relative to the VP's working directory, so + # run from the build root where the dylibs live. + set_tests_properties(mcd_mcp_hello PROPERTIES + TIMEOUT 120 + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) + + # Multi-core: the same platform with two CPUs on one QemuInstance. + add_test( + NAME mcd_mcp_smp + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/test_mcd_smp.py + -e $ + -l ${MCD_LUA} + -m $ + -f ${DEBUG_FW} + ) + set_tests_properties(mcd_mcp_smp PROPERTIES + TIMEOUT 180 + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +else() + message(WARNING + "No AArch64 toolchain (aarch64-linux-gnu-gcc, or clang + ld.lld) found; " + "the MCD integration test (mcd_mcp_hello) will be skipped.") +endif() diff --git a/tests/qbox/mcd/hello-debug.c b/tests/qbox/mcd/hello-debug.c new file mode 100644 index 00000000..72d0a5b7 --- /dev/null +++ b/tests/qbox/mcd/hello-debug.c @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + * + * Debug firmware for the MCD integration test. It spins forever rather than + * powering the board off (PSCI), which would end the SystemC simulation; the + * modelled CPU must stay live for the MCD debugger to attach and control it. + */ + +#define UART_DR ((volatile unsigned int*)0x09000000) + +/* Counter the spin loop bumps every iteration, at a fixed RAM address (below + * the stack at 0x90000000, clear of the scratch region the memory tests use) + * so the test can arm a write-watchpoint on it without parsing the ELF. */ +#define COUNTER ((volatile unsigned long*)0x8ff00000) + +void __attribute__((naked)) _start(void) +{ + /* 0x90000000 is the top of the 256 MB RAM region: the stack. */ + __asm__ volatile( + "ldr x0, =0x90000000\n" + "mov sp, x0\n" + "bl main\n" + "1: b 1b\n"); +} + +void main(void) +{ + const char* msg = "Hello from Qbox (debug)!\r\n"; + while (*msg) *UART_DR = *msg++; + + *COUNTER = 0; + + for (;;) { + (*COUNTER)++; + __asm__ volatile("nop"); + } +} diff --git a/tests/qbox/mcd/mcd-platform.lua b/tests/qbox/mcd/mcd-platform.lua new file mode 100644 index 00000000..6d096881 --- /dev/null +++ b/tests/qbox/mcd/mcd-platform.lua @@ -0,0 +1,96 @@ +-- Platform for the MCD tests: an AArch64 board with an mcd_server, running a +-- firmware that spins forever so the target stays live for a debugger. All cores +-- share one QemuInstance, whose gdb stub presents each vCPU as a gdb thread, so +-- a single gdb_port serves every core. +-- +-- Port, firmware and core count are CCI presets, which must precede the lua file +-- on the command line; string values are JSON-quoted: +-- mcd-vp -p mcd.port=1235 \ +-- -p 'fw="/path/to/hello-debug.elf"' \ +-- -p cores=2 \ +-- --gs_luafile mcd-platform.lua +-- Defaults: port 1235, hello-debug.elf beside this file, one core. + +function script_path() + local src = debug.getinfo(2, "S").source:sub(2) + return src:match("(.*/)") +end +local base = script_path() + +local mcd_port = tonumber(GET("mcd.port")) or 1235 +local num_cores = tonumber(GET("cores")) or 1 + +-- fw wins; ctest passes the cross-compiled firmware from the build tree. +local fw = GET("fw") +if not fw or fw == "" then fw = base .. "hello-debug.elf" end + +platform = { + moduletype = "Container", + quantum_ns = 10000000, + + router = { moduletype = "router", log_level = 0 }, + + ram_0 = { + moduletype = "gs_memory", + target_socket = { + address = 0x80000000, + size = 0x10000000, -- 256 MB + bind = "&router.initiator_socket", + }, + }, + + qemu_inst_mgr = { moduletype = "QemuInstanceManager" }, + + qemu_inst = { + moduletype = "QemuInstance", + args = { "&qemu_inst_mgr", "AARCH64" }, + accel = "tcg", + sync_policy = "multithread-unconstrained", + }, + + charbackend_stdio_0 = { + moduletype = "char_backend_stdio", + read_write = true, + }, + + pl011_uart_0 = { + moduletype = "Pl011", + dylib_path = "uart-pl011", + target_socket = { + address = 0x09000000, + size = 0x1000, + bind = "&router.initiator_socket", + }, + backend_socket = { + bind = "&charbackend_stdio_0.biflow_socket" }, + }, + + load = { + moduletype = "loader", + initiator_socket = { bind = "&router.target_socket" }, + { elf_file = fw }, + }, + + mcd_server = { + moduletype = "mcd_server", + dylib_path = "mcd_server", + mcd_port = mcd_port, + }, + + -- No keep_alive component: mcd_server holds the simulation open only while + -- the debugger is the reason the CPUs are stopped, and releases it on resume. +} + +-- CPUs cpu_0 .. cpu_, added after the table literal so the count can come +-- from the cores preset. +for i = 0, num_cores - 1 do + platform["cpu_" .. i] = { + moduletype = "cpu_arm_cortexA53", + args = { "&qemu_inst" }, + mem = { bind = "&router.target_socket" }, + rvbar = 0x80000000, + has_el3 = true, + has_el2 = true, + psci_conduit = "hvc", + } +end diff --git a/tests/qbox/mcd/test_mcd_mcp.py b/tests/qbox/mcd/test_mcd_mcp.py new file mode 100644 index 00000000..727a12b7 --- /dev/null +++ b/tests/qbox/mcd/test_mcd_mcp.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. +# SPDX-License-Identifier: BSD-3-Clause +# +# Integration test for the mcd_mcp MCP server talking to mcd_server inside the +# hello-qbox virtual platform. Checks: +# 1. connect / status (cores, mem_spaces) +# 2. write_mem / read_mem round-trip (TLM transport_dbg) +# 3. second distinct write/read pattern (no stale-data false pass) +# 4. stop +# 5. read_reg – PC (reg 32, AArch64 gdbstub layout) +# 6. single step – PC must advance +# 7. write_reg / read_reg round-trip on x1 (reg 1) +# 8. snapshot / diff +# 9. regs_dump +# 10. breakpoint: set / list / run / wait_stop hit / clear +# 11. write watchpoint: set / run / wait_stop hit / clear +# 12. run +# 13. describe +# +# Arguments (passed by CTest via CMakeLists.txt): +# -e / --exe path to the virtual-platform binary (mcd-vp) +# -l / --lua path to mcd-platform.lua +# -m / --mcp path to the mcd_mcp binary +# -f / --fw path to the debug firmware ELF (passed to the lua as fw) + +import argparse +import json +import os +import signal +import socket +import subprocess +import sys +import time + + +def find_free_port() -> int: + """Bind to port 0 and let the OS pick; return that port.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +def wait_for_port(host: str, port: int, timeout: float = 10.0) -> None: + """Poll until a TCP port is accepting connections, or raise TimeoutError.""" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + try: + with socket.create_connection((host, port), timeout=0.2): + return + except OSError: + time.sleep(0.1) + raise TimeoutError(f"port {host}:{port} did not open within {timeout}s") + + +def parse_u64_from_text(text: str) -> int: + """Parse the 0x-prefixed hex value returned by mcd_read_reg.""" + return int(text.strip(), 16) + + +class MCP: + """Thin wrapper around an mcd_mcp subprocess using JSON-RPC 2.0 stdio.""" + + def __init__(self, mcp_exe: str) -> None: + self._proc = subprocess.Popen( + [mcp_exe], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + ) + self._id = 0 + self._call("initialize", { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "test", "version": "0"}, + }) + self._notify("notifications/initialized") + + def _call(self, method: str, params: dict) -> dict: + self._id += 1 + req = json.dumps({"jsonrpc": "2.0", "id": self._id, + "method": method, "params": params}) + self._proc.stdin.write(req + "\n") + self._proc.stdin.flush() + line = self._proc.stdout.readline() + return json.loads(line) + + def _notify(self, method: str) -> None: + msg = json.dumps({"jsonrpc": "2.0", "method": method}) + self._proc.stdin.write(msg + "\n") + self._proc.stdin.flush() + + def tool(self, name: str, args: dict) -> str: + """Call a tool; return the text result. Raises on isError=true.""" + resp = self._call("tools/call", {"name": name, "arguments": args}) + if "error" in resp: + raise RuntimeError(f"RPC error calling {name!r}: {resp['error']}") + result = resp["result"] + text = result["content"][0]["text"] + if result.get("isError"): + raise RuntimeError(f"tool {name!r} returned error: {text}") + return text + + def close(self) -> None: + try: + self._proc.stdin.close() + except Exception: + pass + self._proc.terminate() + self._proc.wait(timeout=5) + + +def run_test(vp_exe: str, lua_file: str, mcp_exe: str, fw: str) -> None: + mcd_port = find_free_port() + + print(f"mcd_port={mcd_port}") + + # Port and firmware path are CCI presets, which must precede --gs_luafile to + # be visible to the lua's GET(). "-p name=value" is parsed with + # cci_value::from_json, so a string value must be JSON-quoted. No gdb_port is + # passed: mcd_server assigns a free one per QEMU instance (the stub is per + # instance, not per CPU). + cmd = [vp_exe, "-p", f"mcd.port={mcd_port}"] + if fw: + cmd += ["-p", f'fw="{fw}"'] + cmd += ["--gs_luafile", lua_file] + vp = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + env=dict(os.environ), + ) + + mcp = None + try: + # mcd_server opens its TCP port at end_of_elaboration. + wait_for_port("127.0.0.1", mcd_port, timeout=20.0) + + mcp = MCP(mcp_exe) + + # 1. Connect / status + result = mcp.tool("mcd_connect", {"host": "127.0.0.1", "port": mcd_port}) + print(f"[1] connect: {result}") + assert "connected" in result, f"unexpected connect result: {result!r}" + + status = json.loads(mcp.tool("mcd_status", {})) + print(f"[1] status: {json.dumps(status, indent=2)}") + assert status["connected"], "status should show connected" + assert len(status["cores"]) >= 1, "expected at least one core" + assert len(status["mem_spaces"]) >= 1, "expected at least one mem_space" + + # 2 & 3. Write / read-back two distinct patterns in scratch RAM, near the + # top of the 256 MB region (0x80000000..0x90000000), clear of the firmware. + for scratch, pattern in [ + ("8ffffff0", "deadbeef01020304"), + ("8fffffe0", "0011223344556677"), + ]: + mcp.tool("mcd_write_mem", {"addr": scratch, "data": pattern}) + readback = mcp.tool("mcd_read_mem", {"addr": scratch, "len": 8}) + flat = "".join(readback.split()).lower() + expected = "".join(pattern[i:i+2] for i in range(0, len(pattern), 2)) + assert expected in flat, ( + f"mem round-trip @ {scratch}: expected {expected!r} in {flat!r}" + ) + print(f"[2/3] mem @ 0x{scratch}: OK") + + # 4. Stop + result = mcp.tool("mcd_stop", {}) + print(f"[4] stop: {result}") + assert result == "ok", f"mcd_stop returned unexpected: {result!r}" + + # 5. Read PC (reg 32 in the AArch64 gdbstub layout) + PC_REG = 32 + pc1 = parse_u64_from_text(mcp.tool("mcd_read_reg", {"regno": PC_REG})) + print(f"[5] PC before step: 0x{pc1:016x}") + assert pc1 != 0, "PC should not be zero after reset" + + # 6. Single step - PC must advance + mcp.tool("mcd_step", {"cpu_idx": 0}) + pc2 = parse_u64_from_text(mcp.tool("mcd_read_reg", {"regno": PC_REG})) + print(f"[6] PC after step: 0x{pc2:016x}") + assert pc2 != pc1, f"PC did not change after step (still 0x{pc1:016x})" + + # 7. Write / read-back a GP register (x1 = regno 1) + CANARY = 0xA5A5A5A5DEADC0DE + mcp.tool("mcd_write_reg", {"regno": 1, "value": f"0x{CANARY:016x}"}) + x1 = parse_u64_from_text(mcp.tool("mcd_read_reg", {"regno": 1})) + print(f"[7] x1 read back: 0x{x1:016x}") + assert x1 == CANARY, ( + f"register write/read-back mismatch: wrote 0x{CANARY:016x}, got 0x{x1:016x}" + ) + + # 8. Snapshot / diff: snapshot r0..r32 so the range includes the PC (reg + # 32); stepping the firmware's idle loop changes PC but no GP register. + mcp.tool("mcd_snapshot", {"name": "before_step", "count": 33}) + mcp.tool("mcd_step", {"cpu_idx": 0}) + diff = mcp.tool("mcd_diff", {"name": "before_step"}) + print(f"[8] diff:\n{diff[:400]}") + assert "changed" in diff.lower() or "r32" in diff.lower(), ( + f"diff after step should report changed registers; got: {diff!r}" + ) + + # 9. Regs dump + dump = mcp.tool("mcd_regs_dump", {"cpu_idx": 0}) + print(f"[9] regs_dump (first 400 chars):\n{dump[:400]}") + assert "r0" in dump.lower(), "regs_dump should mention r0" + + # 10. Breakpoint: halt, note the PC, step to a second loop address, then + # arm the breakpoint at the *first* address, so continuing executes at + # least one instruction before trapping back at bp_addr. + mcp.tool("mcd_stop", {}) + bp_addr = parse_u64_from_text(mcp.tool("mcd_read_reg", {"regno": PC_REG})) + mcp.tool("mcd_step", {"cpu_idx": 0}) + stepped = parse_u64_from_text(mcp.tool("mcd_read_reg", {"regno": PC_REG})) + assert stepped != bp_addr, "expected the step to move PC within the loop" + + set_msg = mcp.tool("mcd_set_bp", {"addr": f"{bp_addr:x}", "type": "sw"}) + print(f"[10] set_bp: {set_msg}") + + listing = mcp.tool("mcd_list_bp", {}) + print(f"[10] list_bp:\n{listing}") + assert f"{bp_addr:x}" in listing.lower(), f"bp not listed: {listing!r}" + + mcp.tool("mcd_run", {}) + stop = mcp.tool("mcd_wait_stop", {"timeout_ms": 5000}) + print(f"[10] wait_stop: {stop}") + assert "breakpoint" in stop, f"expected a breakpoint stop, got: {stop!r}" + pc_hit = parse_u64_from_text(mcp.tool("mcd_read_reg", {"regno": PC_REG})) + assert pc_hit == bp_addr, ( + f"breakpoint hit at 0x{pc_hit:016x}, expected 0x{bp_addr:016x}" + ) + + mcp.tool("mcd_clear_bp", {"addr": f"{bp_addr:x}", "type": "sw"}) + listing = mcp.tool("mcd_list_bp", {}) + assert f"{bp_addr:x}" not in listing.lower(), f"bp not cleared: {listing!r}" + print("[10] breakpoint set/hit/clear: OK") + + # 11. Write watchpoint: hello-debug.c bumps a counter at 0x8ff00000 on + # every loop iteration. + COUNTER = 0x8ff00000 + wp_msg = mcp.tool("mcd_set_bp", + {"addr": f"{COUNTER:x}", "type": "write", "len": 8}) + print(f"[11] set write-watchpoint: {wp_msg}") + mcp.tool("mcd_run", {}) + wstop = mcp.tool("mcd_wait_stop", {"timeout_ms": 5000}) + print(f"[11] wait_stop: {wstop}") + assert "watchpoint" in wstop, f"expected a watchpoint stop, got: {wstop!r}" + mcp.tool("mcd_clear_bp", {"addr": f"{COUNTER:x}", "type": "write", "len": 8}) + print("[11] write watchpoint: OK") + + # 12. Resume + result = mcp.tool("mcd_run", {}) + print(f"[12] run: {result}") + assert result == "ok", f"mcd_run returned unexpected: {result!r}" + + # 13. Describe + desc = mcp.tool("mcd_describe", {}) + print(f"[11] describe:\n{desc}") + assert "host:" in desc, "describe output missing 'host:'" + assert "mem_spaces" in desc, "describe output missing 'mem_spaces'" + + print("\nALL ASSERTIONS PASSED") + + finally: + if mcp: + mcp.close() + vp.send_signal(signal.SIGTERM) + try: + out, _ = vp.communicate(timeout=5) + except subprocess.TimeoutExpired: + vp.kill() + out, _ = vp.communicate() + if out: + print("--- VP stdout ---") + print(out[-3000:]) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("-e", "--exe", required=True, + metavar="VP", help="Path to the virtual-platform binary") + parser.add_argument("-l", "--lua", required=True, + metavar="LUA", help="Path to mcd-platform.lua") + parser.add_argument("-m", "--mcp", required=True, + metavar="MCP", help="Path to mcd_mcp binary") + parser.add_argument("-f", "--fw", default="", + metavar="FW", help="Path to the debug firmware ELF") + args = parser.parse_args() + + required = [args.exe, args.lua, args.mcp] + if args.fw: + required.append(args.fw) + for path in required: + if not os.path.isfile(path): + print(f"ERROR: file not found: {path}", file=sys.stderr) + return 1 + + run_test(args.exe, args.lua, args.mcp, args.fw) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/qbox/mcd/test_mcd_smp.py b/tests/qbox/mcd/test_mcd_smp.py new file mode 100644 index 00000000..0a3eb98f --- /dev/null +++ b/tests/qbox/mcd/test_mcd_smp.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. +# SPDX-License-Identifier: BSD-3-Clause +# +# Multi-core (SMP) integration test for mcd_server via mcd_mcp. Both CPUs share a +# single QemuInstance, whose gdb stub state is global and presents each vCPU as a +# gdb thread, so one gdb_port serves every core. Checks: +# 1. two cores are reported +# 2. registers are per core (distinct values written and read back) +# 3. stepping one core advances only that core +# 4. a breakpoint hit is reported +# 5. detaching resumes the target +# +# Arguments (passed by CTest via CMakeLists.txt): +# -e / --exe path to the virtual-platform binary (mcd-vp) +# -l / --lua path to mcd-platform.lua +# -m / --mcp path to the mcd_mcp binary +# -f / --fw path to the debug firmware ELF (passed to the lua as fw) + +import argparse +import json +import os +import subprocess +import sys + +# Both files live in this directory, which is sys.path[0] for a script run by path. +from test_mcd_mcp import MCP, find_free_port, wait_for_port, parse_u64_from_text + +PC_REG = 32 # AArch64 gdbstub register layout: x0..x30 = 0..30, sp = 31, pc = 32 + +# The debug firmware's spin loop, from hello-debug.c compiled for AArch64: each +# step moves PC on by one 4-byte instruction and wraps at the end. +LOOP = [0x8000005C, 0x80000060, 0x80000064, 0x80000068, 0x8000006C, 0x80000070] + + +def read_pc(mcp: MCP, core: int) -> int: + return parse_u64_from_text(mcp.tool("mcd_read_reg", {"regno": PC_REG, "cpu_idx": core})) + + +def run_test(vp_exe: str, lua_file: str, mcp_exe: str, fw: str) -> None: + mcd_port = find_free_port() + print(f"mcd_port={mcd_port} (2 cores)") + + # cores=2 makes the lua instantiate cpu_0 and cpu_1 on the one qemu_inst; + # presets must precede --gs_luafile, and string values be JSON-quoted. + cmd = [vp_exe, "-p", f"mcd.port={mcd_port}", "-p", "cores=2"] + if fw: + cmd += ["-p", f'fw="{fw}"'] + cmd += ["--gs_luafile", lua_file] + vp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, env=dict(os.environ)) + + mcp = None + try: + wait_for_port("127.0.0.1", mcd_port, timeout=20.0) + mcp = MCP(mcp_exe) + mcp.tool("mcd_connect", {"host": "127.0.0.1", "port": mcd_port}) + + # 1. Two cores, discovered from the gdb stub's thread list + status = json.loads(mcp.tool("mcd_status", {})) + cores = status["cores"] + print(f"[1] cores: {json.dumps(cores)}") + assert len(cores) == 2, f"expected 2 cores, got {len(cores)}: {cores}" + + mcp.tool("mcd_stop", {}) + + # 2. Registers are per core: distinct values, so aliasing cannot pass. + want = {0: 0xAAAA0000, 1: 0xBBBB1111} + for core, val in want.items(): + mcp.tool("mcd_write_reg", {"regno": 0, "value": f"0x{val:016x}", "cpu_idx": core}) + for core, val in want.items(): + got = parse_u64_from_text(mcp.tool("mcd_read_reg", {"regno": 0, "cpu_idx": core})) + print(f"[2] core {core} x0 = 0x{got:x}") + assert got == val, f"core {core}: x0 is 0x{got:x}, expected 0x{val:x}" + + # 3. Stepping one core moves only that core, repeated and interleaved with + # run/stop cycles so the 0x03 path is exercised too. + steps = 24 + for k in range(steps): + if k and k % 8 == 0: + mcp.tool("mcd_run", {}) + mcp.tool("mcd_stop", {}) + + before_1, before_0 = read_pc(mcp, 1), read_pc(mcp, 0) + assert before_1 in LOOP, f"core 1 PC 0x{before_1:x} is outside the spin loop" + + mcp.tool("mcd_step", {"cpu_idx": 1}) + + after_1, after_0 = read_pc(mcp, 1), read_pc(mcp, 0) + expect_1 = LOOP[(LOOP.index(before_1) + 1) % len(LOOP)] + assert after_1 == expect_1, ( + f"step {k}: core 1 PC 0x{before_1:x} -> 0x{after_1:x}, " + f"expected 0x{expect_1:x}") + assert after_0 == before_0, ( + f"step {k}: stepping core 1 moved core 0: " + f"0x{before_0:x} -> 0x{after_0:x}") + print(f"[3] {steps} single-core steps: core 1 advanced each time, core 0 never moved") + + # Step core 0 too, so only-core-1-is-steppable would be caught. + before_0 = read_pc(mcp, 0) + mcp.tool("mcd_step", {"cpu_idx": 0}) + after_0 = read_pc(mcp, 0) + assert after_0 != before_0, f"core 0 did not step (PC stayed 0x{before_0:x})" + print(f"[3] core 0 steps as well: 0x{before_0:x} -> 0x{after_0:x}") + + # 4. A breakpoint is hit and reported. QEMU arms Z/z per address space, so + # whichever core arrives first wins; mcd_wait_stop reports a stop only to + # its own core, so ask each in turn and assert only that the hit happened. + bp_addr = 0x80000064 + mcp.tool("mcd_set_bp", {"addr": f"{bp_addr:x}", "type": "sw"}) + mcp.tool("mcd_run", {}) + + hit = None + for core in (0, 1): + stop = mcp.tool("mcd_wait_stop", {"timeout_ms": 5000, "cpu_idx": core}) + print(f"[4] wait_stop core {core}: {stop}") + if "still running" not in stop.lower(): + hit = (core, stop) + break + assert hit is not None, "neither core reported the breakpoint hit" + print(f"[4] breakpoint reported by core {hit[0]}") + mcp.tool("mcd_clear_bp", {"addr": f"{bp_addr:x}", "type": "sw"}) + + # 5. Detaching resumes the target: closing the socket does not restart + # QEMU's VM, so mcd_server must resume on detach or the platform exits. + mcp.tool("mcd_stop", {}) + mcp.tool("mcd_disconnect", {}) + assert vp.poll() is None, "simulation exited after the debugger detached" + + # A fresh session must still work: the resume leaves QEMU owing a + # stop-reply, which must not derail the next connection's handshake. + mcp.tool("mcd_connect", {"host": "127.0.0.1", "port": mcd_port}) + status = json.loads(mcp.tool("mcd_status", {})) + assert len(status["cores"]) == 2, \ + f"reconnect reported {len(status['cores'])} cores, expected 2" + bps = mcp.tool("mcd_list_bp", {}) + print(f"[5] reconnected, cores=2, breakpoints after detach: {bps.strip()}") + mcp.tool("mcd_run", {}) + + print("SMP test: all checks passed") + + finally: + if mcp is not None: + mcp.close() + vp.terminate() + try: + out, _ = vp.communicate(timeout=10) + except subprocess.TimeoutExpired: + vp.kill() + out, _ = vp.communicate() + if out: + print("--- VP output (tail) ---") + print("\n".join(out.splitlines()[-40:])) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("-e", "--exe", required=True) + ap.add_argument("-l", "--lua", required=True) + ap.add_argument("-m", "--mcp", required=True) + ap.add_argument("-f", "--fw", default="") + args = ap.parse_args() + run_test(args.exe, args.lua, args.mcp, args.fw) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/qbox/mcd/test_mcd_unit.py b/tests/qbox/mcd/test_mcd_unit.py new file mode 100644 index 00000000..957d7479 --- /dev/null +++ b/tests/qbox/mcd/test_mcd_unit.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. +# SPDX-License-Identifier: BSD-3-Clause +# +# Pure-software unit tests for mcd_mcp (no virtual platform): JSON-RPC framing, +# tools/list schema, argument validation, error reporting. +# +# Argument (passed by CTest): +# -m / --mcp path to the mcd_mcp binary + +import argparse +import json +import os +import subprocess +import sys + + +class MCP: + """Drive an mcd_mcp subprocess over JSON-RPC 2.0 on stdio.""" + + def __init__(self, mcp_exe: str) -> None: + self._proc = subprocess.Popen( + [mcp_exe], stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True, bufsize=1) + self._id = 0 + + def rpc(self, method: str, params=None, raw: str = None) -> dict: + """Send one request (or a raw line) and return the parsed reply.""" + if raw is not None: + line = raw + else: + self._id += 1 + msg = {"jsonrpc": "2.0", "id": self._id, "method": method} + if params is not None: + msg["params"] = params + line = json.dumps(msg) + self._proc.stdin.write(line + "\n") + self._proc.stdin.flush() + return json.loads(self._proc.stdout.readline()) + + def notify(self, method: str) -> None: + self._proc.stdin.write(json.dumps({"jsonrpc": "2.0", "method": method}) + "\n") + self._proc.stdin.flush() + + def tool(self, name: str, args: dict) -> dict: + return self.rpc("tools/call", {"name": name, "arguments": args}) + + def close(self) -> None: + try: + self._proc.stdin.close() + except Exception: + pass + self._proc.terminate() + self._proc.wait(timeout=5) + + +CHECKS = 0 + + +def check(cond: bool, msg: str) -> None: + global CHECKS + CHECKS += 1 + if not cond: + raise AssertionError(msg) + + +def run(mcp_exe: str) -> None: + m = MCP(mcp_exe) + try: + r = m.rpc("initialize", {"protocolVersion": "2024-11-05", + "capabilities": {}, "clientInfo": {"name": "t", "version": "0"}}) + check(r.get("result", {}).get("protocolVersion") == "2024-11-05", + f"initialize should report protocol version: {r!r}") + check("serverInfo" in r["result"], "initialize should include serverInfo") + m.notify("notifications/initialized") + + # tools/list: every advertised tool must have name/description/inputSchema. + r = m.rpc("tools/list") + tools = r["result"]["tools"] + names = {t["name"] for t in tools} + for expected in ("mcd_connect", "mcd_read_mem", "mcd_write_mem", + "mcd_read_reg", "mcd_write_reg", "mcd_run", + "mcd_stop", "mcd_step", "mcd_status", "mcd_describe", + "mcd_set_bp", "mcd_clear_bp", "mcd_list_bp", + "mcd_wait_stop"): + check(expected in names, f"tools/list missing {expected!r}") + for t in tools: + check("description" in t and t["description"], f"{t['name']} lacks description") + check(t.get("inputSchema", {}).get("type") == "object", + f"{t['name']} inputSchema should be an object") + + r = m.tool("mcd_status", {}) + body = r["result"]["content"][0]["text"] + check(json.loads(body) == {"connected": False}, + f"status before connect should be disconnected: {body!r}") + check(r["result"]["isError"] is False, "status is not an error") + + # Tools needing a connection fail cleanly (isError) rather than crashing. + for name, args in (("mcd_read_reg", {"regno": 0}), + ("mcd_read_mem", {"addr": "1000", "len": 4}), + ("mcd_step", {}), + ("mcd_run", {}), + ("mcd_set_bp", {"addr": "80000000"}), + ("mcd_clear_bp", {"addr": "80000000"}), + ("mcd_list_bp", {}), + ("mcd_wait_stop", {})): + r = m.tool(name, args) + check(r["result"]["isError"] is True, + f"{name} before connect should be an error") + check("not connected" in r["result"]["content"][0]["text"], + f"{name} should explain it is not connected: {r!r}") + + # Missing required argument is reported, not crashed. + r = m.tool("mcd_read_mem", {"addr": "1000"}) # no len + check(r["result"]["isError"] is True, "missing len should error") + check("len" in r["result"]["content"][0]["text"], "error should name the missing arg") + + r = m.tool("mcd_set_bp", {}) # no addr + check(r["result"]["isError"] is True, "missing addr should error") + check("addr" in r["result"]["content"][0]["text"], "error should name the missing arg") + + # Bad breakpoint type is rejected before the connection is checked. + r = m.tool("mcd_set_bp", {"addr": "80000000", "type": "bogus"}) + check(r["result"]["isError"] is True, "bad bp type should error") + check("breakpoint type" in r["result"]["content"][0]["text"], + f"error should explain the bad bp type: {r!r}") + + r = m.tool("nope", {}) + check(r["result"]["isError"] is True, "unknown tool should error") + check("unknown tool" in r["result"]["content"][0]["text"], "should say unknown tool") + + # Unknown method -> JSON-RPC error object (not a tool result). + r = m.rpc("bogus/method") + check(r.get("error", {}).get("code") == -32601, f"unknown method -> -32601: {r!r}") + + # Malformed JSON line -> parse error, id null. + r = m.rpc(None, raw="this is not json") + check(r.get("error", {}).get("code") == -32700, f"bad json -> -32700: {r!r}") + check(r.get("id") is None, "parse error id should be null") + + r = m.tool("mcd_status", {}) + check(json.loads(r["result"]["content"][0]["text"]) == {"connected": False}, + "server should still respond after a parse error") + + print(f"ALL {CHECKS} UNIT CHECKS PASSED") + finally: + m.close() + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("-m", "--mcp", required=True, help="Path to mcd_mcp binary") + args = ap.parse_args() + if not os.path.isfile(args.mcp): + print(f"ERROR: not found: {args.mcp}", file=sys.stderr) + return 1 + run(args.mcp) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 90e83a6d2968497baa43df5b233fc8f79ae371fe Mon Sep 17 00:00:00 2001 From: Mark Burton Date: Mon, 3 Aug 2026 17:14:46 +0200 Subject: [PATCH 3/4] mcd: per-core run, core state, register access and reset Fills in the core services the MCD API expects but the server did not provide. mcd_run_f/mcd_stop_f take a "global" flag, so RUN now accepts a core id and resumes just that core with "vCont;c:"; a new QRY_STATE reports each core running or halted. QRY_REGS reads the gdbstub's target description over qXfer and reports registers with their names, sizes, kinds and groups, so a debugger no longer has to know that 32 means pc. Groups are objects with ids, as mcd_register_group_st and mcd_register_info_st require, and a query can name one group to get just its registers, as mcd_qry_reg_map_f does. Registers are also addressable as memory, which is how the API models them: a "registers" memory space carries MCD_MEM_SPACE_IS_REGISTERS, a register's address is its number, and the core is selected by the address space id holding its hardware thread. READ_MEM and WRITE_MEM in that space go to the same register path as READ_REG and WRITE_REG, so either route sees the same bytes. RESET goes through the stub's monitor passthrough, keeping this module free of any QEMU dependency. QEMU's stub parses no packet while the VM runs, so requests that must reach a running instance halt it for the duration and restore exactly the cores that were running. MCD does not restrict register or memory access to halted cores, so that halt stays invisible to the client. Signed-off-by: Mark Burton --- systemc-components/mcd_mcp/README.md | 2 +- .../mcd_mcp/include/mcd_client.h | 71 +- .../mcd_mcp/include/mcd_debug.h | 53 +- systemc-components/mcd_mcp/src/mcd_client.c | 137 ++- systemc-components/mcd_mcp/src/mcd_debug.cc | 68 +- systemc-components/mcd_mcp/src/mcd_mcp.cc | 166 +++- .../mcd_server/include/mcd_server.h | 137 ++- .../mcd_server/src/mcd_server.cc | 876 ++++++++++++++++-- 8 files changed, 1407 insertions(+), 103 deletions(-) diff --git a/systemc-components/mcd_mcp/README.md b/systemc-components/mcd_mcp/README.md index 4dcbf4a9..8040e416 100644 --- a/systemc-components/mcd_mcp/README.md +++ b/systemc-components/mcd_mcp/README.md @@ -98,7 +98,7 @@ then `mcd_stop`, `mcd_read_reg`, `mcd_step`, `mcd_read_mem`, … to debug. | `mcd_status` | JSON summary of the connection and object model. | | `mcd_describe` | Human-readable topology table. | | `mcd_refresh` | Re-query the target's object model. | -| `mcd_read_mem` / `mcd_write_mem` | Memory access (hex dump / hex byte string), optional `space_id`. | +| `mcd_read_mem` / `mcd_write_mem` | Memory access (hex dump / hex byte string), optional `space_id` and `hw_thread`. The register space (`mem_type` `registers`) addresses registers by number, as MCD models them. | | `mcd_read_reg` / `mcd_write_reg` | Register access by GDB register number, optional `cpu_idx`. | | `mcd_regs_dump` | Dump a range of registers for a core. | | `mcd_run` / `mcd_stop` / `mcd_step` | Run control. | diff --git a/systemc-components/mcd_mcp/include/mcd_client.h b/systemc-components/mcd_mcp/include/mcd_client.h index 983c7357..6671849a 100644 --- a/systemc-components/mcd_mcp/include/mcd_client.h +++ b/systemc-components/mcd_mcp/include/mcd_client.h @@ -30,11 +30,59 @@ typedef struct { uint32_t device_id; } mcd_core_st; +/* Memory space kinds (mcd_mem_type_et), as reported in mem_type. */ +#define MCD_MEM_SPACE_DEFAULT 0x00000000 +#define MCD_MEM_SPACE_IS_REGISTERS 0x00000001 + +/* Interpretation of an address's addr_space_id (mcd_addr_space_type_et); only the + * two values the server uses. */ +#define MCD_NOTUSED_ID 0 /* no address space id */ +#define MCD_HW_THREAD_ID 4 /* the hw thread the address is valid in */ + +/* Id of the server's register memory space: registers are addressed as memory + * there, the address being the register number. */ +#define MCD_REG_SPACE_ID 0xFFFF0000u + typedef struct { uint32_t mem_space_id; char mem_space_name[64]; + uint32_t mem_type; /* MCD_MEM_SPACE_* */ } mcd_mem_space_st; +typedef struct { + uint32_t core_id; + uint8_t running; /* 0 => halted */ +} mcd_core_state_st; + +/* MCD_REG_NAME_LEN, including the terminating zero. */ +#define MCD_REG_NAME_LEN 32 + +/* One register as described by the target; group_id indexes the group table + * (mcd_reg_group_st). The wire strings are variable length; anything longer than + * these fields is truncated. address..addr_space_type are the register's + * mcd_addr_st: its number in the register memory space (MCD_REG_SPACE_ID), valid + * in the hw thread named by addr_space_id (MCD_HW_THREAD_ID). */ +typedef struct { + uint32_t regnum; + uint32_t group_id; + uint32_t bitsize; + uint32_t reg_type; /* mcd_reg_type_et: 0 simple, 1 compound, 2 partial */ + uint32_t hw_thread_id; /* hw thread the register belongs to, 0 if unassigned */ + uint64_t address; + uint32_t mem_space_id; + uint32_t addr_space_id; + uint32_t addr_space_type; /* MCD_NOTUSED_ID or MCD_HW_THREAD_ID */ + char name[MCD_REG_NAME_LEN]; +} mcd_reg_info_st; + +/* One register group. group_id is 1-based; n_registers is the group's own + * register count as reported by the target, independent of any filtering. */ +typedef struct { + uint32_t group_id; + char name[MCD_REG_NAME_LEN]; + uint32_t n_registers; +} mcd_reg_group_st; + /* Values match the mcd_server MCD_BP_* wire constants (and the GDB Z/z type * digit). */ typedef enum { @@ -82,12 +130,31 @@ int mcd_client_qry_systems(mcd_client_t* c, uint32_t* num, mcd_system_st* out); int mcd_client_qry_devices(mcd_client_t* c, uint32_t* num, mcd_device_st* out); int mcd_client_qry_cores(mcd_client_t* c, uint32_t* num, mcd_core_st* out); int mcd_client_qry_mem_spaces(mcd_client_t* c, uint32_t* num, mcd_mem_space_st* out); +/* Which cores are running right now; never waits. */ +int mcd_client_qry_state(mcd_client_t* c, uint32_t* num, mcd_core_state_st* out); +/* The target's register groups and registers, as reported by its debug + * description. group_id 0 asks for every group, non-zero for only that group's + * registers; the group table is always complete. *num_groups and *num_regs + * follow the in/out convention above, each bounding its own array. */ +int mcd_client_qry_regs(mcd_client_t* c, uint32_t cpu_idx, uint32_t group_id, uint32_t* num_groups, + mcd_reg_group_st* groups, uint32_t* num_regs, mcd_reg_info_st* regs); int mcd_client_run(mcd_client_t* c); +/* Resume one core, leaving the rest of its instance halted. */ +int mcd_client_run_core(mcd_client_t* c, uint32_t cpu_idx); int mcd_client_stop(mcd_client_t* c); int mcd_client_step(mcd_client_t* c, uint32_t cpu_idx); -int mcd_client_read_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, uint8_t* buf); -int mcd_client_write_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, const uint8_t* buf); +/* Reset the whole platform; the core list is re-enumerated afterwards. */ +int mcd_client_reset(mcd_client_t* c); +/* addr_space_id selects the address space the address is valid in; 0 means "not + * used", which the server reads as core 0's hw thread. For the register space + * (MCD_REG_SPACE_ID) it is the core's hw thread id and addr is a register + * number; the bytes are then the register values in target byte order, as + * mcd_client_read_reg reports them. */ +int mcd_client_read_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, uint32_t addr_space_id, + uint8_t* buf); +int mcd_client_write_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, uint32_t addr_space_id, + const uint8_t* buf); int mcd_client_read_reg(mcd_client_t* c, uint32_t cpu_idx, uint32_t regno, uint64_t* val); int mcd_client_write_reg(mcd_client_t* c, uint32_t cpu_idx, uint32_t regno, uint64_t val); diff --git a/systemc-components/mcd_mcp/include/mcd_debug.h b/systemc-components/mcd_mcp/include/mcd_debug.h index f35613ee..4297a032 100644 --- a/systemc-components/mcd_mcp/include/mcd_debug.h +++ b/systemc-components/mcd_mcp/include/mcd_debug.h @@ -19,6 +19,7 @@ namespace mcd { struct MemSpace { uint32_t id; std::string name; + uint32_t mem_type; // MCD_MEM_SPACE_*, e.g. MCD_MEM_SPACE_IS_REGISTERS }; struct CoreInfo { @@ -26,6 +27,39 @@ struct CoreInfo { uint32_t device_id; }; +struct CoreState { + uint32_t core_id; + bool running; // false => halted +}; + +struct RegInfo { + uint32_t regnum; + uint32_t group_id; + uint32_t bitsize; + uint32_t reg_type; // mcd_reg_type_et: 0 simple, 1 compound, 2 partial + uint32_t hw_thread_id; // hw thread the register belongs to, 0 if unassigned + // The register's mcd_addr_st: its number in the register memory space, valid + // in the hw thread named by addr_space_id. + uint64_t address; + uint32_t mem_space_id; + uint32_t addr_space_id; + uint32_t addr_space_type; // MCD_NOTUSED_ID or MCD_HW_THREAD_ID + std::string name; +}; + +struct RegGroup { + uint32_t group_id; + std::string name; + uint32_t n_registers; +}; + +// The target's register description: the complete group table, plus the +// registers selected by the group filter. +struct RegMap { + std::vector groups; + std::vector regs; +}; + // Mirrors mcd_bp_type_et on the wire. enum class BpType : uint32_t { SwBreak = 0, @@ -69,8 +103,12 @@ class Connection const std::vector& mem_spaces() const { return m_mem_spaces; } void refresh(); - // Live query, not cached. + // Live queries, not cached. std::vector list_breakpoints(); + std::vector core_states(); + + // Reset the platform, then re-query the (re-enumerated) topology. + void reset(); mcd_client_t* handle() const { return m_client; } @@ -96,6 +134,8 @@ class Core const CoreInfo& info() const; void run(); + // Resume only this core, leaving the rest of its instance halted. + void run_only(); void stop(); void step(); @@ -103,15 +143,20 @@ class Core uint64_t read_reg(uint32_t regno); void write_reg(uint32_t regno, uint64_t val); + // The target's description of its registers, in GDB register-number order. + // group_id 0 returns every group's registers, non-zero only that group's. + RegMap registers(uint32_t group_id = 0); + // kind is the length in bytes (0 => server default). void set_breakpoint(uint64_t addr, BpType type = BpType::SwBreak, uint32_t kind = 0); void clear_breakpoint(uint64_t addr, BpType type = BpType::SwBreak, uint32_t kind = 0); StopEvent wait_stop(uint32_t timeout_ms); - // space_id 0 = physical. - std::vector read_mem(uint64_t addr, uint32_t len, uint32_t space_id = 0); - void write_mem(uint64_t addr, const std::vector& data, uint32_t space_id = 0); + // space_id 0 = physical. addr_space_id 0 = "not used"; in the register space + // (MCD_REG_SPACE_ID) it is the core's hw thread id and addr a register number. + std::vector read_mem(uint64_t addr, uint32_t len, uint32_t space_id = 0, uint32_t addr_space_id = 0); + void write_mem(uint64_t addr, const std::vector& data, uint32_t space_id = 0, uint32_t addr_space_id = 0); // Single T at addr, little-endian. template diff --git a/systemc-components/mcd_mcp/src/mcd_client.c b/systemc-components/mcd_mcp/src/mcd_client.c index 020e3f5a..5e03fc30 100644 --- a/systemc-components/mcd_mcp/src/mcd_client.c +++ b/systemc-components/mcd_mcp/src/mcd_client.c @@ -397,21 +397,134 @@ int mcd_client_qry_mem_spaces(mcd_client_t* c, uint32_t* num, mcd_mem_space_st* uint32_t count = get_u32_le(&resp[0]); uint32_t cap = (num && out) ? *num : 0; uint32_t i; + /* Per space: [mem_space_id u32][mem_type u32][64-byte fixed name]. */ for (i = 0; i < count && i < cap; ++i) { - uint32_t off = 4 + i * (4 + 64); - if (off + 4 + 64 > blen) return -1; + uint32_t off = 4 + i * (4 + 4 + 64); + if (off + 4 + 4 + 64 > blen) return -1; out[i].mem_space_id = get_u32_le(&resp[off]); - memcpy(out[i].mem_space_name, &resp[off + 4], 64); + out[i].mem_type = get_u32_le(&resp[off + 4]); + memcpy(out[i].mem_space_name, &resp[off + 8], 64); out[i].mem_space_name[sizeof(out[i].mem_space_name) - 1] = '\0'; } if (num) *num = i; return 0; } +int mcd_client_qry_state(mcd_client_t* c, uint32_t* num, mcd_core_state_st* out) +{ + uint8_t resp[MCD_CLIENT_SCRATCH]; + uint32_t blen = 0; + if (transact(c, 0x13, NULL, 0, resp, sizeof(resp), &blen) != 0) return -1; + if (blen < 4) return -1; + + uint32_t count = get_u32_le(&resp[0]); + uint32_t cap = (num && out) ? *num : 0; + uint32_t i; + for (i = 0; i < count && i < cap; ++i) { + uint32_t off = 4 + i * 5; + if (off + 5 > blen) return -1; + out[i].core_id = get_u32_le(&resp[off]); + out[i].running = resp[off + 4]; + } + if (num) *num = i; + return 0; +} + +/* The register description is variable length and far larger than the fixed + * scratch buffers the other queries use, so the reply frame is read into a heap + * buffer bounded by MCD_MAX_FRAME. That buffer is freed here; the caller owns + * only @p groups and @p regs. */ +int mcd_client_qry_regs(mcd_client_t* c, uint32_t cpu_idx, uint32_t group_id, uint32_t* num_groups, + mcd_reg_group_st* groups, uint32_t* num_regs, mcd_reg_info_st* regs) +{ + uint8_t req[8]; + put_u32_le(&req[0], cpu_idx); + put_u32_le(&req[4], group_id); + + uint8_t* resp = (uint8_t*)malloc(MCD_MAX_FRAME); + if (!resp) return -1; + + uint32_t blen = 0; + if (transact(c, 0x32, req, sizeof(req), resp, MCD_MAX_FRAME, &blen) != 0 || blen < 4) { + free(resp); + return -1; + } + + /* [n_groups u32] { [group_id u32][n_registers u32][name_len u16][name] } + * [n_regs u32] { [regnum u32][group_id u32][bitsize u32][reg_type u32] + * [hw_thread_id u32][address u64][mem_space_id u32] + * [addr_space_id u32][addr_space_type u32][name_len u16][name] } + * The offset only advances by what has been validated against blen. */ + uint32_t gcount = get_u32_le(&resp[0]); + uint32_t gcap = (num_groups && groups) ? *num_groups : 0; + uint32_t off = 4; + uint32_t g; + for (g = 0; g < gcount; ++g) { + uint32_t name_len; + if (off + 10 > blen) break; + name_len = (uint32_t)resp[off + 8] | ((uint32_t)resp[off + 9] << 8); + if (off + 10 + name_len > blen) break; + + if (g < gcap) { + uint32_t n = name_len < sizeof(groups[g].name) - 1 ? name_len : (uint32_t)sizeof(groups[g].name) - 1; + groups[g].group_id = get_u32_le(&resp[off]); + groups[g].n_registers = get_u32_le(&resp[off + 4]); + memcpy(groups[g].name, &resp[off + 10], n); + groups[g].name[n] = '\0'; + } + off += 10 + name_len; + } + /* A short group table leaves the register count unreadable. */ + if (g < gcount || off + 4 > blen) { + free(resp); + return -1; + } + + uint32_t rcount = get_u32_le(&resp[off]); + uint32_t rcap = (num_regs && regs) ? *num_regs : 0; + off += 4; + uint32_t i; + for (i = 0; i < rcount; ++i) { + uint32_t name_len; + if (off + 42 > blen) break; + name_len = (uint32_t)resp[off + 40] | ((uint32_t)resp[off + 41] << 8); + if (off + 42 + name_len > blen) break; + + if (i < rcap) { + uint32_t n = name_len < sizeof(regs[i].name) - 1 ? name_len : (uint32_t)sizeof(regs[i].name) - 1; + regs[i].regnum = get_u32_le(&resp[off]); + regs[i].group_id = get_u32_le(&resp[off + 4]); + regs[i].bitsize = get_u32_le(&resp[off + 8]); + regs[i].reg_type = get_u32_le(&resp[off + 12]); + regs[i].hw_thread_id = get_u32_le(&resp[off + 16]); + regs[i].address = get_u64_le(&resp[off + 20]); + regs[i].mem_space_id = get_u32_le(&resp[off + 28]); + regs[i].addr_space_id = get_u32_le(&resp[off + 32]); + regs[i].addr_space_type = get_u32_le(&resp[off + 36]); + memcpy(regs[i].name, &resp[off + 42], n); + regs[i].name[n] = '\0'; + } + off += 42 + name_len; + } + free(resp); + + /* Report what was written, not what the server claimed. */ + if (num_groups) *num_groups = g < gcap ? g : gcap; + if (num_regs) *num_regs = i < rcap ? i : rcap; + return 0; +} + /* Run control returns no payload, so capacity 0: any payload the server does * send is a protocol error. */ int mcd_client_run(mcd_client_t* c) { return transact(c, 0x10, NULL, 0, NULL, 0, NULL); } +int mcd_client_run_core(mcd_client_t* c, uint32_t cpu_idx) +{ + uint8_t req[4]; + put_u32_le(req, cpu_idx); + return transact(c, 0x10, req, sizeof(req), NULL, 0, NULL); +} + int mcd_client_stop(mcd_client_t* c) { return transact(c, 0x11, NULL, 0, NULL, 0, NULL); } int mcd_client_step(mcd_client_t* c, uint32_t cpu_idx) @@ -421,15 +534,19 @@ int mcd_client_step(mcd_client_t* c, uint32_t cpu_idx) return transact(c, 0x12, req, sizeof(req), NULL, 0, NULL); } -int mcd_client_read_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, uint8_t* buf) +int mcd_client_reset(mcd_client_t* c) { return transact(c, 0x14, NULL, 0, NULL, 0, NULL); } + +int mcd_client_read_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, uint32_t addr_space_id, + uint8_t* buf) { if (len > MCD_MAX_FRAME) return -1; if (len && !buf) return -1; - uint8_t req[16]; + uint8_t req[20]; put_u64_le(&req[0], addr); put_u32_le(&req[8], len); put_u32_le(&req[12], space_id); + put_u32_le(&req[16], addr_space_id); /* The reply payload is exactly @p len bytes, written into the caller's * buffer, which is therefore also the capacity bound. */ @@ -439,19 +556,21 @@ int mcd_client_read_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t s return 0; } -int mcd_client_write_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, const uint8_t* buf) +int mcd_client_write_mem(mcd_client_t* c, uint64_t addr, uint32_t len, uint32_t space_id, uint32_t addr_space_id, + const uint8_t* buf) { - /* Bound len so the 16 + len payload length cannot wrap. */ + /* Bound len so the 20 + len payload length cannot wrap. */ if (len > MCD_MAX_FRAME) return -1; if (len && !buf) return -1; - uint32_t plen = 16 + len; + uint32_t plen = 20 + len; uint8_t* req = (uint8_t*)malloc(plen); if (!req) return -1; put_u64_le(&req[0], addr); put_u32_le(&req[8], len); put_u32_le(&req[12], space_id); - if (len) memcpy(&req[16], buf, len); + put_u32_le(&req[16], addr_space_id); + if (len) memcpy(&req[20], buf, len); int rc = transact(c, 0x21, req, plen, NULL, 0, NULL); free(req); diff --git a/systemc-components/mcd_mcp/src/mcd_debug.cc b/systemc-components/mcd_mcp/src/mcd_debug.cc index 7583b9f6..352261dc 100644 --- a/systemc-components/mcd_mcp/src/mcd_debug.cc +++ b/systemc-components/mcd_mcp/src/mcd_debug.cc @@ -11,7 +11,10 @@ namespace mcd { namespace { constexpr uint32_t k_max_records = 256; -} +/* A gdbstub target description runs to hundreds of registers, well past + * k_max_records, and each record is too large for a stack array of that many. */ +constexpr uint32_t k_max_regs = 4096; +} // namespace Connection::Connection(const std::string& host, uint16_t port): m_client(nullptr), m_host(host), m_port(port) { @@ -81,7 +84,7 @@ void Connection::populate() throw std::runtime_error("mcd: qry_mem_spaces failed"); } for (uint32_t i = 0; i < num && i < tmp.size(); ++i) { - m_mem_spaces.push_back(MemSpace{ tmp[i].mem_space_id, tmp[i].mem_space_name }); + m_mem_spaces.push_back(MemSpace{ tmp[i].mem_space_id, tmp[i].mem_space_name, tmp[i].mem_type }); } } } @@ -100,6 +103,29 @@ std::vector Connection::list_breakpoints() return out; } +std::vector Connection::core_states() +{ + std::array tmp{}; + uint32_t num = tmp.size(); + if (mcd_client_qry_state(m_client, &num, tmp.data()) != 0) { + throw std::runtime_error("mcd: qry_state failed"); + } + std::vector out; + for (uint32_t i = 0; i < num && i < tmp.size(); ++i) { + out.push_back(CoreState{ tmp[i].core_id, tmp[i].running != 0 }); + } + return out; +} + +void Connection::reset() +{ + if (mcd_client_reset(m_client) != 0) { + throw std::runtime_error("mcd: reset failed"); + } + /* The server dropped its core list, so the cached topology is stale. */ + populate(); +} + const CoreInfo& Core::info() const { const auto& cores = m_conn.cores(); @@ -116,6 +142,13 @@ void Core::run() } } +void Core::run_only() +{ + if (mcd_client_run_core(m_conn.handle(), m_idx) != 0) { + throw std::runtime_error("mcd: run of core " + std::to_string(m_idx) + " failed"); + } +} + void Core::stop() { if (mcd_client_stop(m_conn.handle()) != 0) { @@ -146,6 +179,27 @@ void Core::write_reg(uint32_t regno, uint64_t val) } } +RegMap Core::registers(uint32_t group_id) +{ + std::vector gtmp(k_max_records); + std::vector tmp(k_max_regs); + uint32_t n_groups = static_cast(gtmp.size()); + uint32_t num = static_cast(tmp.size()); + if (mcd_client_qry_regs(m_conn.handle(), m_idx, group_id, &n_groups, gtmp.data(), &num, tmp.data()) != 0) { + throw std::runtime_error("mcd: qry_regs failed"); + } + RegMap out; + for (uint32_t i = 0; i < n_groups && i < gtmp.size(); ++i) { + out.groups.push_back(RegGroup{ gtmp[i].group_id, gtmp[i].name, gtmp[i].n_registers }); + } + for (uint32_t i = 0; i < num && i < tmp.size(); ++i) { + out.regs.push_back(RegInfo{ tmp[i].regnum, tmp[i].group_id, tmp[i].bitsize, tmp[i].reg_type, + tmp[i].hw_thread_id, tmp[i].address, tmp[i].mem_space_id, tmp[i].addr_space_id, + tmp[i].addr_space_type, tmp[i].name }); + } + return out; +} + void Core::set_breakpoint(uint64_t addr, BpType type, uint32_t kind) { if (mcd_client_set_bp(m_conn.handle(), m_idx, static_cast(type), addr, kind) != 0) { @@ -169,19 +223,19 @@ StopEvent Core::wait_stop(uint32_t timeout_ms) return StopEvent{ st.stopped != 0, st.reason, st.watch_addr }; } -std::vector Core::read_mem(uint64_t addr, uint32_t len, uint32_t space_id) +std::vector Core::read_mem(uint64_t addr, uint32_t len, uint32_t space_id, uint32_t addr_space_id) { std::vector buf(len); - if (len && mcd_client_read_mem(m_conn.handle(), addr, len, space_id, buf.data()) != 0) { + if (len && mcd_client_read_mem(m_conn.handle(), addr, len, space_id, addr_space_id, buf.data()) != 0) { throw std::runtime_error("mcd: read_mem failed"); } return buf; } -void Core::write_mem(uint64_t addr, const std::vector& data, uint32_t space_id) +void Core::write_mem(uint64_t addr, const std::vector& data, uint32_t space_id, uint32_t addr_space_id) { - if (!data.empty() && - mcd_client_write_mem(m_conn.handle(), addr, static_cast(data.size()), space_id, data.data()) != 0) { + if (!data.empty() && mcd_client_write_mem(m_conn.handle(), addr, static_cast(data.size()), space_id, + addr_space_id, data.data()) != 0) { throw std::runtime_error("mcd: write_mem failed"); } } diff --git a/systemc-components/mcd_mcp/src/mcd_mcp.cc b/systemc-components/mcd_mcp/src/mcd_mcp.cc index 3dc3d523..6c2d24d0 100644 --- a/systemc-components/mcd_mcp/src/mcd_mcp.cc +++ b/systemc-components/mcd_mcp/src/mcd_mcp.cc @@ -344,6 +344,33 @@ mcd::Core core(uint32_t idx = 0) return mcd::Core(*g_conn, idx); } +// mcd_mem_type_et as a word. The server reports only the default and the +// register space; a combination of other bits prints as its value. +const char* mem_type_name(uint32_t t) +{ + switch (t) { + case MCD_MEM_SPACE_DEFAULT: + return "default"; + case MCD_MEM_SPACE_IS_REGISTERS: + return "registers"; + default: + return "other"; + } +} + +// mcd_addr_space_type_et as a word: how addr_space_id is to be read. +const char* addr_space_type_name(uint32_t t) +{ + switch (t) { + case MCD_NOTUSED_ID: + return "notused"; + case MCD_HW_THREAD_ID: + return "hw_thread"; + default: + return "?"; + } +} + std::string status_json() { if (!g_conn) return "{\"connected\":false}"; @@ -368,8 +395,9 @@ std::string status_json() o += "],\"mem_spaces\":["; for (size_t k = 0; k < g_conn->mem_spaces().size(); ++k) { if (k) o += ","; - o += "{\"id\":" + std::to_string(g_conn->mem_spaces()[k].id) + ",\"name\":\"" + - json_escape(g_conn->mem_spaces()[k].name) + "\"}"; + const mcd::MemSpace& ms = g_conn->mem_spaces()[k]; + o += "{\"id\":" + std::to_string(ms.id) + ",\"name\":\"" + json_escape(ms.name) + + "\",\"mem_type\":" + std::to_string(ms.mem_type) + ",\"type\":\"" + mem_type_name(ms.mem_type) + "\"}"; } o += "]}"; return o; @@ -419,7 +447,10 @@ std::string describe_text() } o += "mem_spaces (" + std::to_string(g_conn->mem_spaces().size()) + "):\n"; for (size_t i = 0; i < g_conn->mem_spaces().size(); ++i) { - o += " id=" + std::to_string(g_conn->mem_spaces()[i].id) + " name=" + g_conn->mem_spaces()[i].name + "\n"; + const mcd::MemSpace& ms = g_conn->mem_spaces()[i]; + char line[64]; + std::snprintf(line, sizeof(line), " mem_type=0x%x (%s)\n", ms.mem_type, mem_type_name(ms.mem_type)); + o += " id=" + std::to_string(ms.id) + " name=" + ms.name + line; } return o; } @@ -437,6 +468,73 @@ std::string regs_dump_text(uint32_t cpu_idx, uint32_t start_regno, uint32_t coun return o; } +std::string core_state_text() +{ + if (!g_conn) throw std::runtime_error("not connected (call mcd_connect first)"); + std::vector st = g_conn->core_states(); + if (st.empty()) return "no cores"; + std::string o = "core state (" + std::to_string(st.size()) + "):\n"; + char line[48]; + for (const auto& s : st) { + std::snprintf(line, sizeof(line), " cpu%-3u %s\n", s.core_id, s.running ? "running" : "halted"); + o += line; + } + return o; +} + +/* mcd_reg_type_et as a word. The server reports only simple and compound: gdb + * expresses a partial register as a of a composite type. */ +const char* reg_type_name(uint32_t t) +{ + switch (t) { + case 0: + return "simple"; + case 1: + return "compound"; + case 2: + return "partial"; + default: + return "?"; + } +} + +std::string reg_names_text(uint32_t cpu_idx, uint32_t group_id) +{ + mcd::RegMap m = core(cpu_idx).registers(group_id); + std::string o = "register groups cpu" + std::to_string(cpu_idx) + " (" + std::to_string(m.groups.size()) + "):\n"; + char line[224]; + for (const auto& g : m.groups) { + std::snprintf(line, sizeof(line), " g%-3u %-20s %4u regs\n", g.group_id, g.name.c_str(), g.n_registers); + o += line; + } + o += "registers cpu" + std::to_string(cpu_idx); + if (group_id) o += " group " + std::to_string(group_id); + o += " (" + std::to_string(m.regs.size()) + "):\n"; + for (const auto& r : m.regs) { + /* Show the group by name; an id absent from the table prints as its id. */ + std::string gname = "g" + std::to_string(r.group_id); + for (const auto& g : m.groups) { + if (g.group_id == r.group_id) { + gname = g.name; + break; + } + } + /* hw_thread_id is 0 when the target assigns the register to no hw thread. */ + char hwt[24] = ""; + if (r.hw_thread_id) std::snprintf(hwt, sizeof(hwt), " hwthread=%u", r.hw_thread_id); + /* The register's mcd_addr_st: reachable as memory in mem_space, at address, + * in the address space addr_space of the named type. */ + std::snprintf(line, sizeof(line), + " r%-4u %-20s %3u bits %-8s %-20s%s address=%llu mem_space=%u addr_space=%u " + "addr_space_type=%s\n", + r.regnum, r.name.c_str(), r.bitsize, reg_type_name(r.reg_type), gname.c_str(), hwt, + static_cast(r.address), r.mem_space_id, r.addr_space_id, + addr_space_type_name(r.addr_space_type)); + o += line; + } + return o; +} + mcd::BpType parse_bp_type(const std::string& s) { if (s == "sw" || s == "swbreak" || s == "break" || s == "breakpoint") return mcd::BpType::SwBreak; @@ -503,6 +601,12 @@ std::string call_tool(const std::string& name, const JsonValue* args) return status_json(); } if (name == "mcd_run") { + // No cpu_idx: resume everything, which is what a debugger's "continue" is. + if (args && args->find("cpu_idx")) { + uint32_t cpu_idx = static_cast(opt_int(args, "cpu_idx", 0)); + core(cpu_idx).run_only(); + return "ok (cpu" + std::to_string(cpu_idx) + " only)"; + } core().run(); return "ok"; } @@ -514,6 +618,18 @@ std::string call_tool(const std::string& name, const JsonValue* args) core(static_cast(opt_int(args, "cpu_idx", 0))).step(); return "ok"; } + if (name == "mcd_core_state") { + return core_state_text(); + } + if (name == "mcd_reset") { + if (!g_conn) throw std::runtime_error("not connected (call mcd_connect first)"); + g_conn->reset(); + return "reset done, " + std::to_string(g_conn->cores().size()) + " cores re-enumerated"; + } + if (name == "mcd_reg_names") { + return reg_names_text(static_cast(opt_int(args, "cpu_idx", 0)), + static_cast(opt_int(args, "group", 0))); + } if (name == "mcd_read_reg") { uint32_t cpu_idx = static_cast(opt_int(args, "cpu_idx", 0)); uint64_t v = core(cpu_idx).read_reg(static_cast(req_int(args, "regno"))); @@ -528,17 +644,22 @@ std::string call_tool(const std::string& name, const JsonValue* args) core(cpu_idx).write_reg(regno, val); return "ok"; } + /* space_id selects the memory space (0 = physical), hw_thread the address space + * the address is valid in: for the register space that is the core's hw thread + * id and addr is a register number. */ if (name == "mcd_read_mem") { uint64_t addr = parse_u64(req_str(args, "addr"), 16); uint32_t len = static_cast(req_int(args, "len")); uint32_t space = static_cast(opt_int(args, "space_id", 0)); - return hex_dump(addr, core().read_mem(addr, len, space)); + uint32_t hw_thread = static_cast(opt_int(args, "hw_thread", 0)); + return hex_dump(addr, core().read_mem(addr, len, space, hw_thread)); } if (name == "mcd_write_mem") { uint64_t addr = parse_u64(req_str(args, "addr"), 16); std::vector data = parse_hex_bytes(req_str(args, "data")); uint32_t space = static_cast(opt_int(args, "space_id", 0)); - core().write_mem(addr, data, space); + uint32_t hw_thread = static_cast(opt_int(args, "hw_thread", 0)); + core().write_mem(addr, data, space, hw_thread); return "ok"; } if (name == "mcd_refresh") { @@ -659,14 +780,28 @@ const char* k_tools_list = "\"port\":{\"type\":\"integer\"}},\"required\":[\"host\"]}}," "{\"name\":\"mcd_disconnect\",\"description\":\"Close the active connection.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," - "{\"name\":\"mcd_status\",\"description\":\"JSON summary of connection, systems, devices, cores, mem_spaces.\"," - "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," - "{\"name\":\"mcd_run\",\"description\":\"Resume the target.\"," + "{\"name\":\"mcd_status\",\"description\":\"JSON summary of connection, systems, devices, cores, and mem_spaces " + "with each space's type ('registers' for the space that addresses registers as memory).\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," + "{\"name\":\"mcd_run\",\"description\":\"Resume the target. With cpu_idx, resume only that core and leave the " + "others halted.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"cpu_idx\":{\"type\":\"integer\"}}}}," "{\"name\":\"mcd_stop\",\"description\":\"Halt the target.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," "{\"name\":\"mcd_step\",\"description\":\"Single-step one core.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"cpu_idx\":{\"type\":\"integer\"}}}}," + "{\"name\":\"mcd_core_state\",\"description\":\"Which cores are running and which are halted. Returns " + "immediately; never waits for a stop.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," + "{\"name\":\"mcd_reset\",\"description\":\"Reset the platform and re-enumerate its cores.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," + "{\"name\":\"mcd_reg_names\",\"description\":\"Table of the target's register groups, then its register names, GDB " + "register numbers, widths, kind (simple or compound) and group names for a core (default cpu0), plus the hw thread " + "id where the target assigns one and the register's address in the register memory space (mem_space, addr_space, " + "addr_space_type), which mcd_read_mem/mcd_write_mem accept. Use it to find the regno for mcd_read_reg. " + "group is a group id from the group table; it lists only that group's registers.\"," + "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"cpu_idx\":{\"type\":\"integer\"}," + "\"group\":{\"type\":\"integer\"}}}}," "{\"name\":\"mcd_read_reg\",\"description\":\"Read a GDB register by number for a core (default cpu0); returns " "0x-prefixed hex. Register numbering follows the target's GDB layout (AArch64: x0..x30=0..30, sp=31, pc=32).\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"cpu_idx\":{\"type\":\"integer\"},\"regno\":{\"type\":" @@ -677,18 +812,23 @@ const char* k_tools_list = "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"cpu_idx\":{\"type\":\"integer\"},\"regno\":{\"type\":" "\"integer\"}," "\"value\":{\"type\":\"string\"}},\"required\":[\"regno\",\"value\"]}}," - "{\"name\":\"mcd_read_mem\",\"description\":\"Read memory; returns a hex dump.\"," + "{\"name\":\"mcd_read_mem\",\"description\":\"Read memory; returns a hex dump. space_id picks the memory space " + "from mcd_status (default 0, physical). The register space (type 'registers') addresses registers as memory: addr " + "is then the register number and hw_thread the core's hw thread id from mcd_reg_names (default 0, meaning cpu0); " + "len must cover whole registers.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"addr\":{\"type\":\"string\"}," - "\"len\":{\"type\":\"integer\"},\"space_id\":{\"type\":\"integer\"}}," + "\"len\":{\"type\":\"integer\"},\"space_id\":{\"type\":\"integer\"},\"hw_thread\":{\"type\":\"integer\"}}," "\"required\":[\"addr\",\"len\"]}}," - "{\"name\":\"mcd_write_mem\",\"description\":\"Write memory from a hex byte string.\"," + "{\"name\":\"mcd_write_mem\",\"description\":\"Write memory from a hex byte string. space_id and hw_thread are as " + "for mcd_read_mem, so writing to the register space writes registers, the data being whole register values in " + "target byte order.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"addr\":{\"type\":\"string\"}," - "\"data\":{\"type\":\"string\"},\"space_id\":{\"type\":\"integer\"}}," + "\"data\":{\"type\":\"string\"},\"space_id\":{\"type\":\"integer\"},\"hw_thread\":{\"type\":\"integer\"}}," "\"required\":[\"addr\",\"data\"]}}," "{\"name\":\"mcd_refresh\",\"description\":\"Re-query the target and return the updated status.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," "{\"name\":\"mcd_describe\",\"description\":\"Human-readable topology table: host, systems, devices, cores, " - "mem_spaces.\"," + "mem_spaces with their types.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}," "{\"name\":\"mcd_regs_dump\",\"description\":\"Dump a range of GDB registers for a core (default: r0..r31 of " "cpu0).\"," diff --git a/systemc-components/mcd_server/include/mcd_server.h b/systemc-components/mcd_server/include/mcd_server.h index a9e025df..160918ae 100644 --- a/systemc-components/mcd_server/include/mcd_server.h +++ b/systemc-components/mcd_server/include/mcd_server.h @@ -74,6 +74,7 @@ typedef struct mcd_core_st { struct mcd_mem_space_st { uint32_t mem_space_id; char mem_space_name[64]; + uint32_t mem_type; /* MCD_MEM_SPACE_* */ }; typedef enum { @@ -89,10 +90,13 @@ typedef enum { #define MCD_OP_RUN 0x10 #define MCD_OP_STOP 0x11 #define MCD_OP_STEP 0x12 +#define MCD_OP_QRY_STATE 0x13 +#define MCD_OP_RESET 0x14 #define MCD_OP_READ_MEM 0x20 #define MCD_OP_WRITE_MEM 0x21 #define MCD_OP_READ_REG 0x30 #define MCD_OP_WRITE_REG 0x31 +#define MCD_OP_QRY_REGS 0x32 #define MCD_OP_SET_BP 0x40 #define MCD_OP_CLR_BP 0x41 #define MCD_OP_LIST_BP 0x42 @@ -105,6 +109,27 @@ typedef enum { #define MCD_BP_WATCH_READ 3 /* read watchpoint (Z3) */ #define MCD_BP_WATCH_ACCESS 4 /* access watchpoint (Z4) */ +/* Memory space kinds reported by MCD_OP_QRY_MEM_SPACES; values match + * mcd_mem_type_et. */ +#define MCD_MEM_SPACE_DEFAULT 0x00000000 /* none of the types below */ +#define MCD_MEM_SPACE_IS_REGISTERS 0x00000001 /* the space contains only registers */ + +/* Interpretation of mcd_addr_st.addr_space_id; values match + * mcd_addr_space_type_et, of which only these two are used. */ +#define MCD_NOTUSED_ID 0 /* no address space id */ +#define MCD_HW_THREAD_ID 4 /* the hw thread the address is valid in */ + +/* Id of the register memory space. MCD models a register that is not memory + * mapped as memory in a space of type MCD_MEM_SPACE_IS_REGISTERS, addressed by + * register number. Router spaces are numbered up from 0 as they are bound, so a + * high sentinel cannot collide with one. */ +#define MCD_REG_SPACE_ID 0xFFFF0000u + +/* Register kinds reported by MCD_OP_QRY_REGS; values match mcd_reg_type_et. */ +#define MCD_REG_TYPE_SIMPLE 0 /* a plain register */ +#define MCD_REG_TYPE_COMPOUND 1 /* built from other types (gdb vector/union/struct) */ +#define MCD_REG_TYPE_PARTIAL 2 /* a sub-field of another register */ + /* Stop reasons reported by MCD_OP_WAIT_STOP. */ #define MCD_STOP_RUNNING 0 /* timed out; target still running */ #define MCD_STOP_HALTED 1 /* already halted (nothing was running) */ @@ -127,6 +152,11 @@ class mcd_server : public sc_core::sc_module public: cci::cci_param p_mcd_port; + /* Interface to listen on: "127.0.0.1" (default), an address, or "*" for all. + * This port is unauthenticated access to all memory and registers. */ + cci::cci_param p_mcd_host; + /* Block in start_of_simulation until a client connects, like QEMU's -S. */ + cci::cci_param p_wait_for_client; mcd_server(const sc_core::sc_module_name& name); ~mcd_server(); @@ -145,6 +175,7 @@ class mcd_server : public sc_core::sc_module void before_end_of_elaboration() override; void end_of_elaboration() override; + void start_of_simulation() override; private: mcd_return_et mcd_qry_servers(uint32_t* num_servers, mcd_server_st* servers); @@ -170,8 +201,11 @@ class mcd_server : public sc_core::sc_module mcd_return_et op_run(const std::vector& req, std::vector& resp); mcd_return_et op_stop(const std::vector& req, std::vector& resp); mcd_return_et op_step(const std::vector& req, std::vector& resp); + mcd_return_et op_qry_state(const std::vector& req, std::vector& resp); + mcd_return_et op_reset(const std::vector& req, std::vector& resp); mcd_return_et op_read_reg(const std::vector& req, std::vector& resp); mcd_return_et op_write_reg(const std::vector& req, std::vector& resp); + mcd_return_et op_qry_regs(const std::vector& req, std::vector& resp); mcd_return_et op_set_bp(const std::vector& req, std::vector& resp); mcd_return_et op_clr_bp(const std::vector& req, std::vector& resp); mcd_return_et op_list_bp(const std::vector& req, std::vector& resp); @@ -228,6 +262,103 @@ class mcd_server : public sc_core::sc_module }; std::vector m_cores; + /* Per-core run state, parallel to m_cores: QEMU's run control is instance + * wide except for a vCont naming individual threads, so this is the only + * record of a single core having been resumed. Guarded by m_gdb_mutex. */ + std::vector m_core_running; + + /* Both require m_gdb_mutex. */ + void set_inst_cores_running(uint32_t inst, bool running); + bool inst_all_cores_running(uint32_t inst); + + /* Register description of one core, from the gdbstub's target XML. Cached per + * instance, since every core of an instance has the same layout. Groups are + * objects with ids, as mcd_register_group_st/mcd_register_info_st require: + * a register carries its group's id, not a name. Id 0 is reserved by MCD, so + * ids run from 1. hw_thread_id is not cached here: it is per core, this is per + * instance. Guarded by m_gdb_mutex. */ + struct reg_t { + uint32_t regnum; + uint32_t group_id; + uint32_t bitsize; + uint32_t reg_type; /* MCD_REG_TYPE_* */ + std::string name; + }; + struct reg_group_t { + uint32_t group_id; + std::string name; + uint32_t n_registers; + }; + struct inst_regs_t { + std::vector groups; + std::vector regs; + }; + std::vector m_inst_regs; + + /* Description of register @p regnum of instance @p inst, or null. Caller must + * hold m_gdb_mutex and have called ensure_regs_known(). */ + const reg_t* find_reg(uint32_t inst, uint32_t regnum) const; + + /* One register of @p core over the RSP session. The value is the register in + * target byte order, i.e. the bytes the stub hex-encodes. Caller must hold + * m_gdb_mutex and keep the instance halted (scoped_halt): the stub parses no + * packet while the VM runs. */ + bool gdb_read_reg(uint32_t core, uint32_t regno, std::vector& value); + bool gdb_write_reg(uint32_t core, uint32_t regno, const uint8_t* value, uint32_t len); + + /* One access to the register memory space (MCD_REG_SPACE_ID): @p address is a + * register number, @p addr_space_id the gdb thread id of the core it is valid + * in (0 => core 0), and @p length must cover whole registers. @p data holds the + * values to write, or receives the values read, in the byte order READ_REG + * reports. Takes m_gdb_mutex itself. */ + mcd_return_et access_reg_space(bool write, uint64_t address, uint32_t length, uint32_t addr_space_id, + std::vector& data); + /* Core index for gdb thread id @p addr_space_id, 0 meaning core 0. */ + bool reg_space_core(uint32_t addr_space_id, uint32_t& core); + /* Split @p length bytes from register number @p regno into whole registers, + * using the widths of the target description. Fails unless they match exactly. */ + struct reg_span_t { + uint32_t regno; + uint32_t bytes; + }; + bool reg_space_split(uint32_t inst, uint32_t regno, uint32_t length, std::vector& out); + + /* QEMU's gdbstub parses no packet while the VM runs: any byte other than the + * 0x03 interrupt makes gdb_read_byte() vm_stop() and answer nothing at all. A + * request that has to reach a running instance therefore halts it first. + * gdb_halt() returns true if this call did the halt, i.e. gdb_resume() is owed. + * Both need m_gdb_mutex. */ + bool gdb_halt(uint32_t idx); + void gdb_resume(uint32_t idx, const std::vector& was_running); + + /* RAII form of the above, for requests with several exit paths: halts on entry + * if needed, restores the running set on exit. MCD does not restrict register + * or memory access to halted cores (mcd_execute_txlist_f has no such + * precondition), so the halt is transparent to the client. exclude() drops a + * core from the set to restore, for STEP, which leaves its core halted. */ + class scoped_halt + { + public: + scoped_halt(mcd_server& server, uint32_t idx); + ~scoped_halt(); + void exclude(uint32_t core); + + private: + mcd_server& m_server; + uint32_t m_idx; + bool m_halted; + std::vector m_was_running; + }; + + /* Fetch and parse the target XML for instance @p idx, once. Caller must hold + * m_gdb_mutex, as for gdb_qxfer() and gdb_monitor(). */ + void ensure_regs_known(uint32_t idx); + /* Read an "qXfer:features:read:" object in 0x400-byte chunks. Returns + * the concatenated payload, or "" if the stub refuses or the transport fails. */ + std::string gdb_qxfer(uint32_t idx, const std::string& annex); + /* Run @p command through the stub's HMP monitor as "qRcmd,". */ + bool gdb_monitor(uint32_t idx, const std::string& command); + /* Last stop reported by an instance, cached until the core it belongs to asks: * a stop arrives once per session, but WAIT_STOP is per core and the stopping * core need not be the one waited on. Guarded by m_gdb_mutex. */ @@ -274,11 +405,13 @@ class mcd_server : public sc_core::sc_module bool gdb_cmd(uint32_t idx, const std::string& cmd, std::string& reply, bool wait_reply = true); /* m_mem_spaces keeps registration order for query; m_transactors maps space_id - * to a callable issuing a transport_dbg and returning the bytes handled. */ + * to a callable issuing a transport_dbg and returning the bytes handled. The + * register space has no transactor: it is served over the RSP session. */ std::vector m_mem_spaces; std::map> m_transactors; - void add_mem_space(uint32_t space_id, const std::string& name, + /* A null @p fn registers a space with no transactor. */ + void add_mem_space(uint32_t space_id, const std::string& name, uint32_t mem_type, std::function fn); }; diff --git a/systemc-components/mcd_server/src/mcd_server.cc b/systemc-components/mcd_server/src/mcd_server.cc index 726b0bec..567f448d 100644 --- a/systemc-components/mcd_server/src/mcd_server.cc +++ b/systemc-components/mcd_server/src/mcd_server.cc @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -99,6 +100,19 @@ static void put_u32(std::vector& v, uint32_t x) v.push_back(static_cast((x >> 24) & 0xff)); } +static void put_u64(std::vector& v, uint64_t x) +{ + for (int i = 0; i < 8; ++i) { + v.push_back(static_cast((x >> (8 * i)) & 0xff)); + } +} + +static void put_u16(std::vector& v, uint16_t x) +{ + v.push_back(static_cast(x & 0xff)); + v.push_back(static_cast((x >> 8) & 0xff)); +} + static uint32_t get_u32(const uint8_t* p) { return static_cast(p[0]) | (static_cast(p[1]) << 8) | (static_cast(p[2]) << 16) | @@ -381,9 +395,66 @@ static uint32_t gdb_classify_stop(const std::string& pkt, uint64_t& watch_addr) return (sig == 5) ? MCD_STOP_BREAK : MCD_STOP_SIGNAL; /* 5 = SIGTRAP */ } +/* Value of attribute @p key inside the element text @p el, or "". The key must be + * a whole attribute name, so "num" does not match "regnum". */ +static std::string xml_attr(const std::string& el, const char* key) +{ + const std::string pat = std::string(key) + "=\""; + std::string::size_type pos = 0; + while ((pos = el.find(pat, pos)) != std::string::npos) { + bool at_start = (pos == 0); + bool preceded_by_space = !at_start && std::isspace(static_cast(el[pos - 1])); + if (at_start || preceded_by_space) { + std::string::size_type beg = pos + pat.size(); + std::string::size_type end = el.find('"', beg); + if (end == std::string::npos) return std::string(); + return el.substr(beg, end - beg); + } + pos += pat.size(); + } + return std::string(); +} + +/* Collect the annexes named by : QEMU's target.xml lists + * its feature files rather than inlining them. */ +static void xml_includes(const std::string& xml, std::vector& out, size_t cap) +{ + std::string::size_type pos = 0; + while (out.size() < cap && (pos = xml.find("href=\"", pos)) != std::string::npos) { + pos += 6; + std::string::size_type end = xml.find('"', pos); + if (end == std::string::npos) return; + std::string href = xml.substr(pos, end - pos); + if (!href.empty()) out.push_back(href); + pos = end + 1; + } +} + +/* Collect the ids of the composite types a feature declares: , + * , . A whose type= names one of them is a + * compound register; anything else is one of gdb's primitive types. */ +static void xml_composite_types(const std::string& xml, std::vector& out, size_t cap) +{ + static const char* const kinds[] = { "', pos); + if (end == std::string::npos) break; + std::string id = xml_attr(xml.substr(pos, end - pos), "id"); + if (!id.empty()) out.push_back(id); + pos = end + 1; + } + } +} + mcd_server::mcd_server(const sc_core::sc_module_name& name) : sc_core::sc_module(name) , p_mcd_port("mcd_port", 1235, "MCD server TCP port") + , p_mcd_host("mcd_host", "127.0.0.1", + "Interface to listen on: an address, or \"*\" for all interfaces. This port grants " + "unauthenticated access to all memory and registers") + , p_wait_for_client("wait_for_client", false, "Wait for an MCD client to connect before starting the simulation") , m_listen_fd(INVALID_SOCK) , m_running(false) , m_client_fd(INVALID_SOCK) @@ -435,6 +506,7 @@ void mcd_server::bind_instance(sc_core::sc_object* inst, const std::string& gdb_ m_gdb_running.push_back(false); m_threads_known.push_back(false); m_last_stop.push_back(last_stop_t{}); + m_inst_regs.push_back(inst_regs_t{}); SCP_INFO(()) << "bind_instance[" << (m_insts.size() - 1) << "]: " << (inst ? inst->name() : "") << " gdb-rsp=" << gdb_hostport; } @@ -468,6 +540,7 @@ socket_t mcd_server::gdb_session(uint32_t idx) m_gdb_fds[idx] = fd; m_gdb_running[idx] = false; + set_inst_cores_running(idx, false); /* a reopened session keeps its core entries */ SCP_INFO(()) << "mcd_server: opened GDB-RSP session to " << m_gdb_ports[idx]; gdb_enumerate_cores(idx); @@ -533,6 +606,7 @@ void mcd_server::gdb_enumerate_cores(uint32_t idx) if (name.empty()) name = "core"; m_cores.push_back(core_t{ idx, tid, name }); + m_core_running.push_back(m_gdb_running[idx]); SCP_INFO(()) << "mcd_server: core " << (m_cores.size() - 1) << " = instance " << idx << " tid " << tid << " '" << name << "'"; } @@ -540,6 +614,23 @@ void mcd_server::gdb_enumerate_cores(uint32_t idx) m_threads_known[idx] = true; } +/* QEMU's vm_start/vm_stop cover every core of an instance, so a stop-reply or a + * global resume settles all of them at once. Caller must hold m_gdb_mutex. */ +void mcd_server::set_inst_cores_running(uint32_t inst, bool running) +{ + for (uint32_t i = 0; i < m_cores.size() && i < m_core_running.size(); ++i) { + if (m_cores[i].inst == inst) m_core_running[i] = running; + } +} + +bool mcd_server::inst_all_cores_running(uint32_t inst) +{ + for (uint32_t i = 0; i < m_cores.size() && i < m_core_running.size(); ++i) { + if (m_cores[i].inst == inst && !m_core_running[i]) return false; + } + return true; +} + /* Open a session to every instance lacking one. Doing so pauses that instance, * and an object-model query must not change run state, so resume unless the * debugger asked for the halt. Caller must hold m_gdb_mutex. */ @@ -550,11 +641,16 @@ void mcd_server::ensure_cores_known() bool was_open = (m_gdb_fds[idx] != INVALID_SOCK); if (gdb_session(idx) == INVALID_SOCK) continue; + /* An already-open session took the early return in gdb_session() and so + * never enumerated: a reset invalidates the core list without closing the + * session. The call is idempotent. */ + gdb_enumerate_cores(idx); if (was_open) continue; /* already the debugger's session; leave its state alone */ std::string reply; if (gdb_cmd(idx, "vCont;c", reply, /*wait_reply=*/false)) { m_gdb_running[idx] = true; + set_inst_cores_running(idx, true); SCP_DEBUG(()) << "mcd_server: resumed instance " << idx << " after core enumeration"; } } @@ -615,6 +711,7 @@ void mcd_server::gdb_close_all() std::string reply; if (gdb_cmd(idx, "vCont;c", reply, /*wait_reply=*/false)) { m_gdb_running[idx] = true; + set_inst_cores_running(idx, true); resumed_any = true; SCP_DEBUG(()) << "mcd_server: resumed instance " << idx << " on debugger detach"; } else { @@ -638,6 +735,7 @@ void mcd_server::gdb_close_all() /* Forget the discovered cores so a later client re-enumerates. Only a full * close resets this; a session dropped mid-use keeps its entry. */ m_cores.clear(); + m_core_running.clear(); std::fill(m_threads_known.begin(), m_threads_known.end(), false); std::fill(m_last_stop.begin(), m_last_stop.end(), last_stop_t{}); @@ -659,6 +757,7 @@ bool mcd_server::gdb_cmd(uint32_t idx, const std::string& cmd, std::string& repl bool ok = gdb_txn(fd, cmd, reply, wait_reply, &stray); if (!stray.empty()) { m_gdb_running[idx] = false; + set_inst_cores_running(idx, false); SCP_INFO(()) << "mcd_server: instance " << idx << " reported a stop while '" << cmd << "' was in flight: '" << stray << "'"; } @@ -667,6 +766,7 @@ bool mcd_server::gdb_cmd(uint32_t idx, const std::string& cmd, std::string& repl CLOSE_SOCKET(fd); m_gdb_fds[idx] = INVALID_SOCK; m_gdb_running[idx] = false; + set_inst_cores_running(idx, false); return false; } @@ -674,6 +774,8 @@ void mcd_server::note_stop(uint32_t idx, uint32_t reason, uint64_t watch_addr, u { if (idx >= m_last_stop.size()) return; m_last_stop[idx] = last_stop_t{ true, reason, watch_addr, tid }; + /* One core hit the breakpoint, but vm_stop halts the whole instance. */ + set_inst_cores_running(idx, false); } /* Non-blocking check for an unsolicited stop-reply: a breakpoint hit becomes a @@ -699,6 +801,7 @@ void mcd_server::drain_pending_stop(uint32_t idx) CLOSE_SOCKET(fd); m_gdb_fds[idx] = INVALID_SOCK; m_gdb_running[idx] = false; + set_inst_cores_running(idx, false); return; } @@ -746,6 +849,7 @@ uint32_t mcd_server::gdb_wait_stop(uint32_t idx, uint32_t timeout_ms, uint64_t& CLOSE_SOCKET(fd); m_gdb_fds[idx] = INVALID_SOCK; m_gdb_running[idx] = false; + set_inst_cores_running(idx, false); return MCD_STOP_HALTED; } @@ -761,20 +865,21 @@ uint32_t mcd_server::gdb_wait_stop(uint32_t idx, uint32_t timeout_ms, uint64_t& return reason; } -void mcd_server::add_mem_space(uint32_t space_id, const std::string& name, +void mcd_server::add_mem_space(uint32_t space_id, const std::string& name, uint32_t mem_type, std::function fn) { mcd_mem_space_st space; space.mem_space_id = space_id; std::memset(space.mem_space_name, 0, sizeof(space.mem_space_name)); std::strncpy(space.mem_space_name, name.c_str(), sizeof(space.mem_space_name) - 1); + space.mem_type = mem_type; m_mem_spaces.push_back(space); - m_transactors[space_id] = std::move(fn); + if (fn) m_transactors[space_id] = std::move(fn); } void mcd_server::bind_target(tlm::tlm_initiator_socket<>* socket, uint32_t space_id, const std::string& name) { - add_mem_space(space_id, name, + add_mem_space(space_id, name, MCD_MEM_SPACE_DEFAULT, [socket](tlm::tlm_generic_payload& txn) -> unsigned int { return (*socket)->transport_dbg(txn); }); SCP_INFO(()) << "bind_target: initiator socket registered for space id=" << space_id << " name='" << name << "'"; } @@ -802,7 +907,7 @@ void mcd_server::before_end_of_elaboration() /* Issue transport_dbg directly on the target socket's export: no extra * initiator socket, and no elaboration-time bind. */ - add_mem_space(space_id, space_name, [ts](tlm::tlm_generic_payload& txn) -> unsigned int { + add_mem_space(space_id, space_name, MCD_MEM_SPACE_DEFAULT, [ts](tlm::tlm_generic_payload& txn) -> unsigned int { return ts->get_base_export()->transport_dbg(txn); }); @@ -886,6 +991,11 @@ void mcd_server::before_end_of_elaboration() bind_instance(sc_obj, "127.0.0.1:" + std::to_string(port)); } } + + /* The register space, last so the router space ids are unaffected. It has no + * transactor: registers are not reachable through TLM, only over the RSP + * session, so READ_MEM/WRITE_MEM special-case this id. */ + add_mem_space(MCD_REG_SPACE_ID, "registers", MCD_MEM_SPACE_IS_REGISTERS, nullptr); } void mcd_server::end_of_elaboration() @@ -896,6 +1006,25 @@ void mcd_server::end_of_elaboration() } } +/* Optionally hold the simulation at time 0 until a debugger is attached, so + * nothing is missed. The accept loop runs on m_thread and publishes m_client_fd, + * so this only has to wait for it. */ +void mcd_server::start_of_simulation() +{ + if (!p_wait_for_client.get_value()) return; + if (m_listen_fd == INVALID_SOCK) { + SCP_WARN(()) << "mcd_server: wait_for_client set but the server is not listening; not waiting"; + return; + } + + SCP_WARN(()) << "mcd_server: waiting for an MCD client on " << p_mcd_host.get_value() << ":" + << p_mcd_port.get_value() << " before starting the simulation"; + while (m_running && m_client_fd == INVALID_SOCK) { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + SCP_INFO(()) << "mcd_server: client connected, starting the simulation"; +} + void mcd_server::start_server() { m_listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); @@ -909,16 +1038,34 @@ void mcd_server::start_server() set_nosigpipe(m_listen_fd); - /* Loopback only, never INADDR_ANY: this port grants unauthenticated read/write - * access to all simulated memory and registers. Remote debuggers tunnel in. */ + /* Loopback by default; mcd_host opts in to a wider interface. The port grants + * unauthenticated read/write access to all memory and registers, so anything + * other than loopback is announced loudly. */ + const std::string host = p_mcd_host.get_value(); struct sockaddr_in addr; std::memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_port = htons(static_cast(p_mcd_port.get_value())); + if (host.empty() || host == "*" || host == "0.0.0.0") { + addr.sin_addr.s_addr = htonl(INADDR_ANY); + /* SC_REPORT_WARNING, not SCP_WARN: this must be visible at the default + * log_level of 0. */ + auto prev = sc_core::sc_report_handler::set_actions(sc_core::SC_WARNING, sc_core::SC_LOG | sc_core::SC_DISPLAY); + SC_REPORT_WARNING("mcd_server", (std::string(name()) + " is listening on ALL interfaces: port " + + std::to_string(p_mcd_port.get_value()) + + " is unauthenticated access to all memory and registers") + .c_str()); + sc_core::sc_report_handler::set_actions(sc_core::SC_WARNING, prev); + } else if (::inet_pton(AF_INET, host.c_str(), &addr.sin_addr) != 1) { + SCP_ERR(()) << "mcd_server: mcd_host \"" << host << "\" is not an IPv4 address or \"*\""; + CLOSE_SOCKET(m_listen_fd); + m_listen_fd = INVALID_SOCK; + return; + } + if (::bind(m_listen_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { - SCP_ERR(()) << "mcd_server: bind() failed on port " << p_mcd_port.get_value() << ": " << sock_err(); + SCP_ERR(()) << "mcd_server: bind() failed on " << host << ":" << p_mcd_port.get_value() << ": " << sock_err(); CLOSE_SOCKET(m_listen_fd); m_listen_fd = INVALID_SOCK; return; @@ -1052,10 +1199,16 @@ mcd_return_et mcd_server::dispatch(uint8_t opcode, const std::vector& r return op_stop(req, resp); case MCD_OP_STEP: return op_step(req, resp); + case MCD_OP_QRY_STATE: + return op_qry_state(req, resp); + case MCD_OP_RESET: + return op_reset(req, resp); case MCD_OP_READ_REG: return op_read_reg(req, resp); case MCD_OP_WRITE_REG: return op_write_reg(req, resp); + case MCD_OP_QRY_REGS: + return op_qry_regs(req, resp); case MCD_OP_SET_BP: return op_set_bp(req, resp); case MCD_OP_CLR_BP: @@ -1209,10 +1362,12 @@ mcd_return_et mcd_server::op_qry_cores(const std::vector&, std::vector< mcd_return_et mcd_server::op_qry_mem_spaces(const std::vector&, std::vector& resp) { /* Response payload: 4-byte LE count, then for each space a 4-byte LE - * mem_space_id followed by the fixed 64-byte mem_space_name. */ + * mem_space_id, a 4-byte LE mem_type (MCD_MEM_SPACE_*), and the fixed 64-byte + * mem_space_name. */ put_u32(resp, static_cast(m_mem_spaces.size())); for (const mcd_mem_space_st& space : m_mem_spaces) { put_u32(resp, space.mem_space_id); + put_u32(resp, space.mem_type); resp.insert(resp.end(), space.mem_space_name, space.mem_space_name + sizeof(space.mem_space_name)); } return MCD_RET_ACT_NONE; @@ -1220,19 +1375,29 @@ mcd_return_et mcd_server::op_qry_mem_spaces(const std::vector&, std::ve mcd_return_et mcd_server::op_read_mem(const std::vector& req, std::vector& resp) { - /* Request: addr u64, length u32, optional space_id u32. A 12-byte payload - * (no space_id) means space 0, "physical". */ + /* Request: addr u64, length u32, optional space_id u32, optional addr_space_id + * u32. A 12-byte payload (no space_id) means space 0, "physical"; an absent or + * zero addr_space_id means core 0's hw thread. */ if (req.size() < 12) return MCD_RET_ERR_GENERAL; uint64_t addr = get_u64(&req[0]); uint32_t length = get_u32(&req[8]); uint32_t space_id = (req.size() >= 16) ? get_u32(&req[12]) : 0u; + uint32_t addr_space_id = (req.size() >= 20) ? get_u32(&req[16]) : 0u; if (length > MCD_MAX_FRAME) { SCP_WARN(()) << "mcd_server: read_mem length " << length << " exceeds the " << MCD_MAX_FRAME << "-byte limit"; return MCD_RET_ERR_GENERAL; } + /* Registers have no transactor, so this must precede the lookup. */ + if (space_id == MCD_REG_SPACE_ID) { + std::vector data; + mcd_return_et rc = access_reg_space(/*write=*/false, addr, length, addr_space_id, data); + if (rc == MCD_RET_ACT_NONE) resp.insert(resp.end(), data.begin(), data.end()); + return rc; + } + auto it = m_transactors.find(space_id); if (it == m_transactors.end() || !it->second) { SCP_WARN(()) << "mcd_server: read_mem with no bound target socket for space id=" << space_id; @@ -1273,9 +1438,10 @@ mcd_return_et mcd_server::op_read_mem(const std::vector& req, std::vect mcd_return_et mcd_server::op_write_mem(const std::vector& req, std::vector& resp) { - /* Request: addr u64, length u32, optional space_id u32, then data - * bytes. Header size (16 or 12 bytes, the latter meaning space 0) is inferred - * from the total payload. */ + /* Request: addr u64, length u32, optional space_id u32, optional addr_space_id + * u32, then data bytes. Header size (20, 16 or 12 bytes; 12 meaning + * space 0) is inferred from the total payload, as is a zero addr_space_id, + * which means core 0's hw thread. */ (void)resp; if (req.size() < 12) return MCD_RET_ERR_GENERAL; @@ -1291,8 +1457,13 @@ mcd_return_et mcd_server::op_write_mem(const std::vector& req, std::vec * sum compares as satisfied, selecting a data offset past the buffer end. */ const uint64_t len64 = length; uint32_t space_id; + uint32_t addr_space_id = 0; std::vector::size_type data_off; - if (static_cast(req.size()) >= 16u + len64) { + if (static_cast(req.size()) >= 20u + len64) { + space_id = get_u32(&req[12]); + addr_space_id = get_u32(&req[16]); + data_off = 20; + } else if (static_cast(req.size()) >= 16u + len64) { space_id = get_u32(&req[12]); data_off = 16; } else if (static_cast(req.size()) >= 12u + len64) { @@ -1302,6 +1473,12 @@ mcd_return_et mcd_server::op_write_mem(const std::vector& req, std::vec return MCD_RET_ERR_GENERAL; } + /* Registers have no transactor, so this must precede the lookup. */ + if (space_id == MCD_REG_SPACE_ID) { + std::vector data(req.begin() + data_off, req.begin() + data_off + length); + return access_reg_space(/*write=*/true, addr, length, addr_space_id, data); + } + auto it = m_transactors.find(space_id); if (it == m_transactors.end() || !it->second) { SCP_WARN(()) << "mcd_server: write_mem with no bound target socket for space id=" << space_id; @@ -1388,35 +1565,96 @@ bool mcd_server::resume_observed() return false; } -mcd_return_et mcd_server::op_run(const std::vector&, std::vector&) +mcd_return_et mcd_server::op_run(const std::vector& req, std::vector&) { if (m_gdb_ports.empty()) { SCP_WARN(()) << "mcd_server: RUN with no GDB-RSP endpoint"; return MCD_RET_ERR_GENERAL; } + /* Request: optional core id u32. An empty payload, or MCD_RUN_ALL_CORES, + * resumes every core of every instance. */ + static const uint32_t MCD_RUN_ALL_CORES = 0xffffffffu; + uint32_t core = (req.size() >= 4) ? get_u32(&req[0]) : MCD_RUN_ALL_CORES; + std::lock_guard lock(m_gdb_mutex); bool all_ok = true; bool resumed_any = false; - /* Run is per instance: continue is a global vm_start(), so one 'c' resumes - * every core of the instance. */ - for (uint32_t idx = 0; idx < m_gdb_ports.size(); ++idx) { - /* m_gdb_running is only what we last told the instance, so drain any - * pending stop-reply before deciding it is already running. */ - drain_pending_stop(idx); - if (m_gdb_running[idx]) continue; /* already running */ + if (core != MCD_RUN_ALL_CORES) { + ensure_cores_known(); + if (core >= m_cores.size()) { + SCP_WARN(()) << "mcd_server: RUN core id " << core << " out of range"; + return MCD_RET_ERR_GENERAL; + } + uint32_t inst = m_cores[core].inst; + drain_pending_stop(inst); + + /* One vCont names the whole set that should end up running, so the cores + * a previous per-core RUN resumed keep going. Snapshot it before the halt + * below, which clears the run state. */ + std::vector wanted; + for (uint32_t i = 0; i < m_cores.size(); ++i) { + if (m_cores[i].inst != inst) continue; + if (i == core || m_core_running[i]) wanted.push_back(i); + } + + /* The stub parses nothing while the VM runs, so a core can only be added + * to a partly-running instance by interrupting it first. */ + bool ready = !m_gdb_running[inst] || gdb_halt(inst); + + /* "vCont;c:..." resumes exactly the cores named: gdb_continue_partial + * leaves every other CPU halted. */ + std::string cmd = "vCont"; + for (uint32_t i : wanted) { + char t[16]; + std::snprintf(t, sizeof(t), ";c:%x", m_cores[i].tid); + cmd += t; + } - /* Read only the '+' ack: the stop-reply arrives only on the next stop. */ std::string reply; - if (!gdb_cmd(idx, "c", reply, /*wait_reply=*/false)) { - SCP_WARN(()) << "mcd_server: RUN forward to " << m_gdb_ports[idx] << " failed"; + if (!ready || !gdb_cmd(inst, cmd, reply, /*wait_reply=*/false)) { + SCP_WARN(()) << "mcd_server: RUN forward to " << m_gdb_ports[inst] << " failed"; all_ok = false; - continue; + } else { + m_gdb_running[inst] = true; + for (uint32_t i : wanted) m_core_running[i] = true; + resumed_any = true; + SCP_INFO(()) << "mcd_server: RUN core=" << core << " -> '" << cmd << "' @" << m_gdb_ports[inst]; + } + } else { + /* Run is per instance: continue is a global vm_start(), so one 'c' resumes + * every core of the instance. */ + for (uint32_t idx = 0; idx < m_gdb_ports.size(); ++idx) { + /* m_gdb_running is only what we last told the instance, so drain any + * pending stop-reply before deciding it is already running. */ + drain_pending_stop(idx); + + if (m_gdb_running[idx]) { + if (inst_all_cores_running(idx)) continue; /* already running */ + /* Running with a core still halted: a per-core RUN resumed only + * part of the instance, and the stub will not read a packet until + * the VM is interrupted. */ + if (!gdb_halt(idx)) { + SCP_WARN(()) << "mcd_server: RUN could not interrupt " << m_gdb_ports[idx] + << " to resume its halted cores"; + all_ok = false; + continue; + } + } + + /* Read only the '+' ack: the stop-reply arrives only on the next stop. */ + std::string reply; + if (!gdb_cmd(idx, "c", reply, /*wait_reply=*/false)) { + SCP_WARN(()) << "mcd_server: RUN forward to " << m_gdb_ports[idx] << " failed"; + all_ok = false; + continue; + } + m_gdb_running[idx] = true; + set_inst_cores_running(idx, true); + resumed_any = true; + SCP_INFO(()) << "mcd_server: RUN -> gdb 'c' @" << m_gdb_ports[idx]; } - m_gdb_running[idx] = true; - resumed_any = true; - SCP_INFO(()) << "mcd_server: RUN -> gdb 'c' @" << m_gdb_ports[idx]; } /* Deciding whether to drop the hold needs the resume to have taken effect: @@ -1470,6 +1708,7 @@ mcd_return_et mcd_server::op_stop(const std::vector&, std::vector gdb 0x03 @" << m_gdb_ports[idx] << " reply='" << reply << "'"; } return all_ok ? MCD_RET_ACT_NONE : MCD_RET_ERR_GENERAL; @@ -1494,6 +1733,10 @@ mcd_return_et mcd_server::op_step(const std::vector& req, std::vector" steps exactly one core, leaving the others halted: * gdb_continue_partial resumes only the CPUs named. A bare 's' would step * whichever core the stub has selected. */ @@ -1513,33 +1756,117 @@ mcd_return_et mcd_server::op_step(const std::vector& req, std::vector '" << cmd << "' @" << m_gdb_ports[inst] << " reply='" << reply << "'"; return MCD_RET_ACT_NONE; } -mcd_return_et mcd_server::op_read_reg(const std::vector& req, std::vector& resp) +mcd_return_et mcd_server::op_qry_state(const std::vector&, std::vector& resp) { - /* Request: core id u32, register number u32. */ - if (req.size() < 8) return MCD_RET_ERR_GENERAL; - uint32_t core = get_u32(&req[0]); - uint32_t regno = get_u32(&req[4]); - + /* Response: 4-byte LE count, then each core as [core_id u32][running u8]. */ std::lock_guard lock(m_gdb_mutex); ensure_cores_known(); - if (core >= m_cores.size()) { - SCP_WARN(()) << "mcd_server: READ_REG core id " << core << " out of range"; + /* A breakpoint hit is a stop-reply nobody has collected yet, so poll for one + * rather than report the core as still running. This never waits. */ + for (uint32_t idx = 0; idx < m_gdb_fds.size(); ++idx) { + drain_pending_stop(idx); + } + + put_u32(resp, static_cast(m_cores.size())); + for (uint32_t i = 0; i < m_cores.size(); ++i) { + put_u32(resp, i); + resp.push_back((i < m_core_running.size() && m_core_running[i]) ? 1u : 0u); + } + return MCD_RET_ACT_NONE; +} + +/* Run @p command through the stub's HMP monitor: gdbserver_start wires one up, so + * "qRcmd," is the only reset QEMU's gdbstub offers (there is no bare 'R'). + * Caller must hold m_gdb_mutex. */ +bool mcd_server::gdb_monitor(uint32_t idx, const std::string& command) +{ + std::string cmd = "qRcmd,"; + for (unsigned char ch : command) { + char b[3]; + std::snprintf(b, sizeof(b), "%02x", ch); + cmd += b; + } + + std::string reply; + if (!gdb_cmd(idx, cmd, reply)) return false; + + /* Console output arrives first as "O" packets; the result follows. The + * loop is bounded so a chatty command cannot hold the session here. */ + for (int skipped = 0; skipped < 64; ++skipped) { + if (reply.size() < 2 || reply[0] != 'O' || reply == "OK") break; + if (!gdb_read_packet(m_gdb_fds[idx], reply)) return false; + } + /* "OK", an empty packet (unsupported) and console output are all accepted; + * only "E xx" is a refusal. */ + if (!reply.empty() && reply[0] == 'E') { + SCP_WARN(()) << "mcd_server: monitor '" << command << "' rejected by " << m_gdb_ports[idx] << ": '" << reply + << "'"; + return false; + } + return true; +} + +mcd_return_et mcd_server::op_reset(const std::vector&, std::vector&) +{ + if (m_gdb_ports.empty()) { + SCP_WARN(()) << "mcd_server: RESET with no GDB-RSP endpoint"; return MCD_RET_ERR_GENERAL; } + + std::lock_guard lock(m_gdb_mutex); + + bool all_ok = true; + for (uint32_t idx = 0; idx < m_gdb_ports.size(); ++idx) { + if (gdb_session(idx) == INVALID_SOCK) { + all_ok = false; + continue; + } + /* The command is a packet like any other, so the instance must be halted + * for the stub to read it. It is left halted: the CPUs have just been + * reset, which is the state a debugger wants to inspect, and the client + * resumes with RUN when it is ready. */ + gdb_halt(idx); + if (!gdb_monitor(idx, "system_reset")) { + SCP_WARN(()) << "mcd_server: RESET forward to " << m_gdb_ports[idx] << " failed"; + all_ok = false; + continue; + } + SCP_INFO(()) << "mcd_server: RESET -> monitor 'system_reset' @" << m_gdb_ports[idx]; + } + + /* The reset re-creates the CPUs' state and may change the register layout, so + * drop everything discovered from the stub; the next query re-enumerates. The + * sessions stay open, so ensure_cores_known() enumerates on them again. */ + m_cores.clear(); + m_core_running.clear(); + std::fill(m_threads_known.begin(), m_threads_known.end(), false); + std::fill(m_last_stop.begin(), m_last_stop.end(), last_stop_t{}); + for (inst_regs_t& d : m_inst_regs) d = inst_regs_t{}; + + update_debug_hold(); + return all_ok ? MCD_RET_ACT_NONE : MCD_RET_ERR_GENERAL; +} + +/* Read one register with 'p'. Caller must hold m_gdb_mutex and keep the instance + * halted: the stub answers nothing while the VM runs. */ +bool mcd_server::gdb_read_reg(uint32_t core, uint32_t regno, std::vector& value) +{ + if (core >= m_cores.size()) return false; uint32_t inst = m_cores[core].inst; /* 'p' reads from the currently selected core (gdbserver_state.g_cpu), so * without this every core of an instance aliases the same registers. */ if (!gdb_select_core(core)) { SCP_WARN(()) << "mcd_server: READ_REG could not select core " << core; - return MCD_RET_ERR_GENERAL; + return false; } char cmd[32]; @@ -1548,52 +1875,43 @@ mcd_return_et mcd_server::op_read_reg(const std::vector& req, std::vect std::string reply; if (!gdb_cmd(inst, cmd, reply)) { SCP_WARN(()) << "mcd_server: READ_REG forward to " << m_gdb_ports[inst] << " failed"; - return MCD_RET_ERR_GENERAL; + return false; } /* Empty reply => 'p' unsupported; "E xx" => error. */ if (reply.empty() || reply[0] == 'E') { SCP_WARN(()) << "mcd_server: READ_REG regno=" << regno << " gdb error '" << reply << "'"; - return MCD_RET_ERR_GENERAL; + return false; } if (reply.size() % 2 != 0) { SCP_WARN(()) << "mcd_server: READ_REG odd-length hex reply '" << reply << "'"; - return MCD_RET_ERR_GENERAL; + return false; } /* Reply is the value hex-encoded in target byte order; decode preserving * that order. */ + value.clear(); for (std::string::size_type i = 0; i + 1 < reply.size(); i += 2) { int hi = hex_nibble(reply[i]); int lo = hex_nibble(reply[i + 1]); if (hi < 0 || lo < 0) { SCP_WARN(()) << "mcd_server: READ_REG bad hex reply '" << reply << "'"; - return MCD_RET_ERR_GENERAL; + return false; } - resp.push_back(static_cast((hi << 4) | lo)); + value.push_back(static_cast((hi << 4) | lo)); } - return MCD_RET_ACT_NONE; + return true; } -mcd_return_et mcd_server::op_write_reg(const std::vector& req, std::vector&) +/* Write one register with 'P'. Same preconditions as gdb_read_reg(). */ +bool mcd_server::gdb_write_reg(uint32_t core, uint32_t regno, const uint8_t* value, uint32_t len) { - /* Request: core id u32, register number u32, raw value in target byte order. */ - if (req.size() < 8) return MCD_RET_ERR_GENERAL; - uint32_t core = get_u32(&req[0]); - uint32_t regno = get_u32(&req[4]); - - std::lock_guard lock(m_gdb_mutex); - ensure_cores_known(); - - if (core >= m_cores.size()) { - SCP_WARN(()) << "mcd_server: WRITE_REG core id " << core << " out of range"; - return MCD_RET_ERR_GENERAL; - } + if (core >= m_cores.size()) return false; uint32_t inst = m_cores[core].inst; /* 'P' writes to the currently selected core, so select it first. */ if (!gdb_select_core(core)) { SCP_WARN(()) << "mcd_server: WRITE_REG could not select core " << core; - return MCD_RET_ERR_GENERAL; + return false; } /* "P=", hex-encoded in the given byte order. */ @@ -1604,23 +1922,445 @@ mcd_return_et mcd_server::op_write_reg(const std::vector& req, std::vec cmd += rn; } cmd += '='; - for (std::vector::size_type i = 8; i < req.size(); ++i) { + for (uint32_t i = 0; i < len; ++i) { char b[3]; - std::snprintf(b, sizeof(b), "%02x", req[i]); + std::snprintf(b, sizeof(b), "%02x", value[i]); cmd += b; } std::string reply; if (!gdb_cmd(inst, cmd, reply)) { SCP_WARN(()) << "mcd_server: WRITE_REG forward to " << m_gdb_ports[inst] << " failed"; + return false; + } + if (reply != "OK") { + SCP_WARN(()) << "mcd_server: WRITE_REG regno=" << regno << " gdb reply '" << reply << "'"; + return false; + } + SCP_INFO(()) << "mcd_server: WRITE_REG regno=" << regno << " OK"; + return true; +} + +mcd_return_et mcd_server::op_read_reg(const std::vector& req, std::vector& resp) +{ + /* Request: core id u32, register number u32. */ + if (req.size() < 8) return MCD_RET_ERR_GENERAL; + uint32_t core = get_u32(&req[0]); + uint32_t regno = get_u32(&req[4]); + + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + if (core >= m_cores.size()) { + SCP_WARN(()) << "mcd_server: READ_REG core id " << core << " out of range"; + return MCD_RET_ERR_GENERAL; + } + + /* The stub answers nothing while the VM runs; halt for the duration. */ + scoped_halt halt(*this, m_cores[core].inst); + + std::vector value; + if (!gdb_read_reg(core, regno, value)) return MCD_RET_ERR_GENERAL; + resp.insert(resp.end(), value.begin(), value.end()); + return MCD_RET_ACT_NONE; +} + +mcd_return_et mcd_server::op_write_reg(const std::vector& req, std::vector&) +{ + /* Request: core id u32, register number u32, raw value in target byte order. */ + if (req.size() < 8) return MCD_RET_ERR_GENERAL; + uint32_t core = get_u32(&req[0]); + uint32_t regno = get_u32(&req[4]); + + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + if (core >= m_cores.size()) { + SCP_WARN(()) << "mcd_server: WRITE_REG core id " << core << " out of range"; return MCD_RET_ERR_GENERAL; } - if (reply == "OK") { - SCP_INFO(()) << "mcd_server: WRITE_REG regno=" << regno << " OK"; + + /* The stub answers nothing while the VM runs; halt for the duration. */ + scoped_halt halt(*this, m_cores[core].inst); + + if (!gdb_write_reg(core, regno, req.data() + 8, static_cast(req.size() - 8))) { + return MCD_RET_ERR_GENERAL; + } + return MCD_RET_ACT_NONE; +} + +/* MCD addresses a register that is not memory mapped as memory in a space of type + * MCD_MEM_SPACE_IS_REGISTERS: the address is the register number and the hw thread + * it is valid in is the addr_space_id. The bytes on the wire are the register + * values in target byte order, exactly what READ_REG/WRITE_REG carry, so a client + * sees the same bytes through either path. */ +mcd_return_et mcd_server::access_reg_space(bool write, uint64_t address, uint32_t length, uint32_t addr_space_id, + std::vector& data) +{ + if (write && data.size() != length) return MCD_RET_ERR_GENERAL; + + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + if (address > 0xffffffffu) { + SCP_WARN(()) << "mcd_server: register space address 0x" << std::hex << address << " is not a register number"; + return MCD_RET_ERR_GENERAL; + } + uint32_t core = 0; + if (!reg_space_core(addr_space_id, core)) { + SCP_WARN(()) << "mcd_server: register space has no core with hw thread id " << addr_space_id; + return MCD_RET_ERR_GENERAL; + } + uint32_t inst = m_cores[core].inst; + + /* Register widths come from the target description, which is fetched with its + * own halt; take ours only afterwards. */ + ensure_regs_known(inst); + update_debug_hold(); + + std::vector spans; + if (!reg_space_split(inst, static_cast(address), length, spans)) return MCD_RET_ERR_GENERAL; + + scoped_halt halt(*this, inst); + + if (write) { + uint32_t off = 0; + for (const reg_span_t& s : spans) { + if (!gdb_write_reg(core, s.regno, data.data() + off, s.bytes)) return MCD_RET_ERR_GENERAL; + off += s.bytes; + } return MCD_RET_ACT_NONE; } - SCP_WARN(()) << "mcd_server: WRITE_REG regno=" << regno << " gdb reply '" << reply << "'"; - return MCD_RET_ERR_GENERAL; + + std::vector out; + for (const reg_span_t& s : spans) { + std::vector value; + if (!gdb_read_reg(core, s.regno, value)) return MCD_RET_ERR_GENERAL; + if (value.size() != s.bytes) { + SCP_WARN(()) << "mcd_server: register " << s.regno << " read as " << value.size() << " bytes, described as " + << s.bytes; + return MCD_RET_ERR_GENERAL; + } + out.insert(out.end(), value.begin(), value.end()); + } + data = std::move(out); + return MCD_RET_ACT_NONE; +} + +/* An address in the register space is valid in one hw thread, named by its gdb + * thread id; 0 is "not used" and means core 0. Caller must hold m_gdb_mutex. */ +bool mcd_server::reg_space_core(uint32_t addr_space_id, uint32_t& core) +{ + if (m_cores.empty()) return false; + if (addr_space_id == 0) { + core = 0; + return true; + } + for (uint32_t c = 0; c < m_cores.size(); ++c) { + if (m_cores[c].tid == addr_space_id) { + core = c; + return true; + } + } + return false; +} + +bool mcd_server::reg_space_split(uint32_t inst, uint32_t regno, uint32_t length, std::vector& out) +{ + out.clear(); + for (uint32_t done = 0, r = regno; done < length; ++r) { + const reg_t* reg = find_reg(inst, r); + if (!reg) { + SCP_WARN(()) << "mcd_server: register space has no register number " << r; + return false; + } + uint32_t bytes = (reg->bitsize + 7) / 8; + if (!bytes || done + bytes > length) { + SCP_WARN(()) << "mcd_server: register space length " << length << " from register " << regno + << " does not cover whole registers (" << reg->name << " is " << reg->bitsize << " bits)"; + return false; + } + out.push_back(reg_span_t{ r, bytes }); + done += bytes; + } + return true; +} + +const mcd_server::reg_t* mcd_server::find_reg(uint32_t inst, uint32_t regnum) const +{ + if (inst >= m_inst_regs.size()) return nullptr; + for (const reg_t& r : m_inst_regs[inst].regs) { + if (r.regnum == regnum) return &r; + } + return nullptr; +} + +/* Halt instance @p idx so a request can reach its stub at all: while the VM runs, + * gdb_read_byte() answers any packet by stopping the VM and sending nothing, so + * the read would just time out. Returns true if this call halted it, i.e. the + * caller owes a gdb_resume(). Caller must hold m_gdb_mutex. */ +bool mcd_server::gdb_halt(uint32_t idx) +{ + drain_pending_stop(idx); + if (!m_gdb_running[idx]) return false; + + socket_t fd = gdb_session(idx); + if (fd == INVALID_SOCK) return false; + + /* Take the hold before the halt: the attach is deferred to the SystemC + * thread, and a halt stops the quantum keeper. */ + debug_hold(true); + + /* Raw 0x03, as op_stop does; QEMU answers with a stop-reply. It is not + * recorded as a stop: it is the reply owed for our own resume, not an event + * the client asked about. */ + const uint8_t brk = 0x03; + std::string reply; + if (!send_all(fd, &brk, 1) || !gdb_read_packet(fd, reply)) { + SCP_WARN(()) << "mcd_server: could not halt " << m_gdb_ports[idx] << " to answer a request"; + return false; + } + m_gdb_running[idx] = false; + set_inst_cores_running(idx, false); + SCP_DEBUG(()) << "mcd_server: halted instance " << idx << " to answer a request: '" << reply << "'"; + return true; +} + +mcd_server::scoped_halt::scoped_halt(mcd_server& server, uint32_t idx) + : m_server(server), m_idx(idx), m_was_running(server.m_core_running) +{ + m_halted = m_server.gdb_halt(idx); +} + +void mcd_server::scoped_halt::exclude(uint32_t core) +{ + if (core < m_was_running.size()) m_was_running[core] = false; +} + +mcd_server::scoped_halt::~scoped_halt() +{ + if (m_halted) m_server.gdb_resume(m_idx, m_was_running); +} + +void mcd_server::gdb_resume(uint32_t idx, const std::vector& was_running) +{ + /* Name the cores that were running, so a core the client left halted stays + * halted: a bare 'c' would resume every core of the instance. */ + std::string cmd = "vCont"; + for (size_t c = 0; c < m_cores.size(); ++c) { + if (m_cores[c].inst == idx && c < was_running.size() && was_running[c]) { + char tid[16]; + std::snprintf(tid, sizeof(tid), ";c:%x", m_cores[c].tid); + cmd += tid; + } + } + if (cmd == "vCont") return; /* nothing was running */ + + std::string reply; + if (!gdb_cmd(idx, cmd, reply, /*wait_reply=*/false)) { + SCP_WARN(()) << "mcd_server: could not resume " << m_gdb_ports[idx] << " after a request; it stays halted"; + return; + } + m_gdb_running[idx] = true; + for (size_t c = 0; c < m_cores.size(); ++c) { + if (m_cores[c].inst == idx && c < was_running.size()) m_core_running[c] = was_running[c]; + } + /* See op_run: the hold may only be dropped once the resume has been observed, + * and with a breakpoint armed it stays regardless. */ + if (m_breakpoints.empty()) resume_observed(); +} + +/* Read one "qXfer:features:read:" object. The reply to each request starts + * with 'm' (more follows) or 'l' (last chunk); everything after that character is + * payload. Caller must hold m_gdb_mutex. */ +std::string mcd_server::gdb_qxfer(uint32_t idx, const std::string& annex) +{ + static const uint32_t chunk = 0x400; + + std::string out; + /* Bounded by the frame cap: a stub that never sends 'l' must not grow this + * without limit. */ + for (uint32_t offset = 0; out.size() < MCD_MAX_FRAME; offset += chunk) { + char range[32]; + std::snprintf(range, sizeof(range), "%x,%x", offset, chunk); + std::string cmd = "qXfer:features:read:" + annex + ":" + range; + + std::string reply; + if (!gdb_cmd(idx, cmd, reply)) { + SCP_WARN(()) << "mcd_server: '" << cmd << "' forward to " << m_gdb_ports[idx] << " failed"; + return std::string(); + } + if (reply.empty() || reply[0] == 'E') { + SCP_WARN(()) << "mcd_server: '" << cmd << "' refused by " << m_gdb_ports[idx] << ": '" << reply << "'"; + return std::string(); + } + if (reply[0] != 'm' && reply[0] != 'l') { + SCP_WARN(()) << "mcd_server: unexpected qXfer reply '" << reply << "'"; + return std::string(); + } + /* Target XML is printable ASCII, so neither the '}' escape nor '*' + * run-length encoding occurs and neither is decoded. */ + out += reply.substr(1); + if (reply[0] == 'l') break; + } + return out; +} + +/* Fetch and parse the gdbstub's register description, once per instance: every + * core of an instance has the same layout. Caller must hold m_gdb_mutex. */ +void mcd_server::ensure_regs_known(uint32_t idx) +{ + if (idx >= m_inst_regs.size() || !m_inst_regs[idx].regs.empty()) return; + + /* Reading the description takes several packets, so the instance has to be + * halted for it; put it back as it was afterwards. */ + scoped_halt halt(*this, idx); + + std::string xml = gdb_qxfer(idx, "target.xml"); + if (xml.empty()) return; + + /* QEMU lists each feature as rather than inlining + * it, so fetch those too. Capped, and one level only, so a self-referential + * include list cannot loop. */ + std::vector annexes; + xml_includes(xml, annexes, 32); + for (const std::string& annex : annexes) { + if (annex == "target.xml") continue; + xml += gdb_qxfer(idx, annex); + } + + /* A reg naming one of these in its type= is compound. MCD_REG_TYPE_PARTIAL is + * never reported: gdb expresses sub-fields as inside a struct or flags + * type, and those are not enumerated. */ + std::vector composites; + xml_composite_types(xml, composites, 256); + + /* Scan for and ; no XML library is involved. + * regnum is given on the first reg of a feature and implicit + 1 after it. */ + inst_regs_t out; + std::string feature = "general"; + uint32_t next_regnum = 0; + + /* Intern a group name, returning its id. Ids start at 1: MCD reserves 0. */ + auto group_id_of = [&out](const std::string& name) { + for (reg_group_t& g : out.groups) { + if (g.name == name) return g.group_id; + } + out.groups.push_back(reg_group_t{ static_cast(out.groups.size() + 1), name, 0 }); + return out.groups.back().group_id; + }; + + for (std::string::size_type pos = 0; pos < xml.size() && out.regs.size() < 4096;) { + std::string::size_type reg = xml.find("', feat); + if (end == std::string::npos) break; + std::string name = xml_attr(xml.substr(feat, end - feat), "name"); + if (!name.empty()) feature = name; + pos = end + 1; + continue; + } + if (reg == std::string::npos) break; + + std::string::size_type end = xml.find('>', reg); + if (end == std::string::npos) break; + std::string el = xml.substr(reg, end - reg); + pos = end + 1; + + /* MCD_REG_NAME_LEN, including the terminating zero. */ + std::string name = xml_attr(el, "name"); + if (name.empty() || name.size() > 31) continue; + + std::string num = xml_attr(el, "regnum"); + if (!num.empty()) next_regnum = static_cast(std::strtoul(num.c_str(), nullptr, 10)); + + std::string group = xml_attr(el, "group"); + if (group.empty()) group = feature; + if (group.size() > 31) group = group.substr(0, 31); + + uint32_t gid = group_id_of(group); + uint32_t bitsize = static_cast(std::strtoul(xml_attr(el, "bitsize").c_str(), nullptr, 10)); + + std::string type = xml_attr(el, "type"); + bool composite = !type.empty() && std::find(composites.begin(), composites.end(), type) != composites.end(); + uint32_t reg_type = composite ? MCD_REG_TYPE_COMPOUND : MCD_REG_TYPE_SIMPLE; + + out.regs.push_back(reg_t{ next_regnum, gid, bitsize, reg_type, name }); + ++next_regnum; + for (reg_group_t& g : out.groups) { + if (g.group_id == gid) ++g.n_registers; + } + } + + m_inst_regs[idx] = std::move(out); + SCP_INFO(()) << "mcd_server: instance " << idx << " describes " << m_inst_regs[idx].regs.size() << " registers in " + << m_inst_regs[idx].groups.size() << " groups"; +} + +mcd_return_et mcd_server::op_qry_regs(const std::vector& req, std::vector& resp) +{ + /* Request: core id u32, and optionally a group id u32 to report only that + * group's registers (mcd_qry_reg_map_f). 0, or absent, means every group. */ + uint32_t core = (req.size() >= 4) ? get_u32(&req[0]) : 0u; + uint32_t want_group = (req.size() >= 8) ? get_u32(&req[4]) : 0u; + + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + if (core >= m_cores.size()) { + SCP_WARN(()) << "mcd_server: QRY_REGS core id " << core << " out of range"; + return MCD_RET_ERR_GENERAL; + } + uint32_t inst = m_cores[core].inst; + /* mcd_register_info_st.hw_thread_id: this core is a gdb thread of its instance. */ + uint32_t hw_thread_id = m_cores[core].tid; + ensure_regs_known(inst); + /* ensure_regs_known() may have halted and resumed the instance. */ + update_debug_hold(); + + const inst_regs_t& d = m_inst_regs[inst]; + if (d.regs.empty()) { + SCP_WARN(()) << "mcd_server: QRY_REGS got no register description from " << m_gdb_ports[inst]; + return MCD_RET_ERR_GENERAL; + } + + /* Response: the group table, then the registers, each naming its group by id + * and carrying its mcd_addr_st, i.e. its register number in the register memory + * space, valid in this core's hw thread: + * [n_groups u32] { [group_id u32][n_registers u32][name_len u16][name] } + * [n_regs u32] { [regnum u32][group_id u32][bitsize u32][reg_type u32] + * [hw_thread_id u32][address u64][mem_space_id u32] + * [addr_space_id u32][addr_space_type u32][name_len u16][name] } */ + put_u32(resp, static_cast(d.groups.size())); + for (const reg_group_t& g : d.groups) { + put_u32(resp, g.group_id); + put_u32(resp, g.n_registers); + put_u16(resp, static_cast(g.name.size())); + resp.insert(resp.end(), g.name.begin(), g.name.end()); + } + + uint32_t n = 0; + for (const reg_t& r : d.regs) { + if (!want_group || r.group_id == want_group) ++n; + } + put_u32(resp, n); + for (const reg_t& r : d.regs) { + if (want_group && r.group_id != want_group) continue; + put_u32(resp, r.regnum); + put_u32(resp, r.group_id); + put_u32(resp, r.bitsize); + put_u32(resp, r.reg_type); + put_u32(resp, hw_thread_id); + put_u64(resp, r.regnum); + put_u32(resp, MCD_REG_SPACE_ID); + put_u32(resp, hw_thread_id); + put_u32(resp, MCD_HW_THREAD_ID); + put_u16(resp, static_cast(r.name.size())); + resp.insert(resp.end(), r.name.begin(), r.name.end()); + } + return MCD_RET_ACT_NONE; } /* Breakpoints use RSP Z/z packets: "Z,," sets, "z..." removes; @@ -1658,6 +2398,9 @@ mcd_return_et mcd_server::op_set_bp(const std::vector& req, std::vector } uint32_t inst = m_cores[core].inst; + /* The stub answers nothing while the VM runs; halt for the duration. */ + scoped_halt halt(*this, inst); + /* QEMU installs Z/z breakpoints per address space, not per vCPU: this arms * every core of the instance. `core` is recorded only for LIST_BP. */ char cmd[48]; @@ -1701,6 +2444,9 @@ mcd_return_et mcd_server::op_clr_bp(const std::vector& req, std::vector } uint32_t inst = m_cores[core].inst; + /* The stub answers nothing while the VM runs; halt for the duration. */ + scoped_halt halt(*this, inst); + char cmd[48]; std::snprintf(cmd, sizeof(cmd), "z%c,%llx,%x", digit, static_cast(addr), kind); From 9eb99d653f99d8ef640f206d0438e5e7a843be78 Mon Sep 17 00:00:00 2001 From: Mark Burton Date: Wed, 5 Aug 2026 14:26:57 +0200 Subject: [PATCH 4/4] tests: add reset-stress test platform Runs N resets inside one simulation to reproduce a reported "platform hangs after some number of resets" bug. The platform uses router, gs_memory, pl011 uart, loader, reset_gpio and mcd_server; the MCP client drives each reset and verifies PC returns to the reset vector and the firmware counter advances. reset_gpio.reset_out must feed back into reset_gpio.reset_in: without that binding the QEMU-side callback blocks forever on the first reset (waits for the SystemC reset to be acknowledged, but no one drives it). Signed-off-by: Mark Burton --- tests/qbox/CMakeLists.txt | 1 + tests/qbox/reset-stress/CMakeLists.txt | 82 ++++++++++ tests/qbox/reset-stress/reset-stress.lua | 99 ++++++++++++ tests/qbox/reset-stress/test_reset_stress.py | 150 +++++++++++++++++++ 4 files changed, 332 insertions(+) create mode 100644 tests/qbox/reset-stress/CMakeLists.txt create mode 100644 tests/qbox/reset-stress/reset-stress.lua create mode 100644 tests/qbox/reset-stress/test_reset_stress.py diff --git a/tests/qbox/CMakeLists.txt b/tests/qbox/CMakeLists.txt index 2c612fe6..5dc0f6b7 100644 --- a/tests/qbox/CMakeLists.txt +++ b/tests/qbox/CMakeLists.txt @@ -351,4 +351,5 @@ add_subdirectory(display) # built on non-Windows platforms (see systemc-components/CMakeLists.txt). if(NOT WIN32) add_subdirectory(mcd) + add_subdirectory(reset-stress) endif() diff --git a/tests/qbox/reset-stress/CMakeLists.txt b/tests/qbox/reset-stress/CMakeLists.txt new file mode 100644 index 00000000..95c6c66b --- /dev/null +++ b/tests/qbox/reset-stress/CMakeLists.txt @@ -0,0 +1,82 @@ +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. +# SPDX-License-Identifier: BSD-3-Clause +# +# Reset-stress test: many resets inside one simulation on a real platform +# (router, RAM, uart, loader, reset_gpio, mcd_server), hunting a hang that only +# appears after repeated resets. + +find_package(Python3 COMPONENTS Interpreter REQUIRED) + +# The VP driver is the example's main.cc: a generic module-factory container. +set(RS_EXAMPLE_DIR ${PROJECT_SOURCE_DIR}/examples/hello-qbox) +set(RS_LUA ${CMAKE_CURRENT_SOURCE_DIR}/reset-stress.lua) + +add_executable(reset-stress-vp ${RS_EXAMPLE_DIR}/main.cc) + +if(APPLE) + target_link_options(reset-stress-vp PRIVATE "LINKER:-dead_strip_dylibs") +else() + target_link_options(reset-stress-vp PRIVATE "LINKER:-as-needed") +endif() + +# The topology comes from the lua at runtime, which loads its components as +# dynamic modules by dylib_path. +target_link_libraries(reset-stress-vp PRIVATE ${TARGET_LIBS} cpu_arm_cortexA53) +add_dependencies(reset-stress-vp + router gs_memory uart-pl011 char_backend_stdio loader mcd_server reset_gpio) + +# Firmware: reuse the MCD test's hello-debug.c (zeroes a counter at 0x8ff00000 +# then increments it forever, so a restart is observable). +set(RS_FW_SRC ${CMAKE_CURRENT_SOURCE_DIR}/../mcd/hello-debug.c) +set(RS_FW ${CMAKE_CURRENT_BINARY_DIR}/hello-debug.elf) + +find_program(AARCH64_GCC aarch64-linux-gnu-gcc) +find_program(CLANG_BIN clang) +find_program(LLD_BIN ld.lld) + +set(RS_HAVE_FW FALSE) +if(AARCH64_GCC) + add_custom_command( + OUTPUT ${RS_FW} + COMMAND ${AARCH64_GCC} -nostdlib -static -ffreestanding + -Ttext=0x80000000 -e _start -Wl,--build-id=none -Wl,-n + -o ${RS_FW} ${RS_FW_SRC} + DEPENDS ${RS_FW_SRC} + COMMENT "Cross-compiling hello-debug.elf for reset-stress (gcc)") + set(RS_HAVE_FW TRUE) +elseif(CLANG_BIN AND LLD_BIN) + add_custom_command( + OUTPUT ${RS_FW} + COMMAND ${CLANG_BIN} --target=aarch64-elf -ffreestanding -nostdlib + -c ${RS_FW_SRC} -o ${RS_FW}.o + COMMAND ${LLD_BIN} ${RS_FW}.o -o ${RS_FW} + -Ttext=0x80000000 -e _start --build-id=none -n + DEPENDS ${RS_FW_SRC} + COMMENT "Cross-compiling hello-debug.elf for reset-stress (clang)") + set(RS_HAVE_FW TRUE) +endif() + +if(RS_HAVE_FW) + add_custom_target(reset-stress-firmware ALL DEPENDS ${RS_FW}) + add_dependencies(reset-stress-vp reset-stress-firmware) + + # Modest count so CI stays quick; the script takes -n for deep manual runs. + add_test( + NAME reset_stress + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/test_reset_stress.py + -e $ + -l ${RS_LUA} + -m $ + -f ${RS_FW} + -n 50 + ) + # Component dylibs are looked up relative to the VP's working directory. + set_tests_properties(reset_stress PROPERTIES + TIMEOUT 900 + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +else() + message(WARNING + "No AArch64 toolchain (aarch64-linux-gnu-gcc, or clang + ld.lld) found; " + "the reset-stress test will be skipped.") +endif() diff --git a/tests/qbox/reset-stress/reset-stress.lua b/tests/qbox/reset-stress/reset-stress.lua new file mode 100644 index 00000000..52cd98b9 --- /dev/null +++ b/tests/qbox/reset-stress/reset-stress.lua @@ -0,0 +1,99 @@ +-- Platform for the reset-stress test: a real AArch64 board (router, RAM, uart, +-- loader) with an mcd_server to drive resets and a reset_gpio bridging the +-- SystemC and QEMU reset domains. The firmware spins forever bumping a counter, +-- so the target stays live and a restart is observable. +-- +-- Presets must precede the lua file on the command line; string values are +-- JSON-quoted: +-- reset-stress-vp -p mcd.port=1235 -p 'fw="/path/hello-debug.elf"' -p cores=1 \ +-- --gs_luafile reset-stress.lua +-- Defaults: port 1235, hello-debug.elf beside this file, one core. + +function script_path() + local src = debug.getinfo(2, "S").source:sub(2) + return src:match("(.*/)") +end +local base = script_path() + +local mcd_port = tonumber(GET("mcd.port")) or 1235 +local num_cores = tonumber(GET("cores")) or 1 + +local fw = GET("fw") +if not fw or fw == "" then fw = base .. "hello-debug.elf" end + +platform = { + moduletype = "Container", + quantum_ns = 10000000, + + router = { moduletype = "router", log_level = 0 }, + + ram_0 = { + moduletype = "gs_memory", + target_socket = { + address = 0x80000000, + size = 0x10000000, -- 256 MB + bind = "&router.initiator_socket", + }, + }, + + qemu_inst_mgr = { moduletype = "QemuInstanceManager" }, + + qemu_inst = { + moduletype = "QemuInstance", + args = { "&qemu_inst_mgr", "AARCH64" }, + accel = "tcg", + sync_policy = "multithread-unconstrained", + }, + + charbackend_stdio_0 = { + moduletype = "char_backend_stdio", + read_write = true, + }, + + pl011_uart_0 = { + moduletype = "Pl011", + dylib_path = "uart-pl011", + target_socket = { + address = 0x09000000, + size = 0x1000, + bind = "&router.initiator_socket", + }, + backend_socket = { + bind = "&charbackend_stdio_0.biflow_socket" }, + }, + + load = { + moduletype = "loader", + initiator_socket = { bind = "&router.target_socket" }, + { elf_file = fw }, + }, + + -- Bridges QEMU's system_reset (which MCD's RESET drives) into a SystemC + -- reset, and back. reset_out must feed reset_in back: the QEMU-side callback + -- blocks until the SystemC reset it started is acknowledged there. + reset_gpio_0 = { + moduletype = "reset_gpio", + dylib_path = "reset_gpio", + args = { "&qemu_inst" }, + reset_out = { bind = "&reset_gpio_0.reset_in" }, + }, + + mcd_server = { + moduletype = "mcd_server", + dylib_path = "mcd_server", + mcd_port = mcd_port, + }, +} + +-- CPUs cpu_0 .. cpu_ on the one qemu_inst, so a single gdb stub serves all. +for i = 0, num_cores - 1 do + platform["cpu_" .. i] = { + moduletype = "cpu_arm_cortexA53", + args = { "&qemu_inst" }, + mem = { bind = "&router.target_socket" }, + rvbar = 0x80000000, + has_el3 = true, + has_el2 = true, + psci_conduit = "hvc", + } +end diff --git a/tests/qbox/reset-stress/test_reset_stress.py b/tests/qbox/reset-stress/test_reset_stress.py new file mode 100644 index 00000000..763fae65 --- /dev/null +++ b/tests/qbox/reset-stress/test_reset_stress.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved. +# SPDX-License-Identifier: BSD-3-Clause +# +# Reset-stress test: many resets inside ONE simulation, hunting a reported +# "platform hangs after some number of resets" bug. Each iteration drives an MCD +# RESET (which forwards to QEMU's system_reset through reset_gpio) and then +# proves the platform is not wedged: +# (a) PC is back at the reset vector 0x80000000 +# (b) after RUN, the firmware counter at 0x8ff00000 advances again +# Every iteration runs under a wall-clock timeout so a hang is reported as a +# hang rather than an infinite wait. +# +# Arguments: +# -e / --exe path to the virtual-platform binary +# -l / --lua path to reset-stress.lua +# -m / --mcp path to the mcd_mcp binary +# -f / --fw path to the debug firmware ELF (passed to the lua as fw) +# -n / --resets number of resets (default 200) +# --cores cores preset (default 1) +# --timeout per-iteration timeout in seconds (default 30) + +import argparse +import concurrent.futures +import os +import signal +import subprocess +import sys +import time + +# test_mcd_mcp.py lives in the sibling mcd/ test directory. +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "mcd")) +from test_mcd_mcp import MCP, find_free_port, wait_for_port, parse_u64_from_text # noqa: E402 + +PC_REG = 32 # AArch64 gdbstub: x0..x30 = 0..30, sp = 31, pc = 32 +RESET_VECTOR = 0x80000000 +COUNTER = 0x8FF00000 + + +def read_counter(mcp: MCP) -> int: + """Read the firmware's 64-bit little-endian counter via mcd_read_mem.""" + dump = mcp.tool("mcd_read_mem", {"addr": f"{COUNTER:x}", "len": 8}) + # hex_dump line: ": b0 b1 ... " + body = dump.split(":", 1)[1] + bytes_ = [int(t, 16) for t in body.split()[:8]] + return int.from_bytes(bytes(bytes_), "little") + + +def one_reset(mcp: MCP) -> int: + """One reset + liveness check. Returns the counter value observed running.""" + mcp.tool("mcd_reset", {}) + + pc = parse_u64_from_text(mcp.tool("mcd_read_reg", {"regno": PC_REG})) + assert pc == RESET_VECTOR, f"PC after reset is 0x{pc:016x}, expected 0x{RESET_VECTOR:08x}" + + mcp.tool("mcd_run", {}) + # Sample until two consecutive reads increase. A single decrease is expected: + # the restarted firmware re-zeroes the counter, so a sample taken before that + # can exceed the next one. Only a total lack of progress means wedged. + prev = read_counter(mcp) + deadline = time.monotonic() + 5.0 + while time.monotonic() < deadline: + time.sleep(0.05) + cur = read_counter(mcp) + if cur > prev: + return cur + prev = cur + raise AssertionError(f"counter stuck at {prev} after reset: platform wedged") + + +def run_test(args) -> None: + mcd_port = find_free_port() + print(f"mcd_port={mcd_port} cores={args.cores} resets={args.resets}") + + cmd = [args.exe, "-p", f"mcd.port={mcd_port}", "-p", f"cores={args.cores}"] + if args.fw: + cmd += ["-p", f'fw="{args.fw}"'] + cmd += ["--gs_luafile", args.lua] + vp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, env=dict(os.environ)) + + mcp = None + done = 0 + pool = concurrent.futures.ThreadPoolExecutor(max_workers=1) + try: + wait_for_port("127.0.0.1", mcd_port, timeout=20.0) + mcp = MCP(args.mcp) + mcp.tool("mcd_connect", {"host": "127.0.0.1", "port": mcd_port}) + + for i in range(1, args.resets + 1): + if vp.poll() is not None: + raise AssertionError( + f"simulation exited after {done} resets (rc={vp.returncode})") + + fut = pool.submit(one_reset, mcp) + try: + counter = fut.result(timeout=args.timeout) + except concurrent.futures.TimeoutError: + alive = vp.poll() is None + print(f"HANG after {done} resets: iteration {i} exceeded " + f"{args.timeout}s; VP alive={alive}", file=sys.stderr) + raise AssertionError(f"HANG after {done} resets (iteration {i})") + + done = i + if i % 10 == 0 or i == 1: + print(f" reset {i}/{args.resets}: counter={counter}", flush=True) + + print(f"reset-stress: {done}/{args.resets} resets, no hang") + + finally: + pool.shutdown(wait=False) + if mcp: + try: + mcp.close() + except Exception: + pass + vp.send_signal(signal.SIGTERM) + try: + out, _ = vp.communicate(timeout=5) + except subprocess.TimeoutExpired: + vp.kill() + out, _ = vp.communicate() + if out: + print("--- VP stdout (tail) ---") + print(out[-3000:]) + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("-e", "--exe", required=True, metavar="VP") + p.add_argument("-l", "--lua", required=True, metavar="LUA") + p.add_argument("-m", "--mcp", required=True, metavar="MCP") + p.add_argument("-f", "--fw", default="", metavar="FW") + p.add_argument("-n", "--resets", type=int, default=200) + p.add_argument("--cores", type=int, default=1) + p.add_argument("--timeout", type=float, default=30.0) + args = p.parse_args() + + required = [args.exe, args.lua, args.mcp] + ([args.fw] if args.fw else []) + for path in required: + if not os.path.isfile(path): + print(f"ERROR: file not found: {path}", file=sys.stderr) + return 1 + + run_test(args) + return 0 + + +if __name__ == "__main__": + sys.exit(main())