// Copyright 2015 Ted Mielczarek. See the COPYRIGHT // file at the top-level directory of this distribution. //! Unwind stack frames for a thread. #[cfg(all(doctest, feature = "http"))] doc_comment::doctest!("../README.md"); mod amd64; mod arm; mod arm64; mod arm64_old; mod mips; pub mod symbols; pub mod system_info; mod x86; use minidump::*; use minidump_common::utils::basename; use scroll::ctx::{SizeWith, TryFromCtx}; use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::convert::TryFrom; use std::io::{self, Write}; use tracing::trace; pub use crate::symbols::*; pub use crate::system_info::*; #[derive(Clone, Copy)] struct GetCallerFrameArgs<'a, P> { callee_frame: &'a StackFrame, grand_callee_frame: Option<&'a StackFrame>, stack_memory: UnifiedMemory<'a, 'a>, modules: &'a MinidumpModuleList, system_info: &'a SystemInfo, symbol_provider: &'a P, } impl

GetCallerFrameArgs<'_, P> { fn valid(&self) -> &MinidumpContextValidity { &self.callee_frame.context.valid } } mod impl_prelude { pub(crate) use super::{ CfiStackWalker, FrameTrust, GetCallerFrameArgs, StackFrame, SymbolProvider, }; } /// Indicates how well the instruction pointer derived during /// stack walking is trusted. Since the stack walker can resort to /// stack scanning, it can wind up with dubious frames. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FrameTrust { /// Unknown None, /// Scanned the stack, found this. Scan, /// Found while scanning stack using call frame info. CfiScan, /// Derived from frame pointer. FramePointer, /// Derived from call frame info. CallFrameInfo, /// Explicitly provided by some external stack walker. PreWalked, /// Given as instruction pointer in a context. Context, } impl FrameTrust { /// Return a string describing how a stack frame was found /// by the stackwalker. pub fn description(&self) -> &'static str { match *self { FrameTrust::Context => "given as instruction pointer in context", FrameTrust::PreWalked => "recovered by external stack walker", FrameTrust::CallFrameInfo => "call frame info", FrameTrust::CfiScan => "call frame info with scanning", FrameTrust::FramePointer => "previous frame's frame pointer", FrameTrust::Scan => "stack scanning", FrameTrust::None => "unknown", } } pub fn as_str(&self) -> &'static str { match *self { FrameTrust::Context => "context", FrameTrust::PreWalked => "prewalked", FrameTrust::CallFrameInfo => "cfi", FrameTrust::CfiScan => "cfi_scan", FrameTrust::FramePointer => "frame_pointer", FrameTrust::Scan => "scan", FrameTrust::None => "non", } } } /// The calling convention of a function. #[derive(Debug, Clone)] pub enum CallingConvention { Cdecl, WindowsThisCall, OtherThisCall, } /// Arguments for this function #[derive(Debug, Clone)] pub struct FunctionArgs { /// What we assumed the calling convention was. pub calling_convention: CallingConvention, /// The actual arguments. pub args: Vec, } /// A function argument. #[derive(Debug, Clone)] pub struct FunctionArg { /// The name of the argument (usually actually just the type). pub name: String, /// The value of the argument. pub value: Option, } /// A stack frame for an inlined function. /// /// See [`StackFrame::inlines`][] for more details. #[derive(Debug, Clone)] pub struct InlineFrame { /// The name of the function pub function_name: String, /// The file name of the stack frame pub source_file_name: Option, /// The line number of the stack frame pub source_line: Option, } /// A single stack frame produced from unwinding a thread's stack. #[derive(Debug, Clone)] pub struct StackFrame { /// The program counter location as an absolute virtual address. /// /// - For the innermost called frame in a stack, this will be an exact /// program counter or instruction pointer value. /// /// - For all other frames, this address is within the instruction that /// caused execution to branch to this frame's callee (although it may /// not point to the exact beginning of that instruction). This ensures /// that, when we look up the source code location for this frame, we /// get the source location of the call, not of the point at which /// control will resume when the call returns, which may be on the next /// line. (If the compiler knows the callee never returns, it may even /// place the call instruction at the very end of the caller's machine /// code, such that the "return address" (which will never be used) /// immediately after the call instruction is in an entirely different /// function, perhaps even from a different source file.) /// /// On some architectures, the return address as saved on the stack or in /// a register is fine for looking up the point of the call. On others, it /// requires adjustment. pub instruction: u64, /// The instruction address (program counter) that execution of this function /// would resume at, if the callee returns. /// /// This is exactly **the return address of the of the callee**. We use this /// nonstandard terminology because just calling this "return address" /// would be ambiguous and too easy to mix up. /// /// **Note:** you should strongly prefer using [`StackFrame::instruction`][], which should /// be the address of the instruction before this one which called the callee. /// That is the instruction that this function was logically "executing" when the /// program's state was captured, and therefore what people expect from /// backtraces. /// /// This is more than a matter of user expections: **there are situations /// where this value is nonsensical but the [`StackFrame::instruction`][] is valid.** /// /// Specifically, if the callee is "noreturn" then *this function should /// never resume execution*. The compiler has no obligation to emit any /// instructions after such a CALL, but CALL still implicitly pushes the /// instruction after itself to the stack. Such a return address may /// therefore be outside the "bounds" of this function!!! /// /// Yes, compilers *can* just immediately jump into the callee for /// noreturn calls, but it's genuinely very helpful for them to emit a /// CALL because it keeps the stack reasonable for backtraces and /// debuggers, which are more interested in [`StackFrame::instruction`][] anyway! /// /// (If this is the top frame of the call stack, then `resume_address` /// and `instruction` are exactly equal and should reflect the actual /// program counter of this thread.) pub resume_address: u64, /// The module in which the instruction resides. pub module: Option, /// Any unloaded modules which overlap with this address. /// /// This is currently only populated if `module` is None. /// /// Since unloaded modules may overlap, there may be more than /// one module. Since a module may be unloaded and reloaded at /// multiple positions, we keep track of all the offsets that /// apply. BTrees are used to produce a more stable output. /// /// So this is a `BTreeMap>`. pub unloaded_modules: BTreeMap>, /// The function name, may be omitted if debug symbols are not available. pub function_name: Option, /// The start address of the function, may be omitted if debug symbols /// are not available. pub function_base: Option, /// The size, in bytes, of the arguments pushed on the stack for this function. /// WIN STACK unwinding needs this value to work; it's otherwise uninteresting. pub parameter_size: Option, /// The source file name, may be omitted if debug symbols are not available. pub source_file_name: Option, /// The (1-based) source line number, may be omitted if debug symbols are /// not available. pub source_line: Option, /// The start address of the source line, may be omitted if debug symbols /// are not available. pub source_line_base: Option, /// Any inline frames that cover the frame address, ordered "inside to outside", /// or "deepest callee to shallowest callee". This is the same order that StackFrames /// appear in. /// /// These frames are "fake" in that they don't actually exist at runtime, and are only /// known because the compiler added debuginfo saying they exist. /// /// As a result, many properties of these frames either don't exist or are /// in some sense "inherited" from the parent real frame. For instance they /// have the same instruction/module by definiton. /// /// If you were to print frames you would want to do something like: /// /// ```ignore /// let mut frame_num = 0; /// for frame in &thread.frames { /// // Inlines come first /// for inline in &frame.inlines { /// print_inline(frame_num, frame, inline); /// frame_num += 1; /// } /// print_frame(frame_num, frame); /// frame_num += 1; /// } /// ``` pub inlines: Vec, /// Amount of trust the stack walker has in the instruction pointer /// of this frame. pub trust: FrameTrust, /// The CPU context containing register state for this frame. pub context: MinidumpContext, /// Any function args we recovered. pub arguments: Option, } impl StackFrame { /// Create a `StackFrame` from a `MinidumpContext`. pub fn from_context(context: MinidumpContext, trust: FrameTrust) -> StackFrame { StackFrame { instruction: context.get_instruction_pointer(), // Initialized the same as `instruction`, but left unmodified during stack walking. resume_address: context.get_instruction_pointer(), module: None, unloaded_modules: BTreeMap::new(), function_name: None, function_base: None, parameter_size: None, source_file_name: None, source_line: None, source_line_base: None, inlines: Vec::new(), arguments: None, trust, context, } } } impl FrameSymbolizer for StackFrame { fn get_instruction(&self) -> u64 { self.instruction } fn set_function(&mut self, name: &str, base: u64, parameter_size: u32) { self.function_name = Some(String::from(name)); self.function_base = Some(base); self.parameter_size = Some(parameter_size); } fn set_source_file(&mut self, file: &str, line: u32, base: u64) { self.source_file_name = Some(String::from(file)); self.source_line = Some(line); self.source_line_base = Some(base); } /// This function can be called multiple times, for the inlines that cover the /// address at various levels of inlining. The call order is from outside to /// inside. fn add_inline_frame(&mut self, name: &str, file: Option<&str>, line: Option) { self.inlines.push(InlineFrame { function_name: name.to_string(), source_file_name: file.map(ToString::to_string), source_line: line, }) } } /// Information about the results of unwinding a thread's stack. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CallStackInfo { /// Everything went great. Ok, /// No `MinidumpContext` was provided, couldn't do anything. MissingContext, /// No stack memory was provided, couldn't unwind past the top frame. MissingMemory, /// The CPU type is unsupported. UnsupportedCpu, /// This thread wrote the minidump, it was skipped. DumpThreadSkipped, } /// A stack of `StackFrame`s produced as a result of unwinding a thread. #[derive(Debug, Clone)] pub struct CallStack { /// The stack frames. /// By convention, the stack frame at index 0 is the innermost callee frame, /// and the frame at the highest index in a call stack is the outermost /// caller. pub frames: Vec, /// Information about this `CallStack`. pub info: CallStackInfo, /// The identifier of the thread. pub thread_id: u32, /// The name of the thread, if known. pub thread_name: Option, /// The GetLastError() value stored in the TEB. pub last_error_value: Option, } impl CallStack { /// Construct a CallStack that just has the unsymbolicated context frame. /// /// This is the desired input for the stack walker. pub fn with_context(context: MinidumpContext) -> Self { Self { frames: vec![StackFrame::from_context(context, FrameTrust::Context)], info: CallStackInfo::Ok, thread_id: 0, thread_name: None, last_error_value: None, } } /// Create a `CallStack` with `info` and no frames. pub fn with_info(id: u32, info: CallStackInfo) -> CallStack { CallStack { info, frames: vec![], thread_id: id, thread_name: None, last_error_value: None, } } /// Write a human-readable description of the call stack to `f`. /// /// This is very verbose, it implements the output format used by /// minidump_stackwalk. pub fn print(&self, f: &mut T) -> io::Result<()> { fn print_registers(f: &mut T, ctx: &MinidumpContext) -> io::Result<()> { let registers: Cow> = match ctx.valid { MinidumpContextValidity::All => { let gpr = ctx.general_purpose_registers(); let set: HashSet<&str> = gpr.iter().cloned().collect(); Cow::Owned(set) } MinidumpContextValidity::Some(ref which) => Cow::Borrowed(which), }; // Iterate over registers in a known order. let mut output = String::new(); for reg in ctx.general_purpose_registers() { if registers.contains(reg) { let reg_val = ctx.format_register(reg); let next = format!(" {reg: >6} = {reg_val}"); if output.chars().count() + next.chars().count() > 80 { // Flush the buffer. writeln!(f, " {output}")?; output.truncate(0); } output.push_str(&next); } } if !output.is_empty() { writeln!(f, " {output}")?; } Ok(()) } if self.frames.is_empty() { writeln!(f, "")?; } let mut frame_count = 0; for frame in &self.frames { // First print out inlines for inline in &frame.inlines { // Frame number let frame_idx = frame_count; frame_count += 1; write!(f, "{frame_idx:2} ")?; // Module name if let Some(ref module) = frame.module { write!(f, "{}", basename(&module.code_file()))?; } // Function name write!(f, "!{}", inline.function_name)?; // Source file and line if let (Some(source_file), Some(source_line)) = (&inline.source_file_name, &inline.source_line) { write!(f, " [{} : {}]", basename(source_file), source_line,)?; } writeln!(f)?; // A fake `trust` writeln!(f, " Found by: inlining")?; } // Now print out the "real frame" let frame_idx = frame_count; frame_count += 1; let addr = frame.instruction; // Frame number write!(f, "{frame_idx:2} ")?; if let Some(module) = &frame.module { // Module name write!(f, "{}", basename(&module.code_file()))?; if let (Some(func_name), Some(func_base)) = (&frame.function_name, &frame.function_base) { // Function name write!(f, "!{func_name}")?; if let (Some(src_file), Some(src_line), Some(src_base)) = ( &frame.source_file_name, &frame.source_line, &frame.source_line_base, ) { // Source file, line, and offset write!( f, " [{} : {} + {:#x}]", basename(src_file), src_line, addr - src_base )?; } else { // We didn't have source info, so just give a byte offset from the func write!(f, " + {:#x}", addr - func_base)?; } } else { // We didn't have a function name, so just give a byte offset from the module write!(f, " + {:#x}", addr - module.base_address())?; } } else { // We didn't even find a module, so just print the raw address write!(f, "{addr:#x}")?; // List off overlapping unloaded modules. // First we need to collect them up by name so that we can print // all the overlaps from one module together and dedupe them. // (!!! was that code deleted?) for (name, offsets) in &frame.unloaded_modules { write!(f, " (unloaded {name}@")?; let mut first = true; for offset in offsets { if first { write!(f, "{offset:#x}")?; } else { // `|` is our separator for multiple entries write!(f, "|{offset:#x}")?; } first = false; } write!(f, ")")?; } } // Print the valid registers writeln!(f)?; print_registers(f, &frame.context)?; // And the trust we have of this result writeln!(f, " Found by: {}", frame.trust.description())?; // Now print out recovered args if let Some(args) = &frame.arguments { use MinidumpRawContext::*; let pointer_width = match &frame.context.raw { X86(_) | Ppc(_) | Sparc(_) | Arm(_) | Mips(_) => 4, Ppc64(_) | Amd64(_) | Arm64(_) | OldArm64(_) => 8, }; let cc_summary = match args.calling_convention { CallingConvention::Cdecl => "cdecl [static function]", CallingConvention::WindowsThisCall => "windows thiscall [C++ member function]", CallingConvention::OtherThisCall => { "non-windows thiscall [C++ member function]" } }; writeln!(f, " Arguments (assuming {cc_summary})")?; for (idx, arg) in args.args.iter().enumerate() { if let Some(val) = arg.value { if pointer_width == 4 { writeln!(f, " arg {} ({}) = 0x{:08x}", idx, arg.name, val)?; } else { writeln!(f, " arg {} ({}) = 0x{:016x}", idx, arg.name, val)?; } } else { writeln!(f, " arg {} ({}) = ", idx, arg.name)?; } } // Add an extra new-line between frames when there's function arguments to make // it more readable. writeln!(f)?; } } Ok(()) } } struct CfiStackWalker<'a, C: CpuContext> { instruction: u64, has_grand_callee: bool, grand_callee_parameter_size: u32, callee_ctx: &'a C, callee_validity: &'a MinidumpContextValidity, caller_ctx: C, caller_validity: HashSet<&'static str>, module: &'a MinidumpModule, stack_memory: UnifiedMemory<'a, 'a>, } impl<'a, C> CfiStackWalker<'a, C> where C: CpuContext + Clone, { fn from_ctx_and_args( ctx: &'a C, args: &'a GetCallerFrameArgs<'a, P>, callee_forwarded_regs: R, ) -> Option where R: Fn(&MinidumpContextValidity) -> HashSet<&'static str>, { let module = args .modules .module_at_address(args.callee_frame.instruction)?; let grand_callee = args.grand_callee_frame; Some(Self { instruction: args.callee_frame.instruction, has_grand_callee: grand_callee.is_some(), grand_callee_parameter_size: grand_callee.and_then(|f| f.parameter_size).unwrap_or(0), callee_ctx: ctx, callee_validity: args.valid(), // Default to forwarding all callee-saved regs verbatim. // The CFI evaluator may clear or overwrite these values. // The stack pointer and instruction pointer are not included. caller_ctx: ctx.clone(), caller_validity: callee_forwarded_regs(args.valid()), module, stack_memory: args.stack_memory, }) } } impl<'a, C> FrameWalker for CfiStackWalker<'a, C> where C: CpuContext, C::Register: TryFrom, u64: TryFrom, C::Register: TryFromCtx<'a, Endian, [u8], Error = scroll::Error> + SizeWith, { fn get_instruction(&self) -> u64 { self.instruction } fn has_grand_callee(&self) -> bool { self.has_grand_callee } fn get_grand_callee_parameter_size(&self) -> u32 { self.grand_callee_parameter_size } fn get_register_at_address(&self, address: u64) -> Option { let result: Option = self.stack_memory.get_memory_at_address(address); result.and_then(|val| u64::try_from(val).ok()) } fn get_callee_register(&self, name: &str) -> Option { self.callee_ctx .get_register(name, self.callee_validity) .and_then(|val| u64::try_from(val).ok()) } fn set_caller_register(&mut self, name: &str, val: u64) -> Option<()> { let memoized = self.caller_ctx.memoize_register(name)?; let val = C::Register::try_from(val).ok()?; self.caller_validity.insert(memoized); self.caller_ctx.set_register(name, val) } fn clear_caller_register(&mut self, name: &str) { self.caller_validity.remove(name); } fn set_cfa(&mut self, val: u64) -> Option<()> { // NOTE: some things have alluded to architectures where this isn't // how the CFA should be handled, but we apparently don't support them yet? let stack_pointer_reg = self.caller_ctx.stack_pointer_register_name(); let val = C::Register::try_from(val).ok()?; self.caller_validity.insert(stack_pointer_reg); self.caller_ctx.set_register(stack_pointer_reg, val) } fn set_ra(&mut self, val: u64) -> Option<()> { let instruction_pointer_reg = self.caller_ctx.instruction_pointer_register_name(); let val = C::Register::try_from(val).ok()?; self.caller_validity.insert(instruction_pointer_reg); self.caller_ctx.set_register(instruction_pointer_reg, val) } } #[tracing::instrument(name = "unwind_frame", level = "trace", skip_all, fields(idx = _frame_idx, fname = args.callee_frame.function_name.as_deref().unwrap_or("")))] async fn get_caller_frame

