use std::array::from_fn;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, OnceLock};
fn unwind_trace_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| std::env::var_os("NTOSEYE_UNWIND_TRACE").is_some())
}
macro_rules! unwind_trace {
($($arg:tt)*) => {
if unwind_trace_enabled() {
eprintln!($($arg)*);
}
};
}
use std::ops::Range;
use pelite::pe64::{
Pe, PeView,
image::{
IMAGE_DIRECTORY_ENTRY_EXCEPTION, IMAGE_SCN_MEM_EXECUTE, RUNTIME_FUNCTION,
UNW_FLAG_CHAININFO, UWOP_ALLOC_LARGE, UWOP_ALLOC_SMALL, UWOP_PUSH_MACHFRAME,
UWOP_PUSH_NONVOL, UWOP_SAVE_NONVOL, UWOP_SAVE_NONVOL_FAR, UWOP_SAVE_XMM128,
UWOP_SAVE_XMM128_FAR, UWOP_SET_FPREG,
},
};
use crate::{
backend::MemoryOps,
bugchecks::looks_like_kernel_pointer,
error::{Error, Result},
gdb::RegisterMap,
guest::{Guest, ModuleInfo, PeImage, WinObject, pe_headers_end, read_pe_image},
kd::{context, context_arm64},
memory::{AddressSpace, DTB_IDENTITY, PAGE_SIZE},
phys::PhysMem,
symbols::{SourceLocation, SymbolStore},
target::{SavedThreadRegisters, Target, ThreadInfo, lookup_register},
trapframe::{decode_kswitch_frame_seed, decode_ktrap_frame_for_thread},
types::{Arch, Dtb, VirtAddr},
};
const STACK_SCAN_BYTES: usize = 0x1000;
const MAX_CHAIN_DEPTH: usize = 32;
const MAX_UNWIND_FRAMES: usize = 1024;
const UWOP_EPILOG: u8 = 6;
const UWOP_SPARE_CODE: u8 = 7;
const UNWIND_REG_NAMES: [&str; 16] = [
"rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi", "r8", "r9", "r10", "r11", "r12", "r13",
"r14", "r15",
];
#[derive(Debug, Clone)]
pub struct ThreadTraceContext {
pub description: String,
pub active_dtb: Dtb,
pub kernel_dtb: Dtb,
pub process_dtb: Option<Dtb>,
pub kernel_modules: Vec<ModuleInfo>,
pub process_modules: Vec<ModuleInfo>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameSource {
Current,
Seed,
Unwind,
Scan,
}
impl FrameSource {
pub fn as_str(self) -> &'static str {
match self {
FrameSource::Current => "current",
FrameSource::Seed => "seed",
FrameSource::Unwind => "unwind",
FrameSource::Scan => "scan",
}
}
}
#[derive(Debug, Clone)]
pub struct StackFrame {
pub sp: u64,
pub ip: u64,
pub symbol: String,
pub source: FrameSource,
pub source_location: Option<SourceLocation>,
}
#[derive(Debug, Clone, Default)]
pub struct StackTrace {
pub frames: Vec<StackFrame>,
pub truncated: usize,
}
#[derive(Debug, Clone)]
pub struct RecoveredFrame {
pub frame: StackFrame,
pub registers: HashMap<String, u64>,
pub frame_base: Option<u64>,
}
#[derive(Debug, Clone, Default)]
pub struct RecoveredStackTrace {
pub frames: Vec<RecoveredFrame>,
pub truncated: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreadStackSource {
TrapFrame { address: VirtAddr },
ContextSwitch { kernel_stack: VirtAddr },
}
impl ThreadStackSource {
pub fn as_str(self) -> &'static str {
match self {
Self::TrapFrame { .. } => "ktrap-frame",
Self::ContextSwitch { .. } => "kernel-stack",
}
}
}
#[derive(Debug, Clone)]
pub struct ThreadStackTrace {
pub source: ThreadStackSource,
pub stacktrace: StackTrace,
}
#[derive(Clone, Debug)]
pub struct ThreadRecoveredStack {
pub source: ThreadStackSource,
pub stacktrace: RecoveredStackTrace,
}
#[derive(Clone, Debug)]
struct RegisterContext {
rip: u64,
rsp: u64,
regs: [Option<u64>; 16],
}
#[derive(Debug, Clone)]
struct CachedModule {
info: ModuleInfo,
image: Arc<PeImage>,
executable_ranges: Vec<(u32, u32)>,
}
#[derive(Debug, Clone)]
struct OwnedModule {
info: ModuleInfo,
dtb: Dtb,
}
#[derive(Debug, Clone, Copy)]
struct UnwindCodeSlot {
code_offset: u8,
unwind_op: u8,
op_info: u8,
raw_op_info: u8,
}
#[derive(Debug, Clone)]
struct ParsedUnwindInfo {
size_of_prolog: u8,
frame_register: u8,
frame_offset: u8,
codes: Vec<UnwindCodeSlot>,
chained_unwind_data: Option<u32>,
}
enum UnwindStep {
Continue,
MachineFrame,
}
enum Unwound {
Stop,
Frame { stack_switch: bool },
}
struct StackTracer<'a> {
trace: &'a ThreadTraceContext,
phys: &'a Arc<PhysMem>,
symbols: &'a SymbolStore,
memory: AddressSpace<'a, PhysMem>,
modules: HashMap<(Dtb, u64), CachedModule>,
stack_pages: RefCell<HashMap<u64, Option<Box<[u8]>>>>,
kernel: Option<&'a WinObject>,
}
pub fn resolve_thread_trace_context(debugger: &Target, cr3: u64) -> ThreadTraceContext {
let dtb_mask = debugger.arch().dtb_page_mask();
let cr3_masked = cr3 & dtb_mask;
let kernel_dtb = debugger.kernel_dtb();
let kernel_dtb_masked = kernel_dtb & dtb_mask;
if kernel_dtb == DTB_IDENTITY || cr3_masked == kernel_dtb_masked {
return ThreadTraceContext {
description: "kernel".to_string(),
active_dtb: kernel_dtb,
kernel_dtb,
process_dtb: None,
kernel_modules: debugger.kernel_modules().unwrap_or_default(),
process_modules: Vec::new(),
};
}
if let Some(proc_info) = debugger.process_for_cr3(cr3_masked) {
let process_modules = debugger
.guest
.as_ref()
.map(|g| g.process_modules(&proc_info).unwrap_or_default())
.unwrap_or_default();
return ThreadTraceContext {
description: format!("{} ({})", proc_info.name, proc_info.pid),
active_dtb: cr3_masked,
kernel_dtb,
process_dtb: Some(proc_info.dtb),
kernel_modules: debugger.kernel_modules().unwrap_or_default(),
process_modules,
};
}
ThreadTraceContext {
description: "unknown".to_string(),
active_dtb: cr3_masked,
kernel_dtb,
process_dtb: None,
kernel_modules: debugger.kernel_modules().unwrap_or_default(),
process_modules: Vec::new(),
}
}
pub fn try_format_symbol(
debugger: &Target,
trace: &ThreadTraceContext,
addr: u64,
) -> Option<String> {
let try_format = |dtb| {
debugger
.symbols
.format_closest_symbol_for_address(dtb, VirtAddr(addr))
};
if let Some(module) = trace.module_for_address(addr) {
return Some(try_format(module.dtb).unwrap_or_else(|| {
let offset = addr.saturating_sub(module.info.base_address.0);
format!("{}+{:#x}", module.info.short_name, offset)
}));
}
if let Some(process_dtb) = trace.process_dtb
&& let Some(symbol) = try_format(process_dtb)
{
return Some(symbol);
}
try_format(trace.kernel_dtb)
}
pub fn format_symbol(debugger: &Target, trace: &ThreadTraceContext, addr: u64) -> String {
try_format_symbol(debugger, trace, addr).unwrap_or_else(|| format!("{addr:#x}"))
}
pub fn preferred_code_dtb(trace: &ThreadTraceContext, addr: u64) -> Dtb {
trace
.module_for_address(addr)
.map(|module| module.dtb)
.unwrap_or(trace.active_dtb)
}
fn frame_source_location(
debugger: &Target,
trace: &ThreadTraceContext,
address: u64,
) -> Option<SourceLocation> {
let module = trace.module_for_address(address)?;
debugger
.symbols
.source_location(module.dtb, VirtAddr(address))
}
fn image_u32(bytes: &[u8], offset: usize) -> Option<u32> {
Some(u32::from_le_bytes(
bytes.get(offset..offset.checked_add(4)?)?.try_into().ok()?,
))
}
fn arm64_function_length(image: &PeImage, unwind_data: u32) -> Option<u32> {
let instructions = match unwind_data & 0b11 {
0 => {
let xdata_rva = (unwind_data & !0b11) as usize;
let header = image_u32(&image.read(xdata_rva, 4)?, 0)?;
header & 0x3ffff
}
1 | 2 => (unwind_data >> 2) & 0x7ff,
_ => return None,
};
(instructions != 0).then(|| instructions * 4)
}
fn lookup_arm64_runtime_function(
image: &PeImage,
pdata: Range<usize>,
rva: u32,
) -> Option<(u32, u32)> {
let entry = |index: usize| image.read(pdata.start + index * 8, 8);
let count = pdata.len() / 8;
let mut low = 0usize;
let mut high = count;
while low < high {
let mid = low + (high - low) / 2;
let begin = image_u32(&entry(mid)?, 0)?;
if begin <= rva {
low = mid + 1;
} else {
high = mid;
}
}
let index = low.checked_sub(1)?;
let found = entry(index)?;
let begin = image_u32(&found, 0)?;
let unwind_data = image_u32(&found, 4)?;
let end = begin.checked_add(arm64_function_length(image, unwind_data)?)?;
(rva >= begin && rva < end).then_some((begin, end))
}
pub fn function_range(
debugger: &Target,
trace: &ThreadTraceContext,
address: u64,
) -> Option<(u64, u64)> {
fn range(image: &PeImage, base: u64, address: u64, arch: Arch) -> Option<(u64, u64)> {
let rva = u32::try_from(address.checked_sub(base)?).ok()?;
let pdata = exception_directory(image)?;
let (begin, end) = match arch {
Arch::Amd64 => {
let Lookup::Found(function) = lookup_runtime_function(
pdata.len() / RUNTIME_FUNCTION_SIZE,
|index| runtime_function_at(image, pdata.start, index),
rva,
) else {
return None;
};
(function.BeginAddress, function.EndAddress)
}
Arch::Arm64 => lookup_arm64_runtime_function(image, pdata, rva)?,
};
Some((base + u64::from(begin), base + u64::from(end)))
}
let mut tracer = StackTracer::new(debugger, trace);
let base = tracer.module_containing(address)?.info.base_address.0;
let arch = debugger.arch();
let image = tracer.module_image(address)?;
if let Some(found) = range(&image, base, address, arch) {
return Some(found);
}
if !image.is_complete() && tracer.upgrade_module_image(address) {
let image = tracer.module_image(address)?;
return range(&image, base, address, arch);
}
None
}
fn function_body_address(
debugger: &Target,
trace: &ThreadTraceContext,
address: u64,
) -> Option<u64> {
let (_, end) = function_range(debugger, trace, address)?;
let mut tracer = StackTracer::new(debugger, trace);
let base = tracer.module_containing(address)?.info.base_address.0;
let mut image = tracer.module_image(address)?;
let mut resolved = resolve_function(&image, base, address);
if matches!(resolved, Resolve::Holed)
&& !image.is_complete()
&& tracer.upgrade_module_image(address)
{
image = tracer.module_image(address)?;
resolved = resolve_function(&image, base, address);
}
let Resolve::Function { unwind_data, begin } = resolved else {
return None;
};
let unwind = parse_unwind_info(&image, unwind_data)?;
let body = base
.checked_add(u64::from(begin))?
.checked_add(u64::from(unwind.size_of_prolog))?;
(body < end).then_some(body)
}
fn recover_context_switch_seed(
debugger: &Target,
process_dtb: Dtb,
kernel_stack: VirtAddr,
) -> Result<RegisterContext> {
let ntoskrnl = &debugger.guest()?.ntoskrnl;
let swap_context = ntoskrnl.symbol("SwapContext")?.address().0;
let ki_swap_context = ntoskrnl.symbol("KiSwapContext")?.address().0;
let trace = resolve_thread_trace_context(debugger, process_dtb);
let body_rip = function_body_address(debugger, &trace, swap_context)
.ok_or_else(|| Error::DebugInfo("SwapContext has no usable PE unwind metadata".into()))?;
let ki_swap_range = function_range(debugger, &trace, ki_swap_context)
.ok_or_else(|| Error::DebugInfo("KiSwapContext has no usable PE unwind metadata".into()))?;
let mut tracer = StackTracer::new(debugger, &trace);
let mut seed = RegisterContext {
rip: body_rip,
rsp: kernel_stack.0,
regs: [None; 16],
};
if !matches!(
tracer.unwind_once(&mut seed),
Unwound::Frame {
stack_switch: false
}
) {
return Err(Error::DebugInfo(
"failed to unwind the saved SwapContext frame".into(),
));
}
if seed.rsp <= kernel_stack.0 || !(ki_swap_range.0..ki_swap_range.1).contains(&seed.rip) {
return Err(Error::DebugInfo(format!(
"SwapContext returned outside KiSwapContext ({:#x}, RSP {:#x})",
seed.rip, seed.rsp
)));
}
let mut caller = seed.clone();
if !matches!(
tracer.unwind_once(&mut caller),
Unwound::Frame {
stack_switch: false
}
) || caller.rsp <= seed.rsp
|| !tracer.is_executable_address(caller.rip)
{
return Err(Error::DebugInfo(
"failed to validate the saved KiSwapContext frame".into(),
));
}
Ok(seed)
}
pub fn build_stacktrace(
debugger: &Target,
register_map: &RegisterMap,
regs: &[u8],
limit: usize,
) -> StackTrace {
let recovered = build_stacktrace_with_context(debugger, register_map, regs, limit);
StackTrace {
frames: recovered
.frames
.into_iter()
.map(|frame| frame.frame)
.collect(),
truncated: recovered.truncated,
}
}
pub fn build_stacktrace_with_context(
debugger: &Target,
register_map: &RegisterMap,
regs: &[u8],
limit: usize,
) -> RecoveredStackTrace {
if debugger.arch() == Arch::Arm64 {
return build_recovered_stacktrace_arm64(debugger, register_map, regs, limit);
}
let cr3 = register_map
.read_u64(debugger.arch().dtb_register(), regs)
.unwrap_or(0);
let trace = resolve_thread_trace_context(debugger, cr3);
build_recovered_stacktrace_seeded(
debugger,
&trace,
RegisterContext::from_registers(register_map, regs),
FrameSource::Current,
limit,
register_map.to_hashmap(regs),
)
}
pub fn build_stacktrace_with_register_values(
debugger: &Target,
register_map: &RegisterMap,
values: &HashMap<String, u64>,
limit: usize,
) -> RecoveredStackTrace {
let register_buffer_size = match debugger.arch() {
Arch::Amd64 => context::REGISTER_BUFFER_SIZE,
Arch::Arm64 => context_arm64::REGISTER_BUFFER_SIZE,
};
let mut bytes = vec![0u8; register_buffer_size];
for (name, value) in values {
let _ = register_map.write_u64(name, &mut bytes, *value);
}
let dtb_name = debugger.arch().dtb_register();
if lookup_register(values, dtb_name)
.filter(|dtb| *dtb != 0)
.is_none()
{
let _ = register_map.write_u64(dtb_name, &mut bytes, debugger.current_dtb());
}
build_stacktrace_with_context(debugger, register_map, &bytes, limit)
}
pub fn frame_base_for_register_values(
debugger: &Target,
values: &HashMap<String, u64>,
) -> Option<u64> {
let lookup = |name: &str| lookup_register(values, name);
let context = RegisterContext {
rip: lookup("rip").or_else(|| lookup("pc")).unwrap_or(0),
rsp: lookup("rsp").or_else(|| lookup("sp")).unwrap_or(0),
regs: from_fn(|index| lookup(UNWIND_REG_NAMES[index])),
};
let dtb = lookup(debugger.arch().dtb_register())
.filter(|dtb| *dtb != 0)
.unwrap_or_else(|| debugger.current_dtb());
let trace = resolve_thread_trace_context(debugger, dtb);
let mut tracer = StackTracer::new(debugger, &trace);
tracer.frame_base_for(&context)
}
pub fn return_address_for_register_values(
debugger: &Target,
values: &HashMap<String, u64>,
) -> Option<u64> {
let lookup = |name: &str| lookup_register(values, name);
let mut context = RegisterContext {
rip: lookup("rip").or_else(|| lookup("pc"))?,
rsp: lookup("rsp").or_else(|| lookup("sp")).unwrap_or(0),
regs: from_fn(|index| lookup(UNWIND_REG_NAMES[index])),
};
let dtb = lookup(debugger.arch().dtb_register())
.filter(|dtb| *dtb != 0)
.unwrap_or_else(|| debugger.current_dtb());
let trace = resolve_thread_trace_context(debugger, dtb);
let mut tracer = StackTracer::new(debugger, &trace);
match tracer.unwind_once(&mut context) {
Unwound::Frame { .. } => (context.rip != 0).then_some(context.rip),
Unwound::Stop => None,
}
}
fn strip_pac(addr: u64) -> u64 {
((addr << 8) as i64 >> 8) as u64
}
fn build_recovered_stacktrace_arm64(
debugger: &Target,
register_map: &RegisterMap,
regs: &[u8],
limit: usize,
) -> RecoveredStackTrace {
let limit = limit.max(1);
let cr3 = register_map
.read_u64(debugger.arch().dtb_register(), regs)
.unwrap_or(0);
let trace = resolve_thread_trace_context(debugger, cr3);
let seed_pc = register_map.read_u64("rip", regs).unwrap_or(0);
let seed_sp = register_map.read_u64("rsp", regs).unwrap_or(0);
let mut seed_context = RegisterContext::from_registers(register_map, regs);
seed_context.rip = seed_pc;
seed_context.rsp = seed_sp;
let mut fp = register_map.read_u64("fp", regs).unwrap_or(0);
let mut raw: Vec<(RegisterContext, FrameSource, u64)> =
vec![(seed_context, FrameSource::Current, fp)];
let memory = debugger.address_space(trace.active_dtb);
for _ in 0..MAX_UNWIND_FRAMES {
if raw.len() >= limit {
break;
}
let mut buf = [0u8; 16];
if memory.read_bytes(VirtAddr(fp), &mut buf).is_err() {
break;
}
let mut next_fp_bytes = [0u8; 8];
next_fp_bytes.copy_from_slice(&buf[..8]);
let next_fp = u64::from_le_bytes(next_fp_bytes);
let mut return_address_bytes = [0u8; 8];
return_address_bytes.copy_from_slice(&buf[8..]);
let ra = strip_pac(u64::from_le_bytes(return_address_bytes));
if ra == 0 || next_fp == 0 || next_fp <= fp {
break;
}
let mut context = RegisterContext::from_registers(register_map, regs);
context.rip = ra;
context.rsp = fp.wrapping_add(16);
context.regs = [None; 16];
raw.push((context, FrameSource::Unwind, next_fp));
fp = next_fp;
}
ensure_frame_module_symbols(
debugger,
&trace,
raw.iter().map(|(context, _, _)| context.rip),
);
let initial_registers = register_map.to_hashmap(regs);
let mut stacktrace = RecoveredStackTrace::default();
for (index, (context, source, fp)) in raw.into_iter().enumerate() {
let mut registers;
if index == 0 {
registers = initial_registers.clone();
} else {
registers = HashMap::new();
if let Some(dtb) = initial_registers.get(debugger.arch().dtb_register()) {
let name = debugger.arch().dtb_register();
registers.insert(name.to_string(), *dtb);
}
registers.insert("fp".to_string(), fp);
registers.insert("sp".to_string(), context.rsp);
registers.insert("pc".to_string(), context.rip);
}
let frame = StackFrame {
sp: context.rsp,
ip: context.rip,
symbol: format_symbol(debugger, &trace, context.rip),
source,
source_location: frame_source_location(debugger, &trace, context.rip),
};
record_recovered_frame(
&mut stacktrace,
limit,
RecoveredFrame {
frame,
registers,
frame_base: (fp != 0).then_some(fp),
},
);
}
stacktrace
}
fn switch_seed_is_plausible(thread: &ThreadInfo, seed: &RegisterContext) -> bool {
let (Some(kernel_stack), Some(stack_limit), Some(stack_base)) =
(thread.kernel_stack, thread.stack_limit, thread.stack_base)
else {
return false;
};
thread.kernel_stack_resident != Some(false)
&& looks_like_kernel_pointer(seed.rip)
&& kernel_stack >= stack_limit
&& kernel_stack < stack_base
&& seed.rsp >= stack_limit.0
&& seed.rsp <= stack_base.0
}
pub fn build_parked_thread_recovered_stack(
debugger: &Target,
thread: &ThreadInfo,
limit: usize,
) -> Result<ThreadRecoveredStack> {
let process_dtb = debugger.thread_process_dtb(thread).ok_or_else(|| {
Error::DebugInfo("parked thread owning process DTB is unavailable".into())
})?;
let trace = resolve_thread_trace_context(debugger, process_dtb);
let mut failures = Vec::new();
if let Some(address) = thread.trap_frame {
match decode_ktrap_frame_for_thread(debugger, process_dtb, address)
.ok()
.and_then(|registers| RegisterContext::from_saved(®isters))
{
Some(seed) if seed.rip != 0 && seed.rsp != 0 => {
return Ok(ThreadRecoveredStack {
source: ThreadStackSource::TrapFrame { address },
stacktrace: build_recovered_stacktrace_seeded(
debugger,
&trace,
seed,
FrameSource::Seed,
limit,
HashMap::from([(debugger.arch().dtb_register().to_string(), process_dtb)]),
),
});
}
_ => failures.push("KTHREAD.TrapFrame is absent or unusable".to_string()),
}
} else {
failures.push("KTHREAD.TrapFrame is not present".to_string());
}
if let Some(kernel_stack) = thread.kernel_stack {
let pdb_seed = decode_kswitch_frame_seed(debugger, process_dtb, kernel_stack)
.ok()
.and_then(|registers| RegisterContext::from_saved(®isters));
let seed = match pdb_seed {
Some(seed) => Ok(seed),
None => recover_context_switch_seed(debugger, process_dtb, kernel_stack),
};
match seed {
Ok(seed) if switch_seed_is_plausible(thread, &seed) => {
return Ok(ThreadRecoveredStack {
source: ThreadStackSource::ContextSwitch { kernel_stack },
stacktrace: build_recovered_stacktrace_seeded(
debugger,
&trace,
seed,
FrameSource::Seed,
limit,
HashMap::from([(debugger.arch().dtb_register().to_string(), process_dtb)]),
),
});
}
Ok(_) => failures.push(
"context-switch seed is outside the captured resident kernel stack".to_string(),
),
Err(error) => failures.push(format!("context-switch seed is unavailable: {error}")),
}
} else {
failures.push("KTHREAD.KernelStack is not present".to_string());
}
Err(Error::DebugInfo(format!(
"parked thread stack unavailable: {}",
failures.join("; ")
)))
}
pub fn build_parked_thread_stack(
debugger: &Target,
thread: &ThreadInfo,
limit: usize,
) -> Result<ThreadStackTrace> {
let recovered = build_parked_thread_recovered_stack(debugger, thread, limit)?;
Ok(ThreadStackTrace {
source: recovered.source,
stacktrace: StackTrace {
frames: recovered
.stacktrace
.frames
.into_iter()
.map(|frame| frame.frame)
.collect(),
truncated: recovered.stacktrace.truncated,
},
})
}
fn build_recovered_stacktrace_seeded(
debugger: &Target,
trace: &ThreadTraceContext,
mut context: RegisterContext,
initial_source: FrameSource,
limit: usize,
initial_registers: HashMap<String, u64>,
) -> RecoveredStackTrace {
let limit = limit.max(1);
let mut raw: Vec<(RegisterContext, FrameSource, Option<u64>)> = Vec::new();
let mut tracer = StackTracer::new(debugger, trace);
let mut seen = HashSet::from([context.rip]);
raw.push((
context.clone(),
initial_source,
tracer.frame_base_for(&context),
));
for _ in 0..MAX_UNWIND_FRAMES {
if raw.len() >= limit {
break;
}
let previous_rip = context.rip;
let previous_rsp = context.rsp;
let stack_switch = match tracer.unwind_once(&mut context) {
Unwound::Stop => break,
Unwound::Frame { stack_switch } => stack_switch,
};
if context.rip == 0 || context.rip == previous_rip {
break;
}
if !stack_switch && context.rsp <= previous_rsp {
break;
}
for index in [0usize, 1, 2, 8, 9, 10, 11] {
context.regs[index] = None;
}
seen.insert(context.rip);
raw.push((
context.clone(),
FrameSource::Unwind,
tracer.frame_base_for(&context),
));
}
let remaining = limit.saturating_sub(raw.len());
for (sp, ip) in tracer.scan_stack(context.rsp, &seen, remaining) {
let scan_context = RegisterContext {
rip: ip,
rsp: sp,
regs: [None; 16],
};
raw.push((
scan_context.clone(),
FrameSource::Scan,
tracer.frame_base_for(&scan_context),
));
}
ensure_frame_module_symbols(
debugger,
trace,
raw.iter().map(|(context, _, _)| context.rip),
);
let mut stacktrace = RecoveredStackTrace::default();
for (index, (context, source, frame_base)) in raw.into_iter().enumerate() {
let mut registers;
if index == 0 {
registers = initial_registers.clone();
} else {
registers = HashMap::new();
if let Some(dtb) = initial_registers.get(debugger.arch().dtb_register()) {
let name = debugger.arch().dtb_register();
registers.insert(name.to_string(), *dtb);
}
}
for (register, name) in UNWIND_REG_NAMES.iter().enumerate() {
if let Some(value) = context.get(register as u8) {
registers.insert((*name).to_string(), value);
}
}
registers.insert("rip".to_string(), context.rip);
registers.insert("rsp".to_string(), context.rsp);
let frame = StackFrame {
sp: context.rsp,
ip: context.rip,
symbol: format_symbol(debugger, trace, context.rip),
source,
source_location: frame_source_location(debugger, trace, context.rip),
};
record_recovered_frame(
&mut stacktrace,
limit,
RecoveredFrame {
frame,
registers,
frame_base,
},
);
}
stacktrace
}
fn ensure_frame_module_symbols(
debugger: &Target,
trace: &ThreadTraceContext,
ips: impl Iterator<Item = u64>,
) {
let mut by_dtb: HashMap<Dtb, Vec<ModuleInfo>> = HashMap::new();
let mut seen: HashSet<(Dtb, u64)> = HashSet::new();
for ip in ips {
let Some(module) = trace.module_for_address(ip) else {
continue;
};
let key = (module.dtb, module.info.base_address.0);
if seen.insert(key)
&& debugger
.symbols
.module_symbol_status(module.dtb, module.info.base_address)
.is_none()
{
by_dtb.entry(module.dtb).or_default().push(module.info);
}
}
for (dtb, modules) in by_dtb {
let _ = if let Some(g) = debugger.guest.as_ref() {
g.load_symbols_for_modules(&debugger.phys, &debugger.symbols, modules, dtb)
} else {
Guest::load_module_symbols(
&debugger.phys,
&debugger.symbols,
modules,
dtb,
false,
debugger.arch(),
)
};
}
}
fn record_recovered_frame(
stacktrace: &mut RecoveredStackTrace,
limit: usize,
frame: RecoveredFrame,
) {
if stacktrace.frames.len() < limit {
stacktrace.frames.push(frame);
} else {
stacktrace.truncated += 1;
}
}
impl RegisterContext {
fn from_registers(register_map: &RegisterMap, regs: &[u8]) -> Self {
let mut register_values = [None; 16];
for (index, name) in UNWIND_REG_NAMES.iter().enumerate() {
register_values[index] = register_map.read_u64(*name, regs).ok();
}
Self {
rip: register_map.read_u64("rip", regs).unwrap_or(0),
rsp: register_map.read_u64("rsp", regs).unwrap_or(0),
regs: register_values,
}
}
fn from_saved(registers: &SavedThreadRegisters) -> Option<Self> {
if let Some(arm64) = registers.arm64.as_ref() {
let rip = arm64.pc?;
let rsp = arm64.sp?;
let mut values = [None; 16];
values[5] = arm64.fp;
return Some(Self {
rip,
rsp,
regs: values,
});
}
let rip = registers.rip?;
let rsp = registers.rsp?;
let mut values = [None; 16];
for (index, name) in UNWIND_REG_NAMES.iter().enumerate() {
values[index] = registers.get(name);
}
Some(Self {
rip,
rsp,
regs: values,
})
}
fn get(&self, register: u8) -> Option<u64> {
match register {
4 => Some(self.rsp),
_ => self.regs.get(register as usize).copied().flatten(),
}
}
fn set(&mut self, register: u8, value: u64) {
if register == 4 {
self.rsp = value;
}
if let Some(slot) = self.regs.get_mut(register as usize) {
*slot = Some(value);
}
}
}
impl ThreadTraceContext {
fn module_for_address(&self, address: u64) -> Option<OwnedModule> {
self.kernel_modules
.iter()
.find(|module| module.contains_address(VirtAddr(address)))
.cloned()
.map(|info| OwnedModule {
info,
dtb: self.kernel_dtb,
})
.or_else(|| {
self.process_modules
.iter()
.find(|module| module.contains_address(VirtAddr(address)))
.cloned()
.map(|info| OwnedModule {
info,
dtb: self.process_dtb.unwrap_or(self.active_dtb),
})
})
}
}
impl<'a> StackTracer<'a> {
fn new(debugger: &'a Target, trace: &'a ThreadTraceContext) -> Self {
Self {
trace,
phys: &debugger.phys,
symbols: &debugger.symbols,
memory: debugger.address_space(trace.active_dtb),
modules: HashMap::new(),
stack_pages: RefCell::new(HashMap::new()),
kernel: debugger.guest.as_ref().map(|guest| &guest.ntoskrnl),
}
}
fn stack_u64(&self, address: u64) -> Result<u64> {
let mut bytes = [0u8; 8];
let mut done = 0usize;
while done < bytes.len() {
let at = address
.checked_add(done as u64)
.ok_or(Error::BadVirtualAddress(VirtAddr(address)))?;
let page = at & !(PAGE_SIZE as u64 - 1);
let offset = (at - page) as usize;
let take = (bytes.len() - done).min(PAGE_SIZE - offset);
let mut pages = self.stack_pages.borrow_mut();
let cached = pages.entry(page).or_insert_with(|| {
let mut buf = vec![0u8; PAGE_SIZE];
self.memory
.read_bytes(VirtAddr(page), &mut buf)
.ok()
.map(|()| buf.into_boxed_slice())
});
let Some(data) = cached else {
return Err(Error::BadVirtualAddress(VirtAddr(at)));
};
bytes[done..done + take].copy_from_slice(&data[offset..offset + take]);
done += take;
}
Ok(u64::from_le_bytes(bytes))
}
fn unwind_once(&mut self, context: &mut RegisterContext) -> Unwound {
unwind_trace!("unwind: rip={:#x} rsp={:#x}", context.rip, context.rsp);
let Some(base_address) = self
.module_containing(context.rip)
.map(|module| module.info.base_address.0)
else {
unwind_trace!("unwind: no module for rip -> leaf");
return self.unwind_leaf(context);
};
let Some(mut image) = self.module_image(context.rip) else {
return Unwound::Stop;
};
let mut resolved = resolve_function(&image, base_address, context.rip);
if matches!(resolved, Resolve::Holed)
&& !image.is_complete()
&& self.upgrade_module_image(context.rip)
{
let Some(upgraded) = self.module_image(context.rip) else {
return Unwound::Stop;
};
image = upgraded;
resolved = resolve_function(&image, base_address, context.rip);
}
let (mut unwind_data, begin) = match resolved {
Resolve::Function { unwind_data, begin } => (unwind_data, begin),
Resolve::Leaf => {
unwind_trace!("unwind: no unwind info for rip -> leaf (true leaf)");
return self.unwind_leaf(context);
}
Resolve::Holed => {
unwind_trace!("unwind: unwind data paged out and unrecoverable -> stop");
return Unwound::Stop;
}
};
let rva = (context.rip - base_address) as u32;
let rip_offset = rva.saturating_sub(begin);
let mut primary = true;
for _ in 0..MAX_CHAIN_DEPTH {
let Some(unwind_info) = parse_unwind_info(&image, unwind_data) else {
unwind_trace!(
"unwind: parse_unwind_info failed/holed at unwind_data={unwind_data:#x} -> stop"
);
return Unwound::Stop;
};
unwind_trace!(
"unwind: rva={:#x} begin={:#x} prolog={:#x} codes={} chained={} in_prolog={}",
rva,
begin,
unwind_info.size_of_prolog,
unwind_info.codes.len(),
unwind_info.chained_unwind_data.is_some(),
primary && rip_offset < unwind_info.size_of_prolog as u32,
);
let in_prolog = primary && rip_offset < unwind_info.size_of_prolog as u32;
match self.apply_unwind_codes(context, &unwind_info, in_prolog, rip_offset) {
Some(UnwindStep::Continue) => {}
Some(UnwindStep::MachineFrame) => {
unwind_trace!(
"unwind: machine frame -> rip={:#x} rsp={:#x}",
context.rip,
context.rsp
);
return Unwound::Frame { stack_switch: true };
}
None => {
unwind_trace!("unwind: malformed unwind codes -> stop");
return Unwound::Stop;
}
}
match unwind_info.chained_unwind_data {
Some(next) => {
unwind_data = next;
primary = false;
}
None => {
let Ok(return_address) = self.stack_u64(context.rsp) else {
unwind_trace!(
"unwind: return-address read failed at rsp={:#x} -> stop",
context.rsp
);
return Unwound::Stop;
};
unwind_trace!(
"unwind: pop return -> rip={return_address:#x} rsp={:#x}",
context.rsp.saturating_add(8)
);
context.rip = return_address;
context.rsp = context.rsp.saturating_add(8);
return Unwound::Frame {
stack_switch: false,
};
}
}
}
Unwound::Stop
}
fn apply_unwind_codes(
&self,
context: &mut RegisterContext,
unwind_info: &ParsedUnwindInfo,
in_prolog: bool,
rip_offset: u32,
) -> Option<UnwindStep> {
let original_context = context.clone();
let mut index = 0usize;
while index < unwind_info.codes.len() {
let slot = unwind_info.codes[index];
let slots_used = unwind_slot_count(slot.unwind_op, slot.op_info);
if slots_used == 0 || index + slots_used > unwind_info.codes.len() {
return None;
}
let executed = !in_prolog || u32::from(slot.code_offset) <= rip_offset;
if executed
&& let UnwindStep::MachineFrame =
self.apply_unwind_code(context, &original_context, unwind_info, index)?
{
return Some(UnwindStep::MachineFrame);
}
index += slots_used;
}
Some(UnwindStep::Continue)
}
fn unwind_leaf(&mut self, context: &mut RegisterContext) -> Unwound {
let Ok(return_address) = self.stack_u64(context.rsp) else {
return Unwound::Stop;
};
if !self.is_executable_address(return_address) {
return Unwound::Stop;
}
context.rip = return_address;
context.rsp = context.rsp.saturating_add(8);
Unwound::Frame {
stack_switch: false,
}
}
fn apply_unwind_code(
&self,
context: &mut RegisterContext,
original_context: &RegisterContext,
unwind_info: &ParsedUnwindInfo,
index: usize,
) -> Option<UnwindStep> {
let slot = unwind_info.codes[index];
match slot.unwind_op {
UWOP_PUSH_NONVOL => {
let saved = self.stack_u64(context.rsp).ok()?;
context.set(slot.op_info, saved);
context.rsp = context.rsp.saturating_add(8);
}
UWOP_ALLOC_SMALL => {
context.rsp = context
.rsp
.saturating_add(((u64::from(slot.op_info) + 1) * 8).max(8));
}
UWOP_ALLOC_LARGE => {
let allocation = if slot.op_info == 0 {
u64::from(slot_u16(&unwind_info.codes, index + 1)?) * 8
} else if slot.op_info == 1 {
u64::from(slot_u16(&unwind_info.codes, index + 1)?)
| (u64::from(slot_u16(&unwind_info.codes, index + 2)?) << 16)
} else {
return None;
};
context.rsp = context.rsp.saturating_add(allocation);
}
UWOP_SET_FPREG => {
context.rsp = frame_base(context, unwind_info)?;
}
UWOP_EPILOG | UWOP_SPARE_CODE => {
}
UWOP_SAVE_NONVOL | UWOP_SAVE_XMM128 => {
let offset = if slot.unwind_op == UWOP_SAVE_NONVOL {
u64::from(slot_u16(&unwind_info.codes, index + 1)?) * 8
} else {
u64::from(slot_u16(&unwind_info.codes, index + 1)?) * 16
};
if slot.unwind_op == UWOP_SAVE_NONVOL {
let base = frame_base(original_context, unwind_info)?;
let saved = self.stack_u64(base + offset).ok()?;
context.set(slot.op_info, saved);
}
}
UWOP_SAVE_NONVOL_FAR | UWOP_SAVE_XMM128_FAR => {
let offset = u64::from(slot_u16(&unwind_info.codes, index + 1)?)
| (u64::from(slot_u16(&unwind_info.codes, index + 2)?) << 16);
let scaled = if slot.unwind_op == UWOP_SAVE_NONVOL_FAR {
offset
} else {
offset * 16
};
if slot.unwind_op == UWOP_SAVE_NONVOL_FAR {
let base = frame_base(original_context, unwind_info)?;
let saved = self.stack_u64(base + scaled).ok()?;
context.set(slot.op_info, saved);
}
}
UWOP_PUSH_MACHFRAME => {
let base = if slot.op_info == 1 {
context.rsp.saturating_add(8)
} else {
context.rsp
};
let return_rip = self.stack_u64(base).ok()?;
let return_rsp = self.stack_u64(base.saturating_add(24)).ok()?;
context.rip = return_rip;
context.rsp = return_rsp;
return Some(UnwindStep::MachineFrame);
}
_ => return None,
}
Some(UnwindStep::Continue)
}
fn frame_base_for(&mut self, context: &RegisterContext) -> Option<u64> {
let fallback = (context.rsp != 0).then_some(context.rsp);
let Some(base_address) = self
.module_containing(context.rip)
.map(|module| module.info.base_address.0)
else {
return fallback;
};
let Some(image) = self.module_image(context.rip) else {
return fallback;
};
let Resolve::Function { unwind_data, .. } =
resolve_function(&image, base_address, context.rip)
else {
return fallback;
};
parse_unwind_info(&image, unwind_data)
.and_then(|info| {
if info.frame_register == 0 {
Some(context.rsp)
} else {
context.get(info.frame_register)
}
})
.or(fallback)
}
fn scan_stack(&mut self, start_rsp: u64, seen: &HashSet<u64>, limit: usize) -> Vec<(u64, u64)> {
let mut frames = Vec::new();
let mut failures = 0usize;
if limit == 0 {
return frames;
}
for slot in 0..(STACK_SCAN_BYTES / 8) {
if frames.len() >= limit {
break;
}
if failures >= 32 {
break;
}
let sp = start_rsp.saturating_add((slot * 8) as u64);
let potential_ip = match self.stack_u64(sp) {
Ok(addr) => {
failures = 0;
addr
}
Err(_) => {
failures += 1;
continue;
}
};
if seen.contains(&potential_ip) || !self.is_executable_address(potential_ip) {
continue;
}
frames.push((sp, potential_ip));
}
frames
}
fn is_executable_address(&mut self, address: u64) -> bool {
let Some(module) = self.module_containing(address) else {
return false;
};
let rva = (address - module.info.base_address.0) as u32;
module
.executable_ranges
.iter()
.any(|(start, end)| rva >= *start && rva < *end)
}
fn module_containing(&mut self, address: u64) -> Option<&CachedModule> {
let module = self.trace.module_for_address(address)?;
self.ensure_module_loaded(&module)?;
self.modules.get(&(module.dtb, module.info.base_address.0))
}
fn module_image(&self, address: u64) -> Option<Arc<PeImage>> {
let module = self.trace.module_for_address(address)?;
self.modules
.get(&(module.dtb, module.info.base_address.0))
.map(|cached| cached.image.clone())
}
fn upgrade_module_image(&mut self, address: u64) -> bool {
let Some(module) = self.trace.module_for_address(address) else {
return false;
};
let key = (module.dtb, module.info.base_address.0);
let disk = {
let Some(cached) = self.modules.get(&key) else {
return false;
};
if cached.image.is_complete() {
return false;
}
self.on_disk_image(&cached.image, &module.info, true)
};
let Some(disk) = disk else {
return false;
};
unwind_trace!(
"unwind: recovered on-disk image for {} (in-memory unwind data paged out)",
module.info.short_name
);
let executable_ranges = executable_ranges(&disk);
self.modules.insert(
key,
CachedModule {
info: module.info.clone(),
image: disk,
executable_ranges,
},
);
true
}
fn ensure_module_loaded(&mut self, module: &OwnedModule) -> Option<()> {
let key = (module.dtb, module.info.base_address.0);
if self.modules.contains_key(&key) {
return Some(());
}
let kernel_image = self
.kernel
.filter(|kernel| {
kernel.base_address == module.info.base_address && kernel.dtb() == module.dtb
})
.and_then(WinObject::image);
let image = match kernel_image {
Some(image) => image,
None => {
let (phys, dtb) = (Arc::clone(self.phys), module.dtb);
match read_pe_image(module.info.base_address, move |address, buf| {
AddressSpace::new(&phys, dtb).read_bytes(address, buf)
}) {
Ok(img) => Arc::new(img),
Err(_) => {
let tds = module.info.time_date_stamp?;
unwind_trace!(
"unwind: in-memory PE unreadable for {}, downloading via timestamp",
module.info.short_name
);
self.symbols
.module_image_on_disk(&module.info.name, tds, module.info.size, true)
.ok()?
}
}
}
};
let image = match self.cached_on_disk_image(&image, &module.info) {
Some(disk) if !image.is_complete() => {
unwind_trace!(
"unwind: using cached on-disk image for {}",
module.info.short_name
);
disk
}
_ => image,
};
let executable_ranges = executable_ranges(&image);
self.modules.insert(
key,
CachedModule {
info: module.info.clone(),
image,
executable_ranges,
},
);
Some(())
}
fn cached_on_disk_image(&self, image: &PeImage, info: &ModuleInfo) -> Option<Arc<PeImage>> {
let disk = self.on_disk_image(image, info, false)?;
let headers_end = pe_headers_end(image.headers())?;
headers_match_relocated(
disk.headers().get(..headers_end)?,
image.headers().get(..headers_end)?,
)
.then_some(disk)
}
fn on_disk_image(
&self,
image: &PeImage,
info: &ModuleInfo,
download: bool,
) -> Option<Arc<PeImage>> {
let view = PeView::from_bytes(image.headers()).ok()?;
self.symbols
.module_image_on_disk(
&info.name,
view.file_header().TimeDateStamp,
view.optional_header().SizeOfImage,
download,
)
.ok()
}
}
fn executable_ranges(image: &PeImage) -> Vec<(u32, u32)> {
let Ok(view) = PeView::from_bytes(image.headers()) else {
return Vec::new();
};
view.section_headers()
.iter()
.filter_map(|section| {
if section.Characteristics & IMAGE_SCN_MEM_EXECUTE == 0 {
return None;
}
let size = section.VirtualSize.max(section.SizeOfRawData);
if size == 0 {
return None;
}
Some((
section.VirtualAddress,
section.VirtualAddress.saturating_add(size),
))
})
.collect()
}
fn headers_match_relocated(disk: &[u8], mapped: &[u8]) -> bool {
if disk.len() != mapped.len() || disk.len() < 0x40 {
return false;
}
let e_lfanew = u32::from_le_bytes(disk[0x3c..0x40].try_into().unwrap()) as usize;
let optional = e_lfanew + 24;
let Some(magic) = disk.get(optional..optional + 2) else {
return false;
};
let image_base = match u16::from_le_bytes([magic[0], magic[1]]) {
0x20b => optional + 24..optional + 32,
0x10b => optional + 28..optional + 32,
_ => return false,
};
if image_base.end > disk.len() {
return false;
}
disk[..image_base.start] == mapped[..image_base.start]
&& disk[image_base.end..] == mapped[image_base.end..]
}
enum Resolve {
Leaf,
Function { unwind_data: u32, begin: u32 },
Holed,
}
fn resolve_function(image: &PeImage, base_address: u64, rip: u64) -> Resolve {
let Some(pdata) = exception_directory(image) else {
return Resolve::Leaf;
};
let rva = (rip - base_address) as u32;
match lookup_runtime_function(
pdata.len() / RUNTIME_FUNCTION_SIZE,
|index| runtime_function_at(image, pdata.start, index),
rva,
) {
Lookup::Found(function) if image.is_present(function.UnwindData as usize, 4) => {
Resolve::Function {
unwind_data: function.UnwindData,
begin: function.BeginAddress,
}
}
Lookup::Found(_) | Lookup::Unreadable => Resolve::Holed,
Lookup::Missing => Resolve::Leaf,
}
}
fn exception_directory(image: &PeImage) -> Option<Range<usize>> {
let view = PeView::from_bytes(image.headers()).ok()?;
let directory = view.data_directory().get(IMAGE_DIRECTORY_ENTRY_EXCEPTION)?;
if directory.Size == 0 {
return None;
}
let start = directory.VirtualAddress as usize;
Some(start..start.checked_add(directory.Size as usize)?)
}
const RUNTIME_FUNCTION_SIZE: usize = 12;
fn runtime_function_at(image: &PeImage, pdata: usize, index: usize) -> Option<RUNTIME_FUNCTION> {
let bytes = image.read(pdata + index * RUNTIME_FUNCTION_SIZE, RUNTIME_FUNCTION_SIZE)?;
Some(RUNTIME_FUNCTION {
BeginAddress: image_u32(&bytes, 0)?,
EndAddress: image_u32(&bytes, 4)?,
UnwindData: image_u32(&bytes, 8)?,
})
}
enum Lookup {
Found(RUNTIME_FUNCTION),
Missing,
Unreadable,
}
fn lookup_runtime_function(
count: usize,
entry: impl Fn(usize) -> Option<RUNTIME_FUNCTION>,
rva: u32,
) -> Lookup {
let mut low = 0usize;
let mut high = count;
while low < high {
let mid = low + (high - low) / 2;
let Some(function) = entry(mid) else {
return Lookup::Unreadable;
};
if rva < function.BeginAddress {
high = mid;
} else if rva >= function.EndAddress {
low = mid + 1;
} else {
return Lookup::Found(function);
}
}
Lookup::Missing
}
fn parse_unwind_info(image: &PeImage, unwind_rva: u32) -> Option<ParsedUnwindInfo> {
let offset = unwind_rva as usize;
let header = image.read(offset, 4)?;
let version_flags = header[0];
let count_of_codes = header[2] as usize;
let frame_register_offset = header[3];
let codes_offset = offset + 4;
let codes_bytes = image.read(codes_offset, count_of_codes.checked_mul(2)?)?;
let aligned_code_count = (count_of_codes + 1) & !1;
let tail_offset = offset + 4 + aligned_code_count * 2;
let chained_unwind_data = if (version_flags >> 3) & UNW_FLAG_CHAININFO != 0 {
let tail = image.read(tail_offset, 12)?;
Some(u32::from_le_bytes([tail[8], tail[9], tail[10], tail[11]]))
} else {
None
};
let mut codes = Vec::with_capacity(count_of_codes);
for raw in codes_bytes.as_chunks::<2>().0 {
codes.push(UnwindCodeSlot {
code_offset: raw[0],
unwind_op: raw[1] & 0x0f,
op_info: raw[1] >> 4,
raw_op_info: raw[1],
});
}
Some(ParsedUnwindInfo {
size_of_prolog: header[1],
frame_register: frame_register_offset & 0x0f,
frame_offset: frame_register_offset >> 4,
codes,
chained_unwind_data,
})
}
fn frame_base(context: &RegisterContext, unwind_info: &ParsedUnwindInfo) -> Option<u64> {
if unwind_info.frame_register == 0 {
return Some(context.rsp);
}
let frame_register = context.get(unwind_info.frame_register)?;
frame_register.checked_sub(u64::from(unwind_info.frame_offset) * 16)
}
fn unwind_slot_count(unwind_op: u8, op_info: u8) -> usize {
match unwind_op {
UWOP_PUSH_NONVOL | UWOP_ALLOC_SMALL | UWOP_SET_FPREG | UWOP_PUSH_MACHFRAME
| UWOP_EPILOG => 1,
UWOP_ALLOC_LARGE => {
if op_info == 0 {
2
} else {
3
}
}
UWOP_SAVE_NONVOL | UWOP_SAVE_XMM128 => 2,
UWOP_SAVE_NONVOL_FAR | UWOP_SAVE_XMM128_FAR | UWOP_SPARE_CODE => 3,
_ => 0,
}
}
fn slot_u16(codes: &[UnwindCodeSlot], index: usize) -> Option<u16> {
let slot = codes.get(index)?;
Some(u16::from_le_bytes([slot.code_offset, slot.raw_op_info]))
}
#[cfg(test)]
mod tests {
use super::{
Lookup, ParsedUnwindInfo, PeImage, RUNTIME_FUNCTION, RegisterContext, frame_base,
lookup_arm64_runtime_function, lookup_runtime_function, parse_unwind_info,
unwind_slot_count,
};
use crate::target::SavedThreadRegisters;
#[test]
fn lookup_runtime_function_resolves_across_a_large_sorted_table() {
let funcs: Vec<RUNTIME_FUNCTION> = (0..64u32)
.map(|i| RUNTIME_FUNCTION {
BeginAddress: i * 0x100,
EndAddress: i * 0x100 + 0x40,
UnwindData: i,
})
.collect();
let lookup = |rva: u32| match lookup_runtime_function(
funcs.len(),
|index| funcs.get(index).copied(),
rva,
) {
Lookup::Found(function) => Some(function.BeginAddress),
Lookup::Missing => None,
Lookup::Unreadable => panic!("table is fully readable"),
};
assert_eq!(lookup(0x0), Some(0x0));
assert_eq!(lookup(0x310), Some(0x300));
assert_eq!(lookup(0x3f00), Some(0x3f00));
assert_eq!(lookup(0x350), None);
assert_eq!(lookup(0x10000), None);
assert!(matches!(
lookup_runtime_function(funcs.len(), |_| None, 0x310),
Lookup::Unreadable
));
}
#[test]
fn lookup_arm64_runtime_function_decodes_packed_and_xdata_lengths() {
let mut image_bytes = vec![0u8; 0x3100];
image_bytes[0x2000..0x2004].copy_from_slice(&(0x80u32 / 4).to_le_bytes());
let mut pdata = Vec::new();
pdata.extend_from_slice(&0x1000u32.to_le_bytes());
pdata.extend_from_slice(&(((0x40u32 / 4) << 2) | 1).to_le_bytes());
pdata.extend_from_slice(&0x1100u32.to_le_bytes());
pdata.extend_from_slice(&0x2000u32.to_le_bytes());
image_bytes[0x3000..0x3000 + pdata.len()].copy_from_slice(&pdata);
let image = PeImage::complete(image_bytes);
let pdata = 0x3000..0x3000 + pdata.len();
assert_eq!(
lookup_arm64_runtime_function(&image, pdata.clone(), 0x103c),
Some((0x1000, 0x1040))
);
assert!(lookup_arm64_runtime_function(&image, pdata.clone(), 0x1040).is_none());
assert_eq!(
lookup_arm64_runtime_function(&image, pdata.clone(), 0x117c),
Some((0x1100, 0x1180))
);
assert!(lookup_arm64_runtime_function(&image, pdata, 0x1180).is_none());
}
#[test]
fn parse_unwind_info_reads_chained_parent() {
let blob = [
0x21, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, ];
let info =
parse_unwind_info(&PeImage::complete(blob.to_vec()), 0).expect("unwind info parses");
assert_eq!(info.chained_unwind_data, Some(0x2000));
}
#[test]
fn parse_unwind_info_without_chain_flag_has_no_parent() {
let blob = [0x01, 0x00, 0x00, 0x00];
let info =
parse_unwind_info(&PeImage::complete(blob.to_vec()), 0).expect("unwind info parses");
assert_eq!(info.chained_unwind_data, None);
}
#[test]
fn slot_count_matches_opcode_encoding() {
assert_eq!(unwind_slot_count(0, 0), 1);
assert_eq!(unwind_slot_count(1, 0), 2);
assert_eq!(unwind_slot_count(1, 1), 3);
assert_eq!(unwind_slot_count(4, 0), 2);
assert_eq!(unwind_slot_count(5, 0), 3);
assert_eq!(unwind_slot_count(6, 0), 1); assert_eq!(unwind_slot_count(7, 0), 3); }
#[test]
fn frame_base_uses_frame_register_when_present() {
let mut regs = [None; 16];
regs[5] = Some(0x2000);
let context = RegisterContext {
rip: 0,
rsp: 0x1800,
regs,
};
let unwind = ParsedUnwindInfo {
size_of_prolog: 0,
frame_register: 5,
frame_offset: 2,
codes: Vec::new(),
chained_unwind_data: None,
};
assert_eq!(frame_base(&context, &unwind), Some(0x1fe0));
}
#[test]
fn saved_register_context_preserves_missing_values() {
let registers = SavedThreadRegisters {
rip: Some(0xffff_f800_1000),
rsp: Some(0xffff_a000_2000),
rbp: Some(0xffff_a000_2100),
..SavedThreadRegisters::default()
};
let context = RegisterContext::from_saved(®isters).unwrap();
assert_eq!(context.rip, 0xffff_f800_1000);
assert_eq!(context.rsp, 0xffff_a000_2000);
assert_eq!(context.get(5), Some(0xffff_a000_2100));
assert_eq!(context.get(3), None);
let missing_rsp = SavedThreadRegisters {
rip: Some(0xffff_f800_1000),
..SavedThreadRegisters::default()
};
assert!(RegisterContext::from_saved(&missing_rsp).is_none());
}
}