diff --git a/Cargo.lock b/Cargo.lock index 1c8300dc..328abb31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,12 +37,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "bit_field" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed8765909f9009617974ab6b7d332625b320b33c326b1e9321382ef1999b5d56" - [[package]] name = "bit_field" version = "0.10.1" @@ -109,6 +103,12 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -184,7 +184,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "crossbeam-utils", ] @@ -194,7 +194,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "crossbeam-epoch", "crossbeam-utils", ] @@ -205,7 +205,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "crossbeam-utils", "lazy_static", "memoffset", @@ -218,7 +218,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "lazy_static", ] @@ -279,6 +279,29 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50045aa8931ae01afbc5d72439e8f57f326becb8c70d07dfc816778eff3d167" +[[package]] +name = "gdbstub" +version = "0.5.0" +source = "git+https://github.com/daniel5151/gdbstub?branch=dev/0.6#9227dfd0b78db5b20859d13c890f6f47e597029e" +dependencies = [ + "bitflags", + "cfg-if 0.1.10", + "log", + "managed", + "num-traits", + "paste", +] + +[[package]] +name = "gdbstub_arch" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e358b9c0e1468eae66099062e47bb502849308b987b74b5e72f1936397c33c16" +dependencies = [ + "gdbstub", + "num-traits", +] + [[package]] name = "goblin" version = "0.4.2" @@ -404,7 +427,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -417,6 +440,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + [[package]] name = "memchr" version = "2.4.0" @@ -439,7 +468,7 @@ source = "git+https://github.com/nix-rust/nix#dab7332eabed8646f6d01a0d0688b4d143 dependencies = [ "bitflags", "cc", - "cfg-if", + "cfg-if 1.0.0", "libc 0.2.98", "memoffset", ] @@ -469,6 +498,12 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "paste" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" + [[package]] name = "plain" version = "0.2.3" @@ -781,6 +816,8 @@ dependencies = [ "either", "env_logger", "envmnt", + "gdbstub", + "gdbstub_arch", "goblin", "kvm-bindings", "kvm-ioctls", @@ -865,7 +902,7 @@ version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] @@ -972,7 +1009,7 @@ version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edcf43654d533a571fe7f7116373098a95ebc01279efdc5038fc45d3b220cb5a" dependencies = [ - "bit_field 0.10.1", + "bit_field", "bitflags", "raw-cpuid", ] @@ -980,10 +1017,9 @@ dependencies = [ [[package]] name = "x86_64" version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95947de37ad0d2d9a4a4dd22e0d042e034e5cbd7ab53edbca0d8035e0a6a64d" +source = "git+https://github.com/mkroening/x86_64?branch=debug-stable#6da998935e7ea79a8db33c7149198a67f95696ec" dependencies = [ - "bit_field 0.9.0", + "bit_field", "bitflags", "volatile", ] diff --git a/Cargo.toml b/Cargo.toml index bb7ec851..76d2f53d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,9 @@ default = [] instrument = ["rftrace", "rftrace-frontend"] [patch.crates-io] +gdbstub = { git = "https://github.com/daniel5151/gdbstub", branch = "dev/0.6" } nix = { git = "https://github.com/nix-rust/nix" } +x86_64 = { git = "https://github.com/mkroening/x86_64", branch = "debug-stable" } [dependencies] bitflags = "1.3" @@ -45,6 +47,8 @@ core_affinity = "0.5" either = "1.6" env_logger = "0.9" envmnt = "0.9" +gdbstub = "0.5" +gdbstub_arch = "0.1" goblin = { version = "0.4", default-features = false, features = ["elf64", "elf32", "endian_fd", "std"] } lazy_static = "1.4" libc = "0.2" @@ -52,6 +56,7 @@ log = "0.4" raw-cpuid = "10.2" rustc-serialize = "0.3" thiserror = "1.0" +x86_64 = "0.14" rftrace = { version = "0.1", optional = true } rftrace-frontend = { version = "0.1", optional = true } diff --git a/src/arch/mod.rs b/src/arch/mod.rs new file mode 100644 index 00000000..832557ef --- /dev/null +++ b/src/arch/mod.rs @@ -0,0 +1,2 @@ +#[cfg(target_arch = "x86_64")] +pub mod x86_64; diff --git a/src/arch/x86_64/mod.rs b/src/arch/x86_64/mod.rs new file mode 100644 index 00000000..8993d24a --- /dev/null +++ b/src/arch/x86_64/mod.rs @@ -0,0 +1 @@ +pub mod registers; diff --git a/src/arch/x86_64/registers/debug.rs b/src/arch/x86_64/registers/debug.rs new file mode 100644 index 00000000..3d8355c3 --- /dev/null +++ b/src/arch/x86_64/registers/debug.rs @@ -0,0 +1,120 @@ +//! Functions to read and write debug registers. + +use std::convert::{TryFrom, TryInto}; + +use gdbstub::target::ext::breakpoints::WatchKind; +use x86_64::{ + registers::debug::{ + DebugAddressRegisterNumber, Dr7Flags, Dr7Value, HwBreakpointCondition, HwBreakpointSize, + TryFromIntError, + }, + VirtAddr, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HwBreakpoint { + addr: VirtAddr, + size: HwBreakpointSize, + condition: HwBreakpointCondition, +} + +impl HwBreakpoint { + pub fn new_breakpoint(addr: u64, kind: usize) -> Result { + Ok(Self { + addr: VirtAddr::new(addr), + size: kind.try_into()?, + condition: HwBreakpointCondition::InstructionExecution, + }) + } + + pub fn new_watchpoint(addr: u64, len: u64, kind: WatchKind) -> Option { + let condition = match kind { + WatchKind::Write => Some(HwBreakpointCondition::DataWrites), + WatchKind::Read => None, + WatchKind::ReadWrite => Some(HwBreakpointCondition::DataReadsWrites), + }?; + + let ret = Self { + addr: VirtAddr::new(addr), + size: usize::try_from(len).ok()?.try_into().ok()?, + condition, + }; + + Some(ret) + } +} + +#[derive(Clone, Copy, Debug)] +pub struct HwBreakpoints([Option; 4]); + +#[derive(Debug)] +pub struct CapacityExceededError(()); + +impl HwBreakpoints { + pub const fn new() -> Self { + Self([None; 4]) + } + + pub fn try_insert(&mut self, hw_breakpoint: HwBreakpoint) -> Result<(), CapacityExceededError> { + if let Some(entry) = self.0.iter_mut().find(|entry| entry.is_none()) { + *entry = Some(hw_breakpoint); + Ok(()) + } else { + Err(CapacityExceededError(())) + } + } + + pub fn take(&mut self, hw_breakpoint: &HwBreakpoint) -> Option { + self.0 + .iter_mut() + .find(|slot| slot.as_ref() == Some(hw_breakpoint))? + .take() + } + + fn control_value(&self) -> Dr7Value { + let dr7_flags = Dr7Flags::LOCAL_EXACT_BREAKPOINT_ENABLE + | Dr7Flags::GLOBAL_EXACT_BREAKPOINT_ENABLE + | Dr7Flags::GENERAL_DETECT_ENABLE; + let mut dr7_value = Dr7Value::from(dr7_flags); + + for (i, hw_breakpoint) in + self.0.iter().enumerate().filter_map(|(i, hw_breakpoint)| { + hw_breakpoint.map(|hw_breakpoint| (i, hw_breakpoint)) + }) { + let n = DebugAddressRegisterNumber::new(i.try_into().unwrap()).unwrap(); + + dr7_value + .flags_mut() + .insert(Dr7Flags::global_breakpoint_enable(n)); + dr7_value.set_condition(n, hw_breakpoint.condition); + dr7_value.set_size(n, hw_breakpoint.size); + } + + dr7_value + } + + pub fn registers(self) -> [u64; 8] { + let control_value = self.control_value(); + let address = |hw_breakpoint: Option| { + hw_breakpoint + .map(|hw_breakpoint| hw_breakpoint.addr.as_u64()) + .unwrap_or(0) + }; + [ + address(self.0[0]), + address(self.0[1]), + address(self.0[2]), + address(self.0[3]), + 0, + 0, + 0, + control_value.bits(), + ] + } +} + +impl Default for HwBreakpoints { + fn default() -> Self { + Self::new() + } +} diff --git a/src/arch/x86_64/registers/mod.rs b/src/arch/x86_64/registers/mod.rs new file mode 100644 index 00000000..59ae6e1a --- /dev/null +++ b/src/arch/x86_64/registers/mod.rs @@ -0,0 +1,2 @@ +#[cfg(target_os = "linux")] +pub mod debug; diff --git a/src/lib.rs b/src/lib.rs index b99fff6b..edcc1601 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ mod macros; #[macro_use] extern crate log; +mod arch; pub mod consts; #[cfg(target_os = "linux")] pub mod linux; diff --git a/src/linux/gdb/breakpoints.rs b/src/linux/gdb/breakpoints.rs new file mode 100644 index 00000000..f522608d --- /dev/null +++ b/src/linux/gdb/breakpoints.rs @@ -0,0 +1,121 @@ +use std::collections::{hash_map::Entry, HashMap}; + +use gdbstub::target::{self, ext::breakpoints::WatchKind, TargetResult}; + +use crate::arch::x86_64::registers; + +use super::GdbUhyve; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct SwBreakpoint { + addr: u64, + kind: usize, +} + +impl SwBreakpoint { + const OPCODE: u8 = 0xcc; + + pub fn new(addr: u64, kind: usize) -> Self { + Self { addr, kind } + } +} + +pub type SwBreakpoints = HashMap>; + +impl target::ext::breakpoints::Breakpoints for GdbUhyve { + #[inline(always)] + fn sw_breakpoint(&mut self) -> Option> { + Some(self) + } + + #[inline(always)] + fn hw_breakpoint(&mut self) -> Option> { + Some(self) + } + + #[inline(always)] + fn hw_watchpoint(&mut self) -> Option> { + Some(self) + } +} + +impl target::ext::breakpoints::SwBreakpoint for GdbUhyve { + fn add_sw_breakpoint(&mut self, addr: u64, kind: usize) -> TargetResult { + let sw_breakpoint = SwBreakpoint::new(addr, kind); + + if let Entry::Vacant(entry) = self.sw_breakpoints.entry(sw_breakpoint) { + let instructions = unsafe { self.vcpu.memory(addr, kind) }; + entry.insert(instructions.into()); + instructions.fill(SwBreakpoint::OPCODE); + Ok(true) + } else { + Ok(false) + } + } + + fn remove_sw_breakpoint(&mut self, addr: u64, kind: usize) -> TargetResult { + let sw_breakpoint = SwBreakpoint::new(addr, kind); + + if let Entry::Occupied(entry) = self.sw_breakpoints.entry(sw_breakpoint) { + let instructions = unsafe { self.vcpu.memory(addr, kind) }; + instructions.copy_from_slice(&entry.remove()); + Ok(true) + } else { + Ok(false) + } + } +} + +impl target::ext::breakpoints::HwBreakpoint for GdbUhyve { + fn add_hw_breakpoint(&mut self, addr: u64, kind: usize) -> TargetResult { + let hw_breakpoint = match registers::debug::HwBreakpoint::new_breakpoint(addr, kind) { + Ok(hw_breakpoint) => hw_breakpoint, + Err(_) => return Ok(false), + }; + + let success = self.hw_breakpoints.try_insert(hw_breakpoint).is_ok(); + Ok(success) + } + + fn remove_hw_breakpoint(&mut self, addr: u64, kind: usize) -> TargetResult { + let hw_breakpoint = match registers::debug::HwBreakpoint::new_breakpoint(addr, kind) { + Ok(hw_breakpoint) => hw_breakpoint, + Err(_) => return Ok(false), + }; + + let success = self.hw_breakpoints.take(&hw_breakpoint).is_some(); + Ok(success) + } +} + +impl target::ext::breakpoints::HwWatchpoint for GdbUhyve { + fn add_hw_watchpoint( + &mut self, + addr: u64, + len: u64, + kind: WatchKind, + ) -> TargetResult { + let hw_breakpoint = match registers::debug::HwBreakpoint::new_watchpoint(addr, len, kind) { + Some(hw_breakpoint) => hw_breakpoint, + None => return Ok(false), + }; + + let success = self.hw_breakpoints.try_insert(hw_breakpoint).is_ok(); + Ok(success) + } + + fn remove_hw_watchpoint( + &mut self, + addr: u64, + len: u64, + kind: WatchKind, + ) -> TargetResult { + let hw_breakpoint = match registers::debug::HwBreakpoint::new_watchpoint(addr, len, kind) { + Some(hw_breakpoint) => hw_breakpoint, + None => return Ok(false), + }; + + let success = self.hw_breakpoints.take(&hw_breakpoint).is_some(); + Ok(success) + } +} diff --git a/src/linux/gdb/mod.rs b/src/linux/gdb/mod.rs new file mode 100644 index 00000000..8ebbed16 --- /dev/null +++ b/src/linux/gdb/mod.rs @@ -0,0 +1,146 @@ +mod breakpoints; +mod regs; +mod section_offsets; + +use gdbstub::target::{ + self, + ext::base::singlethread::{GdbInterrupt, ResumeAction, SingleThreadOps, StopReason}, + Target, TargetError, TargetResult, +}; +use gdbstub_arch::x86::reg::X86_64CoreRegs; +use kvm_bindings::{ + kvm_guest_debug, kvm_guest_debug_arch, BP_VECTOR, DB_VECTOR, KVM_GUESTDBG_ENABLE, + KVM_GUESTDBG_SINGLESTEP, KVM_GUESTDBG_USE_HW_BP, KVM_GUESTDBG_USE_SW_BP, +}; +use std::convert::TryInto; +use x86_64::registers::debug::Dr6Flags; + +use crate::linux::vcpu::UhyveCPU; +use crate::vm::{VcpuStopReason, VirtualCPU}; +use crate::{arch::x86_64::registers::debug::HwBreakpoints, Uhyve}; + +use self::breakpoints::SwBreakpoints; + +use super::HypervisorError; + +pub struct GdbUhyve { + vm: Uhyve, + vcpu: UhyveCPU, + hw_breakpoints: HwBreakpoints, + sw_breakpoints: SwBreakpoints, +} + +impl GdbUhyve { + pub fn new(vm: Uhyve, vcpu: UhyveCPU) -> Self { + Self { + vm, + vcpu, + hw_breakpoints: HwBreakpoints::new(), + sw_breakpoints: SwBreakpoints::new(), + } + } +} + +impl Target for GdbUhyve { + type Arch = gdbstub_arch::x86::X86_64_SSE; + type Error = HypervisorError; + + // --------------- IMPORTANT NOTE --------------- + // Always remember to annotate IDET enable methods with `inline(always)`! + // Without this annotation, LLVM might fail to dead-code-eliminate nested IDET + // implementations, resulting in unnecessary binary bloat. + + #[inline(always)] + fn base_ops(&mut self) -> target::ext::base::BaseOps<'_, Self::Arch, Self::Error> { + target::ext::base::BaseOps::SingleThread(self) + } + + #[inline(always)] + fn breakpoints(&mut self) -> Option> { + Some(self) + } + + #[inline(always)] + fn section_offsets( + &mut self, + ) -> Option> { + Some(self) + } +} + +impl GdbUhyve { + fn apply_guest_debug(&mut self, step: bool) -> Result<(), kvm_ioctls::Error> { + let debugreg = self.hw_breakpoints.registers(); + let mut control = KVM_GUESTDBG_ENABLE | KVM_GUESTDBG_USE_SW_BP | KVM_GUESTDBG_USE_HW_BP; + if step { + control |= KVM_GUESTDBG_SINGLESTEP; + } + let debug_struct = kvm_guest_debug { + control, + pad: 0, + arch: kvm_guest_debug_arch { debugreg }, + }; + self.vcpu.get_vcpu().set_guest_debug(&debug_struct) + } +} + +impl SingleThreadOps for GdbUhyve { + fn resume( + &mut self, + action: ResumeAction, + gdb_interrupt: GdbInterrupt<'_>, + ) -> Result>, Self::Error> { + let step = matches!(action, ResumeAction::Step | ResumeAction::StepWithSignal(_)); + self.apply_guest_debug(step)?; + let mut gdb_interrupt = gdb_interrupt.no_async(); + match self.vcpu.r#continue()? { + VcpuStopReason::Debug(debug) => match debug.exception { + DB_VECTOR => { + let dr6_flags = Dr6Flags::from_bits_truncate(debug.dr6); + if dr6_flags.contains(Dr6Flags::STEP) { + Ok(Some(StopReason::DoneStep)) + } else if dr6_flags.intersects(Dr6Flags::TRAP) { + Ok(Some(StopReason::HwBreak)) + } else { + unreachable!("could not identify KVM debug exit reason") + } + } + BP_VECTOR => Ok(Some(StopReason::SwBreak)), + vector => unreachable!("unknown KVM exception vector: {}", vector), + }, + VcpuStopReason::Exit(code) => { + let status = if code == 0 { 0 } else { 1 }; + Ok(Some(StopReason::Exited(status))) + } + VcpuStopReason::Kick => { + assert!( + gdb_interrupt.pending(), + "VCPU got kicked without a pending GDB interrupt" + ); + Ok(None) + } + } + } + + fn read_registers(&mut self, regs: &mut X86_64CoreRegs) -> TargetResult<(), Self> { + regs::read(self.vcpu.get_vcpu(), regs) + .map_err(|error| TargetError::Errno(error.errno().try_into().unwrap())) + } + + fn write_registers(&mut self, regs: &X86_64CoreRegs) -> TargetResult<(), Self> { + regs::write(regs, self.vcpu.get_vcpu()) + .map_err(|error| TargetError::Errno(error.errno().try_into().unwrap())) + } + + fn read_addrs(&mut self, start_addr: u64, data: &mut [u8]) -> TargetResult<(), Self> { + let src = unsafe { self.vcpu.memory(start_addr, data.len()) }; + data.copy_from_slice(src); + Ok(()) + } + + fn write_addrs(&mut self, start_addr: u64, data: &[u8]) -> TargetResult<(), Self> { + let mem = unsafe { self.vcpu.memory(start_addr, data.len()) }; + mem.copy_from_slice(data); + Ok(()) + } +} diff --git a/src/linux/gdb/regs.rs b/src/linux/gdb/regs.rs new file mode 100644 index 00000000..2627ce32 --- /dev/null +++ b/src/linux/gdb/regs.rs @@ -0,0 +1,239 @@ +use std::convert::TryInto; + +use gdbstub_arch::x86::reg::{X86SegmentRegs, X86_64CoreRegs, X87FpuInternalRegs, F80}; +use kvm_bindings::{kvm_fpu, kvm_regs, kvm_sregs}; +use kvm_ioctls::VcpuFd; + +/// [`kvm_regs`]-related [`X86_64CoreRegs`] fields. +struct Regs { + regs: [u64; 16], + eflags: u32, + rip: u64, +} + +impl From for Regs { + fn from(kvm_regs: kvm_regs) -> Self { + let regs = [ + kvm_regs.rax, + kvm_regs.rbx, + kvm_regs.rcx, + kvm_regs.rdx, + kvm_regs.rsi, + kvm_regs.rdi, + kvm_regs.rbp, + kvm_regs.rsp, + kvm_regs.r8, + kvm_regs.r9, + kvm_regs.r10, + kvm_regs.r11, + kvm_regs.r12, + kvm_regs.r13, + kvm_regs.r14, + kvm_regs.r15, + ]; + // Truncating does not lose information, as upper half of RFLAGS is reserved. + let eflags = kvm_regs.rflags as _; + let rip = kvm_regs.rip; + Self { regs, eflags, rip } + } +} + +impl From for kvm_regs { + fn from(regs: Regs) -> Self { + kvm_regs { + rax: regs.regs[0], + rbx: regs.regs[1], + rcx: regs.regs[2], + rdx: regs.regs[3], + rsi: regs.regs[4], + rdi: regs.regs[5], + rbp: regs.regs[6], + rsp: regs.regs[7], + r8: regs.regs[8], + r9: regs.regs[9], + r10: regs.regs[10], + r11: regs.regs[11], + r12: regs.regs[12], + r13: regs.regs[13], + r14: regs.regs[14], + r15: regs.regs[15], + rflags: regs.eflags.into(), + rip: regs.rip, + } + } +} + +/// [`kvm_sregs`]-related [`X86_64CoreRegs`] fields. +struct Sregs { + segments: X86SegmentRegs, +} + +impl From for Sregs { + fn from(kvm_sregs: kvm_sregs) -> Self { + let segments = X86SegmentRegs { + cs: kvm_sregs.cs.selector.into(), + ss: kvm_sregs.ss.selector.into(), + ds: kvm_sregs.ds.selector.into(), + es: kvm_sregs.es.selector.into(), + fs: kvm_sregs.fs.selector.into(), + gs: kvm_sregs.gs.selector.into(), + }; + Self { segments } + } +} + +impl Sregs { + fn update(self, kvm_sregs: &mut kvm_sregs) { + kvm_sregs.cs.selector = self.segments.cs.try_into().unwrap(); + kvm_sregs.ss.selector = self.segments.ss.try_into().unwrap(); + kvm_sregs.ds.selector = self.segments.ds.try_into().unwrap(); + kvm_sregs.es.selector = self.segments.es.try_into().unwrap(); + kvm_sregs.fs.selector = self.segments.fs.try_into().unwrap(); + kvm_sregs.gs.selector = self.segments.gs.try_into().unwrap(); + } +} + +/// [`kvm_fpu`]-related [`X86_64CoreRegs`] fields. +struct Fpu { + st: [F80; 8], + fpu: X87FpuInternalRegs, + xmm: [u128; 16], + mxcsr: u32, +} + +impl From for Fpu { + fn from(kvm_fpu: kvm_fpu) -> Self { + // For details on `kvm_fpu` see: + // * https://elixir.bootlin.com/linux/v5.13.1/source/arch/x86/include/uapi/asm/kvm.h#L163 + // * https://elixir.bootlin.com/linux/v5.13.1/source/arch/x86/kvm/x86.c#L10181 + // * https://elixir.bootlin.com/linux/v5.13.1/source/arch/x86/include/asm/fpu/types.h#L34 + + let st = kvm_fpu.fpr.map(|fpr| fpr[..10].try_into().unwrap()); + + let fpu = X87FpuInternalRegs { + fctrl: kvm_fpu.fcw.into(), + fstat: kvm_fpu.fsw.into(), + ftag: kvm_fpu.ftwx.into(), + fiseg: kvm_fpu.last_ip as _, + fioff: (kvm_fpu.last_ip >> u32::BITS) as _, + foseg: kvm_fpu.last_dp as _, + fooff: (kvm_fpu.last_dp >> u32::BITS) as _, + fop: kvm_fpu.last_opcode.into(), + }; + + let xmm = kvm_fpu.xmm.map(u128::from_ne_bytes); + + let mxcsr = kvm_fpu.mxcsr; + + Self { + st, + fpu, + xmm, + mxcsr, + } + } +} + +impl From for kvm_fpu { + fn from(fpu: Fpu) -> Self { + let fpr = fpu + .st + .iter() + .map(|fpr| [&fpr[..], &[0; 6][..]].concat().try_into().unwrap()) + .collect::>() + .try_into() + .unwrap(); + + let last_ip = { + let mut last_ip = fpu.fpu.fiseg.into(); + last_ip |= u64::from(fpu.fpu.fioff) << u32::BITS; + last_ip + }; + + let last_dp = { + let mut last_dp = fpu.fpu.foseg.into(); + last_dp |= u64::from(fpu.fpu.fooff) << u32::BITS; + last_dp + }; + + let xmm = IntoIterator::into_iter(fpu.xmm) + .map(u128::to_ne_bytes) + .collect::>() + .try_into() + .unwrap(); + + kvm_fpu { + fpr, + fcw: fpu.fpu.fctrl.try_into().unwrap(), + fsw: fpu.fpu.fstat.try_into().unwrap(), + ftwx: fpu.fpu.ftag.try_into().unwrap(), + pad1: 0, + last_opcode: fpu.fpu.fop.try_into().unwrap(), + last_ip, + last_dp, + xmm, + mxcsr: fpu.mxcsr, + pad2: 0, + } + } +} + +pub fn read(vcpu: &VcpuFd, regs: &mut X86_64CoreRegs) -> Result<(), kvm_ioctls::Error> { + // TODO: Rewrite using destructuring assignment once stabilized + + let Regs { + regs: gp_regs, + eflags, + rip, + } = vcpu.get_regs()?.into(); + regs.regs = gp_regs; + regs.eflags = eflags; + regs.rip = rip; + + let Sregs { segments } = vcpu.get_sregs()?.into(); + regs.segments = segments; + + let Fpu { + st, + fpu, + xmm, + mxcsr, + } = vcpu.get_fpu()?.into(); + regs.st = st; + regs.fpu = fpu; + regs.xmm = xmm; + regs.mxcsr = mxcsr; + + Ok(()) +} + +pub fn write(regs: &X86_64CoreRegs, vcpu: &VcpuFd) -> Result<(), kvm_ioctls::Error> { + let X86_64CoreRegs { + regs, + eflags, + rip, + segments, + st, + fpu, + xmm, + mxcsr, + } = regs.clone(); + + let kvm_regs = Regs { regs, eflags, rip }.into(); + vcpu.set_regs(&kvm_regs)?; + + let mut kvm_sregs = vcpu.get_sregs()?; + Sregs { segments }.update(&mut kvm_sregs); + vcpu.set_sregs(&kvm_sregs)?; + + let kvm_fpu = Fpu { + st, + fpu, + xmm, + mxcsr, + } + .into(); + vcpu.set_fpu(&kvm_fpu)?; + + Ok(()) +} diff --git a/src/linux/gdb/section_offsets.rs b/src/linux/gdb/section_offsets.rs new file mode 100644 index 00000000..c5af9d04 --- /dev/null +++ b/src/linux/gdb/section_offsets.rs @@ -0,0 +1,16 @@ +use gdbstub::target::{self, ext::section_offsets::Offsets}; + +use crate::vm::Vm; + +use super::GdbUhyve; + +impl target::ext::section_offsets::SectionOffsets for GdbUhyve { + fn get_section_offsets(&mut self) -> Result, Self::Error> { + let offset = self.vm.get_offset(); + Ok(Offsets::Sections { + text: offset, + data: offset, + bss: Some(offset), + }) + } +} diff --git a/src/linux/mod.rs b/src/linux/mod.rs index 63b76731..94de284e 100755 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -1,27 +1,39 @@ +pub mod gdb; pub mod uhyve; pub mod vcpu; pub mod virtio; pub mod virtqueue; pub type HypervisorError = kvm_ioctls::Error; +pub type DebugExitInfo = kvm_bindings::kvm_debug_exit_arch; use std::{ - hint, mem, + hint, + io::{self, Read}, + mem, + net::{TcpListener, TcpStream}, os::unix::prelude::JoinHandleExt, sync::{Arc, Barrier}, thread, + time::Duration, }; use core_affinity::CoreId; +use gdbstub::{ + state_machine::Event, + target::{ext::base::multithread::ThreadStopReason, Target}, + ConnectionExt, DisconnectReason, GdbStub, GdbStubStateMachine, +}; use kvm_ioctls::Kvm; use lazy_static::lazy_static; use libc::{SIGRTMAX, SIGRTMIN}; use nix::sys::{ - pthread::{pthread_kill, Pthread}, + pthread::{pthread_kill, pthread_self, Pthread}, signal::{signal, SigHandler, Signal}, }; use crate::{ + linux::gdb::GdbUhyve, vm::{VirtualCPU, Vm}, Uhyve, }; @@ -74,6 +86,14 @@ impl Uhyve { self.load_kernel().expect("Unabled to load the kernel"); } + if self.gdb_port.is_none() { + self.run_no_gdb(cpu_affinity) + } else { + self.run_gdb(cpu_affinity) + } + } + + fn run_no_gdb(self, cpu_affinity: Option>) -> i32 { // After spinning up all vCPU threads, the main thread waits for any vCPU to end execution. let barrier = Arc::new(Barrier::new(2)); @@ -141,4 +161,118 @@ impl Uhyve { ); code[0] } + + fn run_gdb(self, cpu_affinity: Option>) -> i32 { + let cpu_id = 0; + + let local_cpu_affinity = cpu_affinity + .as_ref() + .map(|core_ids| core_ids.get(cpu_id as usize).copied()) + .flatten(); + + match local_cpu_affinity { + Some(core_id) => { + debug!("Trying to pin thread {} to CPU {}", cpu_id, core_id.id); + core_affinity::set_for_current(core_id); // This does not return an error if it fails :( + } + None => debug!("No affinity specified, not binding thread"), + } + + let mut cpu = self.create_cpu(cpu_id).unwrap(); + cpu.init(self.get_entry_point()).unwrap(); + + let connection = wait_for_gdb_connection(self.gdb_port.unwrap()).unwrap(); + + let debugger = gdbstub::GdbStub::new(connection.try_clone().unwrap()); + let mut debuggable_vcpu = GdbUhyve::new(self, cpu); + + match run_debugger(&mut debuggable_vcpu, debugger, connection).unwrap() { + DisconnectReason::TargetExited(code) => code.into(), + DisconnectReason::TargetTerminated(_) => unreachable!(), + DisconnectReason::Disconnect => { + eprintln!("Debugger disconnected."); + 0 + } + DisconnectReason::Kill => { + eprintln!("Kill command received."); + 0 + } + } + } +} + +fn run_debugger( + target: &mut T, + gdb: GdbStub<'_, T, C>, + mut tcp_stream: TcpStream, +) -> Result> { + let parent_thread = pthread_self(); + thread::spawn(move || { + loop { + // Block on TCP stream without consuming any data. + Read::read(&mut tcp_stream, &mut []).unwrap(); + + // Kick VCPU out of KVM_RUN + KickSignal::pthread_kill(parent_thread).unwrap(); + + // Wait for all inputs to be processed and for VCPU to be running again + thread::sleep(Duration::from_millis(20)); + } + }); + + let mut gdb = gdb.run_state_machine()?; + loop { + gdb = match gdb { + GdbStubStateMachine::Pump(mut gdb) => { + let byte = gdb + .borrow_conn() + .read() + .map_err(gdbstub::GdbStubError::ConnectionRead)?; + + let (gdb, disconnect_reason) = gdb.pump(target, byte)?; + if let Some(disconnect_reason) = disconnect_reason { + break Ok(disconnect_reason); + } + gdb + } + GdbStubStateMachine::DeferredStopReason(mut gdb) => { + let byte = gdb + .borrow_conn() + .read() + .map_err(gdbstub::GdbStubError::ConnectionRead)?; + + let (gdb, event) = gdb.pump(target, byte)?; + match event { + Event::None => gdb, + Event::Disconnect(disconnect_reason) => break Ok(disconnect_reason), + Event::CtrlCInterrupt => { + // when an interrupt is received, report the `GdbInterrupt` stop reason. + if let GdbStubStateMachine::DeferredStopReason(gdb) = gdb { + match gdb + .deferred_stop_reason(target, ThreadStopReason::GdbInterrupt)? + { + (_, Some(disconnect_reason)) => break Ok(disconnect_reason), + (gdb, None) => gdb, + } + } else { + gdb + } + } + } + } + } + } +} + +fn wait_for_gdb_connection(port: u16) -> io::Result { + let sockaddr = format!("localhost:{}", port); + eprintln!("Waiting for a GDB connection on {:?}...", sockaddr); + let sock = TcpListener::bind(sockaddr)?; + let (stream, addr) = sock.accept()?; + + // Blocks until a GDB client connects via TCP. + // i.e: Running `target remote localhost:` from the GDB prompt. + + eprintln!("Debugger connected from {}", addr); + Ok(stream) // `TcpStream` implements `gdbstub::Connection` } diff --git a/src/linux/uhyve.rs b/src/linux/uhyve.rs index b093127c..57dc6cd0 100755 --- a/src/linux/uhyve.rs +++ b/src/linux/uhyve.rs @@ -136,6 +136,7 @@ pub struct Uhyve { mask: Option, uhyve_device: Option, virtio_device: Arc>, + pub(super) gdb_port: Option, } impl fmt::Debug for Uhyve { @@ -272,6 +273,11 @@ impl Uhyve { _ => None, }; + assert!( + specs.gdbport.is_none() || specs.num_cpus == 1, + "gdbstub is only supported with one CPU" + ); + let hyve = Uhyve { vm, offset: 0, @@ -286,6 +292,7 @@ impl Uhyve { mask, uhyve_device, virtio_device, + gdb_port: specs.gdbport, }; hyve.init_guest_mem(); diff --git a/src/linux/vcpu.rs b/src/linux/vcpu.rs index befcab38..5535fcd9 100755 --- a/src/linux/vcpu.rs +++ b/src/linux/vcpu.rs @@ -7,8 +7,10 @@ use crate::vm::VcpuStopReason; use crate::vm::VirtualCPU; use kvm_bindings::*; use kvm_ioctls::{VcpuExit, VcpuFd}; +use std::convert::TryInto; use std::path::Path; use std::path::PathBuf; +use std::slice; use std::sync::{Arc, Mutex}; use x86_64::registers::control::{Cr0Flags, Cr4Flags}; @@ -30,6 +32,12 @@ pub struct UhyveCPU { } impl UhyveCPU { + pub unsafe fn memory(&mut self, start_addr: u64, len: usize) -> &mut [u8] { + let phys = self.virt_to_phys(start_addr.try_into().unwrap()); + let host = self.host_address(phys); + slice::from_raw_parts_mut(host as *mut u8, len) + } + pub fn new( id: u32, kernel_path: PathBuf, @@ -423,9 +431,9 @@ impl VirtualCPU for UhyveCPU { } } } - VcpuExit::Debug(_) => { + VcpuExit::Debug(debug) => { info!("Caught Debug Interrupt!"); - return Ok(VcpuStopReason::Debug); + return Ok(VcpuStopReason::Debug(debug)); } VcpuExit::InternalError => { panic!("{:?}", VcpuExit::InternalError) @@ -444,7 +452,9 @@ impl VirtualCPU for UhyveCPU { fn run(&mut self) -> HypervisorResult> { match self.r#continue()? { - VcpuStopReason::Debug => todo!(), + VcpuStopReason::Debug(_) => { + unreachable!("reached debug exit without running in debugging mode") + } VcpuStopReason::Exit(code) => Ok(Some(code)), VcpuStopReason::Kick => Ok(None), } diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 482ff713..f615299c 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -16,6 +16,7 @@ pub mod uhyve; pub mod vcpu; pub type HypervisorError = xhypervisor::Error; +pub type DebugExitInfo = (); impl Uhyve { /// Runs the VM. diff --git a/src/macos/uhyve.rs b/src/macos/uhyve.rs index 7f425a81..2a89a080 100644 --- a/src/macos/uhyve.rs +++ b/src/macos/uhyve.rs @@ -69,6 +69,8 @@ impl Uhyve { )?; } + assert!(specs.gdbport.is_none(), "gdbstub is not supported on macos"); + let hyve = Uhyve { offset: 0, entry_point: 0, diff --git a/src/macos/vcpu.rs b/src/macos/vcpu.rs index 29ede1c6..a4e0de05 100644 --- a/src/macos/vcpu.rs +++ b/src/macos/vcpu.rs @@ -609,7 +609,7 @@ impl VirtualCPU for UhyveCPU { irq_vec ); debug!("Handle breakpoint exception"); - return Ok(VcpuStopReason::Debug); + return Ok(VcpuStopReason::Debug(())); } vmx_exit::VMX_REASON_CPUID => { self.emulate_cpuid(rip)?; @@ -725,7 +725,9 @@ impl VirtualCPU for UhyveCPU { fn run(&mut self) -> HypervisorResult> { match self.r#continue()? { - VcpuStopReason::Debug => unimplemented!(), + VcpuStopReason::Debug(_) => { + unreachable!("reached debug exit without running in debugging mode") + } VcpuStopReason::Exit(code) => Ok(Some(code)), VcpuStopReason::Kick => Ok(None), } diff --git a/src/vm.rs b/src/vm.rs index 3c6f524d..6af73e8f 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -19,6 +19,7 @@ use thiserror::Error; use crate::consts::*; use crate::os::vcpu::UhyveCPU; +use crate::os::DebugExitInfo; use crate::os::HypervisorError; const MHZ_TO_HZ: u64 = 1000000; @@ -191,7 +192,7 @@ pub type LoadKernelResult = Result; /// Reasons for vCPU exits. pub enum VcpuStopReason { /// The vCPU stopped for debugging. - Debug, + Debug(DebugExitInfo), /// The vCPU exited with the specified exit code. Exit(i32),