linux: Support GDB remote serial protocol for x86_64
Add a flag '--gdb <port>' to provide GDB remote protocol interface so
a developer can attach GDB to the guest kernel.
In this CL, we support read/write operations for registers and memories.
BUG=chromium:1141812
TEST=Attach gdb and see register values on workstation and intel DUT
Change-Id: Ia07763870d94e87867f6df43f039196aa703ee59
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/2440221
Commit-Queue: Keiichi Watanabe <keiichiw@chromium.org>
Tested-by: Keiichi Watanabe <keiichiw@chromium.org>
Auto-Submit: Keiichi Watanabe <keiichiw@chromium.org>
Reviewed-by: Daniel Verkamp <dverkamp@chromium.org>
diff --git a/src/linux.rs b/src/linux.rs
index 9152bba..d00e902 100644
--- a/src/linux.rs
+++ b/src/linux.rs
@@ -62,13 +62,17 @@
use vm_control::{
BalloonControlCommand, BalloonControlRequestSocket, BalloonControlResponseSocket,
BalloonControlResult, DiskControlCommand, DiskControlRequestSocket, DiskControlResponseSocket,
- DiskControlResult, IrqSetup, UsbControlSocket, VmControlResponseSocket, VmIrqRequest,
- VmIrqRequestSocket, VmIrqResponse, VmIrqResponseSocket, VmMemoryControlRequestSocket,
- VmMemoryControlResponseSocket, VmMemoryRequest, VmMemoryResponse, VmMsyncRequest,
- VmMsyncRequestSocket, VmMsyncResponse, VmMsyncResponseSocket, VmRunMode,
+ DiskControlResult, IrqSetup, UsbControlSocket, VcpuControl, VmControlResponseSocket,
+ VmIrqRequest, VmIrqRequestSocket, VmIrqResponse, VmIrqResponseSocket,
+ VmMemoryControlRequestSocket, VmMemoryControlResponseSocket, VmMemoryRequest, VmMemoryResponse,
+ VmMsyncRequest, VmMsyncRequestSocket, VmMsyncResponse, VmMsyncResponseSocket, VmRunMode,
};
+#[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+use vm_control::{VcpuDebug, VcpuDebugStatus, VcpuDebugStatusMessage, VmRequest, VmResponse};
use vm_memory::{GuestAddress, GuestMemory};
+#[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+use crate::gdb::{gdb_thread, GdbStub};
use crate::{Config, DiskOption, Executable, SharedDir, SharedDirKind, TouchDeviceOption};
use arch::{
self, LinuxArch, RunnableLinuxVm, SerialHardware, SerialParameters, VcpuAffinity,
@@ -126,6 +130,8 @@
FsDeviceNew(virtio::fs::Error),
GetMaxOpenFiles(io::Error),
GetSignalMask(signal::Error),
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ HandleDebugCommand(<Arch as LinuxArch>::Error),
InputDeviceNew(virtio::InputError),
InputEventsOpen(std::io::Error),
InvalidFdPath,
@@ -160,11 +166,15 @@
ResetTimer(base::Error),
RngDeviceNew(virtio::RngError),
RunnableVcpu(base::Error),
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ SendDebugStatus(Box<mpsc::SendError<VcpuDebugStatusMessage>>),
SettingGidMap(minijail::Error),
SettingMaxOpenFiles(minijail::Error),
SettingSignalMask(base::Error),
SettingUidMap(minijail::Error),
SignalFd(base::SignalFdError),
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ SpawnGdbServer(io::Error),
SpawnVcpu(io::Error),
Timer(base::Error),
ValidateRawFd(base::Error),
@@ -222,6 +232,8 @@
FsDeviceNew(e) => write!(f, "failed to create fs device: {}", e),
GetMaxOpenFiles(e) => write!(f, "failed to get max number of open files: {}", e),
GetSignalMask(e) => write!(f, "failed to retrieve signal mask for vcpu: {}", e),
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ HandleDebugCommand(e) => write!(f, "failed to handle a gdb command: {}", e),
InputDeviceNew(e) => write!(f, "failed to set up input device: {}", e),
InputEventsOpen(e) => write!(f, "failed to open event device: {}", e),
InvalidFdPath => write!(f, "failed parsing a /proc/self/fd/*"),
@@ -263,11 +275,15 @@
ResetTimer(e) => write!(f, "failed to reset Timer: {}", e),
RngDeviceNew(e) => write!(f, "failed to set up rng: {}", e),
RunnableVcpu(e) => write!(f, "failed to set thread id for vcpu: {}", e),
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ SendDebugStatus(e) => write!(f, "failed to send a debug status to GDB thread: {}", e),
SettingGidMap(e) => write!(f, "error setting GID map: {}", e),
SettingMaxOpenFiles(e) => write!(f, "error setting max open files: {}", e),
SettingSignalMask(e) => write!(f, "failed to set the signal mask for vcpu: {}", e),
SettingUidMap(e) => write!(f, "error setting UID map: {}", e),
SignalFd(e) => write!(f, "failed to read signal fd: {}", e),
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ SpawnGdbServer(e) => write!(f, "failed to spawn GDB thread: {}", e),
SpawnVcpu(e) => write!(f, "failed to spawn VCPU thread: {}", e),
Timer(e) => write!(f, "failed to read timer fd: {}", e),
ValidateRawFd(e) => write!(f, "failed to validate raw fd: {}", e),
@@ -300,10 +316,6 @@
VmMsync(VmMsyncResponseSocket),
}
-enum VcpuControl {
- RunState(VmRunMode),
-}
-
impl AsRef<UnixSeqpacket> for TaggedControlSocket {
fn as_ref(&self) -> &UnixSeqpacket {
use self::TaggedControlSocket::*;
@@ -1700,6 +1712,63 @@
#[cfg(any(target_arch = "arm", target_arch = "aarch64"))]
fn inject_interrupt(_irq_chip: &mut dyn IrqChip, _vcpu: &dyn Vcpu, _vcpu_id: usize) {}
+#[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+fn handle_debug_msg<V>(
+ cpu_id: usize,
+ vcpu: &V,
+ guest_mem: &GuestMemory,
+ d: VcpuDebug,
+ reply_channel: &mpsc::Sender<VcpuDebugStatusMessage>,
+) -> Result<()>
+where
+ V: VcpuArch + 'static,
+{
+ match d {
+ VcpuDebug::ReadRegs => {
+ let msg = VcpuDebugStatusMessage {
+ cpu: cpu_id as usize,
+ msg: VcpuDebugStatus::RegValues(
+ Arch::debug_read_registers(vcpu as &V).map_err(Error::HandleDebugCommand)?,
+ ),
+ };
+ reply_channel
+ .send(msg)
+ .map_err(|e| Error::SendDebugStatus(Box::new(e)))
+ }
+ VcpuDebug::WriteRegs(regs) => {
+ Arch::debug_write_registers(vcpu as &V, ®s).map_err(Error::HandleDebugCommand)?;
+ reply_channel
+ .send(VcpuDebugStatusMessage {
+ cpu: cpu_id as usize,
+ msg: VcpuDebugStatus::CommandComplete,
+ })
+ .map_err(|e| Error::SendDebugStatus(Box::new(e)))
+ }
+ VcpuDebug::ReadMem(vaddr, len) => {
+ let msg = VcpuDebugStatusMessage {
+ cpu: cpu_id as usize,
+ msg: VcpuDebugStatus::MemoryRegion(
+ Arch::debug_read_memory(vcpu as &V, guest_mem, vaddr, len)
+ .unwrap_or(Vec::new()),
+ ),
+ };
+ reply_channel
+ .send(msg)
+ .map_err(|e| Error::SendDebugStatus(Box::new(e)))
+ }
+ VcpuDebug::WriteMem(vaddr, buf) => {
+ Arch::debug_write_memory(vcpu as &V, guest_mem, vaddr, &buf)
+ .map_err(Error::HandleDebugCommand)?;
+ reply_channel
+ .send(VcpuDebugStatusMessage {
+ cpu: cpu_id as usize,
+ msg: VcpuDebugStatus::CommandComplete,
+ })
+ .map_err(|e| Error::SendDebugStatus(Box::new(e)))
+ }
+ }
+}
+
fn run_vcpu<V>(
cpu_id: usize,
vcpu: Option<V>,
@@ -1717,6 +1786,9 @@
requires_pvclock_ctrl: bool,
from_main_channel: mpsc::Receiver<VcpuControl>,
use_hypervisor_signals: bool,
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))] to_gdb_channel: Option<
+ mpsc::Sender<VcpuDebugStatusMessage>,
+ >,
) -> Result<JoinHandle<()>>
where
V: VcpuArch + 'static,
@@ -1728,6 +1800,8 @@
// implementation accomplishes that.
let _scoped_exit_evt = ScopedEvent::from(exit_evt);
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ let guest_mem = vm.get_memory().clone();
let runnable_vcpu = runnable_vcpu(
cpu_id,
vcpu,
@@ -1752,6 +1826,12 @@
};
let mut run_mode = VmRunMode::Running;
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ if to_gdb_channel.is_some() {
+ // Wait until a GDB client attaches
+ run_mode = VmRunMode::Breakpoint;
+ }
+
let mut interrupted_by_signal = false;
'vcpu_loop: loop {
@@ -1765,7 +1845,7 @@
Ok(m) => m,
Err(mpsc::TryRecvError::Empty) if run_mode == VmRunMode::Running => {
// If the VM is running and no message is pending, the state won't
- // be changed.
+ // change.
break 'state_loop;
}
Err(mpsc::TryRecvError::Empty) => {
@@ -1788,26 +1868,47 @@
let mut messages = vec![msg];
messages.append(&mut from_main_channel.try_iter().collect());
- for VcpuControl::RunState(new_mode) in messages {
- run_mode = new_mode.clone();
- match run_mode {
- VmRunMode::Running => break 'state_loop,
- VmRunMode::Suspending => {
- // On KVM implementations that use a paravirtualized clock (e.g.
- // x86), a flag must be set to indicate to the guest kernel that a
- // VCPU was suspended. The guest kernel will use this flag to
- // prevent the soft lockup detection from triggering when this VCPU
- // resumes, which could happen days later in realtime.
- if requires_pvclock_ctrl {
- if let Err(e) = vcpu.pvclock_ctrl() {
- error!(
- "failed to tell hypervisor vcpu {} is suspending: {}",
- cpu_id, e
- );
+ for msg in messages {
+ match msg {
+ VcpuControl::RunState(new_mode) => {
+ run_mode = new_mode;
+ match run_mode {
+ VmRunMode::Running => break 'state_loop,
+ VmRunMode::Suspending => {
+ // On KVM implementations that use a paravirtualized
+ // clock (e.g. x86), a flag must be set to indicate to
+ // the guest kernel that a vCPU was suspended. The guest
+ // kernel will use this flag to prevent the soft lockup
+ // detection from triggering when this vCPU resumes,
+ // which could happen days later in realtime.
+ if requires_pvclock_ctrl {
+ if let Err(e) = vcpu.pvclock_ctrl() {
+ error!(
+ "failed to tell hypervisor vcpu {} is suspending: {}",
+ cpu_id, e
+ );
+ }
+ }
+ }
+ VmRunMode::Breakpoint => {}
+ VmRunMode::Exiting => break 'vcpu_loop,
+ }
+ }
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ VcpuControl::Debug(d) => {
+ match &to_gdb_channel {
+ Some(ref ch) => {
+ if let Err(e) = handle_debug_msg(
+ cpu_id, &vcpu, &guest_mem, d, &ch,
+ ) {
+ error!("Failed to handle gdb message: {}", e);
+ }
+ },
+ None => {
+ error!("VcpuControl::Debug received while GDB feature is disabled: {:?}", d);
}
}
}
- VmRunMode::Exiting => break 'vcpu_loop,
}
}
}
@@ -2010,6 +2111,18 @@
_ => panic!("Did not receive a bios or kernel, should be impossible."),
};
+ let mut control_sockets = Vec::new();
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ let gdb_socket = if let Some(port) = cfg.gdb {
+ // GDB needs a control socket to interrupt vcpus.
+ let (gdb_host_socket, gdb_control_socket) =
+ msg_socket::pair::<VmResponse, VmRequest>().map_err(Error::CreateSocket)?;
+ control_sockets.push(TaggedControlSocket::Vm(gdb_host_socket));
+ Some((port, gdb_control_socket))
+ } else {
+ None
+ };
+
let components = VmComponents {
memory_size: cfg
.memory
@@ -2036,6 +2149,8 @@
.collect::<Result<Vec<SDT>>>()?,
rt_cpus: cfg.rt_cpus.clone(),
protected_vm: cfg.protected_vm,
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ gdb: gdb_socket,
};
let control_server_socket = match &cfg.socket_path {
@@ -2045,7 +2160,6 @@
None => None,
};
- let mut control_sockets = Vec::new();
let (wayland_host_socket, wayland_device_socket) =
msg_socket::pair::<VmMemoryResponse, VmMemoryRequest>().map_err(Error::CreateSocket)?;
control_sockets.push(TaggedControlSocket::VmMemory(wayland_host_socket));
@@ -2209,6 +2323,15 @@
drop_capabilities().map_err(Error::DropCapabilities)?;
}
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ // Create a channel for GDB thread.
+ let (to_gdb_channel, from_vcpu_channel) = if linux.gdb.is_some() {
+ let (s, r) = mpsc::channel();
+ (Some(s), Some(r))
+ } else {
+ (None, None)
+ };
+
let mut vcpu_handles = Vec::with_capacity(linux.vcpu_count);
let vcpu_thread_barrier = Arc::new(Barrier::new(linux.vcpu_count + 1));
let use_hypervisor_signals = !linux
@@ -2245,10 +2368,30 @@
linux.vm.check_capability(VmCap::PvClockSuspend),
from_main_channel,
use_hypervisor_signals,
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ to_gdb_channel.clone(),
)?;
vcpu_handles.push((handle, to_vcpu_channel));
}
+ #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
+ // Spawn GDB thread.
+ if let Some((gdb_port_num, gdb_control_socket)) = linux.gdb.take() {
+ let to_vcpu_channels = vcpu_handles
+ .iter()
+ .map(|(_handle, channel)| channel.clone())
+ .collect();
+ let target = GdbStub::new(
+ gdb_control_socket,
+ to_vcpu_channels,
+ from_vcpu_channel.unwrap(), // Must succeed to unwrap()
+ );
+ thread::Builder::new()
+ .name("gdb".to_owned())
+ .spawn(move || gdb_thread(target, gdb_port_num))
+ .map_err(Error::SpawnGdbServer)?;
+ };
+
vcpu_thread_barrier.wait();
'wait: loop {
@@ -2426,14 +2569,14 @@
VmRunMode::Exiting => {
break 'wait;
}
- VmRunMode::Suspending | VmRunMode::Running => {
- if run_mode == VmRunMode::Suspending {
+ other => {
+ if other == VmRunMode::Suspending {
linux.io_bus.notify_resume();
}
for (handle, channel) in &vcpu_handles {
- if let Err(e) = channel.send(
- VcpuControl::RunState(VmRunMode::Running),
- ) {
+ if let Err(e) = channel
+ .send(VcpuControl::RunState(other.clone()))
+ {
error!("failed to send VmRunMode: {}", e);
}
let _ = handle.kill(SIGRTMIN() + 0);