PDB

Compared to DWARF debug info, the PDB debug info are always externalized from the original binary. Nevertheless, the original binary keeps the path of the PDB file in the attribute lief.PE.CodeViewPDB.filename / LIEF::PE::CodeViewPDB::filename().

Based on this fact, lief.Binary.debug_info or LIEF::Binary::debug_info() tries to instantiate a lief.pdb.DebugInfo or a LIEF::pdb::DebugInfo based on this file path. If it fails, it returns a nullptr/None.

One can also instantiate a lief.pdb.DebugInfo/LIEF::pdb::DebugInfo using LIEF::pdb::load() or lief.pdb.load():

import lief

pe = lief.PE.parse("some.exe")
if debug_info := pe.debug_info:
    assert isinstance(debug_info, lief.pdb.DebugInfo)
    print(f"PDB Debug handler: {debug_info}")

# Or you can load the PDB directly:
pdb: lief.pdb.DebugInfo = lief.pdb.load("some.pdb")

At this point, the PDB instance (lief.pdb.DebugInfo/LIEF::pdb::DebugInfo) can be used to explore the PDB debug info:

print("arg={}, guid={}", pdb.age, pdb.guid)

for sym in pdb.public_symbols:
    print("name={}, section={}, RVA={}",
          sym.name, sym.section_name, sym.RVA)

for ty in pdb.types:
    if isinstance(ty, lief.pdb.types.Class):
        print("Class[name]={}", ty.name)

for cu in pdb.compilation_units:
    print("module={}", cu.module_name)
    for src in cu.sources:
        print("  - {}", src)

    for func in cu.functions:
        print("name={}, section={}, RVA={}, code_size={}",
              func.name, func.section_name, func.RVA, func.code_size)

You can find the documentation of the API for the different languages here:

Python API

C++ API

Rust API: https://lief-rs.s3.fr-par.scw.cloud/doc/latest/lief/index.html