diff --git a/Cargo.lock b/Cargo.lock index 642bdbf40f..a630236c44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,10 +41,12 @@ dependencies = [ "acpi_tables 0.1.0", "base 0.1.0", "devices 0.1.0", + "gdbstub 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "hypervisor 0.1.0", "kernel_cmdline 0.1.0", "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "minijail 0.2.1", + "msg_socket 0.1.0", "resources 0.1.0", "sync 0.1.0", "vm_control 0.1.0", @@ -160,6 +162,7 @@ dependencies = [ "devices 0.1.0", "disk 0.1.0", "enumn 0.1.0", + "gdbstub 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "gpu_buffer 0.1.0", "gpu_renderer 0.1.0", "hypervisor 0.1.0", @@ -180,6 +183,7 @@ dependencies = [ "resources 0.1.0", "sync 0.1.0", "tempfile 3.0.7", + "thiserror 1.0.20 (registry+https://github.com/rust-lang/crates.io-index)", "vhost 0.1.0", "vm_control 0.1.0", "vm_memory 0.1.0", @@ -375,6 +379,18 @@ dependencies = [ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "gdbstub" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "managed 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "paste 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "getopts" version = "0.2.18" @@ -540,6 +556,11 @@ dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "memchr" version = "2.3.0" @@ -609,6 +630,14 @@ dependencies = [ "net_sys 0.1.0", ] +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "num_cpus" version = "1.9.0" @@ -893,6 +922,7 @@ version = "0.1.0" dependencies = [ "base 0.1.0", "data_model 0.1.0", + "gdbstub 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "hypervisor 0.1.0", "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "msg_socket 0.1.0", @@ -931,11 +961,13 @@ dependencies = [ "base 0.1.0", "data_model 0.1.0", "devices 0.1.0", + "gdbstub 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "hypervisor 0.1.0", "kernel_cmdline 0.1.0", "kernel_loader 0.1.0", "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "minijail 0.2.1", + "msg_socket 0.1.0", "remain 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "resources 0.1.0", "sync 0.1.0", @@ -960,12 +992,15 @@ dependencies = [ "checksum futures-sink 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "171be33efae63c2d59e6dbba34186fe0d6394fb378069a76dfd80fdcffd43c16" "checksum futures-task 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0bae52d6b29cf440e298856fec3965ee6fa71b06aa7495178615953fd669e5f9" "checksum futures-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d66274fb76985d3c62c886d1da7ac4c0903a8c9f754e8fe0f35a6a6cc39e76" +"checksum gdbstub 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "347c27d24b8ac4a2bcad3ff3d0695271a0510c020bd8134b53d189e973ed58bf" "checksum getopts 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0a7292d30132fb5424b354f5dc02512a86e4c516fe544bb7a25e7f266951b797" "checksum intrusive-collections 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4bca8c0bb831cd60d4dda79a58e3705ca6eb47efb65d665651a8d672213ec3db" "checksum libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8" "checksum log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fcce5fa49cc693c312001daf1d13411c4a5283796bac1084299ea3e567113f" +"checksum managed 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" "checksum memchr 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223" "checksum memoffset 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +"checksum num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" "checksum num_cpus 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5a69d464bdc213aaaff628444e99578ede64e9c854025aa43b9796530afa9238" "checksum paste 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ba7ae1a2180ed02ddfdb5ab70c70d596a26dd642e097bb6fe78b1bde8588ed97" "checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" diff --git a/Cargo.toml b/Cargo.toml index f0a3c6b156..496561e5f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ x = ["devices/x"] virtio-gpu-next = ["gpu_renderer/virtio-gpu-next"] composite-disk = ["protos/composite-disk", "protobuf", "disk/composite-disk"] gfxstream = ["devices/gfxstream"] +gdb = ["gdbstub", "thiserror", "arch/gdb", "vm_control/gdb", "x86_64/gdb"] [dependencies] arch = { path = "arch" } @@ -57,6 +58,7 @@ data_model = "*" devices = { path = "devices" } disk = { path = "disk" } enumn = { path = "enumn" } +gdbstub = { version = "0.4.0", optional = true } gpu_buffer = { path = "gpu_buffer", optional = true } gpu_renderer = { path = "gpu_renderer", optional = true } hypervisor = { path = "hypervisor" } @@ -77,6 +79,7 @@ remain = "*" resources = { path = "resources" } sync = { path = "sync" } tempfile = "*" +thiserror = { version = "1.0.20", optional = true } vhost = { path = "vhost" } vm_control = { path = "vm_control" } acpi_tables = { path = "acpi_tables" } diff --git a/README.md b/README.md index f6b8f5d8ab..4d5b0ee37b 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,33 @@ To use it, ensure that the `XDG_RUNTIME_DIR` enviroment variable is set and that the path `$XDG_RUNTIME_DIR/wayland-0` points to the socket of the Wayland compositor you would like the guest to use. +### GDB Support + +crosvm supports [GDB Remote Serial Protocol] to allow developers to debug guest +kernel via GDB. + +You can enable the feature by `--gdb` flag: + +```sh +# Use uncompressed vmlinux +$ crosvm run --gdb ${USUAL_CROSVM_ARGS} vmlinux +``` + +Then, you can start GDB in another shell. + +```sh +$ gdb vmlinux +(gdb) target remote : +(gdb) c + +``` + +For general techniques for debugging the Linux kernel via GDB, see this +[kernel documentation]. + +[GDB Remote Serial Protocol]: https://sourceware.org/gdb/onlinedocs/gdb/Remote-Protocol.html +[kernel documentation]: https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-debugging.html + ## Defaults The following are crosvm's default arguments and how to override them. diff --git a/arch/Cargo.toml b/arch/Cargo.toml index 851c30367b..6b4070d58d 100644 --- a/arch/Cargo.toml +++ b/arch/Cargo.toml @@ -4,13 +4,18 @@ version = "0.1.0" authors = ["The Chromium OS Authors"] edition = "2018" +[features] +gdb = ["gdbstub", "msg_socket"] + [dependencies] acpi_tables = { path = "../acpi_tables" } devices = { path = "../devices" } +gdbstub = { version = "0.4.0", optional = true } hypervisor = { path = "../hypervisor" } kernel_cmdline = { path = "../kernel_cmdline" } libc = "*" minijail = "*" +msg_socket = { path = "../msg_socket", optional = true } resources = { path = "../resources" } sync = { path = "../sync" } base = { path = "../base" } diff --git a/arch/src/lib.rs b/arch/src/lib.rs index bb971accf6..73460d0574 100644 --- a/arch/src/lib.rs +++ b/arch/src/lib.rs @@ -27,9 +27,15 @@ use hypervisor::{IoEventAddress, Vm}; use minijail::Minijail; use resources::{MmioType, SystemAllocator}; use sync::Mutex; +#[cfg(all(target_arch = "x86_64", feature = "gdb"))] +use vm_control::VmControlRequestSocket; use vm_memory::{GuestAddress, GuestMemory, GuestMemoryError}; use vm_control::BatteryType; + +#[cfg(all(target_arch = "x86_64", feature = "gdb"))] +use gdbstub::arch::x86::reg::X86_64CoreRegs as GdbStubRegs; + #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] use { devices::IrqChipAArch64 as IrqChipArch, @@ -85,6 +91,8 @@ pub struct VmComponents { pub acpi_sdts: Vec, pub rt_cpus: Vec, pub protected_vm: bool, + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + pub gdb: Option<(u32, VmControlRequestSocket)>, // port and control socket. } /// Holds the elements needed to run a Linux VM. Created by `build_vm`. @@ -105,6 +113,8 @@ pub struct RunnableLinuxVm { pub pid_debug_label_map: BTreeMap, pub suspend_evt: Event, pub rt_cpus: Vec, + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + pub gdb: Option<(u32, VmControlRequestSocket)>, } /// The device and optional jail. @@ -174,6 +184,32 @@ pub trait LinuxArch { has_bios: bool, no_smt: bool, ) -> Result<(), Self::Error>; + + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + /// Reads vCPU's registers. + fn debug_read_registers(vcpu: &T) -> Result; + + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + /// Writes vCPU's registers. + fn debug_write_registers(vcpu: &T, regs: &GdbStubRegs) -> Result<(), Self::Error>; + + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + /// Reads bytes from the guest memory. + fn debug_read_memory( + vcpu: &T, + guest_mem: &GuestMemory, + vaddr: GuestAddress, + len: usize, + ) -> Result, Self::Error>; + + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + /// Writes bytes to the specified guest memory. + fn debug_write_memory( + vcpu: &T, + guest_mem: &GuestMemory, + vaddr: GuestAddress, + buf: &[u8], + ) -> Result<(), Self::Error>; } /// Errors for device manager. diff --git a/src/crosvm.rs b/src/crosvm.rs index 9546254272..0260edb2f7 100644 --- a/src/crosvm.rs +++ b/src/crosvm.rs @@ -6,6 +6,8 @@ //! configs. pub mod argument; +#[cfg(all(target_arch = "x86_64", feature = "gdb"))] +pub mod gdb; #[path = "linux.rs"] pub mod platform; #[cfg(feature = "plugin")] @@ -215,6 +217,8 @@ pub struct Config { pub acpi_tables: Vec, pub protected_vm: bool, pub battery_type: Option, + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + pub gdb: Option, } impl Default for Config { @@ -271,6 +275,8 @@ impl Default for Config { acpi_tables: Vec::new(), protected_vm: false, battery_type: None, + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + gdb: None, } } } diff --git a/src/gdb.rs b/src/gdb.rs new file mode 100644 index 0000000000..14d3328950 --- /dev/null +++ b/src/gdb.rs @@ -0,0 +1,254 @@ +// Copyright 2020 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::net::TcpListener; +use std::sync::mpsc; +use std::time::Duration; + +use base::{error, info}; +use msg_socket::{MsgReceiver, MsgSender}; +use sync::Mutex; +use vm_control::{ + VcpuControl, VcpuDebug, VcpuDebugStatus, VcpuDebugStatusMessage, VmControlRequestSocket, + VmRequest, VmResponse, +}; +use vm_memory::GuestAddress; + +use gdbstub::arch::Arch; +use gdbstub::target::ext::base::singlethread::{ResumeAction, SingleThreadOps, StopReason}; +use gdbstub::target::ext::base::BaseOps; +use gdbstub::target::TargetError::NonFatal; +use gdbstub::target::{Target, TargetResult}; +use gdbstub::Connection; +use remain::sorted; +use thiserror::Error as ThisError; + +#[cfg(target_arch = "x86_64")] +use gdbstub::arch::x86::X86_64_SSE as GdbArch; + +#[cfg(target_arch = "x86_64")] +type ArchUsize = u64; + +pub fn gdb_thread(mut gdbstub: GdbStub, port: u32) { + let addr = format!("0.0.0.0:{}", port); + let listener = match TcpListener::bind(addr.clone()) { + Ok(s) => s, + Err(e) => { + error!("Failed to create a TCP listener: {}", e); + return; + } + }; + info!("Waiting for a GDB connection on {:?}...", addr); + + 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 mut gdb = gdbstub::GdbStub::new(connection); + + match gdb.run(&mut gdbstub) { + Ok(reason) => { + info!("GDB session closed: {:?}", reason); + } + Err(e) => { + error!("error occurred in GDB session: {}", e); + } + } + + // Resume the VM when GDB session is disconnected. + if let Err(e) = gdbstub.vm_request(VmRequest::Resume) { + error!("Failed to resume the VM after GDB disconnected: {}", e); + } +} + +#[sorted] +#[derive(ThisError, Debug)] +enum Error { + /// Got an unexpected VM response. + #[error("Got an unexpected VM response: {0}")] + UnexpectedVmResponse(VmResponse), + /// Failed to send a vCPU request. + #[error("failed to send a vCPU request: {0}")] + VcpuRequest(mpsc::SendError), + /// Failed to receive a vCPU response. + #[error("failed to receive a vCPU response: {0}")] + VcpuResponse(mpsc::RecvTimeoutError), + /// Failed to send a VM request. + #[error("failed to send a VM request: {0}")] + VmRequest(msg_socket::MsgError), + /// Failed to receive a VM request. + #[error("failed to receive a VM response: {0}")] + VmResponse(msg_socket::MsgError), +} +type GdbResult = std::result::Result; + +pub struct GdbStub { + vm_socket: Mutex, + vcpu_com: Vec>, + from_vcpu: mpsc::Receiver, +} + +impl GdbStub { + pub fn new( + vm_socket: VmControlRequestSocket, + vcpu_com: Vec>, + from_vcpu: mpsc::Receiver, + ) -> Self { + GdbStub { + vm_socket: Mutex::new(vm_socket), + vcpu_com, + from_vcpu, + } + } + + fn vcpu_request(&self, request: VcpuControl) -> GdbResult { + // We use the only one vCPU when GDB is enabled. + self.vcpu_com[0].send(request).map_err(Error::VcpuRequest)?; + + match self.from_vcpu.recv_timeout(Duration::from_millis(500)) { + Ok(msg) => Ok(msg.msg), + Err(e) => Err(Error::VcpuResponse(e)), + } + } + + fn vm_request(&self, request: VmRequest) -> GdbResult<()> { + let vm_socket = self.vm_socket.lock(); + vm_socket.send(&request).map_err(Error::VmRequest)?; + match vm_socket.recv() { + Ok(VmResponse::Ok) => Ok(()), + Ok(r) => Err(Error::UnexpectedVmResponse(r)), + Err(e) => Err(Error::VmResponse(e)), + } + } +} + +impl Target for GdbStub { + // TODO(keiichiw): Replace `()` with `X86_64CoreRegId` when we update the gdbstub crate. + type Arch = GdbArch<()>; + type Error = &'static str; + + fn base_ops(&mut self) -> BaseOps { + BaseOps::SingleThread(self) + } +} + +impl SingleThreadOps for GdbStub { + fn resume( + &mut self, + _action: ResumeAction, + check_gdb_interrupt: &mut dyn FnMut() -> bool, + ) -> Result, Self::Error> { + // TODO(keiichiw): Support step execution by checking `ResumeAction`. + + self.vm_request(VmRequest::Resume).map_err(|e| { + error!("Failed to resume the target: {}", e); + "Failed to resume the target" + })?; + + // Polling + loop { + std::thread::sleep(Duration::from_millis(100)); + + if check_gdb_interrupt() { + self.vm_request(VmRequest::Suspend).map_err(|e| { + error!("Failed to suspend the target: {}", e); + "Failed to suspend the target" + })?; + return Ok(StopReason::GdbInterrupt); + } + } + } + + fn read_registers( + &mut self, + regs: &mut ::Registers, + ) -> TargetResult<(), Self> { + match self.vcpu_request(VcpuControl::Debug(VcpuDebug::ReadRegs)) { + Ok(VcpuDebugStatus::RegValues(r)) => { + *regs = r; + Ok(()) + } + Ok(s) => { + error!("Unexpected vCPU response for ReadRegs: {:?}", s); + Err(NonFatal) + } + Err(e) => { + error!("Failed to request ReadRegs: {}", e); + Err(NonFatal) + } + } + } + + fn write_registers( + &mut self, + regs: &::Registers, + ) -> TargetResult<(), Self> { + match self.vcpu_request(VcpuControl::Debug(VcpuDebug::WriteRegs(Box::new( + regs.clone(), + )))) { + Ok(VcpuDebugStatus::CommandComplete) => Ok(()), + Ok(s) => { + error!("Unexpected vCPU response for WriteRegs: {:?}", s); + Err(NonFatal) + } + Err(e) => { + error!("Failed to request WriteRegs: {}", e); + Err(NonFatal) + } + } + } + + fn read_addrs( + &mut self, + start_addr: ::Usize, + data: &mut [u8], + ) -> TargetResult<(), Self> { + match self.vcpu_request(VcpuControl::Debug(VcpuDebug::ReadMem( + GuestAddress(start_addr), + data.len(), + ))) { + Ok(VcpuDebugStatus::MemoryRegion(r)) => { + for (dst, v) in data.iter_mut().zip(r.iter()) { + *dst = *v; + } + Ok(()) + } + Ok(s) => { + error!("Unexpected vCPU response for ReadMem: {:?}", s); + Err(NonFatal) + } + Err(e) => { + error!("Failed to request ReadMem: {}", e); + Err(NonFatal) + } + } + } + + fn write_addrs( + &mut self, + start_addr: ::Usize, + data: &[u8], + ) -> TargetResult<(), Self> { + match self.vcpu_request(VcpuControl::Debug(VcpuDebug::WriteMem( + GuestAddress(start_addr), + data.to_owned(), + ))) { + Ok(VcpuDebugStatus::CommandComplete) => Ok(()), + Ok(s) => { + error!("Unexpected vCPU response for WriteMem: {:?}", s); + Err(NonFatal) + } + Err(e) => { + error!("Failed to request WriteMem: {}", e); + Err(NonFatal) + } + } + } +} diff --git a/src/linux.rs b/src/linux.rs index 9152bba535..d00e9029d2 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -62,13 +62,17 @@ use base::{ 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 @@ pub enum Error { FsDeviceNew(virtio::fs::Error), GetMaxOpenFiles(io::Error), GetSignalMask(signal::Error), + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + HandleDebugCommand(::Error), InputDeviceNew(virtio::InputError), InputEventsOpen(std::io::Error), InvalidFdPath, @@ -160,11 +166,15 @@ pub enum Error { ResetTimer(base::Error), RngDeviceNew(virtio::RngError), RunnableVcpu(base::Error), + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + SendDebugStatus(Box>), 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 @@ impl Display for Error { 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 @@ impl Display for Error { 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 @@ enum TaggedControlSocket { VmMsync(VmMsyncResponseSocket), } -enum VcpuControl { - RunState(VmRunMode), -} - impl AsRef for TaggedControlSocket { fn as_ref(&self) -> &UnixSeqpacket { use self::TaggedControlSocket::*; @@ -1700,6 +1712,63 @@ fn inject_interrupt(irq_chip: &mut dyn IrqChipX86_64, vcpu: &dyn VcpuX86_64, vcp #[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( + cpu_id: usize, + vcpu: &V, + guest_mem: &GuestMemory, + d: VcpuDebug, + reply_channel: &mpsc::Sender, +) -> 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( cpu_id: usize, vcpu: Option, @@ -1717,6 +1786,9 @@ fn run_vcpu( requires_pvclock_ctrl: bool, from_main_channel: mpsc::Receiver, use_hypervisor_signals: bool, + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] to_gdb_channel: Option< + mpsc::Sender, + >, ) -> Result> where V: VcpuArch + 'static, @@ -1728,6 +1800,8 @@ where // 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 @@ where }; 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 @@ where 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 @@ where 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 @@ where _ => 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::().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 @@ where .collect::>>()?, 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 @@ where None => None, }; - let mut control_sockets = Vec::new(); let (wayland_host_socket, wayland_device_socket) = msg_socket::pair::().map_err(Error::CreateSocket)?; control_sockets.push(TaggedControlSocket::VmMemory(wayland_host_socket)); @@ -2209,6 +2323,15 @@ fn run_control { 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); diff --git a/src/main.rs b/src/main.rs index 7af6e617e0..2d9cd90836 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1438,7 +1438,17 @@ fn set_argument(cfg: &mut Config, name: &str, value: Option<&str>) -> argument:: let params = parse_battery_options(value)?; cfg.battery_type = Some(params); } - + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + "gdb" => { + let port = value + .unwrap() + .parse() + .map_err(|_| argument::Error::InvalidValue { + value: value.unwrap().to_owned(), + expected: String::from("expected a valid port number"), + })?; + cfg.gdb = Some(port); + } "help" => return Err(argument::Error::PrintHelp), _ => unreachable!(), } @@ -1480,6 +1490,14 @@ fn validate_arguments(cfg: &mut Config) -> std::result::Result<(), argument::Err } } } + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + if cfg.gdb.is_some() { + if cfg.vcpu_count.unwrap_or(1) != 1 { + return Err(argument::Error::ExpectedArgument( + "`gdb` requires the number of vCPU to be 1".to_owned(), + )); + } + } set_default_serial_parameters(&mut cfg.serial_parameters); Ok(()) } @@ -1627,6 +1645,7 @@ writeback=BOOL - Indicates whether the VM can use writeback caching (default: fa Possible key values: type=goldfish - type of battery emulation, defaults to goldfish "), + Argument::value("gdb", "PORT", "(EXPERIMENTAL) gdb on the given port"), Argument::short_flag('h', "help", "Print help message.")]; let mut cfg = Config::default(); diff --git a/vm_control/Cargo.toml b/vm_control/Cargo.toml index e3836798ee..238205970e 100644 --- a/vm_control/Cargo.toml +++ b/vm_control/Cargo.toml @@ -4,8 +4,12 @@ version = "0.1.0" authors = ["The Chromium OS Authors"] edition = "2018" +[features] +gdb = ["gdbstub"] + [dependencies] data_model = { path = "../data_model" } +gdbstub = { version = "0.4.0", optional = true } hypervisor = { path = "../hypervisor" } libc = "*" msg_socket = { path = "../msg_socket" } diff --git a/vm_control/src/gdb.rs b/vm_control/src/gdb.rs new file mode 100644 index 0000000000..2e617bb2e4 --- /dev/null +++ b/vm_control/src/gdb.rs @@ -0,0 +1,32 @@ +// Copyright 2020 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#[cfg(target_arch = "x86_64")] +use gdbstub::arch::x86::reg::X86_64CoreRegs as CoreRegs; +use vm_memory::GuestAddress; + +/// Messages that can be sent to a vCPU to set/get its state from the debugger. +#[derive(Debug)] +pub enum VcpuDebug { + ReadMem(GuestAddress, usize), + ReadRegs, + WriteRegs(Box), + WriteMem(GuestAddress, Vec), +} + +/// Messages that can be sent from a vCPU to update the state to the debugger. +#[derive(Debug)] +pub enum VcpuDebugStatus { + RegValues(CoreRegs), + MemoryRegion(Vec), + CommandComplete, +} + +/// Pair of a vCPU ID and messages that can be sent from the vCPU to update the state to the +/// debugger. +#[derive(Debug)] +pub struct VcpuDebugStatusMessage { + pub cpu: usize, + pub msg: VcpuDebugStatus, +} diff --git a/vm_control/src/lib.rs b/vm_control/src/lib.rs index d323c68645..0e6489b6ce 100644 --- a/vm_control/src/lib.rs +++ b/vm_control/src/lib.rs @@ -10,6 +10,9 @@ //! The wire message format is a little-endian C-struct of fixed size, along with a file descriptor //! if the request type expects one. +#[cfg(all(target_arch = "x86_64", feature = "gdb"))] +pub mod gdb; + use std::fmt::{self, Display}; use std::fs::File; use std::io::{Seek, SeekFrom}; @@ -31,8 +34,18 @@ use resources::{Alloc, GpuMemoryDesc, MmioType, SystemAllocator}; use sync::Mutex; use vm_memory::GuestAddress; +#[cfg(all(target_arch = "x86_64", feature = "gdb"))] +pub use crate::gdb::*; pub use hypervisor::MemSlot; +/// Control the state of a particular VM CPU. +#[derive(Debug)] +pub enum VcpuControl { + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + Debug(VcpuDebug), + RunState(VmRunMode), +} + /// A file descriptor either borrowed or owned by this. #[derive(Debug)] pub enum MaybeOwnedDescriptor { @@ -102,6 +115,8 @@ pub enum VmRunMode { Suspending, /// Indicates that the VM is exiting all processes. Exiting, + /// Indicates that the VM is in a breakpoint waiting for the debugger to do continue. + Breakpoint, } impl Display for VmRunMode { @@ -112,6 +127,7 @@ impl Display for VmRunMode { Running => write!(f, "running"), Suspending => write!(f, "suspending"), Exiting => write!(f, "exiting"), + Breakpoint => write!(f, "breakpoint"), } } } diff --git a/x86_64/Cargo.toml b/x86_64/Cargo.toml index 59f7e9f7ba..b3f0506c4f 100644 --- a/x86_64/Cargo.toml +++ b/x86_64/Cargo.toml @@ -4,15 +4,20 @@ version = "0.1.0" authors = ["The Chromium OS Authors"] edition = "2018" +[features] +gdb = ["gdbstub", "msg_socket", "arch/gdb"] + [dependencies] arch = { path = "../arch" } assertions = { path = "../assertions" } data_model = { path = "../data_model" } devices = { path = "../devices" } +gdbstub = { version = "0.4.0", optional = true } hypervisor = { path = "../hypervisor" } kernel_cmdline = { path = "../kernel_cmdline" } kernel_loader = { path = "../kernel_loader" } libc = "*" +msg_socket = { path = "../msg_socket", optional = true } minijail = "*" remain = "*" resources = { path = "../resources" } diff --git a/x86_64/src/lib.rs b/x86_64/src/lib.rs index 18a34b95d1..460ebe0fa1 100644 --- a/x86_64/src/lib.rs +++ b/x86_64/src/lib.rs @@ -69,6 +69,11 @@ use resources::SystemAllocator; use sync::Mutex; use vm_control::BatteryType; use vm_memory::{GuestAddress, GuestMemory, GuestMemoryError}; +#[cfg(all(target_arch = "x86_64", feature = "gdb"))] +use { + gdbstub::arch::x86::reg::X86_64CoreRegs, + hypervisor::x86_64::{Regs, Sregs}, +}; #[sorted] #[derive(Debug)] @@ -100,7 +105,10 @@ pub enum Error { LoadCmdline(kernel_loader::Error), LoadInitrd(arch::LoadImageError), LoadKernel(kernel_loader::Error), + PageNotPresent, Pstore(arch::pstore::Error), + ReadingGuestMemory(vm_memory::GuestMemoryError), + ReadRegs(base::Error), RegisterIrqfd(base::Error), RegisterVsock(arch::DeviceRegistrationError), SetLint(interrupts::Error), @@ -113,6 +121,9 @@ pub enum Error { SetupRegs(regs::Error), SetupSmbios(smbios::Error), SetupSregs(regs::Error), + TranslatingVirtAddr, + WriteRegs(base::Error), + WritingGuestMemory(GuestMemoryError), ZeroPagePastRamEnd, ZeroPageSetup, } @@ -151,7 +162,10 @@ impl Display for Error { LoadCmdline(e) => write!(f, "error loading command line: {}", e), LoadInitrd(e) => write!(f, "error loading initrd: {}", e), LoadKernel(e) => write!(f, "error loading Kernel: {}", e), + PageNotPresent => write!(f, "error translating address: Page not present"), Pstore(e) => write!(f, "failed to allocate pstore region: {}", e), + ReadingGuestMemory(e) => write!(f, "error reading guest memory {}", e), + ReadRegs(e) => write!(f, "error reading CPU registers {}", e), RegisterIrqfd(e) => write!(f, "error registering an IrqFd: {}", e), RegisterVsock(e) => write!(f, "error registering virtual socket device: {}", e), SetLint(e) => write!(f, "failed to set interrupts: {}", e), @@ -164,6 +178,9 @@ impl Display for Error { SetupRegs(e) => write!(f, "failed to set up registers: {}", e), SetupSmbios(e) => write!(f, "failed to set up SMBIOS: {}", e), SetupSregs(e) => write!(f, "failed to set up sregs: {}", e), + TranslatingVirtAddr => write!(f, "failed to translate virtual address"), + WriteRegs(e) => write!(f, "error writing CPU registers {}", e), + WritingGuestMemory(e) => write!(f, "error writing guest memory {}", e), ZeroPagePastRamEnd => write!(f, "the zero page extends past the end of guest_mem"), ZeroPageSetup => write!(f, "error writing the zero page of guest memory"), } @@ -493,6 +510,8 @@ impl arch::LinuxArch for X8664arch { pid_debug_label_map, suspend_evt, rt_cpus: components.rt_cpus, + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + gdb: components.gdb, }) } @@ -531,6 +550,219 @@ impl arch::LinuxArch for X8664arch { Ok(()) } + + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + fn debug_read_registers(vcpu: &T) -> Result { + // General registers: RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, r8-r15 + let gregs = vcpu.get_regs().map_err(Error::ReadRegs)?; + let regs = [ + gregs.rax, gregs.rbx, gregs.rcx, gregs.rdx, gregs.rsi, gregs.rdi, gregs.rbp, gregs.rsp, + gregs.r8, gregs.r9, gregs.r10, gregs.r11, gregs.r12, gregs.r13, gregs.r14, gregs.r15, + ]; + + // GDB exposes 32-bit eflags instead of 64-bit rflags. + // https://github.com/bminor/binutils-gdb/blob/master/gdb/features/i386/64bit-core.xml + let eflags = gregs.rflags as u32; + let rip = gregs.rip; + + // Segment registers: CS, SS, DS, ES, FS, GS + let sregs = vcpu.get_sregs().map_err(Error::ReadRegs)?; + let sgs = [sregs.cs, sregs.ss, sregs.ds, sregs.es, sregs.fs, sregs.gs]; + let mut segments = [0u32; 6]; + // GDB uses only the selectors. + for i in 0..sgs.len() { + segments[i] = sgs[i].selector as u32; + } + + // TODO(keiichiw): Other registers such as FPU, xmm and mxcsr. + + Ok(X86_64CoreRegs { + regs, + eflags, + rip, + segments, + ..Default::default() + }) + } + + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + fn debug_write_registers(vcpu: &T, regs: &X86_64CoreRegs) -> Result<()> { + // General purpose registers (RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, r8-r15) + RIP + rflags + let orig_gregs = vcpu.get_regs().map_err(Error::ReadRegs)?; + let gregs = 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], + rip: regs.rip, + // Update the lower 32 bits of rflags. + rflags: (orig_gregs.rflags & !(u32::MAX as u64)) | (regs.eflags as u64), + }; + vcpu.set_regs(&gregs).map_err(Error::WriteRegs)?; + + // Segment registers: CS, SS, DS, ES, FS, GS + // Since GDB care only selectors, we call get_sregs() first. + let mut sregs = vcpu.get_sregs().map_err(Error::ReadRegs)?; + sregs.cs.selector = regs.segments[0] as u16; + sregs.ss.selector = regs.segments[1] as u16; + sregs.ds.selector = regs.segments[2] as u16; + sregs.es.selector = regs.segments[3] as u16; + sregs.fs.selector = regs.segments[4] as u16; + sregs.gs.selector = regs.segments[5] as u16; + + vcpu.set_sregs(&sregs).map_err(Error::WriteRegs)?; + + // TODO(keiichiw): Other registers such as FPU, xmm and mxcsr. + + Ok(()) + } + + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + fn debug_read_memory( + vcpu: &T, + guest_mem: &GuestMemory, + vaddr: GuestAddress, + len: usize, + ) -> Result> { + let sregs = vcpu.get_sregs().map_err(Error::ReadRegs)?; + let mut buf = vec![0; len]; + let mut total_read = 0u64; + // Handle reads across page boundaries. + + while total_read < len as u64 { + let (paddr, psize) = phys_addr(guest_mem, vaddr.0 + total_read, &sregs)?; + let read_len = std::cmp::min(len as u64 - total_read, psize - (paddr & (psize - 1))); + guest_mem + .get_slice_at_addr(GuestAddress(paddr), read_len as usize) + .map_err(Error::ReadingGuestMemory)? + .copy_to(&mut buf[total_read as usize..]); + total_read += read_len; + } + Ok(buf) + } + + #[cfg(all(target_arch = "x86_64", feature = "gdb"))] + fn debug_write_memory( + vcpu: &T, + guest_mem: &GuestMemory, + vaddr: GuestAddress, + buf: &[u8], + ) -> Result<()> { + let sregs = vcpu.get_sregs().map_err(Error::ReadRegs)?; + let mut total_written = 0u64; + // Handle writes across page boundaries. + while total_written < buf.len() as u64 { + let (paddr, psize) = phys_addr(guest_mem, vaddr.0 + total_written, &sregs)?; + let write_len = std::cmp::min( + buf.len() as u64 - total_written, + psize - (paddr & (psize - 1)), + ); + + guest_mem + .write_all_at_addr( + &buf[total_written as usize..(total_written as usize + write_len as usize)], + GuestAddress(paddr), + ) + .map_err(Error::WritingGuestMemory)?; + total_written += write_len; + } + Ok(()) + } +} + +#[cfg(all(target_arch = "x86_64", feature = "gdb"))] +// return the translated address and the size of the page it resides in. +fn phys_addr(mem: &GuestMemory, vaddr: u64, sregs: &Sregs) -> Result<(u64, u64)> { + const CR0_PG_MASK: u64 = 1 << 31; + const CR4_PAE_MASK: u64 = 1 << 5; + const CR4_LA57_MASK: u64 = 1 << 12; + const MSR_EFER_LMA: u64 = 1 << 10; + // bits 12 through 51 are the address in a PTE. + const PTE_ADDR_MASK: u64 = ((1 << 52) - 1) & !0x0fff; + const PAGE_PRESENT: u64 = 0x1; + const PAGE_PSE_MASK: u64 = 0x1 << 7; + + const PAGE_SIZE_4K: u64 = 4 * 1024; + const PAGE_SIZE_2M: u64 = 2 * 1024 * 1024; + const PAGE_SIZE_1G: u64 = 1024 * 1024 * 1024; + + fn next_pte(mem: &GuestMemory, curr_table_addr: u64, vaddr: u64, level: usize) -> Result { + let ent: u64 = mem + .read_obj_from_addr(GuestAddress( + (curr_table_addr & PTE_ADDR_MASK) + page_table_offset(vaddr, level), + )) + .map_err(|_| Error::TranslatingVirtAddr)?; + /* TODO - convert to a trace + println!( + "level {} vaddr {:x} table-addr {:x} mask {:x} ent {:x} offset {:x}", + level, + vaddr, + curr_table_addr, + PTE_ADDR_MASK, + ent, + page_table_offset(vaddr, level) + ); + */ + if ent & PAGE_PRESENT == 0 { + return Err(Error::PageNotPresent); + } + Ok(ent) + } + + // Get the offset in to the page of `vaddr`. + fn page_offset(vaddr: u64, page_size: u64) -> u64 { + vaddr & (page_size - 1) + } + + // Get the offset in to the page table of the given `level` specified by the virtual `address`. + // `level` is 1 through 5 in x86_64 to handle the five levels of paging. + fn page_table_offset(addr: u64, level: usize) -> u64 { + let offset = (level - 1) * 9 + 12; + ((addr >> offset) & 0x1ff) << 3 + } + + if sregs.cr0 & CR0_PG_MASK == 0 { + return Ok((vaddr, PAGE_SIZE_4K)); + } + + if sregs.cr4 & CR4_PAE_MASK == 0 { + return Err(Error::TranslatingVirtAddr); + } + + if sregs.efer & MSR_EFER_LMA != 0 { + // TODO - check LA57 + if sregs.cr4 & CR4_LA57_MASK != 0 {} + let p4_ent = next_pte(mem, sregs.cr3, vaddr, 4)?; + let p3_ent = next_pte(mem, p4_ent, vaddr, 3)?; + // TODO check if it's a 1G page with the PSE bit in p2_ent + if p3_ent & PAGE_PSE_MASK != 0 { + // It's a 1G page with the PSE bit in p3_ent + let paddr = p3_ent & PTE_ADDR_MASK | page_offset(vaddr, PAGE_SIZE_1G); + return Ok((paddr, PAGE_SIZE_1G)); + } + let p2_ent = next_pte(mem, p3_ent, vaddr, 2)?; + if p2_ent & PAGE_PSE_MASK != 0 { + // It's a 2M page with the PSE bit in p2_ent + let paddr = p2_ent & PTE_ADDR_MASK | page_offset(vaddr, PAGE_SIZE_2M); + return Ok((paddr, PAGE_SIZE_2M)); + } + let p1_ent = next_pte(mem, p2_ent, vaddr, 1)?; + let paddr = p1_ent & PTE_ADDR_MASK | page_offset(vaddr, PAGE_SIZE_4K); + return Ok((paddr, PAGE_SIZE_4K)); + } + Err(Error::TranslatingVirtAddr) } impl X8664arch {