603 lines
20 KiB
Rust
603 lines
20 KiB
Rust
use core::convert::TryInto;
|
|
|
|
use armv4t_emu::{reg, Memory};
|
|
use gdbstub::common::Signal;
|
|
use gdbstub::target;
|
|
use gdbstub::target::ext::base::singlethread::{SingleThreadBase, SingleThreadResume};
|
|
use gdbstub::target::{Target, TargetError, TargetResult};
|
|
use gdbstub_arch::arm::reg::id::ArmCoreRegId;
|
|
|
|
use crate::emu::{Emu, ExecMode};
|
|
|
|
// Additional GDB extensions
|
|
|
|
mod auxv;
|
|
mod breakpoints;
|
|
mod catch_syscalls;
|
|
mod exec_file;
|
|
mod extended_mode;
|
|
mod host_io;
|
|
mod lldb_register_info_override;
|
|
mod memory_map;
|
|
mod monitor_cmd;
|
|
mod section_offsets;
|
|
mod target_description_xml_override;
|
|
|
|
/// Turn a `ArmCoreRegId` into an internal register number of `armv4t_emu`.
|
|
fn cpu_reg_id(id: ArmCoreRegId) -> Option<u8> {
|
|
match id {
|
|
ArmCoreRegId::Gpr(i) => Some(i),
|
|
ArmCoreRegId::Sp => Some(reg::SP),
|
|
ArmCoreRegId::Lr => Some(reg::LR),
|
|
ArmCoreRegId::Pc => Some(reg::PC),
|
|
ArmCoreRegId::Cpsr => Some(reg::CPSR),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Copy all bytes of `data` to `buf`.
|
|
/// Return the size of data copied.
|
|
pub fn copy_to_buf(data: &[u8], buf: &mut [u8]) -> usize {
|
|
let len = buf.len().min(data.len());
|
|
buf[..len].copy_from_slice(&data[..len]);
|
|
len
|
|
}
|
|
|
|
/// Copy a range of `data` (start at `offset` with a size of `length`) to `buf`.
|
|
/// Return the size of data copied. Returns 0 if `offset >= buf.len()`.
|
|
///
|
|
/// Mainly used by qXfer:_object_:read commands.
|
|
pub fn copy_range_to_buf(data: &[u8], offset: u64, length: usize, buf: &mut [u8]) -> usize {
|
|
let offset = offset as usize;
|
|
if offset > data.len() {
|
|
return 0;
|
|
}
|
|
|
|
let start = offset;
|
|
let end = (offset + length).min(data.len());
|
|
copy_to_buf(&data[start..end], buf)
|
|
}
|
|
|
|
impl Target for Emu {
|
|
// As an example, I've defined a custom architecture based off
|
|
// `gdbstub_arch::arm::Armv4t`. The implementation is in the `custom_arch`
|
|
// module at the bottom of this file.
|
|
//
|
|
// unless you're working with a particularly funky architecture that uses custom
|
|
// registers, you should probably stick to using the simple `target.xml`
|
|
// implementations from the `gdbstub_arch` repo (i.e: `target.xml` files that
|
|
// only specify the <architecture> and <feature>s of the arch, instead of
|
|
// listing out all the registers out manually).
|
|
type Arch = custom_arch::Armv4tCustom;
|
|
type Error = &'static str;
|
|
|
|
// --------------- IMPORTANT NOTE ---------------
|
|
// Always remember to annotate IDET enable methods with `inline(always)`!
|
|
// Without this annotation, LLVM might fail to dead-code-eliminate nested IDET
|
|
// implementations, resulting in unnecessary binary bloat.
|
|
|
|
#[inline(always)]
|
|
fn base_ops(&mut self) -> target::ext::base::BaseOps<'_, Self::Arch, Self::Error> {
|
|
target::ext::base::BaseOps::SingleThread(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_breakpoints(
|
|
&mut self,
|
|
) -> Option<target::ext::breakpoints::BreakpointsOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_extended_mode(
|
|
&mut self,
|
|
) -> Option<target::ext::extended_mode::ExtendedModeOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_monitor_cmd(&mut self) -> Option<target::ext::monitor_cmd::MonitorCmdOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_section_offsets(
|
|
&mut self,
|
|
) -> Option<target::ext::section_offsets::SectionOffsetsOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_target_description_xml_override(
|
|
&mut self,
|
|
) -> Option<
|
|
target::ext::target_description_xml_override::TargetDescriptionXmlOverrideOps<'_, Self>,
|
|
> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_lldb_register_info_override(
|
|
&mut self,
|
|
) -> Option<target::ext::lldb_register_info_override::LldbRegisterInfoOverrideOps<'_, Self>>
|
|
{
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_memory_map(&mut self) -> Option<target::ext::memory_map::MemoryMapOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_catch_syscalls(
|
|
&mut self,
|
|
) -> Option<target::ext::catch_syscalls::CatchSyscallsOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_host_io(&mut self) -> Option<target::ext::host_io::HostIoOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_exec_file(&mut self) -> Option<target::ext::exec_file::ExecFileOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_auxv(&mut self) -> Option<target::ext::auxv::AuxvOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
}
|
|
|
|
impl SingleThreadBase for Emu {
|
|
fn read_registers(
|
|
&mut self,
|
|
regs: &mut custom_arch::ArmCoreRegsCustom,
|
|
) -> TargetResult<(), Self> {
|
|
let mode = self.cpu.mode();
|
|
|
|
for i in 0..13 {
|
|
regs.core.r[i] = self.cpu.reg_get(mode, i as u8);
|
|
}
|
|
regs.core.sp = self.cpu.reg_get(mode, reg::SP);
|
|
regs.core.lr = self.cpu.reg_get(mode, reg::LR);
|
|
regs.core.pc = self.cpu.reg_get(mode, reg::PC);
|
|
regs.core.cpsr = self.cpu.reg_get(mode, reg::CPSR);
|
|
|
|
regs.custom = self.custom_reg;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_registers(&mut self, regs: &custom_arch::ArmCoreRegsCustom) -> TargetResult<(), Self> {
|
|
let mode = self.cpu.mode();
|
|
|
|
for i in 0..13 {
|
|
self.cpu.reg_set(mode, i, regs.core.r[i as usize]);
|
|
}
|
|
self.cpu.reg_set(mode, reg::SP, regs.core.sp);
|
|
self.cpu.reg_set(mode, reg::LR, regs.core.lr);
|
|
self.cpu.reg_set(mode, reg::PC, regs.core.pc);
|
|
self.cpu.reg_set(mode, reg::CPSR, regs.core.cpsr);
|
|
|
|
self.custom_reg = regs.custom;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_single_register_access(
|
|
&mut self,
|
|
) -> Option<target::ext::base::single_register_access::SingleRegisterAccessOps<'_, (), Self>>
|
|
{
|
|
Some(self)
|
|
}
|
|
|
|
fn read_addrs(&mut self, start_addr: u32, data: &mut [u8]) -> TargetResult<(), Self> {
|
|
for (addr, val) in (start_addr..).zip(data.iter_mut()) {
|
|
*val = self.mem.r8(addr)
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn write_addrs(&mut self, start_addr: u32, data: &[u8]) -> TargetResult<(), Self> {
|
|
for (addr, val) in (start_addr..).zip(data.iter().copied()) {
|
|
self.mem.w8(addr, val)
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_resume(
|
|
&mut self,
|
|
) -> Option<target::ext::base::singlethread::SingleThreadResumeOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
}
|
|
|
|
impl SingleThreadResume for Emu {
|
|
fn resume(&mut self, signal: Option<Signal>) -> Result<(), Self::Error> {
|
|
// Upon returning from the `resume` method, the target being debugged should be
|
|
// configured to run according to whatever resume actions the GDB client has
|
|
// specified (as specified by `set_resume_action`, `resume_range_step`,
|
|
// `reverse_{step, continue}`, etc...)
|
|
//
|
|
// In this basic `armv4t` example, the `resume` method simply sets the exec mode
|
|
// of the emulator's interpreter loop and returns.
|
|
//
|
|
// In more complex implementations, it's likely that the target being debugged
|
|
// will be running in another thread / process, and will require some kind of
|
|
// external "orchestration" to set it's execution mode (e.g: modifying the
|
|
// target's process state via platform specific debugging syscalls).
|
|
|
|
if signal.is_some() {
|
|
return Err("no support for continuing with signal");
|
|
}
|
|
|
|
self.exec_mode = ExecMode::Continue;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_reverse_cont(
|
|
&mut self,
|
|
) -> Option<target::ext::base::reverse_exec::ReverseContOps<'_, (), Self>> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_reverse_step(
|
|
&mut self,
|
|
) -> Option<target::ext::base::reverse_exec::ReverseStepOps<'_, (), Self>> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_single_step(
|
|
&mut self,
|
|
) -> Option<target::ext::base::singlethread::SingleThreadSingleStepOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn support_range_step(
|
|
&mut self,
|
|
) -> Option<target::ext::base::singlethread::SingleThreadRangeSteppingOps<'_, Self>> {
|
|
Some(self)
|
|
}
|
|
}
|
|
|
|
impl target::ext::base::singlethread::SingleThreadSingleStep for Emu {
|
|
fn step(&mut self, signal: Option<Signal>) -> Result<(), Self::Error> {
|
|
if signal.is_some() {
|
|
return Err("no support for stepping with signal");
|
|
}
|
|
|
|
self.exec_mode = ExecMode::Step;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl target::ext::base::single_register_access::SingleRegisterAccess<()> for Emu {
|
|
fn read_register(
|
|
&mut self,
|
|
_tid: (),
|
|
reg_id: custom_arch::ArmCoreRegIdCustom,
|
|
buf: &mut [u8],
|
|
) -> TargetResult<usize, Self> {
|
|
match reg_id {
|
|
custom_arch::ArmCoreRegIdCustom::Core(reg_id) => {
|
|
if let Some(i) = cpu_reg_id(reg_id) {
|
|
let w = self.cpu.reg_get(self.cpu.mode(), i);
|
|
buf.copy_from_slice(&w.to_le_bytes());
|
|
Ok(buf.len())
|
|
} else {
|
|
Err(().into())
|
|
}
|
|
}
|
|
custom_arch::ArmCoreRegIdCustom::Custom => {
|
|
buf.copy_from_slice(&self.custom_reg.to_le_bytes());
|
|
Ok(buf.len())
|
|
}
|
|
custom_arch::ArmCoreRegIdCustom::Time => {
|
|
buf.copy_from_slice(
|
|
&(std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_millis() as u32)
|
|
.to_le_bytes(),
|
|
);
|
|
Ok(buf.len())
|
|
}
|
|
custom_arch::ArmCoreRegIdCustom::Unavailable => Ok(0),
|
|
}
|
|
}
|
|
|
|
fn write_register(
|
|
&mut self,
|
|
_tid: (),
|
|
reg_id: custom_arch::ArmCoreRegIdCustom,
|
|
val: &[u8],
|
|
) -> TargetResult<(), Self> {
|
|
let w = u32::from_le_bytes(
|
|
val.try_into()
|
|
.map_err(|_| TargetError::Fatal("invalid data"))?,
|
|
);
|
|
match reg_id {
|
|
custom_arch::ArmCoreRegIdCustom::Core(reg_id) => {
|
|
if let Some(i) = cpu_reg_id(reg_id) {
|
|
self.cpu.reg_set(self.cpu.mode(), i, w);
|
|
Ok(())
|
|
} else {
|
|
Err(().into())
|
|
}
|
|
}
|
|
custom_arch::ArmCoreRegIdCustom::Custom => {
|
|
self.custom_reg = w;
|
|
Ok(())
|
|
}
|
|
// ignore writes
|
|
custom_arch::ArmCoreRegIdCustom::Unavailable
|
|
| custom_arch::ArmCoreRegIdCustom::Time => Ok(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl target::ext::base::reverse_exec::ReverseCont<()> for Emu {
|
|
fn reverse_cont(&mut self) -> Result<(), Self::Error> {
|
|
// FIXME: actually implement reverse step
|
|
eprintln!(
|
|
"FIXME: Not actually reverse-continuing. Performing forwards continue instead..."
|
|
);
|
|
self.exec_mode = ExecMode::Continue;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl target::ext::base::reverse_exec::ReverseStep<()> for Emu {
|
|
fn reverse_step(&mut self, _tid: ()) -> Result<(), Self::Error> {
|
|
// FIXME: actually implement reverse step
|
|
eprintln!(
|
|
"FIXME: Not actually reverse-stepping. Performing single forwards step instead..."
|
|
);
|
|
self.exec_mode = ExecMode::Step;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl target::ext::base::singlethread::SingleThreadRangeStepping for Emu {
|
|
fn resume_range_step(&mut self, start: u32, end: u32) -> Result<(), Self::Error> {
|
|
self.exec_mode = ExecMode::RangeStep(start, end);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
mod custom_arch {
|
|
use core::num::NonZeroUsize;
|
|
|
|
use gdbstub::arch::lldb::{Encoding, Format, Generic, Register, RegisterInfo};
|
|
use gdbstub::arch::{Arch, RegId, Registers, SingleStepGdbBehavior};
|
|
|
|
use gdbstub_arch::arm::reg::id::ArmCoreRegId;
|
|
use gdbstub_arch::arm::reg::ArmCoreRegs;
|
|
use gdbstub_arch::arm::ArmBreakpointKind;
|
|
|
|
/// Implements `Arch` for ARMv4T
|
|
pub enum Armv4tCustom {}
|
|
|
|
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
|
pub struct ArmCoreRegsCustom {
|
|
pub core: ArmCoreRegs,
|
|
pub custom: u32,
|
|
}
|
|
|
|
impl Registers for ArmCoreRegsCustom {
|
|
type ProgramCounter = u32;
|
|
|
|
fn pc(&self) -> Self::ProgramCounter {
|
|
self.core.pc
|
|
}
|
|
|
|
fn gdb_serialize(&self, mut write_byte: impl FnMut(Option<u8>)) {
|
|
self.core.gdb_serialize(&mut write_byte);
|
|
|
|
macro_rules! write_bytes {
|
|
($bytes:expr) => {
|
|
for b in $bytes {
|
|
write_byte(Some(*b))
|
|
}
|
|
};
|
|
}
|
|
|
|
write_bytes!(&self.custom.to_le_bytes());
|
|
}
|
|
|
|
fn gdb_deserialize(&mut self, bytes: &[u8]) -> Result<(), ()> {
|
|
// ensure bytes.chunks_exact(4) won't panic
|
|
if bytes.len() % 4 != 0 {
|
|
return Err(());
|
|
}
|
|
|
|
use core::convert::TryInto;
|
|
let mut regs = bytes
|
|
.chunks_exact(4)
|
|
.map(|c| u32::from_le_bytes(c.try_into().unwrap()));
|
|
|
|
// copied from ArmCoreRegs
|
|
{
|
|
for reg in self.core.r.iter_mut() {
|
|
*reg = regs.next().ok_or(())?
|
|
}
|
|
self.core.sp = regs.next().ok_or(())?;
|
|
self.core.lr = regs.next().ok_or(())?;
|
|
self.core.pc = regs.next().ok_or(())?;
|
|
|
|
// Floating point registers (unused)
|
|
for _ in 0..25 {
|
|
regs.next().ok_or(())?;
|
|
}
|
|
|
|
self.core.cpsr = regs.next().ok_or(())?;
|
|
}
|
|
|
|
self.custom = regs.next().ok_or(())?;
|
|
|
|
if regs.next().is_some() {
|
|
return Err(());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum ArmCoreRegIdCustom {
|
|
Core(ArmCoreRegId),
|
|
Custom,
|
|
// not sent as part of `struct ArmCoreRegsCustom`, and only accessible via the single
|
|
// register read/write functions
|
|
Time,
|
|
/// This pseudo-register is valid but never available
|
|
Unavailable,
|
|
}
|
|
|
|
impl RegId for ArmCoreRegIdCustom {
|
|
fn from_raw_id(id: usize) -> Option<(Self, Option<NonZeroUsize>)> {
|
|
let reg = match id {
|
|
26 => Self::Custom,
|
|
27 => Self::Time,
|
|
28 => Self::Unavailable,
|
|
_ => {
|
|
let (reg, size) = ArmCoreRegId::from_raw_id(id)?;
|
|
return Some((Self::Core(reg), size));
|
|
}
|
|
};
|
|
Some((reg, Some(NonZeroUsize::new(4)?)))
|
|
}
|
|
}
|
|
|
|
impl Arch for Armv4tCustom {
|
|
type Usize = u32;
|
|
type Registers = ArmCoreRegsCustom;
|
|
type RegId = ArmCoreRegIdCustom;
|
|
type BreakpointKind = ArmBreakpointKind;
|
|
|
|
// for _purely demonstrative purposes_, i'll return dummy data from this
|
|
// function, as it will be overwritten by TargetDescriptionXmlOverride.
|
|
//
|
|
// See `examples/armv4t/gdb/target_description_xml_override.rs`
|
|
//
|
|
// in an actual implementation, you'll want to return an actual string here!
|
|
fn target_description_xml() -> Option<&'static str> {
|
|
Some("never gets returned")
|
|
}
|
|
|
|
// (LLDB extension)
|
|
//
|
|
// for _purely demonstrative purposes_, even though this provides a working
|
|
// example, it will get overwritten by RegisterInfoOverride.
|
|
//
|
|
// See `examples/armv4t/gdb/register_info_override.rs`
|
|
fn lldb_register_info(reg_id: usize) -> Option<RegisterInfo<'static>> {
|
|
match ArmCoreRegIdCustom::from_raw_id(reg_id) {
|
|
Some((_, None)) | None => Some(RegisterInfo::Done),
|
|
Some((r, Some(size))) => {
|
|
let name = match r {
|
|
// For the purpose of demonstration, we end the qRegisterInfo packet
|
|
// exchange when reaching the Time register id, so that this register can
|
|
// only be explicitly queried via the single-register read packet.
|
|
ArmCoreRegIdCustom::Time => return Some(RegisterInfo::Done),
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Gpr(i)) => match i {
|
|
0 => "r0",
|
|
1 => "r1",
|
|
2 => "r2",
|
|
3 => "r3",
|
|
4 => "r4",
|
|
5 => "r5",
|
|
6 => "r6",
|
|
7 => "r7",
|
|
8 => "r8",
|
|
9 => "r9",
|
|
10 => "r10",
|
|
11 => "r11",
|
|
12 => "r12",
|
|
_ => "unknown",
|
|
},
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Sp) => "sp",
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Lr) => "lr",
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Pc) => "pc",
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Fpr(_i)) => "padding",
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Fps) => "padding",
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Cpsr) => "cpsr",
|
|
ArmCoreRegIdCustom::Custom => "custom",
|
|
ArmCoreRegIdCustom::Unavailable => "Unavailable",
|
|
_ => "unknown",
|
|
};
|
|
let encoding = match r {
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Gpr(_i)) => Encoding::Uint,
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Sp)
|
|
| ArmCoreRegIdCustom::Core(ArmCoreRegId::Pc)
|
|
| ArmCoreRegIdCustom::Core(ArmCoreRegId::Cpsr)
|
|
| ArmCoreRegIdCustom::Unavailable
|
|
| ArmCoreRegIdCustom::Custom => Encoding::Uint,
|
|
_ => Encoding::Vector,
|
|
};
|
|
let format = match r {
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Gpr(_i)) => Format::Hex,
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Sp)
|
|
| ArmCoreRegIdCustom::Core(ArmCoreRegId::Pc)
|
|
| ArmCoreRegIdCustom::Core(ArmCoreRegId::Cpsr)
|
|
| ArmCoreRegIdCustom::Unavailable
|
|
| ArmCoreRegIdCustom::Custom => Format::Hex,
|
|
_ => Format::VectorUInt8,
|
|
};
|
|
let set = match r {
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Gpr(_i)) => {
|
|
"General Purpose Registers"
|
|
}
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Sp)
|
|
| ArmCoreRegIdCustom::Core(ArmCoreRegId::Pc)
|
|
| ArmCoreRegIdCustom::Core(ArmCoreRegId::Cpsr)
|
|
| ArmCoreRegIdCustom::Unavailable
|
|
| ArmCoreRegIdCustom::Custom => "General Purpose Registers",
|
|
_ => "Floating Point Registers",
|
|
};
|
|
let generic = match r {
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Sp) => Some(Generic::Sp),
|
|
ArmCoreRegIdCustom::Core(ArmCoreRegId::Pc) => Some(Generic::Pc),
|
|
_ => None,
|
|
};
|
|
let reg = Register {
|
|
name,
|
|
alt_name: None,
|
|
bitsize: (usize::from(size)) * 8,
|
|
offset: reg_id * (usize::from(size)),
|
|
encoding,
|
|
format,
|
|
set,
|
|
gcc: None,
|
|
dwarf: Some(reg_id),
|
|
generic,
|
|
container_regs: None,
|
|
invalidate_regs: None,
|
|
};
|
|
Some(RegisterInfo::Register(reg))
|
|
}
|
|
}
|
|
}
|
|
// armv4t supports optional single stepping.
|
|
//
|
|
// notably, x86 is an example of an arch that does _not_ support
|
|
// optional single stepping.
|
|
#[inline(always)]
|
|
fn single_step_gdb_behavior() -> SingleStepGdbBehavior {
|
|
SingleStepGdbBehavior::Optional
|
|
}
|
|
}
|
|
}
|