Path: blob/main/crates/unwinder/src/exception_table.rs
1692 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};2122/// Collector struct for exception handlers per call site.23///24/// # Format25///26/// We keep six different arrays (`Vec`s) that we build as we visit27/// callsites, in ascending offset (address relative to beginning of28/// code segment) order: callsite offsets, frame offsets,29/// tag/destination ranges, tags, tag context SP offset, destination30/// offsets.31///32/// The callsite offsets, frame offsets, and tag/destination ranges33/// logically form a sorted lookup array, allowing us to find34/// information for any single callsite. The frame offset specifies35/// distance down to the SP value at the callsite (in bytes), relative36/// to the FP of that frame. The range denotes a range of indices in37/// the tag/context and destination offset arrays. Ranges are stored38/// with the (exclusive) *end* index only; the start index is implicit39/// as the previous end, or zero if first element.40///41/// The slices of tag, context, and handlers arrays named by `ranges`42/// for each callsite specify a series of handler items for that43/// callsite. The tag and context together allow a44/// dynamic-tag-instance match in the unwinder: the context specifies45/// an offset from SP at the callsite that contains a machine word46/// (e.g. with vmctx) that, together with the static tag index, can be47/// used to perform a dynamic match. A context of `-1` indicates no48/// dynamic context, and a tag of `-1` indicates a catch-all49/// handler. If a handler item matches, control should be transferred50/// to the code offset given in the last array, `handlers`.51///52/// # Example53///54/// An example of this data format:55///56/// ```plain57/// callsites: [0x10, 0x50, 0xf0] // callsites (return addrs) at offsets 0x10, 0x50, 0xf058/// ranges: [2, 4, 5] // corresponding ranges for each callsite59/// frame_offsets: [0, 0x10, 0] // corresponding SP-to-FP offsets for each callsite60/// tags: [1, 5, 1, -1, -1] // tags for each handler at each callsite61/// contexts: [-1, -1, 0x10, 0x20, 0x30] // SP-offset for context for each tag62/// handlers: [0x40, 0x42, 0x6f, 0x71, 0xf5] // handler destinations at each callsite63/// ```64///65/// Expanding this out:66///67/// ```plain68/// callsites: [0x10, 0x50, 0xf0], # PCs relative to some start of return-points.69/// frame_offsets: [0, 0x10, 0], # SP-to-FP offsets at each callsite.70/// ranges: [71/// 2, # callsite 0x10 has tags/handlers indices 0..272/// 4, # callsite 0x50 has tags/handlers indices 2..473/// 5, # callsite 0xf0 has tags/handlers indices 4..574/// ],75/// tags: [76/// # tags for callsite 0x10:77/// 1,78/// 5,79/// # tags for callsite 0x50:80/// 1,81/// -1, # "catch-all"82/// # tags for callsite 0xf0:83/// -1, # "catch-all"84/// ]85/// contexts: [86/// # SP-offsets for context for each tag at callsite 0x10:87/// -1,88/// -1,89/// # for callsite 0x50:90/// 0x10,91/// 0x20,92/// # for callsite 0xf0:93/// 0x30,94/// ]95/// handlers: [96/// # handlers for callsite 0x10:97/// 0x40, # relative PC to handle tag 1 (above)98/// 0x42, # relative PC to handle tag 599/// # handlers for callsite 0x50:100/// 0x6f, # relative PC to handle tag 1101/// 0x71, # relative PC to handle all other tags102/// # handlers for callsite 0xf0:103/// 0xf5, # relative PC to handle all other tags104/// ]105/// ```106#[cfg(feature = "cranelift")]107#[derive(Clone, Debug, Default)]108pub struct ExceptionTableBuilder {109pub callsites: Vec<U32Bytes<LittleEndian>>,110pub frame_offsets: Vec<U32Bytes<LittleEndian>>,111pub ranges: Vec<U32Bytes<LittleEndian>>,112pub tags: Vec<U32Bytes<LittleEndian>>,113pub contexts: Vec<U32Bytes<LittleEndian>>,114pub handlers: Vec<U32Bytes<LittleEndian>>,115last_start_offset: CodeOffset,116}117118#[cfg(feature = "cranelift")]119impl ExceptionTableBuilder {120/// Add a function at a given offset from the start of the121/// compiled code section, recording information about its call122/// sites.123///124/// Functions must be added in ascending offset order.125pub fn add_func<'a>(126&mut self,127start_offset: CodeOffset,128call_sites: impl Iterator<Item = FinalizedMachCallSite<'a>>,129) -> anyhow::Result<()> {130// Ensure that we see functions in offset order.131assert!(start_offset >= self.last_start_offset);132self.last_start_offset = start_offset;133134// Visit each callsite in turn, translating offsets from135// function-local to section-local.136let mut handlers = vec![];137for call_site in call_sites {138let ret_addr = call_site.ret_addr.checked_add(start_offset).unwrap();139handlers.extend(call_site.exception_handlers.iter().cloned());140141let start_idx = u32::try_from(self.tags.len()).unwrap();142let mut context = u32::MAX;143for handler in call_site.exception_handlers {144match handler {145FinalizedMachExceptionHandler::Tag(tag, offset) => {146self.tags.push(U32Bytes::new(LittleEndian, tag.as_u32()));147self.contexts.push(U32Bytes::new(LittleEndian, context));148self.handlers.push(U32Bytes::new(149LittleEndian,150offset.checked_add(start_offset).unwrap(),151));152}153FinalizedMachExceptionHandler::Default(offset) => {154self.tags.push(U32Bytes::new(LittleEndian, u32::MAX));155self.contexts.push(U32Bytes::new(LittleEndian, context));156self.handlers.push(U32Bytes::new(157LittleEndian,158offset.checked_add(start_offset).unwrap(),159));160}161FinalizedMachExceptionHandler::Context(ExceptionContextLoc::SPOffset(162offset,163)) => {164context = *offset;165}166FinalizedMachExceptionHandler::Context(ExceptionContextLoc::GPR(_)) => {167panic!(168"Wasmtime exception unwind info only supports dynamic contexts on the stack"169);170}171}172}173let end_idx = u32::try_from(self.tags.len()).unwrap();174175// Omit empty callsites for compactness.176if end_idx > start_idx {177self.ranges.push(U32Bytes::new(LittleEndian, end_idx));178self.frame_offsets.push(U32Bytes::new(179LittleEndian,180call_site.frame_offset.unwrap_or(u32::MAX),181));182self.callsites.push(U32Bytes::new(LittleEndian, ret_addr));183}184}185186Ok(())187}188189/// Serialize the exception-handler data section, taking a closure190/// to consume slices.191pub fn serialize<F: FnMut(&[u8])>(&self, mut f: F) {192// Serialize the length of `callsites` / `ranges`.193let callsite_count = u32::try_from(self.callsites.len()).unwrap();194f(&callsite_count.to_le_bytes());195// Serialize the length of `tags` / `handlers`.196let handler_count = u32::try_from(self.handlers.len()).unwrap();197f(&handler_count.to_le_bytes());198199// Serialize `callsites`, `ranges`, `tags`, and `handlers` in200// that order.201f(object::bytes_of_slice(&self.callsites));202f(object::bytes_of_slice(&self.frame_offsets));203f(object::bytes_of_slice(&self.ranges));204f(object::bytes_of_slice(&self.tags));205f(object::bytes_of_slice(&self.contexts));206f(object::bytes_of_slice(&self.handlers));207}208209/// Serialize the exception-handler data section to a vector of210/// bytes.211pub fn to_vec(&self) -> Vec<u8> {212let mut bytes = vec![];213self.serialize(|slice| bytes.extend(slice.iter().cloned()));214bytes215}216}217218/// ExceptionTable deserialized from a serialized slice.219///220/// This struct retains borrows of the various serialized parts of the221/// exception table data as produced by222/// [`ExceptionTableBuilder::serialize`].223#[derive(Clone, Debug)]224pub struct ExceptionTable<'a> {225callsites: &'a [U32Bytes<LittleEndian>],226ranges: &'a [U32Bytes<LittleEndian>],227frame_offsets: &'a [U32Bytes<LittleEndian>],228tags: &'a [U32Bytes<LittleEndian>],229contexts: &'a [U32Bytes<LittleEndian>],230handlers: &'a [U32Bytes<LittleEndian>],231}232233/// Wasmtime exception table item, after parsing.234///235/// Note that this is separately defined from the equivalent type in236/// Cranelift, `cranelift_codegen::FinalizedMachExceptionHandler`,237/// because we need this in runtime-only builds when Cranelift is not238/// included.239#[derive(Clone, Debug, PartialEq, Eq)]240pub struct ExceptionHandler {241/// A tag (arbitrary `u32` identifier from CLIF) or `None` for catch-all.242pub tag: Option<u32>,243/// Dynamic context, if provided, with which to interpret the244/// tag. Context is available at the given offset from SP in this245/// frame.246pub context_sp_offset: Option<u32>,247/// Handler code offset.248pub handler_offset: u32,249}250251impl<'a> ExceptionTable<'a> {252/// Parse exception tables from a byte-slice as produced by253/// [`ExceptionTableBuilder::serialize`].254pub fn parse(data: &'a [u8]) -> anyhow::Result<ExceptionTable<'a>> {255let mut data = Bytes(data);256let callsite_count = data257.read::<U32Bytes<LittleEndian>>()258.map_err(|_| anyhow::anyhow!("Unable to read callsite count prefix"))?;259let callsite_count = usize::try_from(callsite_count.get(LittleEndian))?;260let handler_count = data261.read::<U32Bytes<LittleEndian>>()262.map_err(|_| anyhow::anyhow!("Unable to read handler count prefix"))?;263let handler_count = usize::try_from(handler_count.get(LittleEndian))?;264let (callsites, data) =265object::slice_from_bytes::<U32Bytes<LittleEndian>>(data.0, callsite_count)266.map_err(|_| anyhow::anyhow!("Unable to read callsites slice"))?;267let (frame_offsets, data) =268object::slice_from_bytes::<U32Bytes<LittleEndian>>(data, callsite_count)269.map_err(|_| anyhow::anyhow!("Unable to read frame_offsets slice"))?;270let (ranges, data) =271object::slice_from_bytes::<U32Bytes<LittleEndian>>(data, callsite_count)272.map_err(|_| anyhow::anyhow!("Unable to read ranges slice"))?;273let (tags, data) = object::slice_from_bytes::<U32Bytes<LittleEndian>>(data, handler_count)274.map_err(|_| anyhow::anyhow!("Unable to read tags slice"))?;275let (contexts, data) =276object::slice_from_bytes::<U32Bytes<LittleEndian>>(data, handler_count)277.map_err(|_| anyhow::anyhow!("Unable to read contexts slice"))?;278let (handlers, data) =279object::slice_from_bytes::<U32Bytes<LittleEndian>>(data, handler_count)280.map_err(|_| anyhow::anyhow!("Unable to read handlers slice"))?;281282if !data.is_empty() {283anyhow::bail!("Unexpected data at end of serialized exception table");284}285286Ok(ExceptionTable {287callsites,288frame_offsets,289ranges,290tags,291contexts,292handlers,293})294}295296/// Look up the set of handlers, if any, for a given return297/// address (as an offset into the code section).298///299/// The handler for `None` (the catch-all/default handler), if300/// any, will always come last.301///302/// Note: we use raw `u32` types for code offsets here to avoid303/// dependencies on `cranelift-codegen` when this crate is built304/// without compiler backend support (runtime-only config).305///306/// Returns a tuple of `(frame offset, handler iterator)`. The307/// frame offset, if `Some`, specifies the distance from SP to FP308/// at this callsite.309pub fn lookup_pc(&self, pc: u32) -> (Option<u32>, impl Iterator<Item = ExceptionHandler> + '_) {310let callsite_idx = self311.callsites312.binary_search_by_key(&pc, |callsite| callsite.get(LittleEndian))313.ok();314let frame_offset = callsite_idx315.map(|idx| self.frame_offsets[idx])316.and_then(|offset| option_from_u32(offset.get(LittleEndian)));317318(319frame_offset,320callsite_idx321.into_iter()322.flat_map(|callsite_idx| self.handlers_for_callsite(callsite_idx)),323)324}325326/// Look up the frame offset and handler destination if any, for a327/// given return address (as an offset into the code section) and328/// exception tag.329///330/// Note: we use raw `u32` types for code offsets and tags here to331/// avoid dependencies on `cranelift-codegen` when this crate is332/// built without compiler backend support (runtime-only config).333pub fn lookup_pc_tag(&self, pc: u32, tag: u32) -> Option<(u32, u32)> {334// First, look up the callsite in the sorted callsites list.335let callsite_idx = self336.callsites337.binary_search_by_key(&pc, |callsite| callsite.get(LittleEndian))338.ok()?;339let frame_offset =340option_from_u32(self.frame_offsets[callsite_idx].get(LittleEndian)).unwrap_or(0);341342let (tags, _, handlers) = self.tags_contexts_handlers_for_callsite(callsite_idx);343344// Is there any handler with an exact tag match?345if let Ok(handler_idx) = tags.binary_search_by_key(&tag, |tag| tag.get(LittleEndian)) {346return Some((frame_offset, handlers[handler_idx].get(LittleEndian)));347}348349// If not, is there a fallback handler? Note that we serialize350// it with the tag `u32::MAX`, so it is always last in sorted351// order.352if tags.last().map(|v| v.get(LittleEndian)) == Some(u32::MAX) {353return Some((frame_offset, handlers.last().unwrap().get(LittleEndian)));354}355356None357}358359fn tags_contexts_handlers_for_callsite(360&self,361idx: usize,362) -> (363&[U32Bytes<LittleEndian>],364&[U32Bytes<LittleEndian>],365&[U32Bytes<LittleEndian>],366) {367let end_idx = self.ranges[idx].get(LittleEndian);368let start_idx = if idx > 0 {369self.ranges[idx - 1].get(LittleEndian)370} else {3710372};373374// Take the subslices of `tags`, `contexts`, and `handlers`375// corresponding to this callsite.376let start_idx = usize::try_from(start_idx).unwrap();377let end_idx = usize::try_from(end_idx).unwrap();378let tags = &self.tags[start_idx..end_idx];379let contexts = &self.contexts[start_idx..end_idx];380let handlers = &self.handlers[start_idx..end_idx];381(tags, contexts, handlers)382}383384fn handlers_for_callsite(&self, idx: usize) -> impl Iterator<Item = ExceptionHandler> {385let (tags, contexts, handlers) = self.tags_contexts_handlers_for_callsite(idx);386tags.iter()387.zip(contexts.iter())388.zip(handlers.iter())389.map(|((tag, context), handler)| {390let tag = option_from_u32(tag.get(LittleEndian));391let context = option_from_u32(context.get(LittleEndian));392let handler = handler.get(LittleEndian);393ExceptionHandler {394tag,395context_sp_offset: context,396handler_offset: handler,397}398})399}400401/// Provide an iterator over callsites, and for each callsite, the402/// frame offset and arrays of handlers.403pub fn into_iter(self) -> impl Iterator<Item = (u32, Option<u32>, Vec<ExceptionHandler>)> + 'a {404self.callsites405.iter()406.map(|pc| pc.get(LittleEndian))407.enumerate()408.map(move |(i, pc)| {409(410pc,411option_from_u32(self.frame_offsets[i].get(LittleEndian)),412self.handlers_for_callsite(i).collect(),413)414})415}416}417418fn option_from_u32(value: u32) -> Option<u32> {419if value == u32::MAX { None } else { Some(value) }420}421422#[cfg(all(test, feature = "cranelift"))]423mod test {424use super::*;425use cranelift_codegen::entity::EntityRef;426use cranelift_codegen::ir::ExceptionTag;427428#[test]429fn serialize_exception_table() {430let callsites = [431FinalizedMachCallSite {432ret_addr: 0x10,433frame_offset: None,434exception_handlers: &[435FinalizedMachExceptionHandler::Tag(ExceptionTag::new(1), 0x20),436FinalizedMachExceptionHandler::Tag(ExceptionTag::new(2), 0x30),437FinalizedMachExceptionHandler::Default(0x40),438],439},440FinalizedMachCallSite {441ret_addr: 0x48,442frame_offset: None,443exception_handlers: &[],444},445FinalizedMachCallSite {446ret_addr: 0x50,447frame_offset: Some(0x20),448exception_handlers: &[FinalizedMachExceptionHandler::Default(0x60)],449},450];451452let mut builder = ExceptionTableBuilder::default();453builder.add_func(0x100, callsites.into_iter()).unwrap();454let mut bytes = vec![];455builder.serialize(|slice| bytes.extend(slice.iter().cloned()));456457let deserialized = ExceptionTable::parse(&bytes).unwrap();458459let (frame_offset, iter) = deserialized.lookup_pc(0x148);460assert_eq!(frame_offset, None);461assert_eq!(iter.collect::<Vec<ExceptionHandler>>(), vec![]);462463let (frame_offset, iter) = deserialized.lookup_pc(0x110);464assert_eq!(frame_offset, None);465assert_eq!(466iter.collect::<Vec<ExceptionHandler>>(),467vec![468ExceptionHandler {469tag: Some(1),470context_sp_offset: None,471handler_offset: 0x120472},473ExceptionHandler {474tag: Some(2),475context_sp_offset: None,476handler_offset: 0x130477},478ExceptionHandler {479tag: None,480context_sp_offset: None,481handler_offset: 0x140482},483]484);485486let (frame_offset, iter) = deserialized.lookup_pc(0x150);487assert_eq!(frame_offset, Some(0x20));488assert_eq!(489iter.collect::<Vec<ExceptionHandler>>(),490vec![ExceptionHandler {491tag: None,492context_sp_offset: None,493handler_offset: 0x160494}]495);496}497}498499500