Watches for debugger commands (#1028)

This commit is contained in:
Putta Khunchalee 2024-10-12 21:03:51 +07:00 committed by GitHub
parent 0076693dd4
commit c572230317
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 242 additions and 80 deletions

View File

@ -265,6 +265,8 @@ struct RustError *vmm_draw(struct Vmm *vmm);
struct DebugResult vmm_dispatch_debug(struct Vmm *vmm, struct KernelStop *stop);
ptrdiff_t vmm_debug_socket(struct Vmm *vmm);
void vmm_shutdown(struct Vmm *vmm);
bool vmm_shutting_down(struct Vmm *vmm);

View File

@ -27,6 +27,7 @@
#include <QResizeEvent>
#include <QScrollBar>
#include <QSettings>
#include <QSocketNotifier>
#include <QStackedWidget>
#include <QToolBar>
#include <QUrl>
@ -35,6 +36,9 @@
#include <iostream>
#include <utility>
#ifndef _WIN32
#include <fcntl.h>
#endif
#include <string.h>
namespace Args {
@ -55,7 +59,7 @@ MainWindow::MainWindow(
m_games(nullptr),
m_launch(nullptr),
m_screen(nullptr),
m_debug(nullptr)
m_debugNoti(nullptr)
{
setWindowTitle("Obliteration");
@ -393,7 +397,7 @@ void MainWindow::acceptDebuggerFailed(const QString &msg)
this,
"Error",
QString("Failed to accept a debugger connection: %1.").arg(msg));
m_debug.free();
m_debugServer.free();
}
void MainWindow::vmmError(const QString &msg)
@ -440,7 +444,48 @@ void MainWindow::log(VmmLog type, const QString &msg)
}
}
void MainWindow::breakpoint(KernelStop *stop)
void MainWindow::setupDebugger()
{
// Setup GDB session.
dispatchDebug(nullptr);
if (vmm_shutting_down(m_vmm)) {
return;
}
// Enable non-blocking on debug socket. On Windows QSocketNotifier will do this for us.
auto sock = vmm_debug_socket(m_vmm);
#ifndef _WIN32
auto flags = fcntl(sock, F_GETFL);
if (flags < 0) {
QMessageBox::critical(
this,
"Error",
"Couldn't get file flags from debug socket.");
stopDebug();
return;
} else if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) {
QMessageBox::critical(
this,
"Error",
"Couldn't enable non-blocking mode on debug socket.");
stopDebug();
return;
}
#endif
// Watch for incoming data.
m_debugNoti = new QSocketNotifier(QSocketNotifier::Read, this);
m_debugNoti->setSocket(sock);
connect(m_debugNoti, &QSocketNotifier::activated, [this] { dispatchDebug(nullptr); });
m_debugNoti->setEnabled(true);
}
void MainWindow::dispatchDebug(KernelStop *stop)
{
// Do nothing if the previous thread already trigger the shutdown.
if (vmm_shutting_down(m_vmm)) {
@ -471,23 +516,8 @@ void MainWindow::breakpoint(KernelStop *stop)
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);
if (vmm_shutting_down(m_vmm)) {
stopDebug();
}
}
@ -556,7 +586,7 @@ void MainWindow::startDebug(const QString &addr)
// Start debug server.
Rust<RustError> error;
m_debug = debug_server_start(
m_debugServer = debug_server_start(
this,
addr.toStdString().c_str(),
&error,
@ -583,7 +613,7 @@ void MainWindow::startDebug(const QString &addr)
}
});
if (!m_debug) {
if (!m_debugServer) {
auto msg = QString("Failed to start a debug server on %1: %2")
.arg(addr)
.arg(error_message(error));
@ -600,7 +630,7 @@ void MainWindow::startDebug(const QString &addr)
QMessageBox::information(
this,
"Debug",
QString("Waiting for a debugger at %1.").arg(debug_server_addr(m_debug)));
QString("Waiting for a debugger at %1.").arg(debug_server_addr(m_debugServer)));
}
}
@ -610,7 +640,7 @@ void MainWindow::startVmm(Debugger *d)
Rust<Debugger> debug(d);
std::string kernel;
m_debug.free();
m_debugServer.free();
if (QFile::exists(".obliteration-development")) {
auto b = std::filesystem::current_path();
@ -717,9 +747,30 @@ bool MainWindow::requireVmmStopped()
return true;
}
void MainWindow::stopDebug()
{
// 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);
}
}
void MainWindow::killVmm()
{
m_vmm.free();
delete m_debugNoti;
m_debugNoti = nullptr;
}
void MainWindow::vmmHandler(const VmmEvent *ev, void *cx)
@ -751,11 +802,18 @@ void MainWindow::vmmHandler(const VmmEvent *ev, void *cx)
QString::fromUtf8(ev->log.data, ev->log.len));
break;
case VmmEvent_Breakpoint:
QMetaObject::invokeMethod(
w,
&MainWindow::breakpoint,
Qt::BlockingQueuedConnection,
ev->breakpoint.stop);
if (auto stop = ev->breakpoint.stop; stop) {
QMetaObject::invokeMethod(
w,
&MainWindow::dispatchDebug,
Qt::BlockingQueuedConnection,
stop);
} else {
QMetaObject::invokeMethod(
w,
&MainWindow::setupDebugger,
Qt::BlockingQueuedConnection);
}
break;
}
}

