An OCaml library for reading and writing Microsoft PDB (Program Database) files and the CodeView debugging records they contain.
The library is a work in progress and not feature-complete. See Status below for what is and isn't covered.
This project started as my way of understanding Microsoft's PDB and CodeView formats for debugging information. Based on existing experience with DWARF on Unix systems, what is required to read and write valid debugging information on Windows such that debuggers like WinDbg work.
Potentially this work will find it's way into the OCaml compiler. I have treated writing this library as a learning exercise to understand PDB, it should be useful to others but it represents my understanding of the spec. Please post corrections as issues on the repository.
What works today:
- MSF (Multi-Stream File) container: read and write.
- PDB Info stream (Stream 1): version, GUID, age, named-stream map, feature flags.
- TPI and IPI streams: most CodeView leaf kinds covered.
- DBI stream: module list, section contributions, FileInfo source files, Optional Debug Header, EC substream, machine type.
- CodeView symbol records: roughly 30 symbol kinds plus an
Unknownfallback. - C13 debug subsections: Lines, FileChecksums, InlineeLines, StringTable, FrameData, CrossModuleExports, CrossModuleImports, Unknown.
- GSI/PSI hash tables: public and global symbol indices.
- A
Pdb_builderhigh-level API that assembles a complete PDB file from structured inputs (types, symbols, modules, source files). - Windows SEH unwind tables for x86-64 and ARM64 (
Unwindmodule), the PE/COFF analogue of DWARF CFI. - OMAP address-translation streams (
Omapmodule): reader, writer, and binary-search lookup for the post-link RVA remapping found in reordered/PGO retail PDBs. - Old-style FPO data stream (
Fpomodule): the per-function frame info format referenced from the DBI optional debug header'sfpo_datafield. The newer C13FrameDatasubsection is already covered. - An
pdbdumpexample tool, mirroring durin'sdwarfdump.
Known gaps:
- Cross-compilation-unit type merging is implemented by re-serialisation
(LLVM's
MergingTypeTableBuilderapproach): references are remapped onto a shared numbering and identical records deduplicated. The alternative content-hash scheme (LLVM's truncated-BLAKE3GlobalTypeTableBuilder, needed to emit COFF.debug$Hsections) is not implemented. - Many symbol and type variants exist primarily as a parser fallback
(
Unknown); only the kinds we've needed have hand-written fields. - The
Pdb_builder.machinevariantsI386(32-bit x86) andARM(32-bit ARM) are recognised by the builder API and serialised to the correct COFF machine constants, but there is limited testing for 32-bit platforms. Open an issue if you need 32-bit coverage.
- Reader / writer split. Each stream has a read module (
Tpi,Dbi) and a paired_writemodule (Tpi_write,Dbi_write). - Cursor-based reads, buffer-based writes. Readers consume
Object.Buffer.cursor(from theobjectlibrary), writers accumulate intoStdlib.Buffer.t. - Lazy iteration. Type records, symbol records, module lists, and
debug subsections are exposed as
Seq.tso large streams don't materialise eagerly. - No cyclic data. TypeIndex references stay as
u32. A resolver function looks records up by index; the type record itself never holds a pointer to another record. - Unsigned arithmetic everywhere. The
integerslibrary'sUnsigned.UInt32/UInt16are used uniformly for sizes, offsets, and type indices. - Architecture is a parameter, not a separate library. Windows
unwind info uses one of two on-disk formats depending on architecture
(x86-64 SEH vs ARM64 SEH). The
Unwindmodule exposes both via a common interface.
object: multi-format object-file library that provides the PE/COFF reader, theObject.Bufferabstraction, and a few cross-format utilities.integers: forUnsigned.UInt32/UInt16types that aren't in OCaml's stdlib.alcotest,qcheck-core,qcheck-alcotestfor testing.- LLVM 15+ (
llvm-pdbutil) at test time. It is used to validate written PDBs and to convert LLVM YAML fixtures into reference PDBs. Later versions of LLVM provide better PDB support.
dune build
dune runtestTests are organised in three layers:
test/unit/: alcotest + qcheck unit tests for each module, including property-based roundtrip tests.test/integration/: end-to-end tests that build PDBs viaPdb_builder, validate them withllvm-pdbutil, and parse LLVM-built PDBs back through our reader.test/cram/: cram tests for thepdbdumpexample.
The integration suite includes a "LLVM equivalence" harness that, for each
of LLVM's bundled YAML test fixtures, builds an equivalent PDB using our
writer and asserts that llvm-pdbutil dump output matches text-for-text.
The LLVM YAML files are not copied into this repo rather they are read live
from a local checkout of llvm-project, located via the
LLVM_PROJECT_DIR environment variable. Tests skip cleanly when
llvm-pdbutil is not on PATH or the LLVM tree cannot be found.
let () =
let b = Pdb.Pdb_builder.create Pdb.Pdb_builder.AMD64 in
let no_type = Pdb.Type_index.of_u32 Unsigned.UInt32.zero in
let proc =
Pdb.Pdb_builder.add_type b
(Pdb.Codeview_types.Procedure
{ return_type = Pdb.Type_index.int32;
calling_conv = Pdb.Codeview_constants.NearC;
options = 0;
param_count = 0;
arg_list = no_type })
in
let _ =
Pdb.Pdb_builder.add_id b
(Pdb.Codeview_types.FuncId
{ scope_id = no_type;
func_type = proc;
name = "main" })
in
Pdb.Pdb_builder.add_module b
{ name = "main.obj";
obj_file = "main.obj";
symbols = [];
subsections = [];
section_contrib = None;
source_files = [ "main.ml" ] };
print_string (Pdb.Pdb_builder.finalize b)The pdbdump example reads a PDB and dumps human-readable output similar
to llvm-pdbutil dump:
dune exec example/pdbdump.exe -- --summary --types path/to/file.pdbThe PDB and CodeView formats have no formal specification. Everything in this library was derived by reading existing implementations and cross-checking their behaviour against each other. Where one implementation was particularly load-bearing for a given subsystem it is called out below. This project owes a great deal to all of them.
-
LLVM (
llvm/lib/DebugInfo/PDB,llvm/lib/DebugInfo/CodeView, docs, Apache 2.0 WITH LLVM-exception) is the primary reference. It is the most complete open-source PDB reader and writer, andllvm-pdbutilis the ground-truth oracle used throughout this project's test suite. The PDB V1 hash and CRC32-based V8 hash inlib/hash.mlare ports of LLVM'sHash.cpp. A small C reference implementation of those hashes from LLVM is kept verbatim intest/oracle/hash_oracle.c(with the original Apache-2.0 WITH LLVM-exception header preserved) and used as a differential oracle. -
getsentry/pdb (Rust, Apache 2.0 / MIT) is the most mature standalone PDB reader. Its lazy parsing approach, similar in style to
gimlifor DWARF, informed the shape of ourSeq.t-based iteration. -
microsoft/pdb (C++) is Microsoft's reference dump of the PDB sources. It is unbuildable in isolation but is the closest thing to authoritative documentation for the on-disk layout.
-
MolecularMatters/raw_pdb (C++11, BSD-2-Clause) is a zero-copy PDB reader. Useful as a performance and layout reference, and for cross-checking corner cases in MSF page reassembly.
-
The LLVM PDB test fixtures (
llvm/test/DebugInfo/PDB/Inputs) are used live by the LLVM-equivalence harness -- they are not copied into this repo; the test driver reads them out of a local LLVM checkout.
If you spot a misstatement about any of these projects, or an omission, please open an issue.
- object -- the underlying object-file library that provides PE/COFF parsing and the buffer abstraction we read against.