lsda 0.1.0

Read and write the language-specific data area of GCC-compatible exception handling (.gcc_except_table).
Documentation
  • Coverage
  • 100%
    249 out of 249 items documented3 out of 19 items with examples
  • Size
  • Source code size: 253.8 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 4.5 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 2s Average build duration of successful builds.
  • all releases: 2s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Fee0/lsda
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • Fee0

lsda

Crates.io Docs.rs CI MSRV 1.88 License: MIT

A Rust library for reading and writing the language-specific data area (LSDA) of GCC-compatible exception handling: the .gcc_except_table section used by C++ (__gxx_personality_v0), and the LSDA at the end of generic-model .ARM.extab entries.

  • Read and write. Parse LSDAs from section data, or build them in memory and encode them.
  • Lossless. Converting the LSDAs of a section and writing them back reproduces it byte for byte, including alignment padding and padded @TType offsets (see Limitations).
  • Every variant. The DWARF unwinder, the Arm EHABI (word-sized exception specifications) and SJLJ / WebAssembly (index-based call sites); every DW_EH_PE_* pointer encoding; 16-, 32- and 64-bit addresses; little and big endian.
  • Relocation-aware. Read relocatable objects through a Relocate hook, and record relocations for symbol addresses when writing.
  • Portable. no_std (requires alloc), zero-copy reading, no required dependencies.

Specification

The LSDA has no standalone specification. Its format is defined by the producers and consumers below, which agree on it; this crate follows them.

Producer GCC gcc/except.cc (output_one_function_exception_table)
Consumer libstdc++ libsupc++/eh_personality.cc, libgcc unwind-pe.h
Also LLVM EHStreamer.cpp, libc++abi cxa_personality.cpp
Pointer encodings Linux Standard Base Core Specification 5.0, "Exception Frames" (DW_EH_PE_*)

An LSDA is laid out as follows:

@LPStart encoding  u8          DW_EH_PE_omit: landing pads are relative to the function
@LPStart           encoded     only if the encoding is not omit
@TType encoding    u8          DW_EH_PE_omit: no type table
@TType base offset uleb128     only if the encoding is not omit; from the end of this field
call-site encoding u8
call-site length   uleb128
call-site table    start, length, landing pad (encoded), action (uleb128) per entry
action table       filter (sleb128), next displacement (sleb128) per record
padding            to align the type table
type table         entries in @TType encoding; entry N is at base - N * size
--- @TType base ---
exception specs    uleb128 type indices, 0-terminated (Arm EHABI: TARGET2 words)

Coverage

Area Supported
Header @LPStart in any encoding, @TType encoding and base offset, call-site encoding
Call sites DWARF and Arm EHABI (start, length, landing pad, action), SJLJ / Wasm (landing pad, action); lookup of the call site for a location, as the personality routine does
Actions Cleanups, catches and exception specifications; chains, with cycle detection
Type table Every fixed-size DW_EH_PE_* encoding, with pcrel, textrel, datarel, funcrel, aligned and indirect; null entries (catch (...))
Exception specifications ULEB128 type index lists; Arm EHABI R_ARM_TARGET2 word lists
Extent The extent of the action, type and exception specification tables, which is not recorded, inferred as GCC and LLVM emit them
Split functions GCC's hot/cold partitions (an LSDA per part), and LLVM's call-site ranges for -fbasic-block-sections (Lsda::with_call_site_table_end)

Installation

[dependencies]
lsda = "0.1"

Minimum supported Rust version: 1.88 (edition 2024).

Cargo features

Feature Default Description
read ✓ The read module
write ✓ The write module
fallible-iterator Implements fallible_iterator::FallibleIterator for all iterators

Usage

Reading

LSDAs are not self-delimiting, so they are reached by offset: from the L augmentation of an FDE in .eh_frame (for example with gimli), or from an .ARM.extab entry (for example with ehabi).

use lsda::read::{Filter, GccExceptTable};
use lsda::{BaseAddresses, Format, LittleEndian, LsdaOffset};

let table = GccExceptTable::new(section_data, LittleEndian, Format::new(8));
let bases = BaseAddresses::default().set_section(section_address);
let lsda = table.lsda_at(LsdaOffset(lsda_offset))?;

