diff --git a/vmm/src/gdb.rs b/vmm/src/gdb.rs index 21b0a33ed..0b8ac418b 100644 --- a/vmm/src/gdb.rs +++ b/vmm/src/gdb.rs @@ -5,10 +5,36 @@ // // SPDX-License-Identifier: BSD-3-Clause +use std::{os::unix::net::UnixListener, sync::mpsc}; + +use gdbstub::{ + arch::Arch, + common::{Signal, Tid}, + conn::{Connection, ConnectionExt}, + stub::{run_blocking, DisconnectReason, MultiThreadStopReason}, + target::{ + ext::{ + base::{ + multithread::{ + MultiThreadBase, MultiThreadResume, MultiThreadResumeOps, + MultiThreadSingleStep, MultiThreadSingleStepOps, + }, + BaseOps, + }, + breakpoints::{Breakpoints, BreakpointsOps, HwBreakpoint, HwBreakpointOps}, + }, + Target, TargetError, TargetResult, + }, +}; #[cfg(target_arch = "x86_64")] use gdbstub_arch::x86::reg::X86_64CoreRegs as CoreRegs; +#[cfg(target_arch = "x86_64")] +use gdbstub_arch::x86::X86_64_SSE as GdbArch; use vm_memory::GuestAddress; +#[cfg(target_arch = "x86_64")] +type ArchUsize = u64; + #[derive(Debug)] pub enum DebuggableError { SetDebug(hypervisor::HypervisorCpuError), @@ -51,3 +77,464 @@ pub trait Debuggable: vm_migration::Pausable { ) -> std::result::Result<(), DebuggableError>; fn active_vcpus(&self) -> usize; } + +#[derive(Debug)] +pub enum Error { + Vm(crate::vm::Error), + GdbRequest, + GdbResponseNotify(std::io::Error), + GdbResponse(mpsc::RecvError), + GdbResponseTimeout(mpsc::RecvTimeoutError), +} +type GdbResult = std::result::Result; + +#[derive(Debug)] +pub struct GdbRequest { + pub sender: mpsc::Sender, + pub payload: GdbRequestPayload, + pub cpu_id: usize, +} + +#[derive(Debug)] +pub enum GdbRequestPayload { + ReadRegs, + WriteRegs(Box), + ReadMem(GuestAddress, usize), + WriteMem(GuestAddress, Vec), + Pause, + Resume, + SetSingleStep(bool), + SetHwBreakPoint(Vec), + ActiveVcpus, +} + +pub type GdbResponse = std::result::Result; + +#[derive(Debug)] +pub enum GdbResponsePayload { + CommandComplete, + RegValues(Box), + MemoryRegion(Vec), + ActiveVcpus(usize), +} + +pub struct GdbStub { + gdb_sender: mpsc::Sender, + gdb_event: vmm_sys_util::eventfd::EventFd, + vm_event: vmm_sys_util::eventfd::EventFd, + + hw_breakpoints: Vec, + single_step: bool, +} + +impl GdbStub { + pub fn new( + gdb_sender: mpsc::Sender, + gdb_event: vmm_sys_util::eventfd::EventFd, + vm_event: vmm_sys_util::eventfd::EventFd, + ) -> Self { + Self { + gdb_sender, + gdb_event, + vm_event, + hw_breakpoints: Default::default(), + single_step: false, + } + } + + fn vm_request( + &self, + payload: GdbRequestPayload, + cpu_id: usize, + ) -> GdbResult { + let (response_sender, response_receiver) = std::sync::mpsc::channel(); + let request = GdbRequest { + sender: response_sender, + payload, + cpu_id, + }; + self.gdb_sender + .send(request) + .map_err(|_| Error::GdbRequest)?; + self.gdb_event.write(1).map_err(Error::GdbResponseNotify)?; + let res = response_receiver.recv().map_err(Error::GdbResponse)??; + Ok(res) + } +} + +impl Target for GdbStub { + type Arch = GdbArch; + type Error = String; + + #[inline(always)] + fn base_ops(&mut self) -> BaseOps { + BaseOps::MultiThread(self) + } + + #[inline(always)] + fn support_breakpoints(&mut self) -> Option> { + Some(self) + } + + #[inline(always)] + fn guard_rail_implicit_sw_breakpoints(&self) -> bool { + true + } +} + +fn tid_to_cpuid(tid: Tid) -> usize { + tid.get() - 1 +} + +fn cpuid_to_tid(cpu_id: usize) -> Tid { + Tid::new(get_raw_tid(cpu_id)).unwrap() +} + +pub fn get_raw_tid(cpu_id: usize) -> usize { + cpu_id + 1 +} + +impl MultiThreadBase for GdbStub { + fn read_registers( + &mut self, + regs: &mut ::Registers, + tid: Tid, + ) -> TargetResult<(), Self> { + match self.vm_request(GdbRequestPayload::ReadRegs, tid_to_cpuid(tid)) { + Ok(GdbResponsePayload::RegValues(r)) => { + *regs = *r; + Ok(()) + } + Ok(s) => { + error!("Unexpected response for ReadRegs: {:?}", s); + Err(TargetError::NonFatal) + } + Err(e) => { + error!("Failed to request ReadRegs: {:?}", e); + Err(TargetError::NonFatal) + } + } + } + + fn write_registers( + &mut self, + regs: &::Registers, + tid: Tid, + ) -> TargetResult<(), Self> { + match self.vm_request( + GdbRequestPayload::WriteRegs(Box::new(regs.clone())), + tid_to_cpuid(tid), + ) { + Ok(_) => Ok(()), + Err(e) => { + error!("Failed to request WriteRegs: {:?}", e); + Err(TargetError::NonFatal) + } + } + } + + fn read_addrs( + &mut self, + start_addr: ::Usize, + data: &mut [u8], + tid: Tid, + ) -> TargetResult<(), Self> { + match self.vm_request( + GdbRequestPayload::ReadMem(GuestAddress(start_addr), data.len()), + tid_to_cpuid(tid), + ) { + Ok(GdbResponsePayload::MemoryRegion(r)) => { + for (dst, v) in data.iter_mut().zip(r.iter()) { + *dst = *v; + } + Ok(()) + } + Ok(s) => { + error!("Unexpected response for ReadMem: {:?}", s); + Err(TargetError::NonFatal) + } + Err(e) => { + error!("Failed to request ReadMem: {:?}", e); + Err(TargetError::NonFatal) + } + } + } + + fn write_addrs( + &mut self, + start_addr: ::Usize, + data: &[u8], + tid: Tid, + ) -> TargetResult<(), Self> { + match self.vm_request( + GdbRequestPayload::WriteMem(GuestAddress(start_addr), data.to_owned()), + tid_to_cpuid(tid), + ) { + Ok(_) => Ok(()), + Err(e) => { + error!("Failed to request WriteMem: {:?}", e); + Err(TargetError::NonFatal) + } + } + } + + fn list_active_threads( + &mut self, + thread_is_active: &mut dyn FnMut(Tid), + ) -> Result<(), Self::Error> { + match self.vm_request(GdbRequestPayload::ActiveVcpus, 0) { + Ok(GdbResponsePayload::ActiveVcpus(active_vcpus)) => { + (0..active_vcpus).for_each(|cpu_id| { + thread_is_active(cpuid_to_tid(cpu_id)); + }); + Ok(()) + } + Ok(s) => Err(format!("Unexpected response for ActiveVcpus: {:?}", s)), + Err(e) => Err(format!("Failed to request ActiveVcpus: {:?}", e)), + } + } + + #[inline(always)] + fn support_resume(&mut self) -> Option> { + Some(self) + } +} + +impl MultiThreadResume for GdbStub { + fn resume(&mut self) -> Result<(), Self::Error> { + match self.vm_request(GdbRequestPayload::Resume, 0) { + Ok(_) => Ok(()), + Err(e) => Err(format!("Failed to resume the target: {:?}", e)), + } + } + + fn clear_resume_actions(&mut self) -> Result<(), Self::Error> { + if self.single_step { + match self.vm_request(GdbRequestPayload::SetSingleStep(false), 0) { + Ok(_) => { + self.single_step = false; + } + Err(e) => { + return Err(format!("Failed to request SetSingleStep: {:?}", e)); + } + } + } + Ok(()) + } + + fn set_resume_action_continue( + &mut self, + tid: Tid, + signal: Option, + ) -> Result<(), Self::Error> { + if signal.is_some() { + return Err("no support for continuing with signal".to_owned()); + } + match self.vm_request(GdbRequestPayload::Resume, tid_to_cpuid(tid)) { + Ok(_) => Ok(()), + Err(e) => Err(format!("Failed to resume the target: {:?}", e)), + } + } + + #[inline(always)] + fn support_single_step(&mut self) -> Option> { + Some(self) + } +} + +impl MultiThreadSingleStep for GdbStub { + fn set_resume_action_step( + &mut self, + tid: Tid, + signal: Option, + ) -> Result<(), Self::Error> { + if signal.is_some() { + return Err("no support for stepping with signal".to_owned()); + } + + if !self.single_step { + match self.vm_request(GdbRequestPayload::SetSingleStep(true), tid_to_cpuid(tid)) { + Ok(_) => { + self.single_step = true; + } + Err(e) => { + return Err(format!("Failed to request SetSingleStep: {:?}", e)); + } + } + } + match self.vm_request(GdbRequestPayload::Resume, tid_to_cpuid(tid)) { + Ok(_) => Ok(()), + Err(e) => { + return Err(format!("Failed to resume the target: {:?}", e)); + } + } + } +} + +impl Breakpoints for GdbStub { + #[inline(always)] + fn support_hw_breakpoint(&mut self) -> Option> { + Some(self) + } +} + +impl HwBreakpoint for GdbStub { + fn add_hw_breakpoint( + &mut self, + addr: ::Usize, + _kind: ::BreakpointKind, + ) -> TargetResult { + // If we already have 4 breakpoints, we cannot set a new one. + if self.hw_breakpoints.len() >= 4 { + error!("Not allowed to set more than 4 HW breakpoints"); + return Ok(false); + } + + self.hw_breakpoints.push(GuestAddress(addr)); + + let payload = GdbRequestPayload::SetHwBreakPoint(self.hw_breakpoints.clone()); + match self.vm_request(payload, 0) { + Ok(_) => Ok(true), + Err(e) => { + error!("Failed to request SetHwBreakPoint: {:?}", e); + Err(TargetError::NonFatal) + } + } + } + fn remove_hw_breakpoint( + &mut self, + addr: ::Usize, + _kind: ::BreakpointKind, + ) -> TargetResult { + match self.hw_breakpoints.iter().position(|&b| b.0 == addr) { + None => return Ok(false), + Some(pos) => self.hw_breakpoints.remove(pos), + }; + + let payload = GdbRequestPayload::SetHwBreakPoint(self.hw_breakpoints.clone()); + match self.vm_request(payload, 0) { + Ok(_) => Ok(true), + Err(e) => { + error!("Failed to request SetHwBreakPoint: {:?}", e); + Err(TargetError::NonFatal) + } + } + } +} + +enum GdbEventLoop {} + +impl run_blocking::BlockingEventLoop for GdbEventLoop { + type Target = GdbStub; + type Connection = Box>; + type StopReason = MultiThreadStopReason; + + #[allow(clippy::type_complexity)] + fn wait_for_stop_reason( + target: &mut Self::Target, + conn: &mut Self::Connection, + ) -> Result< + run_blocking::Event, + run_blocking::WaitForStopReasonError< + ::Error, + ::Error, + >, + > { + // Polling + loop { + // This read is non-blocking. + match target.vm_event.read() { + Ok(tid) => { + target + .vm_request(GdbRequestPayload::Pause, 0) + .map_err(|_| { + run_blocking::WaitForStopReasonError::Target( + "Failed to pause VM".to_owned(), + ) + })?; + let stop_reason = if target.single_step { + MultiThreadStopReason::DoneStep + } else { + MultiThreadStopReason::HwBreak(Tid::new(tid as usize).unwrap()) + }; + return Ok(run_blocking::Event::TargetStopped(stop_reason)); + } + Err(e) => { + if e.kind() != std::io::ErrorKind::WouldBlock { + return Err(run_blocking::WaitForStopReasonError::Connection(e)); + } + } + } + + if conn.peek().map(|b| b.is_some()).unwrap_or(true) { + let byte = conn + .read() + .map_err(run_blocking::WaitForStopReasonError::Connection)?; + return Ok(run_blocking::Event::IncomingData(byte)); + } + } + } + + fn on_interrupt( + target: &mut Self::Target, + ) -> Result, ::Error> { + target + .vm_request(GdbRequestPayload::Pause, 0) + .map_err(|e| { + error!("Failed to pause the target: {:?}", e); + "Failed to pause the target" + })?; + Ok(Some(MultiThreadStopReason::Signal(Signal::SIGINT))) + } +} + +pub fn gdb_thread(mut gdbstub: GdbStub, path: &str) { + let listener = match UnixListener::bind(path) { + Ok(s) => s, + Err(e) => { + error!("Failed to create a Unix domain socket listener: {}", e); + return; + } + }; + info!("Waiting for a GDB connection on {}...", path); + + let (stream, addr) = match listener.accept() { + Ok(v) => v, + Err(e) => { + error!("Failed to accept a connection from GDB: {}", e); + return; + } + }; + info!("GDB connected from {:?}", addr); + + let connection: Box> = Box::new(stream); + let gdb = gdbstub::stub::GdbStub::new(connection); + + match gdb.run_blocking::(&mut gdbstub) { + Ok(disconnect_reason) => match disconnect_reason { + DisconnectReason::Disconnect => { + info!("GDB client has disconnected. Running..."); + + if let Err(e) = gdbstub.vm_request(GdbRequestPayload::SetSingleStep(false), 0) { + error!("Failed to disable single step: {:?}", e); + } + + if let Err(e) = + gdbstub.vm_request(GdbRequestPayload::SetHwBreakPoint(Vec::new()), 0) + { + error!("Failed to remove breakpoints: {:?}", e); + } + + if let Err(e) = gdbstub.vm_request(GdbRequestPayload::Resume, 0) { + error!("Failed to resume the VM: {:?}", e); + } + } + _ => { + error!("Target exited or terminated"); + } + }, + Err(e) => { + error!("error occurred in GDB session: {}", e); + } + } +}