View File

@ -15,6 +15,7 @@ class LogsViewer;
class ProfileList;
class QCommandLineOption;
class QCommandLineParser;
class QSocketNotifier;
class QStackedWidget;
class Screen;
@ -50,9 +51,11 @@ private:
void vmmError(const QString &msg);
void waitKernelExit(bool success);
void log(VmmLog type, const QString &msg);
void breakpoint(KernelStop *stop);
void setupDebugger();
void dispatchDebug(KernelStop *stop);
bool loadGame(const QString &gameId);
bool requireVmmStopped();
void stopDebug();
void killVmm();
static void vmmHandler(const VmmEvent *ev, void *cx);
@ -64,7 +67,8 @@ private:
LaunchSettings *m_launch;
Screen *m_screen;
QPointer<LogsViewer> m_logs;
Rust<DebugServer> m_debug;
Rust<DebugServer> m_debugServer;
QSocketNotifier *m_debugNoti;
Rust<Vmm> m_vmm; // Destroy first.
};

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::error::RustError;
use std::ffi::{c_char, c_void, CStr, CString};
use std::io::{Read, Write};
use std::io::{ErrorKind, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::ptr::null_mut;
use std::sync::atomic::{AtomicBool, Ordering};
@ -65,7 +65,7 @@ pub unsafe extern "C" fn debug_server_start(
next: 0,
})),
},
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
Err(e) if e.kind() == ErrorKind::WouldBlock => {
sleep(Duration::from_millis(500));
continue;
}
@ -138,11 +138,19 @@ pub struct Debugger {
}
impl Debugger {
#[cfg(unix)]
pub fn socket(&self) -> std::os::fd::RawFd {
std::os::fd::AsRawFd::as_raw_fd(&self.sock)
}
#[cfg(windows)]
pub fn socket(&self) -> std::os::windows::io::RawSocket {
std::os::windows::io::AsRawSocket::as_raw_socket(&self.sock)
}
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;
@ -175,15 +183,40 @@ impl gdbstub::conn::Connection for Debugger {
type Error = std::io::Error;
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)
fn write_all(&mut self, mut buf: &[u8]) -> Result<(), Self::Error> {
while !buf.is_empty() {
let written = match Write::write(&mut self.sock, buf) {
Ok(v) => v,
Err(e) => {
if matches!(e.kind(), ErrorKind::Interrupted | ErrorKind::WouldBlock) {
continue;
} else {
return Err(e);
}
}
};
if written == 0 {
return Err(std::io::Error::from(ErrorKind::WriteZero));
}
buf = &buf[written..];
}
Ok(())
}
fn flush(&mut self) -> Result<(), Self::Error> {
Write::flush(&mut self.sock)
while let Err(e) = Write::flush(&mut self.sock) {
if !matches!(e.kind(), ErrorKind::Interrupted | ErrorKind::WouldBlock) {
return Err(e);
}
}
Ok(())
}
fn on_session_start(&mut self) -> Result<(), Self::Error> {

View File

@ -1,6 +1,63 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pub use self::arch::*;
use crate::debug::Debugger;
use crate::error::RustError;
use gdbstub::stub::state_machine::state::{Idle, Running};
use gdbstub::stub::state_machine::{GdbStubStateMachine, GdbStubStateMachineInner};
use gdbstub::stub::MultiThreadStopReason;
#[cfg_attr(target_arch = "aarch64", path = "aarch64.rs")]
#[cfg_attr(target_arch = "x86_64", path = "x86_64.rs")]
mod arch;
pub fn dispatch_idle(
target: &mut Target,
mut state: GdbStubStateMachineInner<'static, Idle<Target>, Target, Debugger>,
) -> Result<GdbStubStateMachine<'static, Target, Debugger>, RustError> {
let b = state
.borrow_conn()
.read()
.map_err(|e| RustError::with_source("couldn't read data from the debugger", e))?;
state
.incoming_data(target, b)
.map_err(|e| RustError::with_source("couldn't process data from the debugger", e))
}
pub fn dispatch_running(
target: &mut Target,
mut state: GdbStubStateMachineInner<'static, Running, Target, Debugger>,
stop: Option<MultiThreadStopReason<u64>>,
) -> Result<
Result<
GdbStubStateMachine<'static, Target, Debugger>,
GdbStubStateMachineInner<'static, Running, Target, Debugger>,
>,
RustError,
> {
// Check If we are here because of a breakpoint.
if let Some(r) = stop {
return state
.report_stop(target, r)
.map(Ok)
.map_err(|e| RustError::with_source("couldn't report stop reason to the debugger", e));
}
// Check for pending command.
let b = match state.borrow_conn().read() {
Ok(v) => v,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(Err(state)),
Err(e) => {
return Err(RustError::with_source(
"couldn't read data from the debugger",
e,
));
}
};
state
.incoming_data(target, b)
.map(Ok)
.map_err(|e| RustError::with_source("couldn't process data from the debugger", e))
}

View File

@ -447,16 +447,12 @@ pub unsafe extern "C" fn vmm_start(
// Setup GDB stub.
let gdb = match debugger
.map(move |client| -> Result<GdbStub, RustError> {
.map(move |client| {
let mut target = self::debug::Target::new();
let state = gdbstub::stub::GdbStub::new(*client)
.run_state_machine(&mut target)
.map_err(|e| RustError::with_source("couldn't setup a GDB stub", e))?;
Ok(GdbStub {
target,
state: Some(state),
})
gdbstub::stub::GdbStub::new(*client)
.run_state_machine(&mut target)
.map_err(|e| RustError::with_source("couldn't setup a GDB stub", e))
})
.transpose()
{
@ -491,7 +487,7 @@ pub unsafe extern "C" fn vmm_start(
// Create VMM.
let vmm = Vmm {
cpus: vec![main],
cpus: vec![CpuController { thread: main }],
screen,
gdb,
shutdown,
@ -515,24 +511,34 @@ pub unsafe extern "C" fn vmm_draw(vmm: *mut Vmm) -> *mut RustError {
#[no_mangle]
pub unsafe extern "C" fn vmm_dispatch_debug(vmm: *mut Vmm, stop: *mut KernelStop) -> DebugResult {
let vmm = &mut *vmm;
let gdb = vmm.gdb.as_mut().unwrap();
let stop = match stop.is_null() {
// Consume stop reason now to prevent memory leak.
let mut stop = match stop.is_null() {
true => None,
false => Some(Box::from_raw(stop)),
false => Some(Box::from_raw(stop).0),
};
// Setup target object.
let vmm = &mut *vmm;
let mut target = self::debug::Target::new();
loop {
// Check current state.
let mut s = match gdb.state.take().unwrap() {
GdbStubStateMachine::Idle(s) => s,
let r = match vmm.gdb.take().unwrap() {
GdbStubStateMachine::Idle(s) => self::debug::dispatch_idle(&mut target, s),
GdbStubStateMachine::Running(s) => {
gdb.state = Some(s.into());
return DebugResult::Ok;
match self::debug::dispatch_running(&mut target, s, stop.take()) {
Ok(Ok(v)) => Ok(v),
Ok(Err(v)) => {
// No pending data from the debugger.
vmm.gdb = Some(v.into());
return DebugResult::Ok;
}
Err(e) => Err(e),
}
}
GdbStubStateMachine::CtrlCInterrupt(s) => {
match s.interrupt_handled(&mut gdb.target, None::<MultiThreadStopReason<u64>>) {
Ok(v) => gdb.state = Some(v),
match s.interrupt_handled(&mut target, None::<MultiThreadStopReason<u64>>) {
Ok(v) => vmm.gdb = 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() };
@ -543,25 +549,28 @@ pub unsafe extern "C" fn vmm_dispatch_debug(vmm: *mut Vmm, stop: *mut KernelStop
GdbStubStateMachine::Disconnected(_) => return DebugResult::Disconnected,
};
// Read data from the client.
let b = match s.borrow_conn().read() {
Ok(v) => v,
Err(e) => {
let r = RustError::with_source("couldn't read data from a debugger", e);
return DebugResult::Error { reason: r.into_c() };
}
};
match s.incoming_data(&mut gdb.target, b) {
Ok(v) => gdb.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() };
}
match r {
Ok(v) => vmm.gdb = Some(v),
Err(e) => return DebugResult::Error { reason: e.into_c() },
}
}
}
#[no_mangle]
pub unsafe extern "C" fn vmm_debug_socket(vmm: *mut Vmm) -> isize {
let s = match &mut (*vmm).gdb {
Some(v) => v,
None => return -1,
};
match s {
GdbStubStateMachine::Idle(s) => s.borrow_conn().socket() as _,
GdbStubStateMachine::Running(s) => s.borrow_conn().socket() as _,
GdbStubStateMachine::CtrlCInterrupt(s) => s.borrow_conn().socket() as _,
GdbStubStateMachine::Disconnected(s) => s.borrow_conn().socket() as _,
}
}
#[no_mangle]
pub unsafe extern "C" fn vmm_shutdown(vmm: *mut Vmm) {
(*vmm).shutdown.store(true, Ordering::Relaxed);
@ -676,9 +685,9 @@ fn get_page_size() -> Result<NonZero<usize>, std::io::Error> {
/// Manage a virtual machine that run the kernel.
pub struct Vmm {
cpus: Vec<JoinHandle<()>>,
cpus: Vec<CpuController>,
screen: self::screen::Default,
gdb: Option<GdbStub>,
gdb: Option<GdbStubStateMachine<'static, self::debug::Target, Debugger>>,
shutdown: Arc<AtomicBool>,
}
@ -688,7 +697,7 @@ impl Drop for Vmm {
self.shutdown.store(true, Ordering::Relaxed);
for cpu in self.cpus.drain(..) {
cpu.join().unwrap();
cpu.thread.join().unwrap();
}
}
}
@ -788,10 +797,9 @@ struct CpuArgs<H: Hypervisor> {
shutdown: Arc<AtomicBool>,
}
/// States for GDB stub.
struct GdbStub {
target: self::debug::Target,
state: Option<GdbStubStateMachine<'static, self::debug::Target, Debugger>>,
/// Contains objects to control a CPU from a GUI.
struct CpuController {
thread: JoinHandle<()>,
}
/// Represents an error when [`vmm_new()`] fails.