// The call site that covers an offset in the function.
if let Some(call_site) = lsda.find_call_site(pc - function_start)? {
    println!("landing pad +{:#x}", call_site.landing_pad);
    if let Some(first) = call_site.action {
        let mut actions = lsda.actions(first);
        while let Some(action) = actions.next()? {
            match action.filter {
                Filter::Cleanup => println!("cleanup"),
                Filter::Catch(index) => {
                    let type_info = lsda.type_entry(index)?;
                    println!("catch {:#x}", type_info.address(&bases)?);
                }
                Filter::ExceptionSpec(offset) => {
                    let mut types = lsda.exception_spec(offset)?;
                    while let Some(entry) = types.next()? {
                        println!("may throw {entry:?}");
                    }
                }
            }
        }
    }
}

// Or decode every table at once.
let tables = lsda.parse_tables()?;
println!("{} call sites, ends at {:#x}", tables.call_sites.len(), tables.end);

Encoded pointers are returned as Pointer values, which record their encoding and section offset and resolve against BaseAddresses. Use Format::arm_ehabi() for .ARM.extab, and Format::new(n).with_unwinder(Unwinder::SjLj) for SJLJ tables.

Relocatable objects

In a relocatable object the type table entries are the targets of relocations (such as R_X86_64_PC32 against DW.ref._ZTIi). Wrap the section data in a RelocateReader with an implementation of Relocate, which maps each relocated field to its value under a layout of your choice.

Writing

use lsda::write::{Action, Address, CallSite, EndianVec, Filter, GccExceptTable, Lsda};
use lsda::{BaseAddresses, Encoding, Format, LittleEndian};

// try { g(); } catch (int) { ... }
let mut lsda = Lsda::new();
lsda.ttype_encoding = Encoding::INDIRECT_PCREL_SDATA4;
let int = lsda.add_type(Some(Address::Constant(dw_ref_ztii)));
let catch_int = lsda.add_action(Action { filter: Filter::Catch(int), next: None });
lsda.call_sites.push(CallSite::new(0x10, 0x08, 0x40, Some(catch_int)));

let mut table = GccExceptTable::new(Format::new(8));
let id = table.add_lsda(lsda);

let mut section = EndianVec::new(LittleEndian);
let bases = BaseAddresses::default().set_section(section_address);
let offsets = table.write(&mut section, &bases)?;
println!("LSDA at {:#x}", offsets.get(id).0);

The writer computes every offset: action displacements, call-site action indices, exception specification filters, the type table alignment padding and the @TType base offset (padding the ULEB128 when the layout requires it, as assemblers do). Lsda::write writes a single LSDA, for example after the unwinding instructions of an .ARM.extab entry.

For relocatable output, implement RelocateWriter and use Address::Symbol: absolute and pc-relative pointers become Absolute and Relative relocations, and Arm EHABI type references Target2. The addend is also stored in the field, as REL relocations require.

GccExceptTable::from_read converts a parsed section, given the offsets of its LSDAs, for modification and rewriting.

Dumping an ELF file

cargo run --example lsdadump -- path/to/file.o

Finds every LSDA through .eh_frame (or .ARM.exidx) and prints its header, call sites, actions, type table (with symbol names where the relocations of an object file allow) and exception specifications.

