* [PATCH 0/3] accel/mshv: add gdbstub guest debugging support
@ 2026-07-27 14:28 Doru Blânzeanu
2026-07-27 14:28 ` [PATCH 1/3] include/hw/hyperv: add ABI for exception intercepts and pending events Doru Blânzeanu
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Doru Blânzeanu @ 2026-07-27 14:28 UTC (permalink / raw)
To: qemu-devel
Cc: Magnus Kulke, Doru Blânzeanu, Doru Blânzeanu, Wei Liu,
Wei Liu, Magnus Kulke
This series brings guest debugging to the MSHV accelerator by
implementing software breakpoints and single-stepping, so that gdb can
attach to an MSHV guest through QEMU's gdbstub.
It follows the KVM gdbstub implementation closely, and borrows the WHPX
approach where the MSHV interface differs from KVM:
- it installs a partition-wide intercept for the #DB exception
(HVCALL_INSTALL_INTERCEPT). #BP (INT3) is deliberately left to the
guest's own IDT and is never intercepted, as WHPX does.
- only software breakpoints are supported. A breakpoint is an INT1
(opcode 0xf1) patched into guest memory, which raises a #DB when
executed. Hardware breakpoints and watchpoints are not supported.
- it never touches the guest debug registers (DR0-DR7), so guest state
is not clobbered. The only guest register used is RFLAGS.TF, toggled
around the vCPU run for single-stepping and kept out of env->eflags
so it cannot be read back and re-armed.
- it re-injects the #DB back into the guest (via
HV_REGISTER_PENDING_EVENT0) when the faulting RIP does not match a
debugger breakpoint, so a guest-owned #DB is delivered through the
guest's own IDT.
When a vCPU is created we register the accel ops that gdb drives once it
is attached:
- mshv_update_guest_debug
- mshv_insert_gdbstub_breakpoint
- mshv_remove_gdbstub_breakpoint
- mshv_remove_all_gdbstub_breakpoints
For each insert request from gdb the ops save the original byte at the
target address and patch in 0xf1; on a remove request the saved byte is
restored, after which gdb single-steps over the original instruction and
re-inserts the breakpoint. When the vCPU executes the 0xf1 byte a #DB
vmexits to QEMU. mshv_handle_debug() then checks whether the RIP where
the vCPU stopped matches a breakpoint installed by gdb, or whether
single-stepping is active, and if so reports the stop to gdb. If the RIP
does not match any debugger breakpoint the #DB is assumed to be
guest-owned and is re-injected so the guest can handle it.
Known limitations:
- Once installed, the #DB intercept stays for the lifetime
of the VM and every #DB - including those the guest raises itself -
vmexits to QEMU.
- Because attribution relies on matching the faulting RIP against the
debugger's breakpoint list, a guest that legitimately executes INT1 or
raises its own #DB at an address that also holds a debugger breakpoint
cannot be disambiguated; such overlaps are not expected in practice.
The series is organised as:
1. include/hw/hyperv: add ABI for exception intercepts and pending
events
2. accel/mshv: add gdbstub software breakpoint support
3. target/i386/mshv: support single-stepping
I have tested this patch series by using both a guest that doesn't use debugging
and one that triggers the #DB and #BP exceptions (#DB is re-injected and #BP is
not intercepted at all).
Doru Blânzeanu (3):
include/hw/hyperv: add ABI for exception intercepts and pending events
accel/mshv: add gdbstub software breakpoint support
target/i386/mshv: support single-stepping
accel/mshv/mshv-all.c | 165 ++++++++++++++++++++++++++++++++-
include/hw/hyperv/hvgdk_mini.h | 56 +++++++++++
include/system/mshv_int.h | 14 +++
target/i386/mshv/mshv-cpu.c | 136 +++++++++++++++++++++++++++
4 files changed, 370 insertions(+), 1 deletion(-)
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH 1/3] include/hw/hyperv: add ABI for exception intercepts and pending events 2026-07-27 14:28 [PATCH 0/3] accel/mshv: add gdbstub guest debugging support Doru Blânzeanu @ 2026-07-27 14:28 ` Doru Blânzeanu 2026-07-28 12:12 ` Magnus Kulke 2026-07-27 14:28 ` [PATCH 2/3] accel/mshv: add gdbstub software breakpoint support Doru Blânzeanu 2026-07-27 14:28 ` [PATCH 3/3] target/i386/mshv: support single-stepping Doru Blânzeanu 2 siblings, 1 reply; 7+ messages in thread From: Doru Blânzeanu @ 2026-07-27 14:28 UTC (permalink / raw) To: qemu-devel Cc: Magnus Kulke, Doru Blânzeanu, Doru Blânzeanu, Wei Liu, Wei Liu, Magnus Kulke Import the hypervisor definitions needed to intercept guest exceptions and to inject events back into a guest, in preparation for gdbstub debugging support in the MSHV accelerator. Add: - union hv_intercept_parameters, struct hv_input_install_intercept and HVCALL_INSTALL_INTERCEPT, used to install an exception intercept on the partition; - struct hv_x64_exception_intercept_message, the message delivered on an intercepted exception, carrying the faulting RIP, the exception vector and the instruction bytes; - HV_X64_PENDING_EVENT_EXCEPTION, the pending-event type for an exception. These definitions mirror the hypervisor headers and have no functional effect on their own. Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com> --- include/hw/hyperv/hvgdk_mini.h | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/include/hw/hyperv/hvgdk_mini.h b/include/hw/hyperv/hvgdk_mini.h index f8838a31bb..c81928d0c2 100644 --- a/include/hw/hyperv/hvgdk_mini.h +++ b/include/hw/hyperv/hvgdk_mini.h @@ -22,6 +22,9 @@ #define HV_X64_MSR_TSC_FREQUENCY 0x40000022 #define HV_X64_MSR_APIC_FREQUENCY 0x40000023 +/* event_type values for hv_x64_pending_exception_event */ +#define HV_X64_PENDING_EVENT_EXCEPTION 0 + typedef enum hv_register_name { /* Pending Interruption Register */ HV_REGISTER_PENDING_INTERRUPTION = 0x00010002, @@ -236,6 +239,29 @@ enum hv_intercept_type { HV_INTERCEPT_TYPE_INVALID = 0XFFFFFFFF, }; +union hv_intercept_parameters { + /* HV_INTERCEPT_PARAMETERS is defined to be an 8-byte field. */ + uint64_t as_uint64; + /* HV_INTERCEPT_TYPE_X64_IO_PORT */ + uint16_t io_port; + /* HV_INTERCEPT_TYPE_X64_CPUID */ + uint32_t cpuid_index; + /* HV_INTERCEPT_TYPE_X64_APIC_WRITE */ + uint32_t apic_write_mask; + /* HV_INTERCEPT_TYPE_EXCEPTION */ + uint16_t exception_vector; + /* HV_INTERCEPT_TYPE_X64_MSR_INDEX */ + uint32_t msr_index; + /* N.B. Other intercept types do not have any parameters. */ +}; + +struct hv_input_install_intercept { + uint64_t partition_id; + uint32_t access_type; /* mask */ + uint32_t intercept_type; /* enum hv_intercept_type */ + union hv_intercept_parameters intercept_parameter; +} QEMU_PACKED; + struct hv_u128 { uint64_t low_part; uint64_t high_part; @@ -836,6 +862,35 @@ struct hv_x64_memory_intercept_message { uint8_t instruction_bytes[16]; } QEMU_PACKED; +struct hv_x64_exception_intercept_message { + struct hv_x64_intercept_message_header header; + uint16_t exception_vector; + uint8_t exception_info; + uint8_t instruction_byte_count; + uint32_t error_code; + uint64_t exception_parameter; /* DR6 for #DB, CR2 for #PF */ + uint64_t reserved; + uint8_t instruction_bytes[16]; + struct hv_x64_segment_register ds_segment; + struct hv_x64_segment_register ss_segment; + uint64_t rax; + uint64_t rcx; + uint64_t rdx; + uint64_t rbx; + uint64_t rsp; + uint64_t rbp; + uint64_t rsi; + uint64_t rdi; + uint64_t r8; + uint64_t r9; + uint64_t r10; + uint64_t r11; + uint64_t r12; + uint64_t r13; + uint64_t r14; + uint64_t r15; +} QEMU_PACKED; + union hv_message_flags { uint8_t asu8; struct { @@ -943,6 +998,7 @@ struct hv_cpuid { #define HVCALL_GET_PARTITION_PROPERTY 0x0044 #define HVCALL_SET_PARTITION_PROPERTY 0x0045 +#define HVCALL_INSTALL_INTERCEPT 0x004d #define HVCALL_GET_VP_REGISTERS 0x0050 #define HVCALL_SET_VP_REGISTERS 0x0051 #define HVCALL_TRANSLATE_VIRTUAL_ADDRESS 0x0052 -- 2.53.0 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH 1/3] include/hw/hyperv: add ABI for exception intercepts and pending events 2026-07-27 14:28 ` [PATCH 1/3] include/hw/hyperv: add ABI for exception intercepts and pending events Doru Blânzeanu @ 2026-07-28 12:12 ` Magnus Kulke 0 siblings, 0 replies; 7+ messages in thread From: Magnus Kulke @ 2026-07-28 12:12 UTC (permalink / raw) To: Doru Blânzeanu Cc: qemu-devel, Doru Blânzeanu, Wei Liu, Wei Liu, Magnus Kulke On Mon, Jul 27, 2026 at 05:28:05PM +0300, Doru Blânzeanu wrote: > Import the hypervisor definitions needed to intercept guest exceptions > and to inject events back into a guest, in preparation for gdbstub > debugging support in the MSHV accelerator. > > Add: > - union hv_intercept_parameters, struct hv_input_install_intercept and > HVCALL_INSTALL_INTERCEPT, used to install an exception intercept on > the partition; > - struct hv_x64_exception_intercept_message, the message delivered on > an intercepted exception, carrying the faulting RIP, the exception > vector and the instruction bytes; > - HV_X64_PENDING_EVENT_EXCEPTION, the pending-event type for an > exception. > > These definitions mirror the hypervisor headers and have no functional > effect on their own. > > Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com> > --- > include/hw/hyperv/hvgdk_mini.h | 56 ++++++++++++++++++++++++++++++++++ > 1 file changed, 56 insertions(+) > > diff --git a/include/hw/hyperv/hvgdk_mini.h b/include/hw/hyperv/hvgdk_mini.h > index f8838a31bb..c81928d0c2 100644 > --- a/include/hw/hyperv/hvgdk_mini.h > +++ b/include/hw/hyperv/hvgdk_mini.h > @@ -22,6 +22,9 @@ > #define HV_X64_MSR_TSC_FREQUENCY 0x40000022 > #define HV_X64_MSR_APIC_FREQUENCY 0x40000023 > > +/* event_type values for hv_x64_pending_exception_event */ > +#define HV_X64_PENDING_EVENT_EXCEPTION 0 > + > typedef enum hv_register_name { > /* Pending Interruption Register */ > HV_REGISTER_PENDING_INTERRUPTION = 0x00010002, > @@ -236,6 +239,29 @@ enum hv_intercept_type { > HV_INTERCEPT_TYPE_INVALID = 0XFFFFFFFF, > }; > > +union hv_intercept_parameters { > + /* HV_INTERCEPT_PARAMETERS is defined to be an 8-byte field. */ > + uint64_t as_uint64; > + /* HV_INTERCEPT_TYPE_X64_IO_PORT */ > + uint16_t io_port; > + /* HV_INTERCEPT_TYPE_X64_CPUID */ > + uint32_t cpuid_index; > + /* HV_INTERCEPT_TYPE_X64_APIC_WRITE */ > + uint32_t apic_write_mask; > + /* HV_INTERCEPT_TYPE_EXCEPTION */ > + uint16_t exception_vector; > + /* HV_INTERCEPT_TYPE_X64_MSR_INDEX */ > + uint32_t msr_index; > + /* N.B. Other intercept types do not have any parameters. */ > +}; > + > +struct hv_input_install_intercept { > + uint64_t partition_id; > + uint32_t access_type; /* mask */ > + uint32_t intercept_type; /* enum hv_intercept_type */ > + union hv_intercept_parameters intercept_parameter; > +} QEMU_PACKED; > + > struct hv_u128 { > uint64_t low_part; > uint64_t high_part; > @@ -836,6 +862,35 @@ struct hv_x64_memory_intercept_message { > uint8_t instruction_bytes[16]; > } QEMU_PACKED; > > +struct hv_x64_exception_intercept_message { > + struct hv_x64_intercept_message_header header; > + uint16_t exception_vector; > + uint8_t exception_info; > + uint8_t instruction_byte_count; > + uint32_t error_code; > + uint64_t exception_parameter; /* DR6 for #DB, CR2 for #PF */ > + uint64_t reserved; > + uint8_t instruction_bytes[16]; > + struct hv_x64_segment_register ds_segment; > + struct hv_x64_segment_register ss_segment; > + uint64_t rax; > + uint64_t rcx; > + uint64_t rdx; > + uint64_t rbx; > + uint64_t rsp; > + uint64_t rbp; > + uint64_t rsi; > + uint64_t rdi; > + uint64_t r8; > + uint64_t r9; > + uint64_t r10; > + uint64_t r11; > + uint64_t r12; > + uint64_t r13; > + uint64_t r14; > + uint64_t r15; > +} QEMU_PACKED; > + > union hv_message_flags { > uint8_t asu8; > struct { > @@ -943,6 +998,7 @@ struct hv_cpuid { > > #define HVCALL_GET_PARTITION_PROPERTY 0x0044 > #define HVCALL_SET_PARTITION_PROPERTY 0x0045 > +#define HVCALL_INSTALL_INTERCEPT 0x004d > #define HVCALL_GET_VP_REGISTERS 0x0050 > #define HVCALL_SET_VP_REGISTERS 0x0051 > #define HVCALL_TRANSLATE_VIRTUAL_ADDRESS 0x0052 > -- > 2.53.0 Reviewed-by: Magnus Kulke <magnuskulke@linux.microsoft.com> ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 2/3] accel/mshv: add gdbstub software breakpoint support 2026-07-27 14:28 [PATCH 0/3] accel/mshv: add gdbstub guest debugging support Doru Blânzeanu 2026-07-27 14:28 ` [PATCH 1/3] include/hw/hyperv: add ABI for exception intercepts and pending events Doru Blânzeanu @ 2026-07-27 14:28 ` Doru Blânzeanu 2026-07-28 12:37 ` Magnus Kulke 2026-07-27 14:28 ` [PATCH 3/3] target/i386/mshv: support single-stepping Doru Blânzeanu 2 siblings, 1 reply; 7+ messages in thread From: Doru Blânzeanu @ 2026-07-27 14:28 UTC (permalink / raw) To: qemu-devel Cc: Magnus Kulke, Doru Blânzeanu, Doru Blânzeanu, Wei Liu, Wei Liu, Magnus Kulke Implement software breakpoint based guest debugging for the MSHV accelerator, modeled on the WHPX backend. The guest-visible debug registers are not used: a breakpoint is an INT1 (opcode 0xf1) patched into guest memory, which raises a #DB when executed. A partition-wide intercept on #DB is installed the first time a breakpoint (or single step) is requested. The intercept is not uninstalled, so it stays for the lifetime of the VM and every vmexit mshv_handle_debug() decides whether the #DB belongs to gdb (it hit one of our INT1 breakpoints) or to the guest; a guest-owned #DB is handed back through a pending exception event so the guest takes it through its own IDT. Guest INT3 (#BP) is deliberately left to the guest's own IDT and is never intercepted. Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com> --- accel/mshv/mshv-all.c | 165 +++++++++++++++++++++++++++++++++++- include/system/mshv_int.h | 14 +++ target/i386/mshv/mshv-cpu.c | 86 +++++++++++++++++++ 3 files changed, 264 insertions(+), 1 deletion(-) diff --git a/accel/mshv/mshv-all.c b/accel/mshv/mshv-all.c index 72721d0f0d..afcb87b3b8 100644 --- a/accel/mshv/mshv-all.c +++ b/accel/mshv/mshv-all.c @@ -29,6 +29,7 @@ #include "qemu/accel.h" #include "qemu/guest-random.h" +#include "gdbstub/enums.h" #include "accel/accel-ops.h" #include "accel/accel-cpu-ops.h" #include "exec/cpu-common.h" @@ -583,6 +584,10 @@ static int mshv_init(AccelState *as, MachineState *ms) s->nr_as = 1; s->as = g_new0(MshvAddressSpace, s->nr_as); + QTAILQ_INIT(&s->sw_breakpoints); + + as->gdbstub.sstep_flags = SSTEP_ENABLE; + mshv_state = s; mshv_init_irq_routing(s); @@ -607,6 +612,153 @@ static int mshv_destroy_vcpu(CPUState *cpu) return 0; } +struct MshvSwBreakpoint *mshv_find_sw_breakpoint(CPUState *cpu, vaddr pc) +{ + struct MshvSwBreakpoint *bp; + + QTAILQ_FOREACH(bp, &mshv_state->sw_breakpoints, entry) { + if (bp->pc == pc) { + return bp; + } + } + return NULL; +} + +static int mshv_install_exception_intercept(int vm_fd, uint16_t vector) +{ + struct hv_input_install_intercept in = {0}; + struct mshv_root_hvcall args = {0}; + int ret; + + in.access_type = 1 << HV_X64_INTERCEPT_ACCESS_TYPE_EXECUTE; + in.intercept_type = HV_INTERCEPT_TYPE_EXCEPTION; + in.intercept_parameter.as_uint64 = vector; + + args.code = HVCALL_INSTALL_INTERCEPT; + args.in_sz = sizeof(in); + args.in_ptr = (uint64_t)∈ + + ret = mshv_hvcall(vm_fd, &args); + if (ret < 0) { + error_report("Failed to install exception intercept for vector %u", + vector); + } + return ret; +} + +/* + * Install the #DB intercept. This is a permanent, partition-wide latch: it is + * installed once on the first debug use and stays for the lifetime of the VM. + */ +static int mshv_init_exception_intercept(void) +{ + int ret; + + if (mshv_state->exception_intercepts_installed) { + return 0; + } + + /* Only #DB is needed: gdb breakpoints are INT1 (0xf1) */ + ret = mshv_install_exception_intercept(mshv_state->vm, 1); /* #DB */ + if (ret < 0) { + return ret; + } + + mshv_state->exception_intercepts_installed = true; + return 0; +} + +static int mshv_update_guest_debug(CPUState *cpu) +{ + if (cpu_single_stepping(cpu)) { + /* Installs exception intercept only on the first call */ + return mshv_init_exception_intercept(); + } + return 0; +} + +static int mshv_insert_gdbstub_breakpoint(CPUState *cpu, GdbBreakpointType type, + vaddr addr, vaddr len) +{ + struct MshvSwBreakpoint *bp; + int err; + + /* We don't use the guest's debug registers. */ + if (type != GDB_BREAKPOINT_SW) { + return -ENOSYS; + } + + bp = mshv_find_sw_breakpoint(cpu, addr); + if (bp) { + bp->use_count++; + return 0; + } + + bp = g_new(struct MshvSwBreakpoint, 1); + bp->pc = addr; + bp->use_count = 1; + err = mshv_arch_insert_sw_breakpoint(cpu, bp); + if (err) { + g_free(bp); + return err; + } + + QTAILQ_INSERT_HEAD(&mshv_state->sw_breakpoints, bp, entry); + + /* Installs exception intercept only on the first call */ + return mshv_init_exception_intercept(); +} + +static int mshv_remove_gdbstub_breakpoint(CPUState *cpu, GdbBreakpointType type, + vaddr addr, vaddr len) +{ + struct MshvSwBreakpoint *bp; + int err; + + if (type != GDB_BREAKPOINT_SW) { + return -ENOSYS; + } + + bp = mshv_find_sw_breakpoint(cpu, addr); + if (!bp) { + return -ENOENT; + } + + if (bp->use_count > 1) { + bp->use_count--; + return 0; + } + + err = mshv_arch_remove_sw_breakpoint(cpu, bp); + if (err) { + return err; + } + + QTAILQ_REMOVE(&mshv_state->sw_breakpoints, bp, entry); + g_free(bp); + + return 0; +} + +static void mshv_remove_all_gdbstub_breakpoints(CPUState *cpu) +{ + struct MshvSwBreakpoint *bp, *next; + CPUState *tmpcpu; + + QTAILQ_FOREACH_SAFE(bp, &mshv_state->sw_breakpoints, entry, next) { + if (mshv_arch_remove_sw_breakpoint(cpu, bp) != 0) { + /* Fall back to whichever CPU still has it mapped. */ + CPU_FOREACH(tmpcpu) { + if (mshv_arch_remove_sw_breakpoint(tmpcpu, bp) == 0) { + break; + } + } + } + QTAILQ_REMOVE(&mshv_state->sw_breakpoints, bp, entry); + g_free(bp); + } +} + static int mshv_cpu_exec(CPUState *cpu) { hv_message mshv_msg; @@ -637,6 +789,9 @@ static int mshv_cpu_exec(CPUState *cpu) switch (exit_reason) { case MshvVmExitIgnore: break; + case MshvVmExitDebug: + ret = EXCP_DEBUG; + break; default: ret = EXCP_INTERRUPT; break; @@ -708,7 +863,10 @@ static void *mshv_vcpu_thread(void *arg) do { qemu_process_cpu_events(cpu); if (cpu_can_run(cpu)) { - mshv_cpu_exec(cpu); + ret = mshv_cpu_exec(cpu); + if (ret == EXCP_DEBUG) { + cpu_handle_guest_debug(cpu); + } } } while (!cpu->unplug || cpu_can_run(cpu)); @@ -852,6 +1010,11 @@ static void mshv_accel_ops_class_init(ObjectClass *oc, const void *data) ops->synchronize_pre_loadvm = mshv_cpu_synchronize_pre_loadvm; ops->cpus_are_resettable = mshv_cpus_are_resettable; ops->handle_interrupt = generic_handle_interrupt; + + ops->update_guest_debug = mshv_update_guest_debug; + ops->insert_gdbstub_breakpoint = mshv_insert_gdbstub_breakpoint; + ops->remove_gdbstub_breakpoint = mshv_remove_gdbstub_breakpoint; + ops->remove_all_gdbstub_breakpoints = mshv_remove_all_gdbstub_breakpoints; } static const TypeInfo mshv_accel_ops_type = { diff --git a/include/system/mshv_int.h b/include/system/mshv_int.h index b91c4d661a..9244915eb6 100644 --- a/include/system/mshv_int.h +++ b/include/system/mshv_int.h @@ -15,6 +15,7 @@ #define QEMU_MSHV_INT_H #include "hw/hyperv/hvhdk.h" +#include "exec/vaddr.h" #define MSHV_MSR_ENTRIES_COUNT 64 @@ -56,6 +57,13 @@ typedef struct MshvAddressSpace { AddressSpace *as; } MshvAddressSpace; +struct MshvSwBreakpoint { + vaddr pc; + vaddr saved_insn; + int use_count; + QTAILQ_ENTRY(MshvSwBreakpoint) entry; +}; + struct MshvState { AccelState parent_obj; int vm; @@ -70,6 +78,8 @@ struct MshvState { unsigned long *used_gsi_bitmap; unsigned int gsi_count; union hv_partition_processor_features processor_features; + QTAILQ_HEAD(, MshvSwBreakpoint) sw_breakpoints; + bool exception_intercepts_installed; }; typedef struct MshvMsiControl { @@ -85,6 +95,7 @@ typedef enum MshvVmExit { MshvVmExitIgnore = 0, MshvVmExitShutdown = 1, MshvVmExitSpecial = 2, + MshvVmExitDebug = 3, } MshvVmExit; void mshv_init_mmio_emu(void); @@ -98,6 +109,9 @@ int mshv_get_generic_regs(CPUState *cpu, hv_register_assoc *assocs, size_t n_regs); int mshv_arch_store_vcpu_state(const CPUState *cpu); int mshv_arch_load_vcpu_state(CPUState *cpu); +int mshv_arch_insert_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp); +int mshv_arch_remove_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp); +struct MshvSwBreakpoint *mshv_find_sw_breakpoint(CPUState *cpu, vaddr pc); void mshv_arch_init_vcpu(CPUState *cpu); void mshv_arch_destroy_vcpu(CPUState *cpu); void mshv_arch_amend_proc_features( diff --git a/target/i386/mshv/mshv-cpu.c b/target/i386/mshv/mshv-cpu.c index 1c433c408c..2333d3304a 100644 --- a/target/i386/mshv/mshv-cpu.c +++ b/target/i386/mshv/mshv-cpu.c @@ -12,6 +12,7 @@ #include "qemu/osdep.h" #include "qemu/error-report.h" +#include "qemu/main-loop.h" #include "qemu/memalign.h" #include "system/mshv.h" @@ -28,6 +29,7 @@ #include "emulate/x86_decode.h" #include "emulate/x86_emu.h" #include "emulate/x86_flags.h" +#include "gdbstub/enums.h" #include "accel/accel-cpu-target.h" @@ -1932,6 +1934,46 @@ static int handle_pio(CPUState *cpu, const struct hyperv_message *msg) return handle_pio_non_str(cpu, &info); } +/* Re-inject a guest-owned #DB via a pending event */ +static int reinject_exception(CPUState *cpu, + struct hv_x64_exception_intercept_message *info) +{ + hv_register_assoc assoc = { .name = HV_REGISTER_PENDING_EVENT0 }; + + assoc.value.pending_exception_event.event_pending = 1; + assoc.value.pending_exception_event.event_type = + HV_X64_PENDING_EVENT_EXCEPTION; + assoc.value.pending_exception_event.exception_parameter = + info->exception_parameter; + assoc.value.pending_exception_event.vector = info->exception_vector; + + return mshv_set_generic_regs(cpu, &assoc, 1); +} + +/* Check if the intercepted #DB has been set by gdb. */ +static int handle_debug(CPUState *cpu, hv_message *msg) +{ + struct hv_x64_exception_intercept_message *info = (void *)msg->payload; + + /* Only #DB is intercepted */ + if (info->exception_vector != EXCP01_DB) { + return 0; + } + + /* INT1 hit: RIP points at the breakpoint */ + if (mshv_find_sw_breakpoint(cpu, info->header.rip) != NULL) { + return EXCP_DEBUG; + } + + /* Check if single stepping */ + if (cpu_single_stepping(cpu)) { + return EXCP_DEBUG; + } + + /* The guest's own #DB; caller re-injects it. */ + return 0; +} + int mshv_run_vcpu(int vm_fd, CPUState *cpu, hv_message *msg, MshvVmExit *exit) { int ret; @@ -1960,6 +2002,24 @@ int mshv_run_vcpu(int vm_fd, CPUState *cpu, hv_message *msg, MshvVmExit *exit) return MshvVmExitSpecial; } return MshvVmExitIgnore; + case HVMSG_X64_EXCEPTION_INTERCEPT: + bql_lock(); + if (handle_debug(cpu, msg) == EXCP_DEBUG) { + *exit = MshvVmExitDebug; + bql_unlock(); + return 0; + } + /* Not ours - hand the #DB back to the guest and keep running. */ + ret = reinject_exception(cpu, + (struct hv_x64_exception_intercept_message *)msg->payload); + bql_unlock(); + if (ret < 0) { + error_report("failed to reinject exception on vcpu %d", + cpu->cpu_index); + return -1; + } + *exit = MshvVmExitIgnore; + return 0; default: break; } @@ -1973,6 +2033,32 @@ void mshv_remove_vcpu(int vm_fd, int cpu_fd) close(cpu_fd); } +int mshv_arch_insert_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp) +{ + static const uint8_t int1 = 0xf1; + + if (cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&bp->saved_insn, 1, 0) || + cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&int1, 1, 1)) { + return -EINVAL; + } + return 0; +} + +int mshv_arch_remove_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp) +{ + uint8_t int1; + + if (cpu_memory_rw_debug(cpu, bp->pc, &int1, 1, 0)) { + return -EINVAL; + } + if (int1 != 0xf1) { + return 0; + } + if (cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&bp->saved_insn, 1, 1)) { + return -EINVAL; + } + return 0; +} int mshv_create_vcpu(int vm_fd, uint8_t vp_index, int *cpu_fd) { -- 2.53.0 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH 2/3] accel/mshv: add gdbstub software breakpoint support 2026-07-27 14:28 ` [PATCH 2/3] accel/mshv: add gdbstub software breakpoint support Doru Blânzeanu @ 2026-07-28 12:37 ` Magnus Kulke 0 siblings, 0 replies; 7+ messages in thread From: Magnus Kulke @ 2026-07-28 12:37 UTC (permalink / raw) To: Doru Blânzeanu Cc: qemu-devel, Doru Blânzeanu, Wei Liu, Wei Liu, Magnus Kulke On Mon, Jul 27, 2026 at 05:28:06PM +0300, Doru Blânzeanu wrote: > Implement software breakpoint based guest debugging for the MSHV > accelerator, modeled on the WHPX backend. The guest-visible debug > registers are not used: a breakpoint is an INT1 (opcode 0xf1) > patched into guest memory, which raises a #DB when executed. > > A partition-wide intercept on #DB is installed the first time a > breakpoint (or single step) is requested. The intercept is not uninstalled, > so it stays for the lifetime of the VM and every vmexit mshv_handle_debug() > decides whether the #DB belongs to gdb (it hit one of our INT1 breakpoints) or > to the guest; a guest-owned #DB is handed back through a pending exception > event so the guest takes it through its own IDT. > Guest INT3 (#BP) is deliberately left to the guest's own IDT and is never > intercepted. > > Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com> > --- > accel/mshv/mshv-all.c | 165 +++++++++++++++++++++++++++++++++++- > include/system/mshv_int.h | 14 +++ > target/i386/mshv/mshv-cpu.c | 86 +++++++++++++++++++ > 3 files changed, 264 insertions(+), 1 deletion(-) > > diff --git a/accel/mshv/mshv-all.c b/accel/mshv/mshv-all.c > index 72721d0f0d..afcb87b3b8 100644 > --- a/accel/mshv/mshv-all.c > +++ b/accel/mshv/mshv-all.c > @@ -29,6 +29,7 @@ > > #include "qemu/accel.h" > #include "qemu/guest-random.h" > +#include "gdbstub/enums.h" > #include "accel/accel-ops.h" > #include "accel/accel-cpu-ops.h" > #include "exec/cpu-common.h" > @@ -583,6 +584,10 @@ static int mshv_init(AccelState *as, MachineState *ms) > s->nr_as = 1; > s->as = g_new0(MshvAddressSpace, s->nr_as); > > + QTAILQ_INIT(&s->sw_breakpoints); > + > + as->gdbstub.sstep_flags = SSTEP_ENABLE; > + > mshv_state = s; > > mshv_init_irq_routing(s); > @@ -607,6 +612,153 @@ static int mshv_destroy_vcpu(CPUState *cpu) > return 0; > } > > +struct MshvSwBreakpoint *mshv_find_sw_breakpoint(CPUState *cpu, vaddr pc) > +{ > + struct MshvSwBreakpoint *bp; > + > + QTAILQ_FOREACH(bp, &mshv_state->sw_breakpoints, entry) { > + if (bp->pc == pc) { > + return bp; > + } > + } > + return NULL; > +} > + > +static int mshv_install_exception_intercept(int vm_fd, uint16_t vector) > +{ > + struct hv_input_install_intercept in = {0}; > + struct mshv_root_hvcall args = {0}; > + int ret; > + > + in.access_type = 1 << HV_X64_INTERCEPT_ACCESS_TYPE_EXECUTE; > + in.intercept_type = HV_INTERCEPT_TYPE_EXCEPTION; > + in.intercept_parameter.as_uint64 = vector; > + > + args.code = HVCALL_INSTALL_INTERCEPT; > + args.in_sz = sizeof(in); > + args.in_ptr = (uint64_t)∈ > + > + ret = mshv_hvcall(vm_fd, &args); > + if (ret < 0) { > + error_report("Failed to install exception intercept for vector %u", > + vector); > + } > + return ret; > +} > + > +/* > + * Install the #DB intercept. This is a permanent, partition-wide latch: it is > + * installed once on the first debug use and stays for the lifetime of the VM. > + */ > +static int mshv_init_exception_intercept(void) > +{ > + int ret; > + > + if (mshv_state->exception_intercepts_installed) { > + return 0; > + } > + > + /* Only #DB is needed: gdb breakpoints are INT1 (0xf1) */ > + ret = mshv_install_exception_intercept(mshv_state->vm, 1); /* #DB */ > + if (ret < 0) { > + return ret; > + } > + > + mshv_state->exception_intercepts_installed = true; > + return 0; > +} > + > +static int mshv_update_guest_debug(CPUState *cpu) > +{ > + if (cpu_single_stepping(cpu)) { > + /* Installs exception intercept only on the first call */ > + return mshv_init_exception_intercept(); > + } > + return 0; > +} > + > +static int mshv_insert_gdbstub_breakpoint(CPUState *cpu, GdbBreakpointType type, > + vaddr addr, vaddr len) > +{ > + struct MshvSwBreakpoint *bp; > + int err; > + > + /* We don't use the guest's debug registers. */ > + if (type != GDB_BREAKPOINT_SW) { > + return -ENOSYS; > + } > + > + bp = mshv_find_sw_breakpoint(cpu, addr); > + if (bp) { > + bp->use_count++; > + return 0; > + } > + > + bp = g_new(struct MshvSwBreakpoint, 1); > + bp->pc = addr; > + bp->use_count = 1; > + err = mshv_arch_insert_sw_breakpoint(cpu, bp); > + if (err) { > + g_free(bp); > + return err; > + } > + > + QTAILQ_INSERT_HEAD(&mshv_state->sw_breakpoints, bp, entry); > + > + /* Installs exception intercept only on the first call */ > + return mshv_init_exception_intercept(); > +} > + > +static int mshv_remove_gdbstub_breakpoint(CPUState *cpu, GdbBreakpointType type, > + vaddr addr, vaddr len) > +{ > + struct MshvSwBreakpoint *bp; > + int err; > + > + if (type != GDB_BREAKPOINT_SW) { > + return -ENOSYS; > + } > + > + bp = mshv_find_sw_breakpoint(cpu, addr); > + if (!bp) { > + return -ENOENT; > + } > + > + if (bp->use_count > 1) { > + bp->use_count--; > + return 0; > + } > + > + err = mshv_arch_remove_sw_breakpoint(cpu, bp); > + if (err) { > + return err; > + } > + > + QTAILQ_REMOVE(&mshv_state->sw_breakpoints, bp, entry); > + g_free(bp); > + > + return 0; > +} > + > +static void mshv_remove_all_gdbstub_breakpoints(CPUState *cpu) > +{ > + struct MshvSwBreakpoint *bp, *next; > + CPUState *tmpcpu; > + > + QTAILQ_FOREACH_SAFE(bp, &mshv_state->sw_breakpoints, entry, next) { > + if (mshv_arch_remove_sw_breakpoint(cpu, bp) != 0) { > + /* Fall back to whichever CPU still has it mapped. */ > + CPU_FOREACH(tmpcpu) { > + if (mshv_arch_remove_sw_breakpoint(tmpcpu, bp) == 0) { > + break; > + } > + } > + } > + QTAILQ_REMOVE(&mshv_state->sw_breakpoints, bp, entry); > + g_free(bp); > + } > +} > + > static int mshv_cpu_exec(CPUState *cpu) > { > hv_message mshv_msg; > @@ -637,6 +789,9 @@ static int mshv_cpu_exec(CPUState *cpu) > switch (exit_reason) { > case MshvVmExitIgnore: > break; > + case MshvVmExitDebug: > + ret = EXCP_DEBUG; > + break; > default: > ret = EXCP_INTERRUPT; > break; > @@ -708,7 +863,10 @@ static void *mshv_vcpu_thread(void *arg) > do { > qemu_process_cpu_events(cpu); > if (cpu_can_run(cpu)) { > - mshv_cpu_exec(cpu); > + ret = mshv_cpu_exec(cpu); > + if (ret == EXCP_DEBUG) { > + cpu_handle_guest_debug(cpu); > + } > } > } while (!cpu->unplug || cpu_can_run(cpu)); > > @@ -852,6 +1010,11 @@ static void mshv_accel_ops_class_init(ObjectClass *oc, const void *data) > ops->synchronize_pre_loadvm = mshv_cpu_synchronize_pre_loadvm; > ops->cpus_are_resettable = mshv_cpus_are_resettable; > ops->handle_interrupt = generic_handle_interrupt; > + > + ops->update_guest_debug = mshv_update_guest_debug; > + ops->insert_gdbstub_breakpoint = mshv_insert_gdbstub_breakpoint; > + ops->remove_gdbstub_breakpoint = mshv_remove_gdbstub_breakpoint; > + ops->remove_all_gdbstub_breakpoints = mshv_remove_all_gdbstub_breakpoints; > } > > static const TypeInfo mshv_accel_ops_type = { > diff --git a/include/system/mshv_int.h b/include/system/mshv_int.h > index b91c4d661a..9244915eb6 100644 > --- a/include/system/mshv_int.h > +++ b/include/system/mshv_int.h > @@ -15,6 +15,7 @@ > #define QEMU_MSHV_INT_H > > #include "hw/hyperv/hvhdk.h" > +#include "exec/vaddr.h" > > #define MSHV_MSR_ENTRIES_COUNT 64 > > @@ -56,6 +57,13 @@ typedef struct MshvAddressSpace { > AddressSpace *as; > } MshvAddressSpace; > > +struct MshvSwBreakpoint { > + vaddr pc; > + vaddr saved_insn; > + int use_count; > + QTAILQ_ENTRY(MshvSwBreakpoint) entry; > +}; > + > struct MshvState { > AccelState parent_obj; > int vm; > @@ -70,6 +78,8 @@ struct MshvState { > unsigned long *used_gsi_bitmap; > unsigned int gsi_count; > union hv_partition_processor_features processor_features; > + QTAILQ_HEAD(, MshvSwBreakpoint) sw_breakpoints; > + bool exception_intercepts_installed; > }; > > typedef struct MshvMsiControl { > @@ -85,6 +95,7 @@ typedef enum MshvVmExit { > MshvVmExitIgnore = 0, > MshvVmExitShutdown = 1, > MshvVmExitSpecial = 2, > + MshvVmExitDebug = 3, > } MshvVmExit; > > void mshv_init_mmio_emu(void); > @@ -98,6 +109,9 @@ int mshv_get_generic_regs(CPUState *cpu, hv_register_assoc *assocs, > size_t n_regs); > int mshv_arch_store_vcpu_state(const CPUState *cpu); > int mshv_arch_load_vcpu_state(CPUState *cpu); > +int mshv_arch_insert_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp); > +int mshv_arch_remove_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp); > +struct MshvSwBreakpoint *mshv_find_sw_breakpoint(CPUState *cpu, vaddr pc); > void mshv_arch_init_vcpu(CPUState *cpu); > void mshv_arch_destroy_vcpu(CPUState *cpu); > void mshv_arch_amend_proc_features( > diff --git a/target/i386/mshv/mshv-cpu.c b/target/i386/mshv/mshv-cpu.c > index 1c433c408c..2333d3304a 100644 > --- a/target/i386/mshv/mshv-cpu.c > +++ b/target/i386/mshv/mshv-cpu.c > @@ -12,6 +12,7 @@ > > #include "qemu/osdep.h" > #include "qemu/error-report.h" > +#include "qemu/main-loop.h" > #include "qemu/memalign.h" > > #include "system/mshv.h" > @@ -28,6 +29,7 @@ > #include "emulate/x86_decode.h" > #include "emulate/x86_emu.h" > #include "emulate/x86_flags.h" > +#include "gdbstub/enums.h" > > #include "accel/accel-cpu-target.h" > > @@ -1932,6 +1934,46 @@ static int handle_pio(CPUState *cpu, const struct hyperv_message *msg) > return handle_pio_non_str(cpu, &info); > } > > +/* Re-inject a guest-owned #DB via a pending event */ > +static int reinject_exception(CPUState *cpu, > + struct hv_x64_exception_intercept_message *info) > +{ > + hv_register_assoc assoc = { .name = HV_REGISTER_PENDING_EVENT0 }; > + > + assoc.value.pending_exception_event.event_pending = 1; > + assoc.value.pending_exception_event.event_type = > + HV_X64_PENDING_EVENT_EXCEPTION; > + assoc.value.pending_exception_event.exception_parameter = > + info->exception_parameter; > + assoc.value.pending_exception_event.vector = info->exception_vector; > + > + return mshv_set_generic_regs(cpu, &assoc, 1); > +} > + > +/* Check if the intercepted #DB has been set by gdb. */ > +static int handle_debug(CPUState *cpu, hv_message *msg) > +{ > + struct hv_x64_exception_intercept_message *info = (void *)msg->payload; > + > + /* Only #DB is intercepted */ > + if (info->exception_vector != EXCP01_DB) { > + return 0; > + } > + > + /* INT1 hit: RIP points at the breakpoint */ > + if (mshv_find_sw_breakpoint(cpu, info->header.rip) != NULL) { > + return EXCP_DEBUG; > + } > + > + /* Check if single stepping */ > + if (cpu_single_stepping(cpu)) { > + return EXCP_DEBUG; > + } > + > + /* The guest's own #DB; caller re-injects it. */ > + return 0; > +} > + > int mshv_run_vcpu(int vm_fd, CPUState *cpu, hv_message *msg, MshvVmExit *exit) > { > int ret; > @@ -1960,6 +2002,24 @@ int mshv_run_vcpu(int vm_fd, CPUState *cpu, hv_message *msg, MshvVmExit *exit) > return MshvVmExitSpecial; > } > return MshvVmExitIgnore; > + case HVMSG_X64_EXCEPTION_INTERCEPT: maybe we want to move this block into a "handle_exception_intercept()" function, to keep the switch statement readable. > + bql_lock(); > + if (handle_debug(cpu, msg) == EXCP_DEBUG) { handle_debug() is a bit unintuitively named, it looks like this is a bool check (0|EXCP_DEBUG), maybe "bool is_debug_exception()" would be clearer, and we have can a clear bql_lock/unlock() symmetry w/o early return arms. > + *exit = MshvVmExitDebug; > + bql_unlock(); > + return 0; > + } > + /* Not ours - hand the #DB back to the guest and keep running. */ > + ret = reinject_exception(cpu, > + (struct hv_x64_exception_intercept_message *)msg->payload); > + bql_unlock(); > + if (ret < 0) { > + error_report("failed to reinject exception on vcpu %d", > + cpu->cpu_index); > + return -1; > + } > + *exit = MshvVmExitIgnore; > + return 0; > default: > break; > } > @@ -1973,6 +2033,32 @@ void mshv_remove_vcpu(int vm_fd, int cpu_fd) > close(cpu_fd); > } > > +int mshv_arch_insert_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp) > +{ > + static const uint8_t int1 = 0xf1; > + > + if (cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&bp->saved_insn, 1, 0) || > + cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&int1, 1, 1)) { > + return -EINVAL; > + } > + return 0; > +} > + > +int mshv_arch_remove_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp) > +{ > + uint8_t int1; > + > + if (cpu_memory_rw_debug(cpu, bp->pc, &int1, 1, 0)) { > + return -EINVAL; > + } > + if (int1 != 0xf1) { > + return 0; > + } > + if (cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&bp->saved_insn, 1, 1)) { > + return -EINVAL; > + } > + return 0; > +} > > int mshv_create_vcpu(int vm_fd, uint8_t vp_index, int *cpu_fd) > { > -- > 2.53.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 3/3] target/i386/mshv: support single-stepping 2026-07-27 14:28 [PATCH 0/3] accel/mshv: add gdbstub guest debugging support Doru Blânzeanu 2026-07-27 14:28 ` [PATCH 1/3] include/hw/hyperv: add ABI for exception intercepts and pending events Doru Blânzeanu 2026-07-27 14:28 ` [PATCH 2/3] accel/mshv: add gdbstub software breakpoint support Doru Blânzeanu @ 2026-07-27 14:28 ` Doru Blânzeanu 2026-07-28 12:43 ` Magnus Kulke 2 siblings, 1 reply; 7+ messages in thread From: Doru Blânzeanu @ 2026-07-27 14:28 UTC (permalink / raw) To: qemu-devel Cc: Magnus Kulke, Doru Blânzeanu, Doru Blânzeanu, Wei Liu, Wei Liu, Magnus Kulke Single-step by toggling RFLAGS.TF around the vCPU run, as WHPX does. TF is set only on the live register, never in env->eflags, so it is not read back and re-applied by a later register store. The resulting #DB is reported to gdb by mshv_handle_debug() whenever the vCPU is single-stepping. Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com> --- target/i386/mshv/mshv-cpu.c | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/target/i386/mshv/mshv-cpu.c b/target/i386/mshv/mshv-cpu.c index 2333d3304a..036b142113 100644 --- a/target/i386/mshv/mshv-cpu.c +++ b/target/i386/mshv/mshv-cpu.c @@ -1974,17 +1974,67 @@ static int handle_debug(CPUState *cpu, hv_message *msg) return 0; } +/* + * Flip RFLAGS.TF like WHPX. Set it only on the live register, not env->eflags, + * so a later store won't put it back. + */ +static int arch_set_single_step(CPUState *cpu, bool enable) +{ + X86CPU *x86cpu = X86_CPU(cpu); + CPUX86State *env = &x86cpu->env; + hv_register_assoc assoc = { .name = HV_X64_REGISTER_RFLAGS }; + uint64_t rflags; + int ret; + + if (env->regs_page && env->regs_page->isvalid != 0) { + rflags = env->regs_page->rflags; + rflags = enable ? (rflags | TF_MASK) : (rflags & ~TF_MASK); + env->regs_page->rflags = rflags; + env->regs_page->dirty |= (1u << HV_X64_REGISTER_CLASS_FLAGS); + return 0; + } + + ret = mshv_get_generic_regs(cpu, &assoc, 1); + if (ret < 0) { + return ret; + } + rflags = assoc.value.reg64; + rflags = enable ? (rflags | TF_MASK) : (rflags & ~TF_MASK); + assoc.value.reg64 = rflags; + return mshv_set_generic_regs(cpu, &assoc, 1); +} + int mshv_run_vcpu(int vm_fd, CPUState *cpu, hv_message *msg, MshvVmExit *exit) { int ret; enum MshvVmExit exit_reason; int cpu_fd = mshv_vcpufd(cpu); + bool single_step; + + /* enable single stepping by flipping RFLAGS.TF */ + single_step = cpu_single_stepping(cpu); + if (single_step) { + ret = arch_set_single_step(cpu, true); + if (ret < 0) { + error_report("Failed to arm single-step (TF) on vcpu %d: %s", + cpu->cpu_index, strerror(-ret)); + *exit = MshvVmExitShutdown; + return -1; + } + } ret = ioctl(cpu_fd, MSHV_RUN_VP, msg); if (ret < 0) { return MshvVmExitShutdown; } + /* disable single stepping by flipping RFLAGS.TF */ + if (single_step && arch_set_single_step(cpu, false) < 0) { + error_report("Failed to clear single-step (TF) on vcpu %d", + cpu->cpu_index); + return -1; + } + switch (msg->header.message_type) { case HVMSG_UNRECOVERABLE_EXCEPTION: return MshvVmExitShutdown; -- 2.53.0 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH 3/3] target/i386/mshv: support single-stepping 2026-07-27 14:28 ` [PATCH 3/3] target/i386/mshv: support single-stepping Doru Blânzeanu @ 2026-07-28 12:43 ` Magnus Kulke 0 siblings, 0 replies; 7+ messages in thread From: Magnus Kulke @ 2026-07-28 12:43 UTC (permalink / raw) To: Doru Blânzeanu Cc: qemu-devel, Doru Blânzeanu, Wei Liu, Wei Liu, Magnus Kulke On Mon, Jul 27, 2026 at 05:28:07PM +0300, Doru Blânzeanu wrote: > Single-step by toggling RFLAGS.TF around the vCPU run, as WHPX does. > TF is set only on the live register, never in env->eflags, so it is not read > back and re-applied by a later register store. > The resulting #DB is reported to gdb by mshv_handle_debug() whenever the vCPU > is single-stepping. > > Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com> > --- > target/i386/mshv/mshv-cpu.c | 50 +++++++++++++++++++++++++++++++++++++ > 1 file changed, 50 insertions(+) > > diff --git a/target/i386/mshv/mshv-cpu.c b/target/i386/mshv/mshv-cpu.c > index 2333d3304a..036b142113 100644 > --- a/target/i386/mshv/mshv-cpu.c > +++ b/target/i386/mshv/mshv-cpu.c > @@ -1974,17 +1974,67 @@ static int handle_debug(CPUState *cpu, hv_message *msg) > return 0; > } > > +/* > + * Flip RFLAGS.TF like WHPX. Set it only on the live register, not env->eflags, > + * so a later store won't put it back. > + */ > +static int arch_set_single_step(CPUState *cpu, bool enable) > +{ > + X86CPU *x86cpu = X86_CPU(cpu); > + CPUX86State *env = &x86cpu->env; > + hv_register_assoc assoc = { .name = HV_X64_REGISTER_RFLAGS }; > + uint64_t rflags; > + int ret; > + > + if (env->regs_page && env->regs_page->isvalid != 0) { > + rflags = env->regs_page->rflags; > + rflags = enable ? (rflags | TF_MASK) : (rflags & ~TF_MASK); > + env->regs_page->rflags = rflags; > + env->regs_page->dirty |= (1u << HV_X64_REGISTER_CLASS_FLAGS); > + return 0; > + } > + > + ret = mshv_get_generic_regs(cpu, &assoc, 1); > + if (ret < 0) { > + return ret; > + } > + rflags = assoc.value.reg64; > + rflags = enable ? (rflags | TF_MASK) : (rflags & ~TF_MASK); > + assoc.value.reg64 = rflags; > + return mshv_set_generic_regs(cpu, &assoc, 1); > +} > + > int mshv_run_vcpu(int vm_fd, CPUState *cpu, hv_message *msg, MshvVmExit *exit) > { > int ret; > enum MshvVmExit exit_reason; > int cpu_fd = mshv_vcpufd(cpu); > + bool single_step; > + > + /* enable single stepping by flipping RFLAGS.TF */ > + single_step = cpu_single_stepping(cpu); > + if (single_step) { > + ret = arch_set_single_step(cpu, true); > + if (ret < 0) { > + error_report("Failed to arm single-step (TF) on vcpu %d: %s", > + cpu->cpu_index, strerror(-ret)); > + *exit = MshvVmExitShutdown; > + return -1; > + } > + } > > ret = ioctl(cpu_fd, MSHV_RUN_VP, msg); > if (ret < 0) { > return MshvVmExitShutdown; > } > > + /* disable single stepping by flipping RFLAGS.TF */ > + if (single_step && arch_set_single_step(cpu, false) < 0) { > + error_report("Failed to clear single-step (TF) on vcpu %d", > + cpu->cpu_index); > + return -1; > + } > + > switch (msg->header.message_type) { > case HVMSG_UNRECOVERABLE_EXCEPTION: > return MshvVmExitShutdown; > -- > 2.53.0 Reviewed-by: Magnus Kulke <magnuskulke@linux.microsoft.com> ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-07-28 12:43 UTC | newest] Thread overview: 7+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-07-27 14:28 [PATCH 0/3] accel/mshv: add gdbstub guest debugging support Doru Blânzeanu 2026-07-27 14:28 ` [PATCH 1/3] include/hw/hyperv: add ABI for exception intercepts and pending events Doru Blânzeanu 2026-07-28 12:12 ` Magnus Kulke 2026-07-27 14:28 ` [PATCH 2/3] accel/mshv: add gdbstub software breakpoint support Doru Blânzeanu 2026-07-28 12:37 ` Magnus Kulke 2026-07-27 14:28 ` [PATCH 3/3] target/i386/mshv: support single-stepping Doru Blânzeanu 2026-07-28 12:43 ` Magnus Kulke
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.