use core::{
ptr::NonNull,
sync::atomic::{AtomicBool, Ordering},
};
#[cfg(feature = "alloc")]
use alloc::boxed::Box;
use gdbstub::{
conn::{Connection, ConnectionExt},
stub::{GdbStubBuilder, SingleThreadStopReason, state_machine::GdbStubStateMachine},
};
use patina::{component::service::perf_timer::ArchTimerFunctionality, serial::SerialIO};
use patina_internal_cpu::interrupts::{ExceptionType, HandlerType, InterruptHandler, InterruptManager};
use spin::Mutex;
use crate::{
DebugError, Debugger, DebuggerLoggingPolicy, ExceptionInfo,
arch::{DebuggerArch, SystemArch},
dbg_target::PatinaTarget,
system::{SystemState, SystemStateTrait},
transport::{LoggingSuspender, SerialConnection},
};
const GDB_BUFF_LEN: usize = 0x2000;
const GDB_STOP_PACKET: &[u8] = b"$T05thread:01;#07";
const GDB_NACK_PACKET: &[u8] = b"-";
cfg_if::cfg_if! {
if #[cfg(not(feature = "alloc"))] {
static GDB_BUFFER: StaticGdbBuffer = StaticGdbBuffer(core::cell::UnsafeCell::new([0; GDB_BUFF_LEN]));
struct StaticGdbBuffer(core::cell::UnsafeCell<[u8; GDB_BUFF_LEN]>);
unsafe impl Sync for StaticGdbBuffer {}
}
}
unsafe impl Send for ExceptionInfo {}
unsafe impl Sync for ExceptionInfo {}
pub struct PatinaDebugger<T>
where
T: SerialIO + 'static,
{
transport: T,
exception_types: &'static [usize],
log_policy: DebuggerLoggingPolicy,
no_transport_init: bool,
enabled: AtomicBool,
initialized: AtomicBool,
initial_break_timeout: u32,
internal: Mutex<DebuggerInternal<'static, T>>,
system_state: Mutex<SystemState>,
connection_timed_out: AtomicBool,
}
unsafe impl<T: SerialIO> Send for DebuggerInternal<'static, T> {}
struct DebuggerInternal<'a, T>
where
T: SerialIO,
{
gdb: Option<GdbStubStateMachine<'a, PatinaTarget, SerialConnection<'a, T>>>,
gdb_buffer: Option<NonNull<[u8; GDB_BUFF_LEN]>>,
timer: Option<&'a dyn ArchTimerFunctionality>,
initial_breakpoint: bool,
}
impl<T: SerialIO> PatinaDebugger<T> {
pub const fn new(transport: T) -> Self {
PatinaDebugger {
transport,
log_policy: DebuggerLoggingPolicy::SuspendLogging,
no_transport_init: false,
exception_types: SystemArch::DEFAULT_EXCEPTION_TYPES,
enabled: AtomicBool::new(false),
initialized: AtomicBool::new(false),
initial_break_timeout: 0,
internal: Mutex::new(DebuggerInternal {
gdb_buffer: None,
gdb: None,
timer: None,
initial_breakpoint: false,
}),
system_state: Mutex::new(SystemState::new()),
connection_timed_out: AtomicBool::new(false),
}
}
pub const fn with_force_enable(mut self, enabled: bool) -> Self {
if enabled {
self.enabled = AtomicBool::new(true);
}
self
}
pub const fn with_log_policy(mut self, policy: DebuggerLoggingPolicy) -> Self {
self.log_policy = policy;
self
}
pub const fn without_transport_init(mut self) -> Self {
self.no_transport_init = true;
self
}
pub const fn with_exception_types(mut self, exception_types: &'static [usize]) -> Self {
self.exception_types = exception_types;
self
}
pub const fn with_timeout(mut self, timeout_seconds: u32) -> Self {
self.initial_break_timeout = timeout_seconds;
self
}
pub fn enable(&self, enabled: bool) {
self.enabled.store(enabled, Ordering::Relaxed);
}
fn enter_debugger(
&'static self,
exception_info: ExceptionInfo,
restart: bool,
) -> Result<ExceptionInfo, DebugError> {
let mut debug = match self.internal.try_lock() {
Some(inner) => inner,
None => return Err(DebugError::Reentry),
};
let mut target = PatinaTarget::new(exception_info, &self.system_state);
let timeout = match debug.initial_breakpoint {
true => {
debug.initial_breakpoint = false;
self.initial_break_timeout
}
false => 0,
};
let mut gdb = match debug.gdb {
Some(_) => debug.gdb.take().unwrap(),
None => {
while self.transport.try_read().is_some() {}
let gdb_buffer = unsafe { debug.gdb_buffer.ok_or(DebugError::NotInitialized)?.as_mut() };
let conn = SerialConnection::new(&self.transport);
let builder = GdbStubBuilder::new(conn)
.with_packet_buffer(gdb_buffer)
.build()
.map_err(|_| DebugError::GdbStubInit)?;
builder.run_state_machine(&mut target).map_err(|_| DebugError::GdbStubInit)?
}
};
let mut timeout_reached = false;
if let GdbStubStateMachine::Idle(mut inner) = gdb {
if restart {
let _ = inner.borrow_conn().write_all(GDB_NACK_PACKET);
} else {
let _ = inner.borrow_conn().write_all(GDB_STOP_PACKET);
}
if timeout != 0
&& let Some(timer) = debug.timer
{
let frequency = timer.perf_frequency();
let initial_count = timer.cpu_count();
loop {
if (timer.cpu_count() - initial_count) / frequency >= timeout as u64 {
timeout_reached = true;
break;
}
if !matches!(inner.borrow_conn().peek(), Ok(None)) {
break;
}
}
}
gdb = GdbStubStateMachine::Idle(inner);
}
while !target.is_resumed() && !timeout_reached {
gdb = match gdb {
GdbStubStateMachine::Idle(mut gdb) => {
let byte = loop {
match gdb.borrow_conn().read() {
Ok(0x0) => {
log::warn!(
"Debugger: Read 0x00 from the transport. This is unexpected and will be ignored."
);
continue;
}
Ok(b) => break b,
Err(_) => return Err(DebugError::TransportFailure),
}
};
match gdb.incoming_data(&mut target, byte) {
Ok(gdb) => gdb,
Err(e) => return Err(DebugError::GdbStubError(e)),
}
}
GdbStubStateMachine::Running(gdb) => {
match gdb.report_stop(
&mut target,
SingleThreadStopReason::SignalWithThread { tid: (), signal: gdbstub::common::Signal::SIGTRAP },
) {
Ok(gdb) => gdb,
Err(e) => return Err(DebugError::GdbStubError(e)),
}
}
GdbStubStateMachine::CtrlCInterrupt(gdb) => {
match gdb.interrupt_handled(&mut target, None::<SingleThreadStopReason<u64>>) {
Ok(gdb) => gdb,
Err(e) => return Err(DebugError::GdbStubError(e)),
}
}
GdbStubStateMachine::Disconnected(gdb) => gdb.return_to_idle(),
};
}
if timeout_reached {
self.connection_timed_out.store(true, Ordering::Relaxed);
}
if target.reboot_on_resume() {
SystemArch::reboot();
return Err(DebugError::RebootFailure);
}
debug.gdb = Some(gdb);
Ok(target.into_exception_info())
}
}
impl<T: SerialIO> Debugger for PatinaDebugger<T> {
#[cfg(test)]
fn test_initialize(&'static self, initialized: bool) {
self.initialized.store(initialized, Ordering::Relaxed);
}
fn initialize(
&'static self,
interrupt_manager: &mut dyn InterruptManager,
timer: Option<&'static dyn ArchTimerFunctionality>,
) {
if !self.enabled.load(Ordering::Relaxed) {
log::info!("Debugger is disabled.");
return;
}
log::info!("Initializing debugger.");
if !self.no_transport_init {
self.transport.init();
}
SystemArch::initialize();
{
let mut internal = self.internal.lock();
cfg_if::cfg_if! {
if #[cfg(feature = "alloc")] {
if internal.gdb_buffer.is_none() {
internal.gdb_buffer = Some(NonNull::new(Box::leak(Box::new([0u8; GDB_BUFF_LEN]))).unwrap());
}
}
else {
internal.gdb_buffer = Some(NonNull::new(GDB_BUFFER.0.get()).unwrap());
}
}
if timer.is_none() && self.initial_break_timeout != 0 {
log::warn!(
"Debugger initialized with an initial break timeout but no timer service. Ignoring timeout."
);
}
internal.timer = timer;
internal.initial_breakpoint = true;
}
for exception_type in self.exception_types {
let _ = interrupt_manager.unregister_exception_handler(*exception_type);
let res = interrupt_manager.register_exception_handler(*exception_type, HandlerType::Handler(self));
if res.is_err() {
log::error!("Failed to register debugger exception handler for type {exception_type}: {res:?}");
}
}
self.initialized.store(true, Ordering::Relaxed);
log::error!("************************************");
log::error!("*** Initial debug breakpoint! ***");
log::error!("************************************");
SystemArch::breakpoint();
log::info!("Resuming from initial breakpoint.");
}
fn enabled(&'static self) -> bool {
self.enabled.load(Ordering::Relaxed)
}
fn initialized(&'static self) -> bool {
self.initialized.load(Ordering::Relaxed)
}
fn notify_module_load(&'static self, module_name: &str, address: usize, length: usize) {
if !self.enabled() {
return;
}
let breakpoint = {
let mut state = self.system_state.lock();
state.add_module(module_name, address, length);
state.check_module_breakpoints(module_name)
};
if breakpoint {
log::error!("MODULE BREAKPOINT! {module_name} - 0x{address:x} - 0x{length:x}");
SystemArch::breakpoint();
}
}
fn poll_debugger(&'static self) {
const CRTL_C: u8 = 3;
if !self.enabled() {
return;
}
while let Some(byte) = self.transport.try_read() {
if byte == CRTL_C {
SystemArch::breakpoint();
}
}
}
#[cfg(feature = "alloc")]
fn add_monitor_command(
&'static self,
command: &'static str,
description: &'static str,
callback: Box<crate::MonitorCommandFn>,
) {
if !self.enabled() {
return;
}
self.system_state.lock().add_monitor_command(command, description, callback);
}
}
impl<T: SerialIO> InterruptHandler for PatinaDebugger<T> {
fn handle_interrupt(
&'static self,
exception_type: ExceptionType,
context: &mut patina_internal_cpu::interrupts::ExceptionContext,
) {
if self.connection_timed_out.swap(false, Ordering::Relaxed) {
log::error!("********* DEBUGGER BREAK-IN *********");
}
let _log_suspend;
match self.log_policy {
DebuggerLoggingPolicy::SuspendLogging => {
_log_suspend = LoggingSuspender::suspend();
}
DebuggerLoggingPolicy::DisableLogging => {
log::set_max_level(log::LevelFilter::Off);
}
DebuggerLoggingPolicy::FullLogging => {
}
}
if SystemArch::check_memory_poke_test(context) {
log::info!("Memory poke test triggered, ignoring exception.");
return;
}
let mut restart = false;
let mut exception_info = loop {
let exception_info = SystemArch::process_entry(exception_type as u64, context);
let result = self.enter_debugger(exception_info, restart);
match result {
Ok(info) => break info,
Err(DebugError::GdbStubError(gdb_error)) => {
log::error!("GDB Stub error, restarting debugger. {gdb_error:?}");
restart = true;
continue;
}
Err(error) => {
debugger_crash(error, exception_type);
}
}
};
SystemArch::process_exit(&mut exception_info);
*context = exception_info.context;
}
}
fn debugger_crash(error: DebugError, exception_type: ExceptionType) -> ! {
log::set_max_level(log::LevelFilter::Error);
log::error!("DEBUGGER CRASH! Error: {error:?} Exception Type: {exception_type:?}");
#[allow(clippy::empty_loop)]
loop {}
}