( _frame_idx: usize, args: &GetCallerFrameArgs<'_, P>, ) -> Option where P: SymbolProvider + Sync, { match args.callee_frame.context.raw { /* MinidumpRawContext::PPC(ctx) => ctx.get_caller_frame(stack_memory), MinidumpRawContext::PPC64(ctx) => ctx.get_caller_frame(stack_memory), MinidumpRawContext::SPARC(ctx) => ctx.get_caller_frame(stack_memory), */ MinidumpRawContext::Arm(ref ctx) => arm::get_caller_frame(ctx, args).await, MinidumpRawContext::Arm64(ref ctx) => arm64::get_caller_frame(ctx, args).await, MinidumpRawContext::OldArm64(ref ctx) => arm64_old::get_caller_frame(ctx, args).await, MinidumpRawContext::Amd64(ref ctx) => amd64::get_caller_frame(ctx, args).await, MinidumpRawContext::X86(ref ctx) => x86::get_caller_frame(ctx, args).await, MinidumpRawContext::Mips(ref ctx) => mips::get_caller_frame(ctx, args).await, _ => None, } } async fn fill_source_line_info

( frame: &mut StackFrame, modules: &MinidumpModuleList, symbol_provider: &P, ) where P: SymbolProvider + Sync, { // Find the module whose address range covers this frame's instruction. if let Some(module) = modules.module_at_address(frame.instruction) { // FIXME: this shouldn't need to clone, we should be able to use // the same lifetime as the module list that's passed in. frame.module = Some(module.clone()); // This is best effort, so ignore any errors. let _ = symbol_provider.fill_symbol(module, frame).await; // If we got any inlines, reverse them! The symbol format makes it simplest to // emit inlines from the shallowest callee to the deepest one ("inner to outer"), // but we want inlines to be in the same order as the stackwalk itself, which means // we want the deepest frame first (the callee-est frame). frame.inlines.reverse(); } } /// An optional callback when walking frames. /// /// One may convert from other types to this callback type: /// `FnMut(frame_idx: usize, frame: &StackFrame)` types can be converted to a /// callback, and `()` can be converted to no callback (do nothing). pub enum OnWalkedFrame<'a> { None, #[allow(clippy::type_complexity)] Some(Box), } impl From<()> for OnWalkedFrame<'_> { fn from(_: ()) -> Self { Self::None } } impl<'a, F: FnMut(usize, &StackFrame) + Send + 'a> From for OnWalkedFrame<'a> { fn from(f: F) -> Self { Self::Some(Box::new(f)) } } #[tracing::instrument(name = "unwind_thread", level = "trace", skip_all, fields(idx = _thread_idx, tid = stack.thread_id, tname = stack.thread_name.as_deref().unwrap_or("")))] pub async fn walk_stack

( _thread_idx: usize, on_walked_frame: impl Into>, stack: &mut CallStack, stack_memory: Option>, modules: &MinidumpModuleList, system_info: &SystemInfo, symbol_provider: &P, ) where P: SymbolProvider + Sync, { trace!( "starting stack unwind of thread {} {}", stack.thread_id, stack.thread_name.as_deref().unwrap_or(""), ); // All the unwinder code down below in `get_caller_frame` requires a valid `stack_memory`, // where _valid_ means that we can actually read something from it. A call to `memory_range` will validate that, // as it will reject empty stack memory or one with an overflowing `size`. let stack_memory = stack_memory.and_then(|stack_memory| stack_memory.memory_range().map(|_| stack_memory)); // Begin with the context frame, and keep getting callers until there are no more. let mut has_new_frame = !stack.frames.is_empty(); let mut on_walked_frame = on_walked_frame.into(); while has_new_frame { // Symbolicate the new frame let frame_idx = stack.frames.len() - 1; let frame = stack.frames.last_mut().unwrap(); fill_source_line_info(frame, modules, symbol_provider).await; // Report the frame as walked and symbolicated if let OnWalkedFrame::Some(on_walked_frame) = &mut on_walked_frame { on_walked_frame(frame_idx, frame); } let Some(stack_memory) = stack_memory else { break; }; // Walk the new frame let callee_frame = &stack.frames.last().unwrap(); let grand_callee_frame = stack .frames .len() .checked_sub(2) .and_then(|idx| stack.frames.get(idx)); match callee_frame.function_name.as_ref() { Some(name) => trace!("unwinding {}", name), None => trace!("unwinding 0x{:016x}", callee_frame.instruction), } let new_frame = get_caller_frame( frame_idx, &GetCallerFrameArgs { callee_frame, grand_callee_frame, stack_memory, modules, system_info, symbol_provider, }, ) .await; // Check if we're done if let Some(new_frame) = new_frame { stack.frames.push(new_frame); } else { has_new_frame = false; } } trace!( "finished stack unwind of thread {} {}\n", stack.thread_id, stack.thread_name.as_deref().unwrap_or(""), ); } /// Checks if we can dismiss the validity of an instruction based on our symbols, /// to refine the quality of each unwinder's instruction_seems_valid implementation. async fn instruction_seems_valid_by_symbols

( instruction: u64, modules: &MinidumpModuleList, symbol_provider: &P, ) -> bool where P: SymbolProvider + Sync, { // Our input is a candidate return address, but we *really* want to validate the address // of the call instruction *before* the return address. In theory this symbol-based // analysis shouldn't *care* whether we're looking at the call or the instruction // after it, but there is one corner case where the return address can be invalid // but the instruction before it isn't: noreturn. // // If the *callee* is noreturn, then the caller has no obligation to have any instructions // after the call! So e.g. on x86 if you CALL a noreturn function, the return address // that's implicitly pushed *could* be one-past-the-end of the "function". // // This has been observed in practice with `+[NSThread exit]`! // // We don't otherwise need the instruction pointer to be terribly precise, so // subtracting 1 from the address should be sufficient to handle this corner case. let instruction = instruction.saturating_sub(1); // NULL pointer is definitely not valid if instruction == 0 { return false; } if let Some(module) = modules.module_at_address(instruction) { // Create a dummy frame symbolizing implementation to feed into // our symbol provider with the address we're interested in. If // it tries to set a non-empty function name, then we can reasonably // assume the instruction address is valid. //use crate::FrameSymbolizer; struct DummyFrame { instruction: u64, has_name: bool, } impl FrameSymbolizer for DummyFrame { fn get_instruction(&self) -> u64 { self.instruction } fn set_function(&mut self, name: &str, _base: u64, _parameter_size: u32) { self.has_name = !name.is_empty(); } fn set_source_file(&mut self, _file: &str, _line: u32, _base: u64) { // Do nothing } } let mut frame = DummyFrame { instruction, has_name: false, }; if symbol_provider .fill_symbol(module, &mut frame) .await .is_ok() { frame.has_name } else { // If the symbol provider returns an Error, this means that we // didn't have any symbols for the *module*. Just assume the // instruction is valid in this case so that scanning works // when we have no symbols. true } } else { // We couldn't even map this address to a module. Reject the pointer // so that we have *some* way to distinguish "normal" pointers // from instruction address. // // FIXME: this will reject any pointer into JITed code which otherwise // isn't part of a normal well-defined module. We can potentially use // MemoryInfoListStream (windows) and /proc/self/maps (linux) to refine // this analysis and allow scans to walk through JITed code. false } } #[cfg(test)] mod amd64_unittest; #[cfg(test)] mod arm64_unittest; #[cfg(test)] mod arm_unittest; #[cfg(test)] mod x86_unittest;