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 <[email protected]> --- 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
