Expose precise offset information in wasmtime::FrameInfo (#1495)
* Consolidate trap/frame information This commit removes `TrapRegistry` in favor of consolidating this information in the `FRAME_INFO` we already have in the `wasmtime` crate. This allows us to keep information generally in one place and have one canonical location for "map this PC to some original wasm stuff". The intent for this is to next update with enough information to go from a program counter to a position in the original wasm file. * Expose module offset information in `FrameInfo` This commit implements functionality for `FrameInfo`, the wasm stack trace of a `Trap`, to return the module/function offset. This allows knowing the precise wasm location of each stack frame, instead of only the main trap itself. The intention here is to provide more visibility into the wasm source when something traps, so you know precisely where calls were and where traps were, in order to assist in debugging. Eventually we might use this information for mapping back to native source languages as well (given sufficient debug information). This change makes a previously-optional artifact of compilation always computed on the cranelift side of things. This `ModuleAddressMap` is then propagated to the same store of information other frame information is stored within. This also removes the need for passing a `SourceLoc` with wasm traps or to wasm trap creation, since the backtrace's wasm frames will be able to infer their own `SourceLoc` from the relevant program counters.
This commit is contained in:
@@ -2,10 +2,10 @@ use crate::trampoline::{generate_global_export, generate_memory_export, generate
|
||||
use crate::values::{from_checked_anyfunc, into_checked_anyfunc, Val};
|
||||
use crate::Mutability;
|
||||
use crate::{ExternType, GlobalType, MemoryType, TableType, ValType};
|
||||
use crate::{Func, Store};
|
||||
use crate::{Func, Store, Trap};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use std::slice;
|
||||
use wasmtime_environ::{ir, wasm};
|
||||
use wasmtime_environ::wasm;
|
||||
use wasmtime_runtime::{self as runtime, InstanceHandle};
|
||||
|
||||
// Externals
|
||||
@@ -422,14 +422,8 @@ impl Table {
|
||||
let src_table_index = src_table.wasmtime_table_index();
|
||||
let src_table = src_table.wasmtime_handle.get_defined_table(src_table_index);
|
||||
|
||||
runtime::Table::copy(
|
||||
dst_table,
|
||||
src_table,
|
||||
dst_index,
|
||||
src_index,
|
||||
len,
|
||||
ir::SourceLoc::default(),
|
||||
)?;
|
||||
runtime::Table::copy(dst_table, src_table, dst_index, src_index, len)
|
||||
.map_err(Trap::from_jit)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::cmp;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use wasmtime_environ::entity::EntityRef;
|
||||
use wasmtime_environ::ir;
|
||||
use wasmtime_environ::wasm::FuncIndex;
|
||||
use wasmtime_environ::Module;
|
||||
use wasmtime_environ::{FunctionAddressMap, Module, TrapInformation};
|
||||
use wasmtime_jit::CompiledModule;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -11,7 +13,7 @@ lazy_static::lazy_static! {
|
||||
/// This global cache is used during `Trap` creation to symbolicate frames.
|
||||
/// This is populated on module compilation, and it is cleared out whenever
|
||||
/// all references to a module are dropped.
|
||||
pub static ref FRAME_INFO: GlobalFrameInfo = GlobalFrameInfo::default();
|
||||
pub static ref FRAME_INFO: RwLock<GlobalFrameInfo> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -25,7 +27,7 @@ pub struct GlobalFrameInfo {
|
||||
///
|
||||
/// The key of this map is the highest address in the module and the value
|
||||
/// is the module's information, which also contains the start address.
|
||||
ranges: RwLock<BTreeMap<usize, ModuleFrameInfo>>,
|
||||
ranges: BTreeMap<usize, ModuleFrameInfo>,
|
||||
}
|
||||
|
||||
/// An RAII structure used to unregister a module's frame information when the
|
||||
@@ -38,93 +40,164 @@ pub struct GlobalFrameInfoRegistration {
|
||||
|
||||
struct ModuleFrameInfo {
|
||||
start: usize,
|
||||
functions: BTreeMap<usize, (usize, FuncIndex)>,
|
||||
functions: BTreeMap<usize, FunctionInfo>,
|
||||
module: Arc<Module>,
|
||||
}
|
||||
|
||||
struct FunctionInfo {
|
||||
start: usize,
|
||||
index: FuncIndex,
|
||||
traps: Vec<TrapInformation>,
|
||||
instr_map: FunctionAddressMap,
|
||||
}
|
||||
|
||||
impl GlobalFrameInfo {
|
||||
/// Registers a new compiled module's frame information.
|
||||
///
|
||||
/// This function will register the `names` information for all of the
|
||||
/// compiled functions within `module`. If the `module` has no functions
|
||||
/// then `None` will be returned. Otherwise the returned object, when
|
||||
/// dropped, will be used to unregister all name information from this map.
|
||||
pub fn register(&self, module: &CompiledModule) -> Option<GlobalFrameInfoRegistration> {
|
||||
let mut min = usize::max_value();
|
||||
let mut max = 0;
|
||||
let mut functions = BTreeMap::new();
|
||||
for (i, allocated) in module.finished_functions() {
|
||||
let (start, end) = unsafe {
|
||||
let ptr = (**allocated).as_ptr();
|
||||
let len = (**allocated).len();
|
||||
(ptr as usize, ptr as usize + len)
|
||||
};
|
||||
if start < min {
|
||||
min = start;
|
||||
}
|
||||
if end > max {
|
||||
max = end;
|
||||
}
|
||||
let func_index = module.module().local.func_index(i);
|
||||
assert!(functions.insert(end, (start, func_index)).is_none());
|
||||
}
|
||||
if functions.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut ranges = self.ranges.write().unwrap();
|
||||
// First up assert that our chunk of jit functions doesn't collide with
|
||||
// any other known chunks of jit functions...
|
||||
if let Some((_, prev)) = ranges.range(max..).next() {
|
||||
assert!(prev.start > max);
|
||||
}
|
||||
if let Some((prev_end, _)) = ranges.range(..=min).next_back() {
|
||||
assert!(*prev_end < min);
|
||||
}
|
||||
|
||||
// ... then insert our range and assert nothing was there previously
|
||||
let prev = ranges.insert(
|
||||
max,
|
||||
ModuleFrameInfo {
|
||||
start: min,
|
||||
functions,
|
||||
module: module.module().clone(),
|
||||
},
|
||||
);
|
||||
assert!(prev.is_none());
|
||||
Some(GlobalFrameInfoRegistration { key: max })
|
||||
}
|
||||
|
||||
/// Fetches information about a program counter in a backtrace.
|
||||
/// Fetches frame information about a program counter in a backtrace.
|
||||
///
|
||||
/// Returns an object if this `pc` is known to some previously registered
|
||||
/// module, or returns `None` if no information can be found.
|
||||
pub fn lookup(&self, pc: usize) -> Option<FrameInfo> {
|
||||
let ranges = self.ranges.read().ok()?;
|
||||
let (end, info) = ranges.range(pc..).next()?;
|
||||
pub fn lookup_frame_info(&self, pc: usize) -> Option<FrameInfo> {
|
||||
let (module, func) = self.func(pc)?;
|
||||
|
||||
// Use our relative position from the start of the function to find the
|
||||
// machine instruction that corresponds to `pc`, which then allows us to
|
||||
// map that to a wasm original source location.
|
||||
let rel_pos = pc - func.start;
|
||||
let pos = match func
|
||||
.instr_map
|
||||
.instructions
|
||||
.binary_search_by_key(&rel_pos, |map| map.code_offset)
|
||||
{
|
||||
// Exact hit!
|
||||
Ok(pos) => Some(pos),
|
||||
|
||||
// This *would* be at the first slot in the array, so no
|
||||
// instructions cover `pc`.
|
||||
Err(0) => None,
|
||||
|
||||
// This would be at the `nth` slot, so check `n-1` to see if we're
|
||||
// part of that instruction. This happens due to the minus one when
|
||||
// this function is called form trap symbolication, where we don't
|
||||
// always get called with a `pc` that's an exact instruction
|
||||
// boundary.
|
||||
Err(n) => {
|
||||
let instr = &func.instr_map.instructions[n - 1];
|
||||
if instr.code_offset <= rel_pos && rel_pos < instr.code_offset + instr.code_len {
|
||||
Some(n - 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// In debug mode for now assert that we found a mapping for `pc` within
|
||||
// the function, because otherwise something is buggy along the way and
|
||||
// not accounting for all the instructions. This isn't super critical
|
||||
// though so we can omit this check in release mode.
|
||||
debug_assert!(pos.is_some(), "failed to find instruction for {:x}", pc);
|
||||
|
||||
let instr = match pos {
|
||||
Some(pos) => func.instr_map.instructions[pos].srcloc,
|
||||
None => func.instr_map.start_srcloc,
|
||||
};
|
||||
Some(FrameInfo {
|
||||
module_name: module.module.name.clone(),
|
||||
func_index: func.index.index() as u32,
|
||||
func_name: module.module.func_names.get(&func.index).cloned(),
|
||||
instr,
|
||||
func_start: func.instr_map.start_srcloc,
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetches trap information about a program counter in a backtrace.
|
||||
pub fn lookup_trap_info(&self, pc: usize) -> Option<&TrapInformation> {
|
||||
let (_module, func) = self.func(pc)?;
|
||||
let idx = func
|
||||
.traps
|
||||
.binary_search_by_key(&((pc - func.start) as u32), |info| info.code_offset)
|
||||
.ok()?;
|
||||
Some(&func.traps[idx])
|
||||
}
|
||||
|
||||
fn func(&self, pc: usize) -> Option<(&ModuleFrameInfo, &FunctionInfo)> {
|
||||
let (end, info) = self.ranges.range(pc..).next()?;
|
||||
if pc < info.start || *end < pc {
|
||||
return None;
|
||||
}
|
||||
let (end, (start, func_index)) = info.functions.range(pc..).next()?;
|
||||
if pc < *start || *end < pc {
|
||||
let (end, func) = info.functions.range(pc..).next()?;
|
||||
if pc < func.start || *end < pc {
|
||||
return None;
|
||||
}
|
||||
Some(FrameInfo {
|
||||
module_name: info.module.name.clone(),
|
||||
func_index: func_index.index() as u32,
|
||||
func_name: info.module.func_names.get(func_index).cloned(),
|
||||
})
|
||||
Some((info, func))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GlobalFrameInfoRegistration {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(mut map) = FRAME_INFO.ranges.write() {
|
||||
map.remove(&self.key);
|
||||
if let Ok(mut info) = FRAME_INFO.write() {
|
||||
info.ranges.remove(&self.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a new compiled module's frame information.
|
||||
///
|
||||
/// This function will register the `names` information for all of the
|
||||
/// compiled functions within `module`. If the `module` has no functions
|
||||
/// then `None` will be returned. Otherwise the returned object, when
|
||||
/// dropped, will be used to unregister all name information from this map.
|
||||
pub fn register(module: &CompiledModule) -> Option<GlobalFrameInfoRegistration> {
|
||||
let mut min = usize::max_value();
|
||||
let mut max = 0;
|
||||
let mut functions = BTreeMap::new();
|
||||
for (((i, allocated), traps), instrs) in module
|
||||
.finished_functions()
|
||||
.iter()
|
||||
.zip(module.traps().values())
|
||||
.zip(module.address_transform().values())
|
||||
{
|
||||
let (start, end) = unsafe {
|
||||
let ptr = (**allocated).as_ptr();
|
||||
let len = (**allocated).len();
|
||||
(ptr as usize, ptr as usize + len)
|
||||
};
|
||||
min = cmp::min(min, start);
|
||||
max = cmp::max(max, end);
|
||||
let func = FunctionInfo {
|
||||
start,
|
||||
index: module.module().local.func_index(i),
|
||||
traps: traps.to_vec(),
|
||||
instr_map: (*instrs).clone(),
|
||||
};
|
||||
assert!(functions.insert(end, func).is_none());
|
||||
}
|
||||
if functions.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut info = FRAME_INFO.write().unwrap();
|
||||
// First up assert that our chunk of jit functions doesn't collide with
|
||||
// any other known chunks of jit functions...
|
||||
if let Some((_, prev)) = info.ranges.range(max..).next() {
|
||||
assert!(prev.start > max);
|
||||
}
|
||||
if let Some((prev_end, _)) = info.ranges.range(..=min).next_back() {
|
||||
assert!(*prev_end < min);
|
||||
}
|
||||
|
||||
// ... then insert our range and assert nothing was there previously
|
||||
let prev = info.ranges.insert(
|
||||
max,
|
||||
ModuleFrameInfo {
|
||||
start: min,
|
||||
functions,
|
||||
module: module.module().clone(),
|
||||
},
|
||||
);
|
||||
assert!(prev.is_none());
|
||||
Some(GlobalFrameInfoRegistration { key: max })
|
||||
}
|
||||
|
||||
/// Description of a frame in a backtrace for a [`Trap`].
|
||||
///
|
||||
/// Whenever a WebAssembly trap occurs an instance of [`Trap`] is created. Each
|
||||
@@ -137,6 +210,8 @@ pub struct FrameInfo {
|
||||
module_name: Option<String>,
|
||||
func_index: u32,
|
||||
func_name: Option<String>,
|
||||
func_start: ir::SourceLoc,
|
||||
instr: ir::SourceLoc,
|
||||
}
|
||||
|
||||
impl FrameInfo {
|
||||
@@ -178,4 +253,23 @@ impl FrameInfo {
|
||||
pub fn func_name(&self) -> Option<&str> {
|
||||
self.func_name.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the offset within the original wasm module this frame's program
|
||||
/// counter was at.
|
||||
///
|
||||
/// The offset here is the offset from the beginning of the original wasm
|
||||
/// module to the instruction that this frame points to.
|
||||
pub fn module_offset(&self) -> usize {
|
||||
self.instr.bits() as usize
|
||||
}
|
||||
|
||||
/// Returns the offset from the original wasm module's function to this
|
||||
/// frame's program counter.
|
||||
///
|
||||
/// The offset here is the offset from the beginning of the defining
|
||||
/// function of this frame (within the wasm module) to the instruction this
|
||||
/// frame points to.
|
||||
pub fn func_offset(&self) -> usize {
|
||||
(self.instr.bits() - self.func_start.bits()) as usize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::frame_info::{GlobalFrameInfoRegistration, FRAME_INFO};
|
||||
use crate::frame_info::GlobalFrameInfoRegistration;
|
||||
use crate::runtime::Store;
|
||||
use crate::types::{
|
||||
ExportType, ExternType, FuncType, GlobalType, ImportType, Limits, MemoryType, Mutability,
|
||||
@@ -670,6 +670,6 @@ and for re-adding support for interface types you can see this issue:
|
||||
if info.is_some() {
|
||||
return;
|
||||
}
|
||||
*info = Some(FRAME_INFO.register(&self.inner.compiled));
|
||||
*info = Some(super::frame_info::register(&self.inner.compiled));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ pub(crate) fn create_handle(
|
||||
unsafe {
|
||||
Ok(InstanceHandle::new(
|
||||
Arc::new(module),
|
||||
store.compiler().trap_registry().register_traps(Vec::new()),
|
||||
finished_functions.into_boxed_slice(),
|
||||
trampolines,
|
||||
imports,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::frame_info::FRAME_INFO;
|
||||
use crate::frame_info::{GlobalFrameInfo, FRAME_INFO};
|
||||
use crate::FrameInfo;
|
||||
use backtrace::Backtrace;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use wasmtime_environ::ir::TrapCode;
|
||||
|
||||
/// A struct representing an aborted instruction execution, with a message
|
||||
/// indicating the cause.
|
||||
@@ -29,10 +30,12 @@ impl Trap {
|
||||
/// assert_eq!("unexpected error", trap.message());
|
||||
/// ```
|
||||
pub fn new<I: Into<String>>(message: I) -> Self {
|
||||
Trap::new_with_trace(message.into(), Backtrace::new_unresolved())
|
||||
let info = FRAME_INFO.read().unwrap();
|
||||
Trap::new_with_trace(&info, None, message.into(), Backtrace::new_unresolved())
|
||||
}
|
||||
|
||||
pub(crate) fn from_jit(jit: wasmtime_runtime::Trap) -> Self {
|
||||
let info = FRAME_INFO.read().unwrap();
|
||||
match jit {
|
||||
wasmtime_runtime::Trap::User(error) => {
|
||||
// Since we're the only one using the wasmtime internals (in
|
||||
@@ -47,20 +50,72 @@ impl Trap {
|
||||
.downcast()
|
||||
.expect("only `Trap` user errors are supported")
|
||||
}
|
||||
wasmtime_runtime::Trap::Wasm { desc, backtrace } => {
|
||||
Trap::new_with_trace(desc.to_string(), backtrace)
|
||||
wasmtime_runtime::Trap::Jit { pc, backtrace } => {
|
||||
let code = info
|
||||
.lookup_trap_info(pc)
|
||||
.map(|info| info.trap_code)
|
||||
.unwrap_or(TrapCode::StackOverflow);
|
||||
Trap::new_wasm(&info, Some(pc), code, backtrace)
|
||||
}
|
||||
wasmtime_runtime::Trap::Wasm {
|
||||
trap_code,
|
||||
backtrace,
|
||||
} => Trap::new_wasm(&info, None, trap_code, backtrace),
|
||||
wasmtime_runtime::Trap::OOM { backtrace } => {
|
||||
Trap::new_with_trace("out of memory".to_string(), backtrace)
|
||||
Trap::new_with_trace(&info, None, "out of memory".to_string(), backtrace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_with_trace(message: String, native_trace: Backtrace) -> Self {
|
||||
fn new_wasm(
|
||||
info: &GlobalFrameInfo,
|
||||
trap_pc: Option<usize>,
|
||||
code: TrapCode,
|
||||
backtrace: Backtrace,
|
||||
) -> Self {
|
||||
use wasmtime_environ::ir::TrapCode::*;
|
||||
let desc = match code {
|
||||
StackOverflow => "call stack exhausted",
|
||||
HeapOutOfBounds => "out of bounds memory access",
|
||||
TableOutOfBounds => "undefined element: out of bounds table access",
|
||||
OutOfBounds => "out of bounds",
|
||||
IndirectCallToNull => "uninitialized element",
|
||||
BadSignature => "indirect call type mismatch",
|
||||
IntegerOverflow => "integer overflow",
|
||||
IntegerDivisionByZero => "integer divide by zero",
|
||||
BadConversionToInteger => "invalid conversion to integer",
|
||||
UnreachableCodeReached => "unreachable",
|
||||
Interrupt => "interrupt",
|
||||
User(_) => unreachable!(),
|
||||
};
|
||||
let msg = format!("wasm trap: {}", desc);
|
||||
Trap::new_with_trace(info, trap_pc, msg, backtrace)
|
||||
}
|
||||
|
||||
fn new_with_trace(
|
||||
info: &GlobalFrameInfo,
|
||||
trap_pc: Option<usize>,
|
||||
message: String,
|
||||
native_trace: Backtrace,
|
||||
) -> Self {
|
||||
let mut wasm_trace = Vec::new();
|
||||
for frame in native_trace.frames() {
|
||||
let pc = frame.ip() as usize;
|
||||
if let Some(info) = FRAME_INFO.lookup(pc) {
|
||||
if pc == 0 {
|
||||
continue;
|
||||
}
|
||||
// Note that we need to be careful about the pc we pass in here to
|
||||
// lookup frame information. This program counter is used to
|
||||
// translate back to an original source location in the origin wasm
|
||||
// module. If this pc is the exact pc that the trap happened at,
|
||||
// then we look up that pc precisely. Otherwise backtrace
|
||||
// information typically points at the pc *after* the call
|
||||
// instruction (because otherwise it's likely a call instruction on
|
||||
// the stack). In that case we want to lookup information for the
|
||||
// previous instruction (the call instruction) so we subtract one as
|
||||
// the lookup.
|
||||
let pc_to_lookup = if Some(pc) == trap_pc { pc } else { pc - 1 };
|
||||
if let Some(info) = info.lookup_frame_info(pc_to_lookup) {
|
||||
wasm_trace.push(info);
|
||||
}
|
||||
}
|
||||
@@ -105,7 +160,7 @@ impl fmt::Display for Trap {
|
||||
writeln!(f, "\nwasm backtrace:")?;
|
||||
for (i, frame) in self.trace().iter().enumerate() {
|
||||
let name = frame.module_name().unwrap_or("<unknown>");
|
||||
write!(f, " {}: {}!", i, name)?;
|
||||
write!(f, " {}: {:#6x} - {}!", i, frame.module_offset(), name)?;
|
||||
match frame.func_name() {
|
||||
Some(name) => match rustc_demangle::try_demangle(name) {
|
||||
Ok(name) => write!(f, "{}", name)?,
|
||||
|
||||
Reference in New Issue
Block a user