Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions docs/libqbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand Down
13 changes: 9 additions & 4 deletions examples/hello-qbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion platforms/cortex-m55-remote/src/remote_cpu.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
37 changes: 31 additions & 6 deletions qemu-components/common/include/cpu.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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<unsigned int>& 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);
Expand Down Expand Up @@ -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
Expand Down
51 changes: 43 additions & 8 deletions qemu-components/common/include/qemu-instance.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ class QemuInstance : public sc_core::sc_module
public:
TargetSignalSocket<bool> 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<unsigned int> 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;
Expand Down Expand Up @@ -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.' */
Expand Down Expand Up @@ -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 <gdb_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 "
Expand Down Expand Up @@ -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());
}

/**
Expand Down Expand Up @@ -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 */
});
}

Expand Down Expand Up @@ -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)
{
Expand Down
2 changes: 2 additions & 0 deletions systemc-components/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions systemc-components/mcd_mcp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
119 changes: 119 additions & 0 deletions systemc-components/mcd_mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<!--
Copyright (c) 2026 Qualcomm Innovation Center, Inc. All Rights Reserved.
SPDX-License-Identifier: BSD-3-Clause
-->

# 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=<n>` and
`-p 'fw="<path>"'` (string values are JSON, so the path is quoted).
Loading
Loading