Skip to main content

Module cookbook

Module cookbook 

Source
Expand description

§Cookbook

Worked examples for each part of the public API. Feature-gated APIs have their own pages: docs::conversion for convert, and MappedGsym for mmap.

GSYM stores names and paths as bytes, so the examples use byte strings throughout.

§The semantic model

Function and its parts are plain owned data with public fields: build them however you like and hand them to a builder. Two newtypes keep unrelated integers apart: FileIndex is a file-table index, and CallSiteFlags is a flag set that keeps bits this crate does not recognize.

use gsym::{
    AddressRange, CallSite, CallSiteFlags, FileEntry, FileIndex, Function,
    InlineNode, LineEntry,
};

// Ranges are half-open: [start, end).
let range = AddressRange::new(0x1000, 0x1020);
assert!(range.is_valid());
assert!(!range.is_empty());
assert_eq!(range.size(), 0x20);
assert!(range.contains(0x1010));
assert!(range.contains_range(AddressRange::new(0x1004, 0x1008)));

// File-table index zero is reserved for the empty entry.
let file_index = FileIndex::new(1);
assert_eq!(file_index.get(), 1);
assert_eq!(FileIndex::from(1_u32), file_index);
assert_eq!(u32::from(file_index), 1);
assert_eq!(u64::from(file_index), 1);
assert_eq!(FileIndex::ZERO.get(), 0);

let file = FileEntry::new(b"src", b"main.rs");
assert_eq!(file.basename, b"main.rs");

let line = LineEntry::new(0x1000, file_index, 12);

// An inline node describes where in its *parent* the inlined call appears.
let inline = InlineNode {
    ranges: vec![AddressRange::new(0x1004, 0x100c)],
    name: b"inlined".to_vec(),
    call_file: file_index,
    call_line: 8,
    children: Vec::new(),
};

// Unknown flag bits round-trip.
let mut flags = CallSiteFlags::INTERNAL | CallSiteFlags::EXTERNAL;
assert!(flags.contains(CallSiteFlags::INTERNAL));
assert_eq!(flags.bits(), 3);
flags |= CallSiteFlags::from_bits_retain(0x80);
assert_eq!(u8::from(flags), 0x83);
assert!(CallSiteFlags::default().is_empty());

let call_site = CallSite {
    return_offset: 4,
    flags,
    match_regex: vec![b"callee.*".to_vec()],
};

let alias = Function::new(range, b"alias");
let function = Function {
    lines: vec![line],
    inline: Some(inline),
    merged: vec![alias],
    call_sites: vec![call_site],
    ..Function::new(range, b"main")
};
assert_eq!(function.name, b"main");

§Building and writing

GsymBuilder turns semantic records into an image. Encoding is deterministic: the same inputs and options always produce the same bytes. Chained setters cover the common cases, and BuilderOptions sets everything at once. Writing consumes the builder.

use gsym::{
    AddressRange, BuilderOptions, Endian, FileEntry, Function, FunctionSetPolicy,
    GsymBuilder, GsymVersion, LineEntry, WriterOptions,
};

fn make_builder() -> gsym::Result<GsymBuilder> {
    let options = BuilderOptions {
        writer: WriterOptions {
            version: GsymVersion::V2,
            endian: Endian::Big,
            base_address: Some(0x2000),
            build_id: vec![1, 2, 3, 4],
        },
        executable_ranges: vec![AddressRange::new(0x2000, 0x3000)]
            .into_boxed_slice(),
        repair_zero_sized_functions: true,
        merge_equal_address_functions: false,
    };

    // Later setters win over the options they were constructed with.
    let mut builder = GsymBuilder::with_options(options)
        .version(GsymVersion::V1)
        .endian(Endian::Little)
        .base_address(0x2000)
        .build_id([0xaa, 0xbb])
        .repair_zero_sized_functions(true)
        .function_set(FunctionSetPolicy::Deduplicate)
        .executable_ranges([AddressRange::new(0x2000, 0x3000)]);

    // Files are interned: adding the same entry twice returns the same index.
    let source = builder.add_file(FileEntry::new(b"src", b"lib.rs"))?;
    builder.add_function(Function {
        lines: vec![LineEntry::new(0x2010, source, 20)],
        ..Function::new(AddressRange::new(0x2010, 0x2020), b"built")
    })?;

    assert_eq!(builder.options().writer.version, GsymVersion::V1);
    assert_eq!(builder.files().len(), 2);
    assert_eq!(builder.functions().len(), 1);
    Ok(builder)
}

