udbserver 0.3.0

Provide Unicorn emulator with a debug server
Documentation
#[cfg(feature = "capi")]
mod capi;

mod arch;
mod emu;
mod reg;

use gdbstub::conn::ConnectionExt;
use gdbstub::stub::state_machine::GdbStubStateMachine;
use gdbstub::stub::{DisconnectReason, GdbStubBuilder, SingleThreadStopReason};
use gdbstub::target::ext::breakpoints::WatchKind;
use singlyton::SingletonOption;
use std::any::Any;
use std::borrow::BorrowMut;
use std::io::ErrorKind;
use std::net::{TcpListener, TcpStream};
use unicorn_engine::unicorn_const::HookType;
use unicorn_engine::Unicorn;

type DynResult<T> = Result<T, Box<dyn std::error::Error>>;

static GDBSTUB: SingletonOption<Box<dyn Any>> = SingletonOption::new();
static EMU: SingletonOption<Box<dyn Any>> = SingletonOption::new();

fn wait_for_tcp(port: u16) -> DynResult<TcpStream> {
    let sockaddr = format!("0.0.0.0:{}", port);
    eprintln!("Waiting for a GDB connection on {:?}...", sockaddr);

    let sock = TcpListener::bind(sockaddr)?;
    let (stream, addr) = sock.accept()?;
    eprintln!("Debugger connected from {}", addr);

    Ok(stream)
}

pub fn udbserver<T: 'static>(uc: &mut Unicorn<T>, port: u16, start_addr: u64) -> DynResult<()> {
    let code_hook = uc.add_code_hook(1, 0, |_, _, _| {}).expect("Failed to add empty code hook");
    let mem_hook = uc
        .add_mem_hook(HookType::MEM_READ, 1, 0, |_, _, _, _, _| true)
        .expect("Failed to add empty mem hook");
    if start_addr != 0 {
        uc.add_code_hook(start_addr, start_addr, move |_, _, _| {
            udbserver_start::<T>(port).expect("Failed to start udbserver")
        })
        .expect("Failed to add udbserver hook");
    }
    let emu = emu::Emu::new(
        unsafe { std::mem::transmute::<&mut Unicorn<T>, &'static mut Unicorn<'static, T>>(uc) },
        code_hook,
        mem_hook,
    )?;
    EMU.replace(Box::new(emu));
    if start_addr == 0 {
        udbserver_start::<T>(port).expect("Failed to start udbserver");
    }
    Ok(())
}

fn udbserver_start<T: 'static>(port: u16) -> DynResult<()> {
    if GDBSTUB.is_some() {
        return Ok(());
    }
    {
        let mut emu_any = EMU.get_mut();
        let emu = emu_any.downcast_mut::<emu::Emu<T>>().expect("Failed to downcast EMU");
        let gdbstub = GdbStubBuilder::new(wait_for_tcp(port)?).build()?.run_state_machine(emu)?;
        GDBSTUB.replace(Box::new(gdbstub));
    }
    udbserver_loop::<T>()
}

pub(crate) fn udbserver_resume<T: 'static>(addr: Option<u64>) -> DynResult<()> {
    let gdb_any = GDBSTUB.take().unwrap();
    let mut gdb = *gdb_any
        .downcast::<GdbStubStateMachine<emu::Emu<T>, TcpStream>>()
        .expect("Failed to downcast GDBSTUB");
    let reason = if let Some(watch_addr) = addr {
        SingleThreadStopReason::Watch {
            tid: (),
            kind: WatchKind::Write,
            addr: watch_addr,
        }
    } else {
        SingleThreadStopReason::DoneStep
    };
    if let GdbStubStateMachine::Running(gdb_inner) = gdb {
        let mut emu_any = EMU.get_mut();
        let emu = emu_any.downcast_mut::<emu::Emu<T>>().expect("Failed to downcast EMU");
        match gdb_inner.report_stop(emu.borrow_mut(), reason) {
            Ok(gdb_state) => gdb = gdb_state,
            Err(_) => return Ok(()),
        }
    }
    GDBSTUB.replace(Box::new(gdb));
    udbserver_loop::<T>()
}

fn is_disconnect_error(error: &std::io::Error) -> bool {
    matches!(
        error.kind(),
        ErrorKind::UnexpectedEof | ErrorKind::ConnectionReset | ErrorKind::ConnectionAborted | ErrorKind::BrokenPipe | ErrorKind::NotConnected
    )
}

fn udbserver_loop<T: 'static>() -> DynResult<()> {
    let gdb_any = GDBSTUB.take().unwrap();
    let mut gdb = *gdb_any
        .downcast::<GdbStubStateMachine<emu::Emu<T>, TcpStream>>()
        .expect("Failed to downcast GDBSTUB");
    loop {
        gdb = match gdb {
            GdbStubStateMachine::Idle(mut gdb_inner) => {
                let byte = match gdb_inner.borrow_conn().read() {
                    Ok(byte) => byte,
                    Err(error) if is_disconnect_error(&error) => return handle_disconnect(DisconnectReason::Disconnect),
                    Err(error) => return Err(Box::new(error)),
                };
                let mut emu_any = EMU.get_mut();
                let emu = emu_any.downcast_mut::<emu::Emu<T>>().expect("Failed to downcast EMU");
                gdb_inner.incoming_data(emu.borrow_mut(), byte)?
            }
            GdbStubStateMachine::Running(_) => break,
            GdbStubStateMachine::CtrlCInterrupt(_) => break,
            GdbStubStateMachine::Disconnected(gdb_inner) => return handle_disconnect(gdb_inner.get_reason()),
        }
    }
    GDBSTUB.replace(Box::new(gdb));
    Ok(())
}

fn handle_disconnect(reason: DisconnectReason) -> DynResult<()> {
    EMU.take();
    #[cfg(feature = "capi")]
    capi::clean();
    match reason {
        DisconnectReason::Disconnect => {
            println!("Disconnect!");
            Ok(())
        }
        DisconnectReason::TargetExited(code) => {
            println!("Target exited with code {}!", code);
            Ok(())
        }
        DisconnectReason::TargetTerminated(sig) => {
            println!("Target terminated with signal {}!", sig);
            Ok(())
        }
        DisconnectReason::Kill => {
            println!("GDB sent a kill command!");
            Ok(())
        }
    }
}