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) { 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..8040e416 --- /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` 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. | +| `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..6671849a --- /dev/null +++ b/systemc-components/mcd_mcp/include/mcd_client.h @@ -0,0 +1,175 @@ +/* + * 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; + +/* 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 { + 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); +/* 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); +/* 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); + +/* 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..4297a032 --- /dev/null +++ b/systemc-components/mcd_mcp/include/mcd_debug.h @@ -0,0 +1,186 @@ +/* + * 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; + uint32_t mem_type; // MCD_MEM_SPACE_*, e.g. MCD_MEM_SPACE_IS_REGISTERS +}; + +struct CoreInfo { + uint32_t core_id; + 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, + 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 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; } + +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(); + // Resume only this core, leaving the rest of its instance halted. + void run_only(); + 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); + + // 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. 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 + 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..5e03fc30 --- /dev/null +++ b/systemc-components/mcd_mcp/src/mcd_client.c @@ -0,0 +1,682 @@ +/* + * 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; + /* 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 + 4 + 64); + if (off + 4 + 4 + 64 > blen) return -1; + out[i].mem_space_id = get_u32_le(&resp[off]); + 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) +{ + uint8_t req[4]; + put_u32_le(req, cpu_idx); + return transact(c, 0x12, req, sizeof(req), NULL, 0, NULL); +} + +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[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. */ + 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, uint32_t addr_space_id, + const uint8_t* buf) +{ + /* 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 = 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); + 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); + 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..352261dc --- /dev/null +++ b/systemc-components/mcd_mcp/src/mcd_debug.cc @@ -0,0 +1,243 @@ +/* + * 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; +/* 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) +{ + 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, tmp[i].mem_type }); + } + } +} + +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; +} + +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(); + 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::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) { + 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"); + } +} + +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) { + 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, uint32_t addr_space_id) +{ + std::vector buf(len); + 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, uint32_t addr_space_id) +{ + 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"); + } +} + +} // 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..6c2d24d0 --- /dev/null +++ b/systemc-components/mcd_mcp/src/mcd_mcp.cc @@ -0,0 +1,951 @@ +/* + * 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); +} + +// 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}"; + 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 += ","; + 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; +} + +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) { + 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; +} + +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; +} + +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; + 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") { + // 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"; + } + 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_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"))); + 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"; + } + /* 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)); + 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)); + 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") { + 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, 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\":" + "\"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. 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\"},\"hw_thread\":{\"type\":\"integer\"}}," + "\"required\":[\"addr\",\"len\"]}}," + "{\"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\"},\"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 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).\"," + "\"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..160918ae --- /dev/null +++ b/systemc-components/mcd_server/include/mcd_server.h @@ -0,0 +1,420 @@ +/* + * 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]; + uint32_t mem_type; /* MCD_MEM_SPACE_* */ +}; + +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_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 +#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) */ + +/* 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) */ +#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; + /* 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(); + + /** + * @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; + void start_of_simulation() 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_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); + 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; + + /* 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. */ + 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. The + * register space has no transactor: it is served over the RSP session. */ + std::vector m_mem_spaces; + std::map> m_transactors; + + /* 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); +}; + +} // 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..567f448d --- /dev/null +++ b/systemc-components/mcd_server/src/mcd_server.cc @@ -0,0 +1,2558 @@ +/* + * 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 +#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 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) | + (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 */ +} + +/* 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) + , 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{}); + m_inst_regs.push_back(inst_regs_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; + 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); + 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 }); + m_core_running.push_back(m_gdb_running[idx]); + SCP_INFO(()) << "mcd_server: core " << (m_cores.size() - 1) << " = instance " << idx << " tid " << tid << " '" + << name << "'"; + } + + 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. */ +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; + /* 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"; + } + } + + /* 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; + set_inst_cores_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(); + 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{}); + + /* 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; + set_inst_cores_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; + set_inst_cores_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 }; + /* 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 + * 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; + set_inst_cores_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; + set_inst_cores_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, 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); + 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, 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 << "'"; +} + +/* 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, MCD_MEM_SPACE_DEFAULT, [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)); + } + } + + /* 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() +{ + if (p_mcd_port.get_value()) { + SCP_INFO(()) << "Starting MCD server on TCP port " << p_mcd_port.get_value(); + start_server(); + } +} + +/* 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); + 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 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_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 " << host << ":" << 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_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: + 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, 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; +} + +mcd_return_et mcd_server::op_read_mem(const std::vector& req, std::vector& resp) +{ + /* 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; + 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, 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; + + 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; + uint32_t addr_space_id = 0; + std::vector::size_type data_off; + 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) { + space_id = 0; + data_off = 12; + } else { + 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; + 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& 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; + + 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; + } + + std::string reply; + 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; + } 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]; + } + } + + /* 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; + set_inst_cores_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; + + /* The stub answers nothing while the VM runs; halt for the duration. */ + scoped_halt halt(*this, inst); + halt.exclude(core); /* a stepped core stays halted */ + + /* "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; + m_core_running[core] = 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_qry_state(const std::vector&, std::vector& resp) +{ + /* Response: 4-byte LE count, then each core as [core_id u32][running u8]. */ + std::lock_guard lock(m_gdb_mutex); + ensure_cores_known(); + + /* 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 false; + } + + 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 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 false; + } + if (reply.size() % 2 != 0) { + SCP_WARN(()) << "mcd_server: READ_REG odd-length hex reply '" << reply << "'"; + 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 false; + } + value.push_back(static_cast((hi << 4) | lo)); + } + return true; +} + +/* 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) +{ + 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 false; + } + + /* "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 (uint32_t i = 0; i < len; ++i) { + char b[3]; + 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; + } + + /* 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; + } + + 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; + * 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; + + /* 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]; + 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; + + /* 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); + + 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..5dc0f6b7 100644 --- a/tests/qbox/CMakeLists.txt +++ b/tests/qbox/CMakeLists.txt @@ -346,3 +346,10 @@ 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) + add_subdirectory(reset-stress) +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()) 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())