// `to_bytes` and `write_to` produce identical output.
let bytes = make_builder()?.to_bytes()?;
assert!(!bytes.is_empty());

let mut sink = Vec::new();
make_builder()?.write_to(&mut sink)?;
assert_eq!(sink, bytes);

let empty = GsymBuilder::new();
assert!(empty.functions().is_empty());

§Reading a file

Gsym::open reads a path into an owned snapshot and validates it:

use gsym::Gsym;

let reader = Gsym::open("app.gsym")?;
assert!(reader.verify()?.functions > 0);

Gsym::parse accepts any D: AsRef<[u8]> without copying it, including a borrowed slice, a Vec<u8>, a Box<[u8]>, or an Arc<[u8]>. Parsing validates the table layout, and the reader API is the same either way.

use gsym::{AddressRange, Function, Gsym, GsymBuilder};

let mut builder = GsymBuilder::new().build_id([0xde, 0xad]);
builder.add_function(Function::new(
    AddressRange::new(0x3000, 0x3010),
    b"reader",
))?;
let bytes = builder.to_bytes()?;

// Borrowed storage: no copy, and the reader points at the caller's bytes.
let reader: Gsym<&[u8]> = Gsym::parse(bytes.as_slice())?;
assert_eq!(reader.as_ref().as_ptr(), bytes.as_ptr());

// Header metadata borrows from the input.
let header: gsym::Header<'_> = reader.header();
assert_eq!(header.version, gsym::GsymVersion::V1);
assert_eq!(header.endian, gsym::Endian::Little);
assert_eq!(header.address_count, 1);
assert!(matches!(header.address_offset_size, 1 | 2 | 4 | 8));
assert_eq!(header.base_address, 0x3000);
assert_eq!(header.build_id, [0xde, 0xad]);
assert_eq!(reader.build_id(), [0xde, 0xad]);

// Reserved entries: string offset zero and file index zero are always empty.
assert_eq!(reader.string(0)?, b"");
assert_eq!(reader.file(0_u32)?, (&b""[..], &b""[..]));

// Function records can be reached by index without decoding them.
let direct: gsym::FunctionRef<'_> = reader.function(0)?;
assert_eq!(direct.index(), 0);
assert_eq!(direct.start(), 0x3000);
assert_eq!(direct.range(), AddressRange::new(0x3000, 0x3010));
assert_eq!(direct.name(), b"reader");
assert_eq!(direct.decode()?.name, b"reader");
assert!(reader.get_function(1)?.is_none()); // out of bounds, not an error

// Iteration follows address-table order.
let mut functions: gsym::Functions<'_, &[u8]> = reader.functions();
assert_eq!(functions.len(), 1);
assert_eq!(
    functions.next().transpose()?.map(|function| function.name()),
    Some(&b"reader"[..]),
);
assert!(functions.next().is_none());

// Verification checks the whole file, not just what one lookup reads.
let verified: gsym::VerifyReport = reader.verify()?;
assert_eq!(verified.functions, 1);
assert_eq!(verified.files, 1);
assert_eq!(verified.strings, 2);
assert!(verified.function_info_bytes > 0);

let owned = Gsym::parse(bytes.clone())?;
assert_eq!(owned.into_inner(), bytes);

§Looking up addresses

Gsym::lookup is the ergonomic path: it returns the frames and call-site patterns for an address. Gsym::lookup_with_options reuses caller-owned scratch storage and can skip optional records. Gsym::for_each_frame also avoids allocating the result collection by handing each frame to a closure.

use gsym::{
    AddressRange, CallSite, Error, FrameLookupOptions, Function, Gsym,
    GsymBuilder, LineEntry, LookupOptions, LookupScratch,
};

let mut builder = GsymBuilder::new();
let file = builder.add_file(gsym::FileEntry::new(b"src", b"lookup.rs"))?;
builder.add_function(Function {
    lines: vec![LineEntry::new(0x4000, file, 42)],
    call_sites: vec![CallSite {
        return_offset: 4,
        match_regex: vec![b"target.*".to_vec()],
        ..CallSite::default()
    }],
    ..Function::new(AddressRange::new(0x4000, 0x4010), b"lookup")
})?;
let bytes = builder.to_bytes()?;
let reader = Gsym::parse(bytes)?;

let hit: gsym::Lookup<'_> = reader
    .lookup(0x4004)?
    .ok_or(Error::InvalidModel("expected lookup hit"))?;
