Path: blob/main/crates/unwinder/src/exception_table.rs
3068 views
//! Compact representation of exception handlers associated with1//! callsites, for use when searching a Cranelift stack for a handler.2//!3//! This module implements (i) conversion from the metadata provided4//! alongside Cranelift's compilation result (as provided by5//! [`cranelift_codegen::MachBufferFinalized::call_sites`]) to its6//! format, and (ii) use of its format to find a handler efficiently.7//!8//! The format has been designed so that it can be mapped in from disk9//! and used without post-processing; this enables efficient10//! module-loading in runtimes such as Wasmtime.1112use object::{Bytes, LittleEndian, U32Bytes};1314#[cfg(feature = "cranelift")]15use alloc::vec;16use alloc::vec::Vec;17#[cfg(feature = "cranelift")]18use cranelift_codegen::{19ExceptionContextLoc, FinalizedMachCallSite, FinalizedMachExceptionHandler, binemit::CodeOffset,20};21use wasmtime_environ::prelude::*;2223/// Collector struct for exception handlers per call site.24///25/// # Format26///27/// We keep six different arrays (`Vec`s) that we build as we visit28/// callsites, in ascending offset (address relative to beginning of29/// code segment) order: callsite offsets, frame offsets,30/// tag/destination ranges, tags, tag context SP offset, destination31/// offsets.32///33/// The callsite offsets, frame offsets, and tag/destination ranges34/// logically form a sorted lookup array, allowing us to find35/// information for any single callsite. The frame offset specifies36/// distance down to the SP value at the callsite (in bytes), relative37/// to the FP of that frame. The range denotes a range of indices in38/// the tag/context and destination offset arrays. Ranges are stored39/// with the (exclusive) *end* index only; the start index is implicit40/// as the previous end, or zero if first element.41///42/// The slices of tag, context, and handlers arrays named by `ranges`43/// for each callsite specify a series of handler items for that44/// callsite. The tag and context together allow a45/// dynamic-tag-instance match in the unwinder: the context specifies46/// an offset from SP at the callsite that contains a machine word47/// (e.g. with vmctx) that, together with the static tag index, can be48/// used to perform a dynamic match. A context of `-1` indicates no49/// dynamic context, and a tag of `-1` indicates a catch-all50/// handler. If a handler item matches, control should be transferred51/// to the code offset given in the last array, `handlers`.52///53/// # Example54///55/// An example of this data format:56///57/// ```plain58/// callsites: [0x10, 0x50, 0xf0] // callsites (return addrs) at offsets 0x10, 0x50, 0xf059/// ranges: [2, 4, 5] // corresponding ranges for each callsite60/// frame_offsets: [0, 0x10, 0] // corresponding SP-to-FP offsets for each callsite61/// tags: [1, 5, 1, -1, -1] // tags for each handler at each callsite62/// contexts: [-1, -1, 0x10, 0x20, 0x30] // SP-offset for context for each tag63/// handlers: [0x40, 0x42, 0x6f, 0x71, 0xf5] // handler destinations at each callsite64/// ```65///66/// Expanding this out:67///68/// ```plain69/// callsites: [0x10, 0x50, 0xf0], # PCs relative to some start of return-points.70/// frame_offsets: [0, 0x10, 0], # SP-to-FP offsets at each callsite.71/// ranges: [72/// 2, # callsite 0x10 has tags/handlers indices 0..273/// 4, # callsite 0x50 has tags/handlers indices 2..474/// 5, # callsite 0xf0 has tags/handlers indices 4..575/// ],76/// tags: [77/// # tags for callsite 0x10:78/// 1,79/// 5,80/// # tags for callsite 0x50:81/// 1,82/// -1, # "catch-all"83/// # tags for callsite 0xf0:84/// -1, # "catch-all"85/// ]86/// contexts: [87/// # SP-offsets for context for each tag at callsite 0x10:88/// -1,89/// -1,90/// # for callsite 0x50:91/// 0x10,92/// 0x20,93/// # for callsite 0xf0:94/// 0x30,95/// ]96/// handlers: [97/// # handlers for callsite 0x10:98/// 0x40, # relative PC to handle tag 1 (above)99/// 0x42, # relative PC to handle tag 5100/// # handlers for callsite 0x50:101/// 0x6f, # relative PC to handle tag 1102/// 0x71, # relative PC to handle all other tags103/// # handlers for callsite 0xf0:104/// 0xf5, # relative PC to handle all other tags105/// ]106/// ```107#[cfg(feature = "cranelift")]108#[derive(Clone, Debug, Default)]109pub struct ExceptionTableBuilder {110pub callsites: Vec<U32Bytes<LittleEndian>>,111pub frame_offsets: Vec<U32Bytes<LittleEndian>>,112pub ranges: Vec<U32Bytes<LittleEndian>>,113pub tags: Vec<U32Bytes<LittleEndian>>,114pub contexts: Vec<U32Bytes<LittleEndian>>,115pub handlers: Vec<U32Bytes<LittleEndian>>,116last_start_offset: CodeOffset,117}118119#[cfg(feature = "cranelift")]120impl ExceptionTableBuilder {121/// Add a function at a given offset from the start of the122/// compiled code section, recording information about its call123/// sites.124///125/// Functions must be added in ascending offset order.126pub fn add_func<'a>(127&mut self,128start_offset: CodeOffset,129call_sites: impl Iterator<Item = FinalizedMachCallSite<'a>>,130) -> Result<()> {131// Ensure that we see functions in offset order.132assert!(start_offset >= self.last_start_offset);133self.last_start_offset = start_offset;134135// Visit each callsite in turn, translating offsets from136// function-local to section-local.137let mut handlers = vec![];138for call_site in call_sites {139let ret_addr = call_site.ret_addr.checked_add(start_offset).unwrap();140handlers.extend(call_site.exception_handlers.iter().cloned());141142let start_idx = u32::try_from(self.tags.len()).unwrap();143let mut context = u32::MAX;144for handler in call_site.exception_handlers {145match handler {146FinalizedMachExceptionHandler::Tag(tag, offset) => {147self.tags.push(U32Bytes::new(LittleEndian, tag.as_u32()));148self.contexts.push(U32Bytes::new(LittleEndian, context));149self.handlers.push(U32Bytes::new(150LittleEndian,151offset.checked_add(start_offset).unwrap(),152));153}154FinalizedMachExceptionHandler::Default(offset) => {155self.tags.push(U32Bytes::new(LittleEndian, u32::MAX));156self.contexts.push(U32Bytes::new(LittleEndian, context));157self.handlers.push(U32Bytes::new(158LittleEndian,159offset.checked_add(start_offset).unwrap(),160));161}162FinalizedMachExceptionHandler::Context(ExceptionContextLoc::SPOffset(163offset,164)) => {165context = *offset;166}167FinalizedMachExceptionHandler::Context(ExceptionContextLoc::GPR(_)) => {168panic!(169"Wasmtime exception unwind info only supports dynamic contexts on the stack"170);171}172}173}174let end_idx = u32::try_from(self.tags.len()).unwrap();175176// Omit empty callsites for compactness.177if end_idx > start_idx {178self.ranges.push(U32Bytes::new(LittleEndian, end_idx));179self.frame_offsets.push(U32Bytes::new(180LittleEndian,181call_site.frame_offset.unwrap_or(u32::MAX),182));183self.callsites.push(U32Bytes::new(LittleEndian, ret_addr));184}185}186187Ok(())188}189190/// Serialize the exception-handler data section, taking a closure191/// to consume slices.192pub fn serialize<F: FnMut(&[u8])>(&self, mut f: F) {193// Serialize the length of `callsites` / `ranges`.194let callsite_count = u32::try_from(self.callsites.len()).unwrap();195f(&callsite_count.to_le_bytes());196// Serialize the length of `tags` / `handlers`.197let handler_count = u32::try_from(self.handlers.len()).unwrap();198f(&handler_count.to_le_bytes());199200// Serialize `callsites`, `ranges`, `tags`, and `handlers` in201// that order.202f(object::bytes_of_slice(&self.callsites));203f(object::bytes_of_slice(&self.frame_offsets));204f(object::bytes_of_slice(&self.ranges));205f(object::bytes_of_slice(&self.tags));206f(object::bytes_of_slice(&self.contexts));207f(object::bytes_of_slice(&self.handlers));208}209210/// Serialize the exception-handler data section to a vector of211/// bytes.212pub fn to_vec(&self) -> Vec<u8> {213let mut bytes = vec![];214self.serialize(|slice| bytes.extend(slice.iter().cloned()));215bytes216}217}218219/// ExceptionTable deserialized from a serialized slice.220///221/// This struct retains borrows of the various serialized parts of the222/// exception table data as produced by223/// [`ExceptionTableBuilder::serialize`].224#[derive(Clone, Debug)]225pub struct ExceptionTable<'a> {226callsites: &'a [U32Bytes<LittleEndian>],227ranges: &'a [U32Bytes<LittleEndian>],228frame_offsets: &'a [U32Bytes<LittleEndian>],229tags: &'a [U32Bytes<LittleEndian>],230contexts: &'a [U32Bytes<LittleEndian>],231handlers: &'a [U32Bytes<LittleEndian>],232}233234/// Wasmtime exception table item, after parsing.235///236/// Note that this is separately defined from the equivalent type in237/// Cranelift, `cranelift_codegen::FinalizedMachExceptionHandler`,238/// because we need this in runtime-only builds when Cranelift is not239/// included.240#[derive(Clone, Debug, PartialEq, Eq)]241pub struct ExceptionHandler {242/// A tag (arbitrary `u32` identifier from CLIF) or `None` for catch-all.243pub tag: Option<u32>,244/// Dynamic context, if provided, with which to interpret the245/// tag. Context is available at the given offset from SP in this246/// frame.247pub context_sp_offset: Option<u32>,248/// Handler code offset.249pub handler_offset: u32,250}251252impl<'a> ExceptionTable<'a> {253/// Parse exception tables from a byte-slice as produced by254/// [`ExceptionTableBuilder::serialize`].255pub fn parse(data: &'a [u8]) -> Result<ExceptionTable<'a>> {256let mut data = Bytes(data);257let callsite_count = data258.read::<U32Bytes<LittleEndian>>()259.map_err(|_| format_err!("Unable to read callsite count prefix"))?;260let callsite_count = usize::try_from(callsite_count.get(LittleEndian))?;261let handler_count = data262.read::<U32Bytes<LittleEndian>>()263.map_err(|_| format_err!("Unable to read handler count prefix"))?;264let handler_count = usize::try_from(handler_count.get(LittleEndian))?;265let (callsites, data) =266object::slice_from_bytes::<U32Bytes<LittleEndian>>(data.0, callsite_count)267.map_err(|_| format_err!("Unable to read callsites slice"))?;268let (frame_offsets, data) =269object::slice_from_bytes::<U32Bytes<LittleEndian>>(data, callsite_count)270.map_err(|_| format_err!("Unable to read frame_offsets slice"))?;271let (ranges, data) =272object::slice_from_bytes::<U32Bytes<LittleEndian>>(data, callsite_count)273.map_err(|_| format_err!("Unable to read ranges slice"))?;274let (tags, data) = object::slice_from_bytes::<U32Bytes<LittleEndian>>(data, handler_count)275.map_err(|_| format_err!("Unable to read tags slice"))?;276let (contexts, data) =277object::slice_from_bytes::<U32Bytes<LittleEndian>>(data, handler_count)278.map_err(|_| format_err!("Unable to read contexts slice"))?;279let (handlers, data) =280object::slice_from_bytes::<U32Bytes<LittleEndian>>(data, handler_count)281.map_err(|_| format_err!("Unable to read handlers slice"))?;282283if !data.is_empty() {284bail!("Unexpected data at end of serialized exception table");285}286287Ok(ExceptionTable {288callsites,289frame_offsets,290ranges,291tags,292contexts,293handlers,294})295}296297/// Look up the set of handlers, if any, for a given return298/// address (as an offset into the code section).299///300/// The handler for `None` (the catch-all/default handler), if301/// any, will always come last.302///303/// Note: we use raw `u32` types for code offsets here to avoid304/// dependencies on `cranelift-codegen` when this crate is built305/// without compiler backend support (runtime-only config).306///307/// Returns a tuple of `(frame offset, handler iterator)`. The308/// frame offset, if `Some`, specifies the distance from SP to FP309/// at this callsite.310pub fn lookup_pc(&self, pc: u32) -> (Option<u32>, impl Iterator<Item = ExceptionHandler> + '_) {311let callsite_idx = self312.callsites313.binary_search_by_key(&pc, |callsite| callsite.get(LittleEndian))314.ok();315let frame_offset = callsite_idx316.map(|idx| self.frame_offsets[idx])317.and_then(|offset| option_from_u32(offset.get(LittleEndian)));318319(320frame_offset,321callsite_idx322.into_iter()323.flat_map(|callsite_idx| self.handlers_for_callsite(callsite_idx)),324)325}326327/// Look up the frame offset and handler destination if any, for a328/// given return address (as an offset into the code section) and329/// exception tag.330///331/// Note: we use raw `u32` types for code offsets and tags here to332/// avoid dependencies on `cranelift-codegen` when this crate is333/// built without compiler backend support (runtime-only config).334pub fn lookup_pc_tag(&self, pc: u32, tag: u32) -> Option<(u32, u32)> {335// First, look up the callsite in the sorted callsites list.336let callsite_idx = self337.callsites338.binary_search_by_key(&pc, |callsite| callsite.get(LittleEndian))339.ok()?;340let frame_offset =341option_from_u32(self.frame_offsets[callsite_idx].get(LittleEndian)).unwrap_or(0);342343let (tags, _, handlers) = self.tags_contexts_handlers_for_callsite(callsite_idx);344345// Is there any handler with an exact tag match?346if let Ok(handler_idx) = tags.binary_search_by_key(&tag, |tag| tag.get(LittleEndian)) {347return Some((frame_offset, handlers[handler_idx].get(LittleEndian)));348}349350// If not, is there a fallback handler? Note that we serialize351// it with the tag `u32::MAX`, so it is always last in sorted352// order.353if tags.last().map(|v| v.get(LittleEndian)) == Some(u32::MAX) {354return Some((frame_offset, handlers.last().unwrap().get(LittleEndian)));355}356357None358}359360fn tags_contexts_handlers_for_callsite(361&self,362idx: usize,363) -> (364&[U32Bytes<LittleEndian>],365&[U32Bytes<LittleEndian>],366&[U32Bytes<LittleEndian>],367) {368let end_idx = self.ranges[idx].get(LittleEndian);369let start_idx = if idx > 0 {370self.ranges[idx - 1].get(LittleEndian)371} else {3720373};374375// Take the subslices of `tags`, `contexts`, and `handlers`376// corresponding to this callsite.377let start_idx = usize::try_from(start_idx).unwrap();378let end_idx = usize::try_from(end_idx).unwrap();379let tags = &self.tags[start_idx..end_idx];380let contexts = &self.contexts[start_idx..end_idx];381let handlers = &self.handlers[start_idx..end_idx];382(tags, contexts, handlers)383}384385fn handlers_for_callsite(&self, idx: usize) -> impl Iterator<Item = ExceptionHandler> {386let (tags, contexts, handlers) = self.tags_contexts_handlers_for_callsite(idx);387tags.iter()388.zip(contexts.iter())389.zip(handlers.iter())390.map(|((tag, context), handler)| {391let tag = option_from_u32(tag.get(LittleEndian));392let context = option_from_u32(context.get(LittleEndian));393let handler = handler.get(LittleEndian);394ExceptionHandler {395tag,396context_sp_offset: context,397handler_offset: handler,398}399})400}401402/// Provide an iterator over callsites, and for each callsite, the403/// frame offset and arrays of handlers.404pub fn into_iter(self) -> impl Iterator<Item = (u32, Option<u32>, Vec<ExceptionHandler>)> + 'a {405self.callsites406.iter()407.map(|pc| pc.get(LittleEndian))408.enumerate()409.map(move |(i, pc)| {410(411pc,412option_from_u32(self.frame_offsets[i].get(LittleEndian)),413self.handlers_for_callsite(i).collect(),414)415})416}417}418419fn option_from_u32(value: u32) -> Option<u32> {420if value == u32::MAX { None } else { Some(value) }421}422423#[cfg(all(test, feature = "cranelift"))]424mod test {425use super::*;426use cranelift_codegen::entity::EntityRef;427use cranelift_codegen::ir::ExceptionTag;428429#[test]430fn serialize_exception_table() {431let callsites = [432FinalizedMachCallSite {433ret_addr: 0x10,434frame_offset: None,435exception_handlers: &[436FinalizedMachExceptionHandler::Tag(ExceptionTag::new(1), 0x20),437FinalizedMachExceptionHandler::Tag(ExceptionTag::new(2), 0x30),438FinalizedMachExceptionHandler::Default(0x40),439],440},441FinalizedMachCallSite {442ret_addr: 0x48,443frame_offset: None,444exception_handlers: &[],445},446FinalizedMachCallSite {447ret_addr: 0x50,448frame_offset: Some(0x20),449exception_handlers: &[FinalizedMachExceptionHandler::Default(0x60)],450},451];452453let mut builder = ExceptionTableBuilder::default();454builder.add_func(0x100, callsites.into_iter()).unwrap();455let mut bytes = vec![];456builder.serialize(|slice| bytes.extend(slice.iter().cloned()));457458let deserialized = ExceptionTable::parse(&bytes).unwrap();459460let (frame_offset, iter) = deserialized.lookup_pc(0x148);461assert_eq!(frame_offset, None);462assert_eq!(iter.collect::<Vec<ExceptionHandler>>(), vec![]);463464let (frame_offset, iter) = deserialized.lookup_pc(0x110);465assert_eq!(frame_offset, None);466assert_eq!(467iter.collect::<Vec<ExceptionHandler>>(),468vec![469ExceptionHandler {470tag: Some(1),471context_sp_offset: None,472handler_offset: 0x120473},474ExceptionHandler {475tag: Some(2),476context_sp_offset: None,477handler_offset: 0x130478},479ExceptionHandler {480tag: None,481context_sp_offset: None,482handler_offset: 0x140483},484]485);486487let (frame_offset, iter) = deserialized.lookup_pc(0x150);488assert_eq!(frame_offset, Some(0x20));489assert_eq!(490iter.collect::<Vec<ExceptionHandler>>(),491vec![ExceptionHandler {492tag: None,493context_sp_offset: None,494handler_offset: 0x160495}]496);497}498}499500501