---
docs/system/target-riscv.rst | 60 ++++++++++
target/riscv/cpu.c | 128 ++++++++++++++++++++++
target/riscv/cpu_cfg_fields.h.inc | 3 +
target/riscv/tcg/tcg-cpu.c | 33 ++++++
tests/functional/riscv32/meson.build | 4 +
tests/functional/riscv32/test_cpu_arch.py | 61 +++++++++++
tests/functional/riscv64/meson.build | 4 +
tests/functional/riscv64/test_cpu_arch.py | 80 ++++++++++++++
8 files changed, 373 insertions(+)
create mode 100644 tests/functional/riscv32/test_cpu_arch.py
create mode 100644 tests/functional/riscv64/test_cpu_arch.py
diff --git a/docs/system/target-riscv.rst b/docs/system/target-riscv.rst
index 89b2cb732c2..3ec53dbf9e5 100644
--- a/docs/system/target-riscv.rst
+++ b/docs/system/target-riscv.rst
@@ -73,6 +73,66 @@ undocumented; you can get a complete list by running
riscv/virt
riscv/xiangshan-kunminghu
+RISC-V CPU options
+------------------
+
+RISC-V CPUs support various options to configure ISA extensions and other
+features. These options can be specified using the ``-cpu`` command line
+option.
+
+ISA extension configuration
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Individual ISA extensions can be enabled or disabled using boolean properties::
+
+ $ qemu-system-riscv64 -M virt -cpu rv64,v=true,zba=false
+
+The ``arch`` property
+^^^^^^^^^^^^^^^^^^^^^
+
+The ``arch`` property provides a convenient way to inspect the current ISA
+configuration:
+
+* ``arch=dump``
+
+ Print the current ISA configuration and exit. This shows the full ISA string
+ and the status of all supported extensions::
+
+ $ qemu-system-riscv64 -M virt -cpu rv64,arch=dump
+ $ qemu-riscv64 -cpu rv64,v=true,arch=dump /bin/true
+
+ The dump shows the final configuration after all CPU properties are applied,
+ regardless of where ``arch=dump`` appears in the option list. For example,
+ both of the following commands show the same result with vector extension
+ enabled::
+
+ $ qemu-riscv64 -cpu rv64,v=true,arch=dump /bin/true
+ $ qemu-riscv64 -cpu rv64,arch=dump,v=true /bin/true
+
+Privilege-implied extensions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Some RISC-V extensions cannot be individually enabled or disabled. These
+extensions are automatically enabled based on the privilege specification
+version configured for the CPU. Examples include:
+
+- ``shcounterenw``, ``shgatpa``, ``shtvala``, ``shvsatpa``, ``shvstvala``,
+ ``shvstvecd`` - Enabled when privilege spec 1.12 or later is supported
+- ``ssccptr``, ``sscounterenw``, ``ssstrict``, ``sstvala``, ``sstvecd``,
+ ``ssu64xl`` - Enabled when privilege spec 1.12 or later is supported
+- ``ziccamoa``, ``ziccif``, ``zicclsm``, ``za64rs`` - Enabled when
+ privilege spec 1.12 or later is supported
+
+These extensions appear in the ``arch=dump`` output under "Privilege Implied
+Extensions" section. To control these extensions, use the ``priv_spec``
+property to set the privilege specification version::
+
+ $ qemu-riscv64 -cpu rv64,h=false,priv_spec=v1.11.0,arch=dump /bin/true
+
+Note: Some extensions like H (Hypervisor) require a minimum privilege spec
+version. When lowering the privilege spec, you may need to disable such
+extensions first.
+
RISC-V CPU firmware
-------------------
diff --git a/target/riscv/cpu.c b/target/riscv/cpu.c
index 8f26d8b8b07..2886b7ebcdd 100644
--- a/target/riscv/cpu.c
+++ b/target/riscv/cpu.c
@@ -896,6 +896,129 @@ static void riscv_cpu_satp_mode_finalize(RISCVCPU *cpu, Error **errp)
}
#endif
+static inline const char *riscv_ext_status_str(bool enabled)
+{
+ return enabled ? "enabled" : "disabled";
+}
+
+/*
+ * Helper function to print multi-letter extension entries for arch=dump.
+ * Does not print section header.
+ */
+static void riscv_cpu_dump_multiext_entries(RISCVCPU *cpu,
+ const RISCVCPUMultiExtConfig *exts)
+{
+ for (const RISCVCPUMultiExtConfig *prop = exts;
+ prop && prop->name; prop++) {
+ bool enabled = isa_ext_is_enabled(cpu, prop->offset);
+ qemu_printf(" %-20s %-8s\n",
+ prop->name,
+ riscv_ext_status_str(enabled));
+ }
+}
+
+/*
+ * Helper function to print multi-letter extensions for arch=dump.
+ */
+static void riscv_cpu_dump_multiext(RISCVCPU *cpu, const char *title,
+ const RISCVCPUMultiExtConfig *exts)
+{
+ qemu_printf("%s:\n", title);
+ qemu_printf(" %-20s %-8s\n", "Name", "Status");
+ qemu_printf(" %-20s %-8s\n", "----", "------");
+
+ riscv_cpu_dump_multiext_entries(cpu, exts);
+ qemu_printf("\n");
+}
+
+/*
+ * Check if an extension offset corresponds to a privilege-implied extension.
+ * These are extensions that map to has_priv_1_11, has_priv_1_12, or
+ * has_priv_1_13 instead of individual ext_* fields.
+ */
+static bool riscv_ext_is_priv_implied(uint32_t offset)
+{
+ return offset == CPU_CFG_OFFSET(has_priv_1_11) ||
+ offset == CPU_CFG_OFFSET(has_priv_1_12) ||
+ offset == CPU_CFG_OFFSET(has_priv_1_13);
+}
+
+/*
+ * Helper function to print privilege-implied extensions for arch=dump.
+ * These are extensions that are automatically enabled based on the
+ * privilege specification version.
+ */
+static void riscv_cpu_dump_priv_implied_exts(RISCVCPU *cpu)
+{
+ qemu_printf("Privilege Implied Extensions:\n");
+ qemu_printf(" %-20s %-8s\n", "Name", "Status");
+ qemu_printf(" %-20s %-8s\n", "----", "------");
+
+ for (const RISCVIsaExtData *edata = isa_edata_arr; edata->name; edata++) {
+ if (riscv_ext_is_priv_implied(edata->ext_enable_offset)) {
+ bool enabled = isa_ext_is_enabled(cpu, edata->ext_enable_offset);
+ qemu_printf(" %-20s %-8s\n",
+ edata->name,
+ riscv_ext_status_str(enabled));
+ }
+ }
+ qemu_printf("\n");
+}
+
+/*
+ * Print detailed ISA configuration and exit.
+ * Called when arch=dump is specified.
+ */
+static G_NORETURN void riscv_cpu_dump_isa_config(RISCVCPU *cpu)
+{
+ RISCVCPUClass *mcc = RISCV_CPU_GET_CLASS(cpu);
+ CPURISCVState *env = &cpu->env;
+ g_autofree char *isa_str = riscv_isa_string(cpu);
+ int xlen = riscv_cpu_max_xlen(mcc);
+
+ qemu_printf("\n");
+ qemu_printf("RISC-V ISA Configuration (arch=dump)\n");
+ qemu_printf("=====================================\n\n");
+
+ /* Print full ISA string */
+ qemu_printf("Full ISA string: %s\n\n", isa_str);
+
+ /* Print base information */
+ qemu_printf("Base: RV%d\n", xlen);
+ qemu_printf("Privilege spec: %s\n\n", priv_spec_to_str(env->priv_ver));
+
+ /* Print single-letter extensions */
+ qemu_printf("Standard Extensions (single-letter):\n");
+ qemu_printf(" %-20s %-8s\n", "Name", "Status");
+ qemu_printf(" %-20s %-8s\n", "----", "------");
+
+ for (int i = 0; misa_bits[i] != 0; i++) {
+ uint32_t bit = misa_bits[i];
+ const char *name = riscv_get_misa_ext_name(bit);
+ bool enabled = env->misa_ext & bit;
+
+ qemu_printf(" %-20s %-8s\n",
+ name,
+ riscv_ext_status_str(enabled));
+ }
+ qemu_printf("\n");
+
+ /* Print multi-letter standard extensions (including named features) */
+ qemu_printf("Standard Extensions (multi-letter):\n");
+ qemu_printf(" %-20s %-8s\n", "Name", "Status");
+ qemu_printf(" %-20s %-8s\n", "----", "------");
+ riscv_cpu_dump_multiext_entries(cpu, riscv_cpu_extensions);
+ riscv_cpu_dump_multiext_entries(cpu, riscv_cpu_named_features);
+ qemu_printf("\n");
+
+ riscv_cpu_dump_multiext(cpu, "Vendor Extensions", riscv_cpu_vendor_exts);
+ riscv_cpu_dump_multiext(cpu, "Experimental Extensions",
+ riscv_cpu_experimental_exts);
+ riscv_cpu_dump_priv_implied_exts(cpu);
+
+ exit(0);
+}
+
void riscv_cpu_finalize_features(RISCVCPU *cpu, Error **errp)
{
Error *local_err = NULL;
@@ -943,6 +1066,11 @@ static void riscv_cpu_realize(DeviceState *dev, Error **errp)
return;
}
+ /* Check for arch=dump request after all features are finalized */
+ if (cpu->cfg.arch_dump_requested) {
+ riscv_cpu_dump_isa_config(cpu);
+ }
+
riscv_cpu_register_gdb_regs_for_features(cs);
#ifndef CONFIG_USER_ONLY
diff --git a/target/riscv/cpu_cfg_fields.h.inc b/target/riscv/cpu_cfg_fields.h.inc
index a154ecdc792..87ef228a17d 100644
--- a/target/riscv/cpu_cfg_fields.h.inc
+++ b/target/riscv/cpu_cfg_fields.h.inc
@@ -155,6 +155,9 @@ BOOL_FIELD(misa_w)
BOOL_FIELD(short_isa_string)
+/* arch= property flags */
+BOOL_FIELD(arch_dump_requested)
+
TYPED_FIELD(uint32_t, mvendorid, 0)
TYPED_FIELD(uint64_t, marchid, 0)
TYPED_FIELD(uint64_t, mimpid, 0)
diff --git a/target/riscv/tcg/tcg-cpu.c b/target/riscv/tcg/tcg-cpu.c
index bb03f8dc0ca..f7187472cd2 100644
--- a/target/riscv/tcg/tcg-cpu.c
+++ b/target/riscv/tcg/tcg-cpu.c
@@ -1551,6 +1551,37 @@ static void riscv_cpu_add_multiext_prop_array(Object *obj,
}
}
+/*
+ * arch= property handler for ISA string configuration.
+ * This is a write-only property used to trigger actions like arch=dump.
+ */
+static void cpu_set_arch(Object *obj, Visitor *v, const char *name,
+ void *opaque, Error **errp)
+{
+ RISCVCPU *cpu = RISCV_CPU(obj);
+ g_autofree char *value = NULL;
+
+ if (!visit_type_str(v, name, &value, errp)) {
+ return;
+ }
+
+ if (g_strcmp0(value, "dump") == 0) {
+ cpu->cfg.arch_dump_requested = true;
+ } else {
+ error_setg(errp, "unknown arch option '%s'. "
+ "Supported options: dump", value);
+ }
+}
+
+static void riscv_cpu_add_arch_property(Object *obj)
+{
+ object_property_add(obj, "arch", "str",
+ NULL, cpu_set_arch,
+ NULL, NULL);
+ object_property_set_description(obj, "arch",
+ "ISA configuration string (write-only). Use 'dump' to print ISA config.");
+}
+
/*
* Add CPU properties with user-facing flags.
*
@@ -1570,6 +1601,8 @@ static void riscv_cpu_add_user_properties(Object *obj)
riscv_cpu_add_multiext_prop_array(obj, riscv_cpu_experimental_exts);
riscv_cpu_add_profiles(obj);
+
+ riscv_cpu_add_arch_property(obj);
}
/*
diff --git a/tests/functional/riscv32/meson.build b/tests/functional/riscv32/meson.build
index f3ebbb8db5d..a3f7a3dffce 100644
--- a/tests/functional/riscv32/meson.build
+++ b/tests/functional/riscv32/meson.build
@@ -8,3 +8,7 @@ tests_riscv32_system_quick = [
tests_riscv32_system_thorough = [
'tuxrun',
]
+
+tests_riscv32_linuxuser_quick = [
+ 'cpu_arch',
+]
diff --git a/tests/functional/riscv32/test_cpu_arch.py b/tests/functional/riscv32/test_cpu_arch.py
new file mode 100644
index 00000000000..7b2f87cad88
--- /dev/null
+++ b/tests/functional/riscv32/test_cpu_arch.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+#
+# Test RISC-V CPU arch= property
+#
+# Copyright (c) 2026 SiFive, Inc.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from subprocess import run
+
+from qemu_test import QemuUserTest
+
+
+class RiscvCpuArch(QemuUserTest):
+ """Test RISC-V CPU arch= property"""
+
+ def run_qemu(self, cpu_opt, bin_path='/bin/true'):
+ """Run qemu-riscv32 with specified CPU option"""
+ cmd = [self.qemu_bin, '-cpu', cpu_opt, bin_path]
+ return run(cmd, text=True, capture_output=True)
+
+ def test_arch_dump(self):
+ """Test arch=dump prints ISA configuration and exits"""
+ res = self.run_qemu('rv32,arch=dump')
+
+ self.assertEqual(res.returncode, 0,
+ f"arch=dump should exit with 0, got {res.returncode}")
+
+ # Check for expected output sections
+ self.assertIn('RISC-V ISA Configuration', res.stdout)
+ self.assertIn('Full ISA string:', res.stdout)
+ self.assertIn('Standard Extensions (single-letter):', res.stdout)
+ self.assertIn('Standard Extensions (multi-letter):', res.stdout)
+ self.assertIn('Vendor Extensions:', res.stdout)
+ # Check it's RV32
+ self.assertIn('Base: RV32', res.stdout)
+
+ def test_arch_dump_shows_enabled_extensions(self):
+ """Test arch=dump correctly shows enabled extensions"""
+ res = self.run_qemu('rv32,arch=dump')
+
+ # Default rv32 should have these enabled
+ self.assertRegex(res.stdout, r'i\s+enabled')
+ self.assertRegex(res.stdout, r'm\s+enabled')
+ self.assertRegex(res.stdout, r'a\s+enabled')
+ self.assertRegex(res.stdout, r'f\s+enabled')
+ self.assertRegex(res.stdout, r'd\s+enabled')
+ self.assertRegex(res.stdout, r'c\s+enabled')
+
+ def test_arch_invalid_option(self):
+ """Test invalid arch= option shows error with supported options"""
+ res = self.run_qemu('rv32,arch=invalid')
+
+ self.assertNotEqual(res.returncode, 0,
+ "Invalid arch option should fail")
+ self.assertIn("unknown arch option 'invalid'", res.stderr)
+ self.assertIn("Supported options:", res.stderr)
+
+
+if __name__ == '__main__':
+ QemuUserTest.main()
diff --git a/tests/functional/riscv64/meson.build b/tests/functional/riscv64/meson.build
index c1704d92751..82a29dcc974 100644
--- a/tests/functional/riscv64/meson.build
+++ b/tests/functional/riscv64/meson.build
@@ -13,3 +13,7 @@ tests_riscv64_system_thorough = [
'sifive_u',
'tuxrun',
]
+
+tests_riscv64_linuxuser_quick = [
+ 'cpu_arch',
+]
diff --git a/tests/functional/riscv64/test_cpu_arch.py b/tests/functional/riscv64/test_cpu_arch.py
new file mode 100644
index 00000000000..b0af8991397
--- /dev/null
+++ b/tests/functional/riscv64/test_cpu_arch.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+#
+# Test RISC-V CPU arch= property
+#
+# Copyright (c) 2026 SiFive, Inc.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from subprocess import run
+
+from qemu_test import QemuUserTest
+
+
+class RiscvCpuArch(QemuUserTest):
+ """Test RISC-V CPU arch= property"""
+
+ def run_qemu(self, cpu_opt, bin_path='/bin/true'):
+ """Run qemu-riscv64 with specified CPU option"""
+ cmd = [self.qemu_bin, '-cpu', cpu_opt, bin_path]
+ return run(cmd, text=True, capture_output=True)
+
+ def test_arch_dump(self):
+ """Test arch=dump prints ISA configuration and exits"""
+ res = self.run_qemu('rv64,arch=dump')
+
+ self.assertEqual(res.returncode, 0,
+ f"arch=dump should exit with 0, got {res.returncode}")
+
+ # Check for expected output sections
+ self.assertIn('RISC-V ISA Configuration', res.stdout)
+ self.assertIn('Full ISA string:', res.stdout)
+ self.assertIn('Standard Extensions (single-letter):', res.stdout)
+ self.assertIn('Standard Extensions (multi-letter):', res.stdout)
+ self.assertIn('Vendor Extensions:', res.stdout)
+
+ def test_arch_dump_shows_enabled_extensions(self):
+ """Test arch=dump correctly shows enabled extensions"""
+ res = self.run_qemu('rv64,arch=dump')
+
+ # Default rv64 should have these enabled
+ self.assertRegex(res.stdout, r'i\s+enabled')
+ self.assertRegex(res.stdout, r'm\s+enabled')
+ self.assertRegex(res.stdout, r'a\s+enabled')
+ self.assertRegex(res.stdout, r'f\s+enabled')
+ self.assertRegex(res.stdout, r'd\s+enabled')
+ self.assertRegex(res.stdout, r'c\s+enabled')
+
+ def test_arch_dump_with_vector(self):
+ """Test arch=dump shows vector extension when enabled"""
+ res = self.run_qemu('rv64,v=true,arch=dump')
+
+ self.assertEqual(res.returncode, 0)
+ self.assertRegex(res.stdout, r'v\s+enabled')
+
+ def test_arch_dump_position_independence(self):
+ """Test arch=dump shows final config regardless of position"""
+ # arch=dump before v=true
+ res1 = self.run_qemu('rv64,arch=dump,v=true')
+ # arch=dump after v=true
+ res2 = self.run_qemu('rv64,v=true,arch=dump')
+
+ self.assertEqual(res1.returncode, 0)
+ self.assertEqual(res2.returncode, 0)
+
+ # Both should show v enabled
+ self.assertRegex(res1.stdout, r'v\s+enabled')
+ self.assertRegex(res2.stdout, r'v\s+enabled')
+
+ def test_arch_invalid_option(self):
+ """Test invalid arch= option shows error with supported options"""
+ res = self.run_qemu('rv64,arch=invalid')
+
+ self.assertNotEqual(res.returncode, 0,
+ "Invalid arch option should fail")
+ self.assertIn("unknown arch option 'invalid'", res.stderr)
+ self.assertIn("Supported options:", res.stderr)
+
+
+if __name__ == '__main__':
+ QemuUserTest.main()
--
2.52.0
© 2016 - 2026 Red Hat, Inc.