From 612ca5358093ebf10ee3285da19bca2d3c39f1f4 Mon Sep 17 00:00:00 2001 From: Putta Khunchalee Date: Wed, 9 Oct 2024 06:51:18 +0700 Subject: [PATCH] Moves GDB server to GUI thread (#1022) --- gui/CMakeLists.txt | 4 +- gui/core.h | 64 ++++-- gui/main.cpp | 6 +- gui/main_window.cpp | 312 ++++++++++++++++++++++++----- gui/main_window.hpp | 15 +- gui/src/vmm/debug/mod.rs | 69 ++----- gui/src/vmm/hw/debugger/context.rs | 57 ++++++ gui/src/vmm/hw/debugger/mod.rs | 41 ++++ gui/src/vmm/hw/mod.rs | 9 + gui/src/vmm/mod.rs | 297 ++++++++++++--------------- kernel/src/debug/mod.rs | 12 ++ kernel/src/main.rs | 7 + src/obconf/src/env/vm.rs | 17 ++ 13 files changed, 609 insertions(+), 301 deletions(-) create mode 100644 gui/src/vmm/hw/debugger/context.rs create mode 100644 gui/src/vmm/hw/debugger/mod.rs create mode 100644 kernel/src/debug/mod.rs diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index b270f398c..808ce8811 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -1,5 +1,5 @@ # External dependencies. -find_package(Qt6 REQUIRED COMPONENTS Svg Widgets) +find_package(Qt6 REQUIRED COMPONENTS Network Svg Widgets) find_package(Threads REQUIRED) if(WIN32 OR (UNIX AND NOT APPLE)) @@ -64,7 +64,7 @@ endif() target_compile_features(obliteration PRIVATE cxx_std_17) -target_link_libraries(obliteration PRIVATE Qt6::Svg Qt6::Widgets) +target_link_libraries(obliteration PRIVATE Qt6::Network Qt6::Svg Qt6::Widgets) target_link_libraries(obliteration PRIVATE Threads::Threads) target_link_libraries(obliteration PRIVATE ${LIBGUI}) diff --git a/gui/core.h b/gui/core.h index 336823955..030e12945 100644 --- a/gui/core.h +++ b/gui/core.h @@ -44,6 +44,11 @@ enum VmmLog { VmmLog_Error, }; +/** + * Reason for [`VmmEvent::Breakpoint`]. + */ +struct KernelStop; + /** * Contains settings to launch the kernel. */ @@ -86,21 +91,15 @@ struct VmmScreen { */ enum VmmEvent_Tag { VmmEvent_Error, - VmmEvent_WaitingDebugger, - VmmEvent_DebuggerDisconnected, VmmEvent_Exiting, VmmEvent_Log, + VmmEvent_Breakpoint, }; struct VmmEvent_Error_Body { const struct RustError *reason; }; -struct VmmEvent_WaitingDebugger_Body { - const char *addr; - size_t len; -}; - struct VmmEvent_Exiting_Body { bool success; }; @@ -111,13 +110,38 @@ struct VmmEvent_Log_Body { size_t len; }; +struct VmmEvent_Breakpoint_Body { + struct KernelStop *stop; +}; + struct VmmEvent { enum VmmEvent_Tag tag; union { struct VmmEvent_Error_Body error; - struct VmmEvent_WaitingDebugger_Body waiting_debugger; struct VmmEvent_Exiting_Body exiting; struct VmmEvent_Log_Body log; + struct VmmEvent_Breakpoint_Body breakpoint; + }; +}; + +/** + * Result of [`vmm_dispatch_debug()`]. + */ +enum DebugResult_Tag { + DebugResult_Ok, + DebugResult_Disconnected, + DebugResult_ReadFailed, + DebugResult_Error, +}; + +struct DebugResult_Error_Body { + struct RustError *reason; +}; + +struct DebugResult { + enum DebugResult_Tag tag; + union { + struct DebugResult_Error_Body error; }; }; @@ -183,18 +207,26 @@ struct RustError *update_firmware(const char *root, void *cx, void (*status)(const char*, uint64_t, uint64_t, void*)); -void vmm_free(struct Vmm *vmm); +struct Vmm *vmm_start(const char *kernel, + const struct VmmScreen *screen, + const struct Profile *profile, + bool (*debug)(void*, const uint8_t*, size_t, int*), + void (*event)(const struct VmmEvent*, void*), + void *cx, + struct RustError **err); -struct Vmm *vmm_run(const char *kernel, - const struct VmmScreen *screen, - const struct Profile *profile, - const char *debug, - void (*event)(const struct VmmEvent*, void*), - void *cx, - struct RustError **err); +void vmm_free(struct Vmm *vmm); struct RustError *vmm_draw(struct Vmm *vmm); +struct DebugResult vmm_dispatch_debug(struct Vmm *vmm, + struct KernelStop *stop, + bool (*read)(uint8_t*, void*)); + +void vmm_shutdown(struct Vmm *vmm); + +bool vmm_shutting_down(struct Vmm *vmm); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/gui/main.cpp b/gui/main.cpp index 0346745ef..72c741698 100644 --- a/gui/main.cpp +++ b/gui/main.cpp @@ -222,10 +222,8 @@ int main(int argc, char *argv[]) win.restoreGeometry(); // Run main window. - auto debug = args.value(Args::debug); - - if (!debug.isEmpty()) { - win.startVmm(debug); + if (args.isSet(Args::debug)) { + win.startDebug(args.value(Args::debug)); } return QApplication::exec(); diff --git a/gui/main_window.cpp b/gui/main_window.cpp index 8afad1f11..a94ccec24 100644 --- a/gui/main_window.cpp +++ b/gui/main_window.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,8 @@ #include #include #include +#include +#include #include #include @@ -54,7 +57,8 @@ MainWindow::MainWindow( m_profiles(nullptr), m_games(nullptr), m_launch(nullptr), - m_screen(nullptr) + m_screen(nullptr), + m_debug(nullptr) { setWindowTitle("Obliteration"); @@ -111,7 +115,13 @@ MainWindow::MainWindow( #endif connect(m_launch, &LaunchSettings::saveClicked, this, &MainWindow::saveProfile); - connect(m_launch, &LaunchSettings::startClicked, this, &MainWindow::startVmm); + connect(m_launch, &LaunchSettings::startClicked, [this](const QString &debug) { + if (debug.isEmpty()) { + startVmm(); + } else { + startDebug(debug); + } + }); m_main->addWidget(m_launch); @@ -224,19 +234,22 @@ void MainWindow::closeEvent(QCloseEvent *event) // Ask user to confirm. if (m_vmm) { - QMessageBox confirm(this); - - confirm.setText("Do you want to exit?"); - confirm.setInformativeText("The running game will be terminated."); - confirm.setStandardButtons(QMessageBox::Cancel | QMessageBox::Yes); - confirm.setDefaultButton(QMessageBox::Cancel); - confirm.setIcon(QMessageBox::Warning); - - if (confirm.exec() != QMessageBox::Yes) { - return; + // Ask user to confirm only if we did not shutdown the VMM programmatically. + if (!vmm_shutting_down(m_vmm)) { + QMessageBox confirm(this); + + confirm.setText("Do you want to exit?"); + confirm.setInformativeText("The running game will be terminated."); + confirm.setStandardButtons(QMessageBox::Cancel | QMessageBox::Yes); + confirm.setDefaultButton(QMessageBox::Cancel); + confirm.setIcon(QMessageBox::Warning); + + if (confirm.exec() != QMessageBox::Yes) { + return; + } } - m_vmm.free(); + killVmm(); } // Close child windows. @@ -364,7 +377,7 @@ void MainWindow::updateScreen() error = vmm_draw(m_vmm); if (error) { - m_vmm.free(); + killVmm(); QMessageBox::critical( this, @@ -379,7 +392,7 @@ void MainWindow::updateScreen() void MainWindow::vmmError(const QString &msg) { - m_vmm.free(); + killVmm(); QMessageBox::critical(this, "Error", msg); @@ -390,26 +403,9 @@ void MainWindow::vmmError(const QString &msg) } } -void MainWindow::waitingDebugger(const QString &addr) -{ - if (!m_args.isSet(Args::debug)) { - QMessageBox::information( - this, - "Debug", - QString("The VMM are waiting for a debugger at %1.").arg(addr)); - } -} - -void MainWindow::debuggerDisconnected() -{ - if (m_args.isSet(Args::debug)) { - close(); - } -} - void MainWindow::waitKernelExit(bool success) { - m_vmm.free(); + killVmm(); if (!success) { QMessageBox::critical( @@ -438,6 +434,92 @@ void MainWindow::log(VmmLog type, const QString &msg) } } +void MainWindow::breakpoint(KernelStop *stop) +{ + // Do nothing if the previous thread already trigger the shutdown. + if (vmm_shutting_down(m_vmm)) { + return; + } + + // Dispatch debug events. + auto r = vmm_dispatch_debug(m_vmm, stop, [](uint8_t *buf, void *cx) -> bool { + auto w = reinterpret_cast(cx); + + while (w->m_debug->bytesAvailable() == 0) { + QCoreApplication::processEvents(); + } + + return w->m_debug->getChar(reinterpret_cast(buf)); + }); + + switch (r.tag) { + case DebugResult_Ok: + break; + case DebugResult_Disconnected: + // It is not safe to let the kernel running since it is assume there are a debugger. + vmm_shutdown(m_vmm); + break; + case DebugResult_ReadFailed: + QMessageBox::critical( + this, + "Error", + QString("Failed to read data from the debugger: %1").arg(m_debug->errorString())); + vmm_shutdown(m_vmm); + break; + case DebugResult_Error: + { + Rust e(r.error.reason); + + QMessageBox::critical( + this, + "Error", + QString("Failed to dispatch debug events: %1").arg(error_message(e))); + } + + vmm_shutdown(m_vmm); + break; + } + + if (!vmm_shutting_down(m_vmm)) { + return; + } + + // We can't free the VMM here because the thread that trigger this method are waiting + // for us to return. + if (m_args.isSet(Args::debug)) { + QMetaObject::invokeMethod( + this, + &MainWindow::close, + Qt::QueuedConnection); + } else { + QMetaObject::invokeMethod( + this, + &MainWindow::waitKernelExit, + Qt::QueuedConnection, + true); + } +} + +std::optional MainWindow::sendDebug(const uint8_t *data, size_t len) +{ + while (len) { + auto r = m_debug->write(reinterpret_cast(data), len); + + if (r < 0) { + return m_debug->error(); + } + + data += r; + len -= r; + + while (m_debug->bytesToWrite() != 0) { + QCoreApplication::processEvents(); + } + } + + return {}; +} + bool MainWindow::loadGame(const QString &gameId) { auto gamesDirectory = readGamesDirectorySetting(); @@ -498,7 +580,103 @@ void MainWindow::restoreGeometry() } } -void MainWindow::startVmm(const QString &debugAddr) +void MainWindow::startDebug(const QString &addr) +{ + // Check address format. + auto parts = addr.split(':'); + + if (parts.size() > 2) { + QMessageBox::critical( + this, + "Error", + QString("%1 is not a valid debug server address").arg(addr)); + return; + } + + // Parse IP address. + QHostAddress ip(parts[0]); + + if (ip.isNull()) { + QMessageBox::critical( + this, + "Error", + QString("%1 is not a valid debug server address").arg(addr)); + return; + } + + // Parse port. + unsigned short port = 0; + + if (parts.size() == 2) { + bool ok; + + port = parts[1].toUShort(&ok); + + if (!ok) { + QMessageBox::critical( + this, + "Error", + QString("%1 is not a valid debug server address").arg(addr)); + return; + } + } + + // Start server. + auto server = new QTcpServer(this); + + server->setListenBacklogSize(1); + server->setMaxPendingConnections(1); + + connect( + server, + &QTcpServer::acceptError, + [this](QAbstractSocket::SocketError e) { + QMessageBox::critical( + this, + "Error", + QString("Failed to accept a debugger connection (%1).").arg(e)); + }); + + connect( + server, + &QTcpServer::pendingConnectionAvailable, + this, [this, server]() { + m_debug = server->nextPendingConnection(); + m_debug->setParent(this); + m_debug->setSocketOption(QAbstractSocket::LowDelayOption, 1); + + startVmm(); + + server->deleteLater(); + }, + Qt::SingleShotConnection); + + if (!server->listen(ip, port)) { + auto msg = QString("Failed to start a debug server on %1: %2") + .arg(addr) + .arg(server->errorString()); + + QMessageBox::critical(this, "Error", msg); + delete server; + return; + } + + // Swap launch settings with the screen now to prevent user update settings. + m_main->setCurrentIndex(1); + + // Tell the user to connect a debugger. + if (!m_args.isSet(Args::debug)) { + auto addr = server->serverAddress(); + auto port = server->serverPort(); + + QMessageBox::information( + this, + "Debug", + QString("Waiting for a debugger at %1:%2.").arg(addr.toString()).arg(port)); + } +} + +void MainWindow::startVmm() { // Get full path to kernel binary. std::string kernel; @@ -543,8 +721,7 @@ void MainWindow::startVmm(const QString &debugAddr) // Swap launch settings with the screen before getting a Vulkan surface otherwise it will fail. m_main->setCurrentIndex(1); - // Run. - auto debug = debugAddr.toStdString(); + // Setup the screen. VmmScreen screen; Rust error; Rust vmm; @@ -559,27 +736,38 @@ void MainWindow::startVmm(const QString &debugAddr) screen.vk_surface = reinterpret_cast(QVulkanInstance::surfaceForWindow(m_screen)); if (!screen.vk_surface) { - m_main->setCurrentIndex(0); QMessageBox::critical(this, "Error", "Couldn't create VkSurfaceKHR."); + + m_main->setCurrentIndex(0); + + delete m_debug; + m_debug = nullptr; return; } #endif - vmm = vmm_run( + // Run. + vmm = vmm_start( kernel.c_str(), &screen, m_launch->currentProfile(), - debug.empty() ? nullptr : debug.c_str(), + m_debug + ? qOverload(MainWindow::sendDebug) + : nullptr, MainWindow::vmmHandler, this, &error); if (!vmm) { - m_main->setCurrentIndex(0); QMessageBox::critical( this, "Error", QString("Couldn't run %1: %2").arg(kernel.c_str()).arg(error_message(error))); + + m_main->setCurrentIndex(0); + + delete m_debug; + m_debug = nullptr; return; } @@ -602,12 +790,20 @@ bool MainWindow::requireVmmStopped() return false; } - m_vmm.free(); + killVmm(); } return true; } +void MainWindow::killVmm() +{ + m_vmm.free(); + + delete m_debug; + m_debug = nullptr; +} + void MainWindow::vmmHandler(const VmmEvent *ev, void *cx) { // This method will be called from non-main thread. @@ -621,19 +817,6 @@ void MainWindow::vmmHandler(const VmmEvent *ev, void *cx) Qt::QueuedConnection, QString(error_message(ev->error.reason))); break; - case VmmEvent_WaitingDebugger: - QMetaObject::invokeMethod( - w, - &MainWindow::waitingDebugger, - Qt::QueuedConnection, - QString::fromUtf8(ev->waiting_debugger.addr, ev->waiting_debugger.len)); - break; - case VmmEvent_DebuggerDisconnected: - QMetaObject::invokeMethod( - w, - &MainWindow::debuggerDisconnected, - Qt::QueuedConnection); - break; case VmmEvent_Exiting: QMetaObject::invokeMethod( w, @@ -649,5 +832,26 @@ void MainWindow::vmmHandler(const VmmEvent *ev, void *cx) ev->log.ty, QString::fromUtf8(ev->log.data, ev->log.len)); break; + case VmmEvent_Breakpoint: + QMetaObject::invokeMethod( + w, + &MainWindow::breakpoint, + Qt::BlockingQueuedConnection, + ev->breakpoint.stop); + break; + } +} + +bool MainWindow::sendDebug(void *cx, const uint8_t *data, size_t len, int *err) +{ + // This method always be called from a main thread. + auto w = reinterpret_cast(cx); + auto r = w->sendDebug(data, len); + + if (r) { + *err = *r; + return false; + } else { + return true; } } diff --git a/gui/main_window.hpp b/gui/main_window.hpp index 5593f6a24..3e6d483ad 100644 --- a/gui/main_window.hpp +++ b/gui/main_window.hpp @@ -2,6 +2,7 @@ #include "core.hpp" +#include #include #include #include @@ -9,6 +10,8 @@ #include #endif +#include + class GameListModel; class LaunchSettings; class LogsViewer; @@ -16,6 +19,7 @@ class ProfileList; class QCommandLineOption; class QCommandLineParser; class QStackedWidget; +class QTcpSocket; class Screen; class MainWindow final : public QMainWindow { @@ -33,10 +37,10 @@ class MainWindow final : public QMainWindow { bool loadProfiles(); bool loadGames(); void restoreGeometry(); - void startVmm(const QString &debugAddr); + void startDebug(const QString &addr); + void startVmm(); protected: void closeEvent(QCloseEvent *event) override; - private slots: void installPkg(); void openSystemFolder(); @@ -47,14 +51,16 @@ private slots: void updateScreen(); private: void vmmError(const QString &msg); - void waitingDebugger(const QString &addr); - void debuggerDisconnected(); void waitKernelExit(bool success); void log(VmmLog type, const QString &msg); + void breakpoint(KernelStop *stop); + std::optional sendDebug(const uint8_t *data, size_t len); bool loadGame(const QString &gameId); bool requireVmmStopped(); + void killVmm(); static void vmmHandler(const VmmEvent *ev, void *cx); + static bool sendDebug(void *cx, const uint8_t *data, size_t len, int *err); const QCommandLineParser &m_args; QStackedWidget *m_main; @@ -63,6 +69,7 @@ private slots: LaunchSettings *m_launch; Screen *m_screen; QPointer m_logs; + QTcpSocket *m_debug; Rust m_vmm; // Destroy first. }; diff --git a/gui/src/vmm/debug/mod.rs b/gui/src/vmm/debug/mod.rs index 6a4ca18f3..4f6917eeb 100644 --- a/gui/src/vmm/debug/mod.rs +++ b/gui/src/vmm/debug/mod.rs @@ -2,78 +2,49 @@ pub use self::arch::*; use gdbstub::conn::Connection; -use std::io::{Read, Write}; -use std::net::TcpStream; +use std::ffi::{c_int, c_void}; #[cfg_attr(target_arch = "aarch64", path = "aarch64.rs")] #[cfg_attr(target_arch = "x86_64", path = "x86_64.rs")] mod arch; -/// Implementation of [`Connection`] using `select` system call to check if data available. +/// Encapsulates a C++ function on Qt side to provide [`Connection`] implementation. pub struct Client { - sock: TcpStream, - buf: Vec, - next: usize, + fp: unsafe extern "C" fn(*mut c_void, *const u8, usize, *mut c_int) -> bool, + cx: *mut c_void, } impl Client { - pub fn new(sock: TcpStream) -> Self { - Self { - sock, - buf: Vec::new(), - next: 0, - } - } - - pub fn read(&mut self) -> Result { - // Fill the buffer if needed. - while self.next == self.buf.len() { - use std::io::ErrorKind; - - // Clear previous data. - self.buf.clear(); - self.next = 0; - - // Read. - let mut buf = [0; 1024]; - let len = match self.sock.read(&mut buf) { - Ok(v) => v, - Err(e) if e.kind() == ErrorKind::Interrupted => continue, - Err(e) => return Err(e), - }; - - if len == 0 { - return Err(std::io::Error::from(ErrorKind::UnexpectedEof)); - } - - self.buf.extend_from_slice(&buf[..len]); - } - - // Get byte. - let b = self.buf[self.next]; - - self.next += 1; - - Ok(b) + pub fn new( + fp: unsafe extern "C" fn(*mut c_void, *const u8, usize, *mut c_int) -> bool, + cx: *mut c_void, + ) -> Self { + Self { fp, cx } } } impl Connection for Client { - type Error = std::io::Error; + type Error = c_int; fn write(&mut self, byte: u8) -> Result<(), Self::Error> { - Write::write_all(&mut self.sock, std::slice::from_ref(&byte)) + self.write_all(std::slice::from_ref(&byte)) } fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { - Write::write_all(&mut self.sock, buf) + let mut e = 0; + + if unsafe { (self.fp)(self.cx, buf.as_ptr(), buf.len(), &mut e) } { + Ok(()) + } else { + Err(e) + } } fn flush(&mut self) -> Result<(), Self::Error> { - Write::flush(&mut self.sock) + Ok(()) } fn on_session_start(&mut self) -> Result<(), Self::Error> { - self.sock.set_nodelay(true) + Ok(()) } } diff --git a/gui/src/vmm/hw/debugger/context.rs b/gui/src/vmm/hw/debugger/context.rs new file mode 100644 index 000000000..8d1050a28 --- /dev/null +++ b/gui/src/vmm/hw/debugger/context.rs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +use super::Debugger; +use crate::vmm::hv::CpuIo; +use crate::vmm::hw::{read_u8, DeviceContext, MmioError}; +use crate::vmm::VmmEvent; +use obconf::{DebuggerMemory, StopReason}; +use std::error::Error; +use std::mem::offset_of; +use std::ptr::null_mut; +use thiserror::Error; + +/// Implementation of [`DeviceContext`]. +pub struct Context<'a> { + dev: &'a Debugger, +} + +impl<'a> Context<'a> { + pub fn new(dev: &'a Debugger) -> Self { + Self { dev } + } +} + +impl<'a> DeviceContext for Context<'a> { + fn exec(&mut self, exit: &mut dyn CpuIo) -> Result> { + // Check field. + let off = exit.addr() - self.dev.addr; + + if off == offset_of!(DebuggerMemory, stop) { + let stop = read_u8(exit).map_err(|e| ExecError::ReadFailed(off, e))?; + let stop: StopReason = stop + .try_into() + .map_err(|_| Box::new(ExecError::InvalidStop(stop)))?; + let stop = match stop { + StopReason::WaitForDebugger => null_mut(), + }; + + unsafe { self.dev.event.invoke(VmmEvent::Breakpoint { stop }) }; + } else { + return Err(Box::new(ExecError::UnknownField(off))); + } + + return Ok(true); + } +} + +/// Represents an error when [`Context::exec()`] fails. +#[derive(Debug, Error)] +enum ExecError { + #[error("unknown field at offset {0:#}")] + UnknownField(usize), + + #[error("couldn't read data for offset {0:#}")] + ReadFailed(usize, #[source] MmioError), + + #[error("{0:#} is not a valid stop reason")] + InvalidStop(u8), +} diff --git a/gui/src/vmm/hw/debugger/mod.rs b/gui/src/vmm/hw/debugger/mod.rs new file mode 100644 index 000000000..9eb146dea --- /dev/null +++ b/gui/src/vmm/hw/debugger/mod.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +use self::context::Context; +use super::{Device, DeviceContext}; +use crate::vmm::hv::Hypervisor; +use crate::vmm::VmmEventHandler; +use obconf::DebuggerMemory; +use std::num::NonZero; + +mod context; + +/// Virtual device for the kernel to communicate with the debugger. +pub struct Debugger { + addr: usize, + len: NonZero, + event: VmmEventHandler, +} + +impl Debugger { + pub fn new(addr: usize, block_size: NonZero, event: VmmEventHandler) -> Self { + let len = size_of::() + .checked_next_multiple_of(block_size.get()) + .and_then(NonZero::new) + .unwrap(); + + Self { addr, len, event } + } +} + +impl Device for Debugger { + fn addr(&self) -> usize { + self.addr + } + + fn len(&self) -> NonZero { + self.len + } + + fn create_context<'a>(&'a self, _: &'a H) -> Box { + Box::new(Context::new(self)) + } +} diff --git a/gui/src/vmm/hw/mod.rs b/gui/src/vmm/hw/mod.rs index ed34c3ef3..8ba2eedba 100644 --- a/gui/src/vmm/hw/mod.rs +++ b/gui/src/vmm/hw/mod.rs @@ -8,9 +8,11 @@ use std::sync::Arc; use thiserror::Error; pub use self::console::*; +pub use self::debugger::*; pub use self::vmm::*; mod console; +mod debugger; mod vmm; pub fn setup_devices( @@ -25,10 +27,12 @@ pub fn setup_devices( let vmm = b.push(|addr| Vmm::new(addr, block_size, event)); let console = b.push(|addr| Console::new(addr, block_size, event)); + let debugger = b.push(|addr| Debugger::new(addr, block_size, event)); DeviceTree { vmm, console, + debugger, map: b.map, } } @@ -91,6 +95,7 @@ fn read_bin<'b>( pub struct DeviceTree { vmm: Arc, console: Arc, + debugger: Arc, map: BTreeMap>>, } @@ -103,6 +108,10 @@ impl DeviceTree { self.console.as_ref() } + pub fn debugger(&self) -> &impl Device { + self.debugger.as_ref() + } + /// Returns iterator ordered by physical address. pub fn map(&self) -> impl Iterator)> + '_ { self.map.iter().map(|(addr, dev)| (*addr, dev.as_ref())) diff --git a/gui/src/vmm/mod.rs b/gui/src/vmm/mod.rs index 0fb81d547..84b0a01fe 100644 --- a/gui/src/vmm/mod.rs +++ b/gui/src/vmm/mod.rs @@ -8,22 +8,19 @@ use self::ram::{Ram, RamMap}; use self::screen::Screen; use crate::error::RustError; use crate::profile::Profile; -use gdbstub::stub::state_machine::state::Running; -use gdbstub::stub::state_machine::{GdbStubStateMachine, GdbStubStateMachineInner}; +use gdbstub::stub::state_machine::GdbStubStateMachine; use gdbstub::stub::{GdbStub, MultiThreadStopReason}; use obconf::{BootEnv, ConsoleType, Vm}; use std::cmp::max; use std::collections::BTreeMap; use std::error::Error; -use std::ffi::{c_char, c_void, CStr}; +use std::ffi::{c_char, c_int, c_void, CStr}; use std::io::Read; -use std::net::TcpListener; use std::num::NonZero; use std::ptr::null_mut; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use std::thread::{sleep, JoinHandle}; -use std::time::Duration; +use std::thread::JoinHandle; use thiserror::Error; #[cfg_attr(target_arch = "aarch64", path = "aarch64.rs")] @@ -37,29 +34,15 @@ mod ram; mod screen; #[no_mangle] -pub unsafe extern "C" fn vmm_free(vmm: *mut Vmm) { - drop(Box::from_raw(vmm)); -} - -#[no_mangle] -pub unsafe extern "C" fn vmm_run( +pub unsafe extern "C" fn vmm_start( kernel: *const c_char, screen: *const VmmScreen, profile: *const Profile, - debug: *const c_char, + debug: Option bool>, event: unsafe extern "C" fn(*const VmmEvent, *mut c_void), cx: *mut c_void, err: *mut *mut RustError, ) -> *mut Vmm { - // Setup debug server. - let debug = match setup_debug_server(debug) { - Ok(v) => v, - Err(e) => { - *err = e.into_c(); - return null_mut(); - } - }; - // Check if path UTF-8. let path = match CStr::from_ptr(kernel).to_str() { Ok(v) => v, @@ -424,6 +407,11 @@ pub unsafe extern "C" fn vmm_run( let env = BootEnv::Vm(Vm { vmm: devices.vmm().addr(), console: devices.console().addr(), + debugger: if debug.is_some() { + devices.debugger().addr() + } else { + 0 + }, host_page_size, }); @@ -450,6 +438,29 @@ pub unsafe extern "C" fn vmm_run( } }; + // Setup GDB stub. + let debugger = match debug + .map(move |fp| -> Result { + let mut target = self::debug::Target::new(); + let client = self::debug::Client::new(fp, cx); + let state = GdbStub::new(client) + .run_state_machine(&mut target) + .map_err(|e| RustError::with_source("couldn't setup a GDB stub", e))?; + + Ok(Debugger { + target, + state: Some(state), + }) + }) + .transpose() + { + Ok(v) => v, + Err(e) => { + *err = e.into_c(); + return null_mut(); + } + }; + // Setup arguments for main CPU. let shutdown = Arc::new(AtomicBool::new(false)); let args = CpuArgs { @@ -463,7 +474,7 @@ pub unsafe extern "C" fn vmm_run( // Spawn a thread to drive main CPU. let e_entry = file.entry(); - let main = move || main_cpu(&args, e_entry, map, debug); + let main = move || main_cpu(&args, e_entry, map); let main = match std::thread::Builder::new().spawn(main) { Ok(v) => v, Err(e) => { @@ -476,12 +487,19 @@ pub unsafe extern "C" fn vmm_run( let vmm = Vmm { cpus: vec![main], screen, + debugger, shutdown, + cx, }; Box::into_raw(vmm.into()) } +#[no_mangle] +pub unsafe extern "C" fn vmm_free(vmm: *mut Vmm) { + drop(Box::from_raw(vmm)); +} + #[no_mangle] pub unsafe extern "C" fn vmm_draw(vmm: *mut Vmm) -> *mut RustError { match (*vmm).screen.update() { @@ -490,33 +508,68 @@ pub unsafe extern "C" fn vmm_draw(vmm: *mut Vmm) -> *mut RustError { } } -/// # Safety -/// `addr` cannot be null and must point to a null-terminated string. -unsafe fn setup_debug_server(addr: *const c_char) -> Result, RustError> { - // Get listen address. - let addr = match addr.is_null() { - true => return Ok(None), - false => CStr::from_ptr(addr) - .to_str() - .map_err(|_| RustError::new("address to listen for a debugger is not UTF-8"))?, +#[no_mangle] +pub unsafe extern "C" fn vmm_dispatch_debug( + vmm: *mut Vmm, + stop: *mut KernelStop, + read: unsafe extern "C" fn(*mut u8, *mut c_void) -> bool, +) -> DebugResult { + let vmm = &mut *vmm; + let debug = vmm.debugger.as_mut().unwrap(); + let stop = match stop.is_null() { + true => None, + false => Some(Box::from_raw(stop)), }; - // Setup server. - let sock = TcpListener::bind(addr) - .map_err(|e| RustError::with_source("couldn't listen for a debugger", e))?; + loop { + // Check current state. + let s = match debug.state.take().unwrap() { + GdbStubStateMachine::Idle(s) => s, + GdbStubStateMachine::Running(s) => { + debug.state = Some(s.into()); + return DebugResult::Ok; + } + GdbStubStateMachine::CtrlCInterrupt(s) => { + match s.interrupt_handled(&mut debug.target, None::>) { + Ok(v) => debug.state = Some(v), + Err(e) => { + let r = RustError::with_source("couldn't handle CTRL+C from a debugger", e); + return DebugResult::Error { reason: r.into_c() }; + } + } + continue; + } + GdbStubStateMachine::Disconnected(_) => return DebugResult::Disconnected, + }; + + // Read data from the client. + let mut b = 0; + + if !read(&mut b, vmm.cx) { + return DebugResult::ReadFailed; + } + + match s.incoming_data(&mut debug.target, b) { + Ok(v) => debug.state = Some(v), + Err(e) => { + let r = RustError::with_source("couldn't process data from a debugger", e); + return DebugResult::Error { reason: r.into_c() }; + } + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn vmm_shutdown(vmm: *mut Vmm) { + (*vmm).shutdown.store(true, Ordering::Relaxed); +} - sock.set_nonblocking(true) - .map(|_| Some(sock)) - .map_err(|e| RustError::with_source("couldn't enable non-blocking on a debug server", e)) +#[no_mangle] +pub unsafe extern "C" fn vmm_shutting_down(vmm: *mut Vmm) -> bool { + (*vmm).shutdown.load(Ordering::Relaxed) } -fn main_cpu( - args: &CpuArgs, - entry: usize, - map: RamMap, - debug: Option, -) { - // Create vCPU. +fn main_cpu(args: &CpuArgs, entry: usize, map: RamMap) { let mut cpu = match args.hv.create_cpu(0) { Ok(v) => v, Err(e) => { @@ -532,81 +585,10 @@ fn main_cpu( return; } - // Check if debug. - let debug = match debug { - Some(v) => v, - None => { - run_cpu(cpu, args, None); - return; - } - }; - - // Get server address. - let addr = match debug.local_addr() { - Ok(v) => v.to_string(), - Err(e) => { - let e = RustError::with_source("couldn't get debug server address", e); - unsafe { args.event.invoke(VmmEvent::Error { reason: &e }) }; - return; - } - }; - - // Tell the user to connect a debugger. - let len = addr.len(); - let addr = addr.as_ptr().cast(); - - unsafe { args.event.invoke(VmmEvent::WaitingDebugger { addr, len }) }; - - // Wait for a debugger. - let client = loop { - use std::io::ErrorKind; - - if args.shutdown.load(Ordering::Relaxed) { - return; - } - - // Try accept a connection. - let e = match debug.accept() { - Ok(v) => break self::debug::Client::new(v.0), - Err(e) => e, - }; - - match e.kind() { - ErrorKind::WouldBlock => sleep(Duration::from_millis(500)), - ErrorKind::Interrupted => {} - _ => { - let e = RustError::with_source("couldn't accept a debugger connection", e); - unsafe { args.event.invoke(VmmEvent::Error { reason: &e }) }; - return; - } - } - }; - - // Setup GDB stub. - let mut target = self::debug::Target::new(); - let gdb = match GdbStub::new(client).run_state_machine(&mut target) { - Ok(v) => v, - Err(e) => { - let e = RustError::with_source("couldn't setup a GDB stub", e); - unsafe { args.event.invoke(VmmEvent::Error { reason: &e }) }; - return; - } - }; - - // Run GDB until the client is waiting for the target to report a stop reason. - let gdb = match run_gdb(args, &mut target, gdb) { - Some(v) => v, - None => return, - }; - - run_cpu(cpu, args, Some(gdb)); + run_cpu(cpu, args); } -fn run_cpu( - mut cpu: H::Cpu<'_>, - args: &CpuArgs, - gdb: Option>, -) { +fn run_cpu(mut cpu: H::Cpu<'_>, args: &CpuArgs) { // Build device contexts for this CPU. let mut devices = args .devices @@ -667,55 +649,6 @@ fn exec_io<'a>( dev.exec(&mut io) } -fn run_gdb<'a, H: Hypervisor>( - args: &CpuArgs, - target: &mut self::debug::Target, - mut state: GdbStubStateMachine<'a, self::debug::Target, self::debug::Client>, -) -> Option> { - loop { - // Check current state. - let mut s = match state { - GdbStubStateMachine::Idle(s) => s, - GdbStubStateMachine::Running(s) => return Some(s), - GdbStubStateMachine::CtrlCInterrupt(s) => { - state = match s.interrupt_handled(target, None::>) { - Ok(v) => v, - Err(e) => { - let e = RustError::with_source("couldn't handle CTRL+C from a debugger", e); - unsafe { args.event.invoke(VmmEvent::Error { reason: &e }) }; - return None; - } - }; - - continue; - } - GdbStubStateMachine::Disconnected(_) => { - unsafe { args.event.invoke(VmmEvent::DebuggerDisconnected) }; - return None; - } - }; - - // Read data from the client. - let b = match s.borrow_conn().read() { - Ok(v) => v, - Err(e) => { - let e = RustError::with_source("couldn't read data from a debugger", e); - unsafe { args.event.invoke(VmmEvent::Error { reason: &e }) }; - return None; - } - }; - - state = match s.incoming_data(target, b) { - Ok(v) => v, - Err(e) => { - let e = RustError::with_source("couldn't process data from a debugger", e); - unsafe { args.event.invoke(VmmEvent::Error { reason: &e }) }; - return None; - } - }; - } -} - #[cfg(unix)] fn get_page_size() -> Result, std::io::Error> { let v = unsafe { libc::sysconf(libc::_SC_PAGE_SIZE) }; @@ -742,7 +675,9 @@ fn get_page_size() -> Result, std::io::Error> { pub struct Vmm { cpus: Vec>, screen: self::screen::Default, + debugger: Option, shutdown: Arc, + cx: *mut c_void, } impl Drop for Vmm { @@ -792,11 +727,6 @@ pub enum VmmEvent { Error { reason: *const RustError, }, - WaitingDebugger { - addr: *const c_char, - len: usize, - }, - DebuggerDisconnected, Exiting { success: bool, }, @@ -805,6 +735,9 @@ pub enum VmmEvent { data: *const c_char, len: usize, }, + Breakpoint { + stop: *mut KernelStop, + }, } /// Log category. @@ -830,6 +763,20 @@ impl From for VmmLog { } } +/// Reason for [`VmmEvent::Breakpoint`]. +#[allow(dead_code)] +pub struct KernelStop(MultiThreadStopReason); + +/// Result of [`vmm_dispatch_debug()`]. +#[allow(dead_code)] +#[repr(C)] +pub enum DebugResult { + Ok, + Disconnected, + ReadFailed, + Error { reason: *mut RustError }, +} + /// Encapsulates arguments for a function to run a CPU. struct CpuArgs { hv: H, @@ -840,6 +787,12 @@ struct CpuArgs { shutdown: Arc, } +/// Contains data for a debugger. +struct Debugger { + target: self::debug::Target, + state: Option>, +} + /// Represents an error when [`vmm_new()`] fails. #[derive(Debug, Error)] enum VmmError { diff --git a/kernel/src/debug/mod.rs b/kernel/src/debug/mod.rs new file mode 100644 index 000000000..521726016 --- /dev/null +++ b/kernel/src/debug/mod.rs @@ -0,0 +1,12 @@ +use core::ptr::{addr_of_mut, write_volatile}; +use obconf::{DebuggerMemory, StopReason, Vm}; + +pub fn wait_debugger(env: &Vm) { + let debug = env.debugger as *mut DebuggerMemory; + + if debug.is_null() { + return; + } + + unsafe { write_volatile(addr_of_mut!((*debug).stop), StopReason::WaitForDebugger) }; +} diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 42e382487..647c05a56 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -1,6 +1,7 @@ #![no_std] #![cfg_attr(not(test), no_main)] +use crate::config::boot_env; use crate::context::Context; use crate::malloc::KernelHeap; use crate::proc::{ProcMgr, Thread}; @@ -16,6 +17,7 @@ mod arch; mod config; mod console; mod context; +mod debug; mod imgfmt; mod lock; mod malloc; @@ -66,6 +68,11 @@ unsafe extern "C" fn _start(env: &'static BootEnv, conf: &'static Config) -> ! { } fn main(pmgr: Arc) -> ! { + // Wait for debugger. + match boot_env() { + BootEnv::Vm(vm) => crate::debug::wait_debugger(vm), + } + // Activate stage 2 heap. info!("Activating stage 2 heap."); diff --git a/src/obconf/src/env/vm.rs b/src/obconf/src/env/vm.rs index 4a6155575..6e7203830 100644 --- a/src/obconf/src/env/vm.rs +++ b/src/obconf/src/env/vm.rs @@ -7,6 +7,8 @@ pub struct Vm { pub vmm: usize, /// Address of [ConsoleMemory]. pub console: usize, + /// Address of [DebuggerMemory]. + pub debugger: usize, /// Page size on the host. pub host_page_size: NonZero, } @@ -56,3 +58,18 @@ pub enum ConsoleType { Warn, Error, } + +/// Layout of a memory for Memory-mapped I/O to communicate with a debugger. +#[cfg(feature = "virt")] +#[repr(C)] +pub struct DebuggerMemory { + pub stop: StopReason, +} + +/// Reason why the kernel stopped the execution. +#[cfg(feature = "virt")] +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +pub enum StopReason { + WaitForDebugger, +}