Moves GDB server to GUI thread (#1022)
Some checks are pending
Development Build / Build (push) Waiting to run
Development Build / Update PRs (push) Waiting to run

This commit is contained in:
Putta Khunchalee 2024-10-09 06:51:18 +07:00 committed by GitHub
parent 7a65dbb25a
commit 612ca53580
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 607 additions and 299 deletions

View File

@ -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})

View File

@ -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*));
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);
void vmm_free(struct Vmm *vmm);
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);
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

View File

@ -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();

View File

@ -20,6 +20,7 @@
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QHostAddress>
#include <QIcon>
#include <QMenuBar>
#include <QMessageBox>
@ -28,6 +29,8 @@
#include <QScrollBar>
#include <QSettings>
#include <QStackedWidget>
#include <QTcpServer>
#include <QTcpSocket>
#include <QToolBar>
#include <QUrl>
@ -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);
// 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);
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;
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<MainWindow *>(cx);
while (w->m_debug->bytesAvailable() == 0) {
QCoreApplication::processEvents();
}
return w->m_debug->getChar(reinterpret_cast<char *>(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<RustError> 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<QAbstractSocket::SocketError> MainWindow::sendDebug(const uint8_t *data, size_t len)
{
while (len) {
auto r = m_debug->write(reinterpret_cast<const char *>(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<RustError> error;
Rust<Vmm> vmm;
@ -559,27 +736,38 @@ void MainWindow::startVmm(const QString &debugAddr)
screen.vk_surface = reinterpret_cast<size_t>(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<void *, const uint8_t *, size_t, int *>(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<MainWindow *>(cx);
auto r = w->sendDebug(data, len);
if (r) {
*err = *r;
return false;
} else {
return true;
}
}

View File

@ -2,6 +2,7 @@
#include "core.hpp"
#include <QAbstractSocket>
#include <QList>
#include <QMainWindow>
#include <QPointer>
@ -9,6 +10,8 @@
#include <QVulkanInstance>
#endif
#include <optional>
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 @@ public:
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<QAbstractSocket::SocketError> 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:
LaunchSettings *m_launch;
Screen *m_screen;
QPointer<LogsViewer> m_logs;
QTcpSocket *m_debug;
Rust<Vmm> m_vmm; // Destroy first.
};

View File

@ -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<u8>,
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<u8, std::io::Error> {
// 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(())
}
}

View File

@ -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<bool, Box<dyn Error>> {
// 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),
}

View File

@ -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<usize>,
event: VmmEventHandler,
}
impl Debugger {
pub fn new(addr: usize, block_size: NonZero<usize>, event: VmmEventHandler) -> Self {
let len = size_of::<DebuggerMemory>()
.checked_next_multiple_of(block_size.get())
.and_then(NonZero::new)
.unwrap();
Self { addr, len, event }
}
}
impl<H: Hypervisor> Device<H> for Debugger {
fn addr(&self) -> usize {
self.addr
}
fn len(&self) -> NonZero<usize> {
self.len
}
fn create_context<'a>(&'a self, _: &'a H) -> Box<dyn DeviceContext + 'a> {
Box::new(Context::new(self))
}
}

View File

@ -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<H: Hypervisor>(
@ -25,10 +27,12 @@ pub fn setup_devices<H: Hypervisor>(
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<H: Hypervisor> {
vmm: Arc<Vmm>,
console: Arc<Console>,
debugger: Arc<Debugger>,
map: BTreeMap<usize, Arc<dyn Device<H>>>,
}
@ -103,6 +108,10 @@ impl<H: Hypervisor> DeviceTree<H> {
self.console.as_ref()
}
pub fn debugger(&self) -> &impl Device<H> {
self.debugger.as_ref()
}
/// Returns iterator ordered by physical address.
pub fn map(&self) -> impl Iterator<Item = (usize, &dyn Device<H>)> + '_ {
self.map.iter().map(|(addr, dev)| (*addr, dev.as_ref()))

View File

@ -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<unsafe extern "C" fn(*mut c_void, *const u8, usize, *mut c_int) -> 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<Debugger, RustError> {
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<Option<TcpListener>, 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::<MultiThreadStopReason<u64>>) {
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,
};
sock.set_nonblocking(true)
.map(|_| Some(sock))
.map_err(|e| RustError::with_source("couldn't enable non-blocking on a debug server", e))
// 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() };
}
}
}
}
fn main_cpu<H: Hypervisor>(
args: &CpuArgs<H>,
entry: usize,
map: RamMap,
debug: Option<TcpListener>,
) {
// Create vCPU.
#[no_mangle]
pub unsafe extern "C" fn vmm_shutdown(vmm: *mut Vmm) {
(*vmm).shutdown.store(true, Ordering::Relaxed);
}
#[no_mangle]
pub unsafe extern "C" fn vmm_shutting_down(vmm: *mut Vmm) -> bool {
(*vmm).shutdown.load(Ordering::Relaxed)
}
fn main_cpu<H: Hypervisor>(args: &CpuArgs<H>, 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<H: Hypervisor>(
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<H: Hypervisor>(
mut cpu: H::Cpu<'_>,
args: &CpuArgs<H>,
gdb: Option<GdbStubStateMachineInner<Running, self::debug::Target, self::debug::Client>>,
) {
fn run_cpu<H: Hypervisor>(mut cpu: H::Cpu<'_>, args: &CpuArgs<H>) {
// 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<H>,
target: &mut self::debug::Target,
mut state: GdbStubStateMachine<'a, self::debug::Target, self::debug::Client>,
) -> Option<GdbStubStateMachineInner<'a, Running, self::debug::Target, self::debug::Client>> {
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::<MultiThreadStopReason<u64>>) {
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<NonZero<usize>, std::io::Error> {
let v = unsafe { libc::sysconf(libc::_SC_PAGE_SIZE) };
@ -742,7 +675,9 @@ fn get_page_size() -> Result<NonZero<usize>, std::io::Error> {
pub struct Vmm {
cpus: Vec<JoinHandle<()>>,
screen: self::screen::Default,
debugger: Option<Debugger>,
shutdown: Arc<AtomicBool>,
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<ConsoleType> for VmmLog {
}
}
/// Reason for [`VmmEvent::Breakpoint`].
#[allow(dead_code)]
pub struct KernelStop(MultiThreadStopReason<u64>);
/// 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<H: Hypervisor> {
hv: H,
@ -840,6 +787,12 @@ struct CpuArgs<H: Hypervisor> {
shutdown: Arc<AtomicBool>,
}
/// Contains data for a debugger.
struct Debugger {
target: self::debug::Target,
state: Option<GdbStubStateMachine<'static, self::debug::Target, self::debug::Client>>,
}
/// Represents an error when [`vmm_new()`] fails.
#[derive(Debug, Error)]
enum VmmError {

12
kernel/src/debug/mod.rs Normal file
View File

@ -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) };
}

View File

@ -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<ProcMgr>) -> ! {
// Wait for debugger.
match boot_env() {
BootEnv::Vm(vm) => crate::debug::wait_debugger(vm),
}
// Activate stage 2 heap.
info!("Activating stage 2 heap.");

View File

@ -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<usize>,
}
@ -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,
}