let frame: gsym::LookupFrame<'_> = hit.frames()[0];
assert_eq!(hit.address, 0x4004);
assert_eq!(hit.function, AddressRange::new(0x4000, 0x4010));
assert_eq!(frame.name, b"lookup");
assert_eq!(frame.directory, b"src");
assert_eq!(frame.basename, b"lookup.rs");
assert_eq!(frame.line, 42);
assert_eq!(frame.offset, 4);
assert!(!frame.inlined);
assert_eq!(hit.call_site_patterns()[0], b"target.*");

// Names only: the line program, inline tree, and call sites are never read.
let mut scratch = LookupScratch::with_capacity(8);
let lean = reader
    .lookup_with_options(
        0x4004,
        LookupOptions {
            line_information: false,
            inline_frames: false,
            call_sites: false,
        },
        &mut scratch,
    )?
    .ok_or(Error::InvalidModel("expected lookup hit"))?;
assert_eq!(lean.frames()[0].line, 0);
assert!(lean.call_site_patterns().is_empty());

// No allocation: frames go to a closure and scratch is reused.
let mut visited = 0;
let found = reader.for_each_frame(
    0x4004,
    FrameLookupOptions {
        line_information: true,
        inline_frames: true,
    },
    &mut scratch,
    |_| visited += 1,
)?;
assert!(found);
assert_eq!(visited, 1);

§Decoding, transcoding, and segmenting

Gsym::decode_all turns a whole file back into owned semantic records, which you can edit and re-encode. Use to_builder when the decoded model is worth keeping, and into_builder or the consuming transcode when it is not. Gsym::transcode is the one-shot form for changing version or byte order.

use gsym::{
    AddressRange, DecodedGsym, Endian, Function, Gsym, GsymBuilder, GsymSegment,
    GsymVersion, TranscodeOptions,
};

fn source() -> gsym::Result<Vec<u8>> {
    let mut builder = GsymBuilder::new();
    builder.add_function(Function::new(
        AddressRange::new(0x5000, 0x5010),
        b"transform",
    ))?;
    builder.to_bytes()
}

// `None` means "keep whatever the input used".
let options = TranscodeOptions {
    version: Some(GsymVersion::V2),
    endian: Some(Endian::Big),
};
let source = source()?;

let rewritten = Gsym::parse(source.as_slice())?.transcode(options)?;
assert_eq!(Gsym::parse(rewritten)?.header().version, GsymVersion::V2);

let decoded: DecodedGsym = Gsym::parse(source.as_slice())?.decode_all()?;
assert_eq!(decoded.source_version, GsymVersion::V1);
assert_eq!(decoded.source_endian, Endian::Little);
assert_eq!(decoded.base_address, 0x5000);
assert!(decoded.build_id.is_empty());
assert_eq!(decoded.files.len(), 1);
assert_eq!(decoded.functions.len(), 1);
let cloned_builder = decoded.to_builder(options)?;
assert_eq!(cloned_builder.functions().len(), 1);

let moved_builder = Gsym::parse(source.as_slice())?
    .decode_all()?
    .into_builder(options)?;
assert_eq!(moved_builder.functions().len(), 1);

let transcoded = Gsym::parse(source.as_slice())?
    .decode_all()?
    .transcode(options)?;
assert_eq!(Gsym::parse(transcoded)?.header().endian, Endian::Big);

let decoded = Gsym::parse(source)?.decode_all()?;
let segments: Vec<GsymSegment> = decoded.segments(4096, options)?;
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].first_address, 0x5000);
assert_eq!(segments[0].end_address, 0x5010);
assert_eq!(segments[0].function_count, 1);
assert!(!segments[0].bytes().is_empty());

§Versions, byte order, and errors

Error is #[non_exhaustive]: match the cases that need specific recovery and keep a fallback arm for the rest.

use gsym::{Endian, Error, Gsym, GsymVersion, Result};

assert!(matches!(Endian::native(), Endian::Little | Endian::Big));
assert_eq!(GsymVersion::default(), GsymVersion::V1);

fn validate(bytes: &[u8]) -> Result<()> {
    match Gsym::parse(bytes) {
        Ok(gsym) => {
            gsym.verify()?;
            Ok(())
        }
        // "Not a GSYM file at all" is often worth treating as a normal answer.
        Err(Error::InvalidMagic(magic)) => {
            assert_eq!(magic, 0);
            Ok(())
        }
        Err(error) => Err(error),
    }
}

validate(&[0; 48])?;

§Further reading

  • Symbolication for unslid addresses, reading frames, storage choices, performance, and threading
  • docs::conversion for building GSYM from ELF and DWARF (convert feature)
  • Format for what the bytes these examples produce contain