pdb Rust Library

repository·master·Indexed 19 days ago

https://github.com/getsentry/pdb

A Rust library for parsing Microsoft PDB (Program Database) files to extract debugging information such as symbols, types, and modules. It features lazy parsing, platform independence (no dependency on Windows or DIA SDK), and preservation of on-disk formats. The library provides tools for handling Relative Virtual Addresses (RVA), resolving cross-module references, and accessing the Debug Information (DBI) stream.

Tokens
16.4K
Snippets
53
Records
72
Agent score
64%

What's inside pdb

  1. Parse Microsoft PDB files with `pdb`

    master

    The pdb library is a Rust-based parser for Microsoft Program Database (PDB) files. It allows you to extract debugging information such as symbols, types, and modules from Windows-compiled binaries.

    Key design features:

    • On-disk format preservation: Works with the original data as formatted on-disk for as long as possible.
    • Lazy parsing: Only parses the specific data you request.
    • Platform independence: Does not depend on Windows, the DIA SDK, or the target's native byte ordering, making it suitable for cross-platform use.
  2. Run `pdb` example programs

    master

    You can run the included example programs using cargo. Use the following command structure:

    cargo run --release --example <name>

    Available examples:

    • pdb_symbols: Prints the name and location of every function and data value in the symbol table.
    • pdb2hpp: Generates an approximation of a C++ header file for a requested type.
    • pdb_lines: Outputs line number information for every symbol in every module.
    cargo run --release --example pdb_symbols
  3. Represent parsed Type Information (TPI) identifiers with IdData

    master

    The IdData<'t> enum is the primary container for parsed data representing different types of identifiers (IDs) found in Type Information (TPI). It encapsulates various specific ID structures, allowing you to handle different kinds of metadata (like functions, strings, or build info) through a single type.

    Supported variants include:

    • Function(FunctionId<'t>): Represents a global function, typically inlined.
    • MemberFunction(MemberFunctionId<'t>): Represents a member function, typically inlined.
    • BuildInfo(BuildInfoId): Contains tool, version, and command-line build information.
    • StringList(StringListId): A list of substrings.
    • String(StringId<'t>): A single string, often used for namespaces.
    • UserDefinedTypeSource(UserDefinedTypeSourceId): Source and line information for a User Defined Type (UDT) definition.
  4. Understand `BinaryAnnotation` variants and line emission

    master

    The BinaryAnnotation enum represents the parsed state changes in a PDB binary annotation stream. Some annotations are purely state-setting (like ChangeFile or ChangeColumnStart), while others are "emitting" annotations that trigger the creation of a line record.

    You can check if an annotation will trigger a line record by calling .emits_line_info() on the variant.

    // Example of checking if an annotation emits line info
    let annotation = BinaryAnnotation::ChangeCodeOffset(100);
    if annotation.emits_line_info() {
        // This annotation triggers a line record emission
    }
  5. Understand and use FrameTable to access stack frame information

    master

    A FrameTable contains FrameData entries that describe the stack layout of functions in a PDB. These entries are ordered by their internal PDB Relative Virtual Address (PdbInternalRva).

    Key behaviors:

    • Multiple Entries: A single function might be described by multiple entries; the entry where is_function_start is true marks the beginning of the function.
    • Missing Data: Not all functions have frame data. If no data is present, functions are assumed to have normal stack frames.
    • Address Conversion: FrameData uses PdbInternalRva. To convert these to actual Rva values, use PdbInternalRva::to_rva.

    To retrieve information for a specific function, use iter_at_rva(rva). This returns an iterator starting at the entry covering that RVA (or the closest preceding element).

    # use pdb::{PDB, Rva, FallibleIterator};
    #
    # fn test() -> pdb::Result<()> {
    # let source = std::fs::File::open("fixtures/self/foo.pdb")?;
    # let mut pdb = PDB::open(source)?;
    #
    // Read the frame table once and reuse it
    let frame_table = pdb.frame_table()?;
    let mut frames = frame_table.iter();
    
    // Iterate frame data in RVA order
    while let Some(frame) = frames.next()? {
        println!("{:#?}", frame);
    }
    # Ok(())
    # }
  6. Iterate over PDB symbols using `FallibleIterator`

    master

    Many collections in the pdb crate, such as the symbol table, implement FallibleIterator. This allows you to iterate over items while gracefully handling potential parsing errors during the iteration process using the .next()? pattern.

    // Example of iterating with FallibleIterator
    let mut symbols = symbol_table.iter();
    while let Some(symbol) = symbols.next()? {
        // symbol is successfully retrieved or error is returned
    }
  7. Use PDBInformation to verify PDB file identity

    master

    The PDBInformation struct provides metadata from the PDB information stream used to verify if a PDB file matches a specific binary.

    To ensure a match, the guid values must be identical, and the PDB age must be equal to or higher than the image's age. Note that the age field in PDBInformation is a count of how many times the PDB has been written and may differ from the age declared in the image; for a more reliable match, consider using DebugInformation::age.

    // Conceptual usage for matching
    if info.guid == image_guid && info.age >= image_age {
        // PDB is a valid match
    }
  8. Resolve cross-module type and ID references

    master

    When compiling with LTO, the compiler may reference types and IDs across modules. These are identified as ItemIndexes where the most significant bit is set to 1.

    To resolve a Local<I> index (where I is a TypeIndex or IdIndex) to a global one:

    1. Look up the index in the CrossModuleImports of the current module.
    2. Use the StringTable to resolve the referenced module's name.
    3. Find the Module with that name and load its ModuleInfo (matching names case-insensitively).
    4. Resolve the local index into a global one using CrossModuleExports.
  9. The `SymbolData` enum

    master

    The SymbolData<'t> enum is the primary way to interact with parsed PDB symbols. It contains the structured representation of various symbol types found in the stream.

    Common variants include:

    • Procedure(ProcedureSymbol<'t>): Represents functions or methods.
    • Data(DataSymbol<'t>): Represents static data like global variables.
    • Public(PublicSymbol<'t>): Represents public symbols with mangled names.
    • Constant(ConstantSymbol<'t>): Represents constant values.
    • UserDefinedType(UserDefinedTypeSymbol<'t>): Represents UDTs.
    • InlineSite(InlineSiteSymbol<'t>): Represents the callsite of an inlined function.
    • ScopeEnd: A marker for the end of a scope.

    You can use the .name() method on any SymbolData variant to attempt to retrieve a RawString<'t> name if the symbol type supports it.

    #[non_exhaustive]
    #[derive(Clone, Debug, Eq, PartialEq)]
    pub enum SymbolData<'t> {
        ScopeEnd,
        ObjName(ObjNameSymbol<'t>),
        RegisterVariable(RegisterVariableSymbol<'t>),
        Constant(ConstantSymbol<'t>),
        UserDefinedType(UserDefinedTypeSymbol<'t>),
        MultiRegisterVariable(MultiRegisterVariableSymbol<'t>),
        Data(DataSymbol<'t>),
        Public(PublicSymbol<'t>),
        Procedure(ProcedureSymbol<'t>),
        ThreadStorage(ThreadStorageSymbol<'t>),
        CompileFlags(CompileFlagsSymbol<'t>),
        UsingNamespace(UsingNamespaceSymbol<'t>),
        ProcedureReference(ProcedureReferenceSymbol<'t>),
        DataReference(DataReferenceSymbol<'t>),
        AnnotationReference(AnnotationReferenceSymbol<'t>),
        Trampoline(TrampolineSymbol),
        Export(ExportSymbol<'t>),
        Local(LocalSymbol<'t>),
        BuildInfo(BuildInfoSymbol),
        InlineSite(InlineSiteSymbol<'t>),
        InlineSiteEnd,
        ProcedureEnd,
        Label(LabelSymbol<'t>),
        Block(BlockSymbol<'t>),
        RegisterRelative(RegisterRelativeSymbol<'t>),
        Thunk(ThunkSymbol<'t>),
        SeparatedCode(SeparatedCodeSymbol),
    }
  10. How AddressMap works for address translation

    master

    An AddressMap is a helper used to translate between different types of addresses and offsets in a PDB and its corresponding PE binary. This is necessary because some Windows binaries are optimized (reordered) for paging reduction, meaning the addresses used in the PDB (internal) may not match the actual addresses in the executable (RVA).

    The AddressMap handles four primary address types:

    1. Rva: A Relative Virtual Address in the actual binary. These correspond to instruction pointers in stack traces and symbol addresses used by debuggers.
    2. PdbInternalRva: An RVA as it would have appeared in the binary before optimization. These are used within the PDB and must be converted to an actual Rva to be useful for debugging.
    3. SectionOffset: An offset into a section of the actual binary. It uses a 1-based section index (where 0 is a null pointer).
    4. PdbInternalSectionOffset: An offset into a section of the original (unoptimized) binary, used throughout the PDB.

    To use it, obtain an instance via PDB::address_map(). Once obtained, you can call conversion methods (like .to_rva() or .to_internal_offset()) on any of these types, passing the AddressMap as the translator.

    // Compute the address map once and reuse it
    let address_map = pdb.address_map()?;
    
    // Example: Converting a symbol offset to an RVA
    match pubsym.offset.to_rva(&address_map) {
        Some(rva) => println!("symbol is at {}", rva),
        None => println!("symbol refers to eliminated code"),
    }
  11. The `Symbol` struct and its lifecycle

    master

    A Symbol<'t> represents a single record from a PDB symbol stream. Internally, it is a reference to a slice of bytes (&[u8]) owned by a parent SymbolTable.

    Important: Because Symbols are references to data owned by the SymbolTable, a Symbol must not outlive its parent SymbolTable to avoid use-after-free errors.

    /// Represents a symbol from the symbol table.
    ///
    /// A `Symbol` is represented internally as a `&[u8]`, and in general the bytes inside are not
    /// inspected in any way before calling any of the accessor methods.
    ///
    /// To avoid copying, `Symbol`s exist as references to data owned by the parent `SymbolTable`.
    /// Therefore, a `Symbol` may not outlive its parent `SymbolTable`.
    #[derive(Copy, Clone, PartialEq)]
    pub struct Symbol<'t> {
        index: SymbolIndex,
        data: &'t [u8],
    }
  12. Iterate through the PDB Symbol Table

    master

    The SymbolTable provides access to the names, locations, and metadata of functions, data, and types within a PDB file. You can traverse the table sequentially using an iterator. Because the table is structured as a series of records with varying lengths, iteration is similar to traversing a linked list.

    To find specific information, you can iterate through all symbols and use symbol.parse() to convert the raw data into structured SymbolData variants (like Public, Procedure, or Data).

    # use pdb::FallibleIterator;
    # fn test() -> pdb::Result<usize> {
    # let file = std::fs::File::open("fixtures/self/foo.pdb")?;
    # let mut pdb = pdb::PDB::open(file)?;
    
    let symbol_table = pdb.global_symbols()?;
    let address_map = pdb.address_map()?;
    
    let mut count: usize = 0;
    let mut symbols = symbol_table.iter();
    while let Some(symbol) = symbols.next()? {
        match symbol.parse() {
            Ok(pdb::SymbolData::Public(data)) if data.function => {
                // Found a function location
                let rva = data.offset.to_rva(&address_map).unwrap_or_default();
                println!("{} is {}", rva, data.name);
                count += 1;
            }
            _ => {}
        }
    }
    # Ok(count)
    # }