lsda
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
@TTypeoffsets (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
Relocatehook, and record relocations for symbol addresses when writing. - Portable.
no_std(requiresalloc), 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
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
[]
= "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 ;
use ;
let table = new;
let bases = default.set_section;
let lsda = table.lsda_at?;
// The call site that covers an offset in the function.
if let Some = lsda.find_call_site?
// Or decode every table at once.
let tables = lsda.parse_tables?;
println!;
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 ;
use ;
// try { g(); } catch (int) { ... }
let mut lsda = new;
lsda.ttype_encoding = INDIRECT_PCREL_SDATA4;
let int = lsda.add_type;
let catch_int = lsda.add_action;
lsda.call_sites.push;
let mut table = new;
let id = table.add_lsda;
let mut section = new;
let bases = default.set_section;
let offsets = table.write?;
println!;
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
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_frameor.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_tablesreads 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_readkeeps them, so round trips stay lossless. - Round-trip conditions.
from_readconverts 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@TTypebase 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_readcompares 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_absptrapplication, 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_alignedis 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@TTypeencoding must have size 4 and not be aligned: the personality routines readR_ARM_TARGET2words in place, whatever the encoding says. R_ARM_TARGET2values are raw. Their interpretation is platform-specific (absolute, GOT-relative or place-relative).
Testing
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_infosymbols 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):
LSDA_ORACLE64=target/oracle/oracle64 LSDA_ORACLE32=target/oracle/oracle32 \
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:
LSDA_CORPUS=../lsda-corpus
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.