/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use anyhow::Context; use futures_executor::{block_on, ThreadPool}; use minidump::{
system_info::Cpu, Minidump, MinidumpException, MinidumpMemoryList, MinidumpMiscInfo,
MinidumpModule, MinidumpModuleList, MinidumpSystemInfo, MinidumpThread, MinidumpThreadList,
MinidumpUnloadedModule, MinidumpUnloadedModuleList, Module, UnifiedMemoryList,
}; use minidump_unwind::{
symbols::debuginfo::DebugInfoSymbolProvider, symbols::SymbolProvider, walk_stack, CallStack,
CallStackInfo, SystemInfo,
}; use std::borrow::Cow; use std::fmt::Debug; use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; use std::sync::Arc;
use serde_json::{json, Value as JsonValue};
#[cfg(windows)] mod windows;
/// Analyze a minidump file to augment a corresponding .extra file with stack trace information. #[derive(Debug)] pubstruct MinidumpAnalyzer<'a> {
all_stacks: bool,
minidump: &'a Path,
extras: Option<&'a Path>,
}
impl<'a> MinidumpAnalyzer<'a> { /// Create a new MinidumpAnalyzer for the given minidump file. pubfn new(minidump: &'a Path) -> Self {
MinidumpAnalyzer {
all_stacks: false,
minidump,
extras: None,
}
}
/// Generate all stacks, rather than just those of the crashing thread. pubfn all_threads(mutself, value: bool) -> Self { self.all_stacks = value; self
}
/// Set the extras file explicitly, rather than deriving from the minidump path. pubfn extras_file(mutself, path: &'a Path) -> Self { self.extras = Some(path); self
}
/// Return the effective extras file to read. pubfn get_extras_file(&self) -> Cow<'a, Path> { self.extras
.map(Cow::Borrowed)
.unwrap_or_else(|| Cow::Owned(extra_path_from_minidump(&self.minidump)))
}
/// Analyze the thread(s) and put stacks in the associated .extra file. pubfn analyze(self) -> anyhow::Result<()> { let extra_file = self.get_extras_file(); let extra_file = extra_file.as_ref();
std::fs::write(extra_file, extra_json.to_string())
.context("while writing modified extra file")?;
Ok(())
}
/// Analyze the thread(s) and alter the given extra JSON data. pubfn analyze_json(self, extra_json: &mut JsonValue) -> anyhow::Result<()> { let minidump = Minidump::read_path(&self.minidump).context("while reading minidump")?;
let proc = processor::Processor::new(&minidump)?;
let (crashing_thread_idx, call_stacks) = proc.get_call_stacks(self.all_stacks)?; let crash_type = proc.crash_reason(); let crash_address = proc.crash_address(); let used_modules = get_used_modules(&call_stacks, proc.main_module());
// Set the `StackTraces` field in the extra JSON data. letmut stack_traces = json!({ "error": call_stack_error(&call_stacks), "crash_type": crash_type, "crash_address": format!("{crash_address:#x}"), "crash_thread": crashing_thread_idx, "main_module": proc.main_module().and_then(|m| module_index(&used_modules, m)), "modules": used_modules.iter().map(|module| { let code_file = module.code_file(); let code_file_path: &std::path::Path = code_file.as_ref().as_ref();
json!({ "base_address": format!("{:#x}", module.base_address()), "end_address": format!("{:#x}", module.base_address() + module.size()), "filename": file_name_str(code_file_path), "code_id": module.code_identifier().as_ref().map(|id| id.as_str()), // `debug_file` may be a file path with additional components; we just want the // final component. See bug 1931237. "debug_file": module.debug_file().as_deref().and_then(|s| file_name_str(Path::new(s))), "debug_id": module.debug_identifier().map(|debug| debug.breakpad().to_string()), "version": module.version().as_deref()
})
}).collect::<Vec<_>>(), "unloaded_modules": proc.unloaded_modules().map(|module| { let code_file = module.code_file(); let code_file_path: &std::path::Path = code_file.as_ref().as_ref();
json!({ "base_address": format!("{:#x}", module.base_address()), "end_address": format!("{:#x}", module.base_address() + module.size()), "filename": code_file_path.file_name().map(|s| s.to_string_lossy()), "code_id": module.code_identifier().as_ref().map(|id| id.as_str()),
})
}).collect::<Vec<_>>(), "threads": call_stacks.iter().map(|call_stack| call_stack_to_json(call_stack, &used_modules)).collect::<Vec<_>>()
});
// StackTraces should not have null values (upstream processing expects the values to be // omitted).
remove_nulls(&mut stack_traces);
extra_json["StackTraces"] = serde_json::to_string(&stack_traces).unwrap().into();
let module_signature_info = proc.module_signature_info(); if !module_signature_info.is_null() { // ModuleSignatureInfo is sent as a crash annotation so must be string. This differs from // StackTraces which isn't actually sent (it's just read and removed by the crash // reporter client).
extra_json["ModuleSignatureInfo"] = serde_json::to_string(&module_signature_info)
.unwrap()
.into();
}
Ok(())
}
}
fn extra_path_from_minidump(minidump: &Path) -> PathBuf { letmut ret = minidump.to_owned();
ret.set_extension("extra");
ret
}
/// Parse the extra file (JSON). fn parse_extra_file(path: &Path) -> anyhow::Result<JsonValue> { letmut extra_file_content = String::new();
File::open(path)
.context("while opening extra file")?
.read_to_string(&mut extra_file_content)
.context("while reading extra file")?;
serde_json::from_str(&extra_file_content).context("while parsing extra file JSON")
}
/// Get a list of modules used in call stacks, ordered by base address. fn get_used_modules<'a>(
call_stacks: impl IntoIterator<Item = &'a CallStack>,
main_module: Option<&'a MinidumpModule>,
) -> Vec<&'a MinidumpModule> { letmut v = call_stacks
.into_iter()
.flat_map(|call_stack| call_stack.frames.iter())
.filter_map(|frame| frame.module.as_ref()) // Always include the main module.
.chain(main_module)
.collect::<Vec<_>>();
v.sort_by_key(|m| m.base_address());
v.dedup_by_key(|m| m.base_address());
v
}
mod processor { usesuper::*;
pubstruct Processor<'a> {
runtime: ThreadPool, // We create a Context abstraction to easily spawn tokio tasks (which must be 'static).
context: Arc<Ctx<'a>>,
}
/// Concurrently execute the given futures, returning a Vec of the results. asyncfn concurrently<'a, I, Fut, R>(runtime: &R, iter: I) -> Vec<Fut::Output> where
R: futures_util::task::Spawn,
I: IntoIterator<Item = Fut>,
Fut: std::future::Future + Send + 'a, // It's possible, though very obtuse, to support `'a` on the Output. We don't need it // though, so we keep it `'static` to simplify things.
Fut::Output: Send + 'static,
{ use futures_util::{
future::{join_all, BoxFuture, FutureExt},
task::SpawnExt,
};
join_all(iter.into_iter().map(|f| { let fut: BoxFuture<'a, Fut::Output> = f.boxed(); // Safety: It is safe to transmute to a static lifetime because we await the output of // the future while the `'a` lifetime is guaranteed to be valid (before exit from this // function). let fut: BoxFuture<'static, Fut::Output> = unsafe { std::mem::transmute(fut) };
runtime.spawn_with_handle(fut).expect("spawn failed")
}))
.await
}
impl<'a> Processor<'a> { pubfn new<T>(minidump: &'a Minidump<T>) -> anyhow::Result<Self> where
T: std::ops::Deref<Target = [u8]>,
{ let system_info = minidump.get_stream::<MinidumpSystemInfo>()?; let misc_info = minidump.get_stream::<MinidumpMiscInfo>().ok(); let module_list = minidump
.get_stream::<MinidumpModuleList>()
.unwrap_or_default(); let unloaded_module_list = minidump
.get_stream::<MinidumpUnloadedModuleList>()
.unwrap_or_default(); let memory_list = minidump
.get_stream::<MinidumpMemoryList>()
.unwrap_or_default(); let thread_list = minidump.get_stream::<MinidumpThreadList>()?; let exception = minidump.get_stream::<MinidumpException>()?;
// TODO Something like SystemInfo::current() to get the active system's info? let processor_system_info = SystemInfo {
os: system_info.os,
os_version: None,
os_build: None,
cpu: system_info.cpu,
cpu_info: None,
cpu_microcode_version: None,
cpu_count: 1,
};
let symbol_provider = BoxedSymbolProvider(match system_info.cpu { // DebugInfoSymbolProvider only supports x86_64 and Arm64 right now
Cpu::X86_64 | Cpu::Arm64 => Box::new(block_on(DebugInfoSymbolProvider::new(
&system_info,
&module_list,
))),
_ => Box::new(breakpad_symbols::Symbolizer::new(
breakpad_symbols::SimpleSymbolSupplier::new(vec![]),
)),
});
/// Get the minidump system info. pubfn system_info(&self) -> &MinidumpSystemInfo {
&self.context.system_info
}
/// Get the crash reason string. pubfn crash_reason(&self) -> String { self.context
.exception
.get_crash_reason(self.system_info().os, self.system_info().cpu)
.to_string()
}
/// Get the crash address. pubfn crash_address(&self) -> u64 { self.context
.exception
.get_crash_address(self.system_info().os, self.system_info().cpu)
}
/// Get call stacks for the given threads. /// /// Call stacks will be concurrently calculated. fn thread_call_stacks<'b>(
&self,
threads: impl IntoIterator<Item = &'b MinidumpThread<'b>>,
) -> anyhow::Result<Vec<CallStack>> {
Ok(block_on(concurrently(
&self.runtime,
threads
.into_iter()
.map(|thread| self.context.thread_call_stack(thread)),
))
.into_iter()
.collect())
}
/// Get the call stacks to process. /// /// The first value in the returned tuple is the crashing thread index. The second value is the /// set of call stacks to process. pubfn get_call_stacks(&self, all_stacks: bool) -> anyhow::Result<(usize, Vec<CallStack>)> { // Derive additional arguments used in stack walking. let crashing_thread = self
.context
.thread_list
.get_thread(self.context.exception.get_crashing_thread_id())
.ok_or(anyhow::anyhow!( "exception thread id missing in thread list"
))?;
Ok(if all_stacks {
( self.context
.thread_list
.threads
.iter()
.position(|t| t.raw.thread_id == crashing_thread.raw.thread_id)
.expect("get_thread() returned a thread that doesn't exist"), self.thread_call_stacks(&self.context.thread_list.threads)?,
)
} else {
(0, self.thread_call_stacks([crashing_thread])?)
})
}
/// Get all modules, ordered by address. #[cfg(windows)] pubfn ordered_modules(&self) -> impl Iterator<Item = &MinidumpModule> { self.context.module_list.by_addr()
}
/// Get all unloaded modules, ordered by address. pubfn unloaded_modules(&self) -> impl Iterator<Item = &MinidumpUnloadedModule> { self.context.unloaded_module_list.by_addr()
}
/// Get the index of the main module. /// /// Returns `None` when no main module exists (only when there are modules). pubfn main_module(&self) -> Option<&MinidumpModule> { self.context.module_list.main_module()
}
/// Get the json representation of module signature information. #[cfg(windows)] pubfn module_signature_info(&self) -> JsonValue { // JSON with structure { <binary_org_name>: [<code_file filename>...], ... } letmut ret = json!({}); for module inself
.ordered_modules()
.map(|m| m as &dyn Module)
.chain(self.unloaded_modules().map(|m| m as &dyn Module))
{ let code_file = module.code_file(); let code_file_path: &std::path::Path = code_file.as_ref().as_ref(); iflet Some(org_name) = windows::binary_org_name(code_file_path) { let entry = &mut ret[org_name];
if entry.is_null() {
*entry = json!([]);
}
entry.as_array_mut().unwrap().push(
code_file_path
.file_name()
.map(|s| s.to_string_lossy())
.into(),
);
} else {
log::warn!("couldn't get binary org name for {code_file}");
}
}
ret
}
/// Get the json representation of module signature information. /// /// This is currently unimplemented and returns null. #[cfg(unix)] pubfn module_signature_info(&self) -> JsonValue {
JsonValue::Null
}
}
impl Ctx<'_> { /// Compute the call stack for a single thread. pubasyncfn thread_call_stack(&self, thread: &MinidumpThread<'_>) -> CallStack { let context = if thread.raw.thread_id == self.exception.get_crashing_thread_id() { self.exception
.context(&self.system_info, self.misc_info.as_ref())
} else {
thread.context(&self.system_info, self.misc_info.as_ref())
}
.map(|c| c.into_owned());
// Always choose the memory region that is referenced by the context, // as the exception context may refer to a different memory region than // the thread context, which in turn would fail to stack walk. iflet Some(stack_ptr) = call_stack
.frames
.first()
.map(|frame| frame.context.get_stack_pointer())
{ let contains_stack_ptr = stack_memory
.as_ref()
.and_then(|memory| memory.get_memory_at_address::<u64>(stack_ptr))
.is_some(); if !contains_stack_ptr {
stack_memory = self
.memory_list
.memory_at_address(stack_ptr)
.or(stack_memory);
}
}
#[async_trait::async_trait] impl SymbolProvider for BoxedSymbolProvider { asyncfn fill_symbol(
&self,
module: &(dyn Module + Sync),
frame: &mut (dyn minidump_unwind::FrameSymbolizer + Send),
) -> Result<(), minidump_unwind::FillSymbolError> { // Initialize the function name to a dummy value to allow stack scanning to work when the // DebugInfoSymbolProvider is used. It will be overwritten by `fill_symbol` if necessary, // however either way we don't care about symbols at all here. This works around // `minidump-unwind`'s behavior of only doing stack scanning if fill_symbol (1) fails or // (2) succeeds and sets a function name. By always setting a function name, we can ensure // that unwinding will always attempt to scan the stack if necessary. // // TODO: Remove this workaround once minidump-unwind is updated to a version which includes // https://github.com/rust-minidump/rust-minidump/pull/1117.
frame.set_function("<unknown>", 0, 0); self.0.fill_symbol(module, frame).await
}
/// Get the index of `needle` in `modules`. fn module_index(modules: &[&MinidumpModule], needle: &MinidumpModule) -> Option<usize> {
modules
.iter()
.position(|o| o.base_address() == needle.base_address())
}
/// Convert a call stack to json (in a form appropriate for the extra json file). fn call_stack_to_json(call_stack: &CallStack, modules: &[&MinidumpModule]) -> JsonValue {
json!({ "frames": call_stack.frames.iter().map(|frame| {
json!({ "ip": format!("{:#x}", frame.instruction), "module_index": frame.module.as_ref().and_then(|m| module_index(modules, m)), "trust": frame.trust.as_str(),
})
}).collect::<Vec<_>>()
})
}
fn call_stack_error(stacks: &[CallStack]) -> Option<String> { for (_i, s) in stacks.iter().enumerate() { match s.info {
CallStackInfo::Ok | CallStackInfo::DumpThreadSkipped => (),
CallStackInfo::UnsupportedCpu => { // If the CPU is unsupported, it ought to be the same error for every thread. return Some("unsupported cpu".into());
} // We ignore these errors as they are permissible wrt the overall status.
CallStackInfo::MissingContext | CallStackInfo::MissingMemory => (),
}
}
None
}
/// Remove all object entries which have null values. fn remove_nulls(value: &mut JsonValue) { match value {
JsonValue::Array(vals) => { for v in vals {
remove_nulls(v);
}
}
JsonValue::Object(kvs) => {
kvs.retain(|_, v| !v.is_null()); for v in kvs.values_mut() {
remove_nulls(v);
}
}
_ => (),
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.