Limitations

  • Offsets come from elsewhere. The crate does not parse .eh_frame or .ARM.extab; the caller provides the offset of each LSDA.
  • Inferred extent. The lengths of the action, type and exception specification tables are not recorded. parse_tables reads the action records reachable from the call sites, the exception specifications they reference, and the type table up to the highest referenced index. Data that nothing refers to is not decoded: GCC keeps the action records and exception specifications of call sites that optimization removed, and LLVM lists the types of Arm EHABI exception specifications. from_read keeps them, so round trips stay lossless.
  • Round-trip conditions. from_read converts an LSDA only if writing it reproduces its bytes exactly, and keeps it as raw bytes otherwise. It handles what producers emit: unreferenced records, entries and specifications, padded @TType base offsets, and padded LEB128 call-site values (RISC-V linkers leave them). It keeps raw other non-minimal LEB128 values, other unreferenced data, and LLVM call-site ranges before the last of a function, since the model has a single call-site table. Gaps between LSDAs that are not zero alignment padding are kept as raw bytes too.
  • Call-site ranges need their end. The call-site table length of an LLVM call-site range extends over the ranges after it. The caller, which knows where the next range starts from its FDE, ends the table there with Lsda::with_call_site_table_end.
  • Relocated values. Through a RelocateReader, from_read compares against the stored (unrelocated) bytes, so LSDAs with relocated fields are kept raw. Apply the relocations to a copy of the section first to convert them.
  • Call-site values are offsets. Call-site encodings must have the DW_EH_PE_absptr application, as every producer emits. Values are truncated to the address size, as the personality routine reads them, and a region must end within the address space.
  • Strict pointer encodings. DW_EH_PE_aligned is only valid on its own (libgcc reads other combinations unaligned, libc++abi not at all), and a LEB128 pointer must fit in an address. For the Arm EHABI the address size is 4, and the @TType encoding must have size 4 and not be aligned: the personality routines read R_ARM_TARGET2 words in place, whatever the encoding says.
  • R_ARM_TARGET2 values are raw. Their interpretation is platform-specific (absolute, GOT-relative or place-relative).

Testing

cargo test

The integration tests use objects and shared libraries built by GCC 7.5 and clang 10 from tests/data/src/eh.cpp (rebuild with tests/data/build.sh): cleanups, single and multiple handlers, nested try blocks, dynamic exception specifications, throw(), noexcept, rethrow, and a function with enough handlers for multi-byte LEB128 values. The targets are x86-64, i386, AArch64, big-endian PowerPC64 and Arm EHABI. GCC 14 and clang 19 builds add split functions: hot/cold partitions and basic block sections. They verify:

  • every LSDA parses identically through the relocation layer and from the relocated section, and LSDAs do not overlap;
  • every call site is found by find_call_site;
  • each function decodes to what its source says, with type_info symbols named through the relocations of the object files;
  • a byte-identical from_read → write round trip of every section, with every LSDA converted.

tests/fuzz.rs checks robustness and consistency. Random sections and mutations of the fixtures, in both byte orders and with every unwinder variant and address size, go through every read path; every call must return Ok or Err, and whenever from_read succeeds, writing must reproduce the section exactly. Random LSDAs in every encoding must read back as written and convert back to the same bytes. It runs 20,000 iterations per test by default; set LSDA_FUZZ_ITERATIONS for longer runs (preferably in debug mode, where integer overflow panics).

Differential test against libstdc++

tests/oracle.rs checks that the crate decodes LSDAs as the GNU C++ personality routine does. tests/personality/oracle.c runs libgcc's own pointer decoding (unwind-pe.h, downloaded from GCC 14.2 and changed only to not follow indirect pointers) and the LSDA logic of libstdc++'s eh_personality.cc: it finds the call site for each location, then walks its actions, type table entries and exception specifications. The test compares its output with the crate's for the fixtures, random LSDAs in every encoding, and mutations of both that the crate accepts. It needs a C compiler for 64- and 32-bit targets (on Debian and Ubuntu, gcc-multilib):

sh tests/personality/build.sh target/oracle
LSDA_ORACLE64=target/oracle/oracle64 LSDA_ORACLE32=target/oracle/oracle32 \
    cargo test --release --test oracle -- --nocapture

Set LSDA_ORACLE_ITERATIONS for more random LSDAs. The test is skipped without the oracles; CI runs it.

Real-world corpus

tests/corpus.rs checks every ELF file in the directory named by LSDA_CORPUS: every LSDA must parse completely and find its call sites, every section must convert and write back byte for byte, and, with the oracles set, every little-endian LSDA must decode as libstdc++ decodes it. scripts/fetch_corpus.py downloads a corpus of Debian packages, mostly C++ libraries built by GCC 14 and libc++ built by clang 19, for x86-64, i386, AArch64, Arm EHABI, PowerPC64, big-endian s390x and RISC-V:

python scripts/fetch_corpus.py ../lsda-corpus
LSDA_CORPUS=../lsda-corpus cargo test --release --test corpus -- --nocapture

With Debian 13, this checks 249,507 LSDAs in 144 files, all converted and written back identically, of which 222,230 also match libstdc++.

License

Licensed under the MIT License.