unit PdbParser;
{ courtesy codex, May 14, 2026, Thaddy de Koning }
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils;
type
EPdbError = class(Exception);
TPdbInfo = record
Version: DWord;
Signature: DWord;
Age: DWord;
Guid: TGuid;
end;
TPdbStreamInfo = record
Size: Int64;
Blocks: array of DWord;
end;
TPdbDbiInfo = record
VersionSignature: LongInt;
VersionHeader: DWord;
Age: DWord;
GlobalSymbolStream: SmallInt;
PublicSymbolStream: SmallInt;
SymbolRecordStream: SmallInt;
end;
TPdbPublicSymbol = record
Name: string;
Segment: Word;
Offset: DWord;
Flags: DWord;
end;
TPdbPublicSymbolArray = array of TPdbPublicSymbol;
{ TPdbFile parses the MSF container used by Microsoft PDB files.
It is intentionally small: it reads the stream directory, PDB info stream,
DBI stream indices, and simple CodeView S_PUB32/S_PUB32_ST records. }
TPdbFile = class
private
FData: TBytes;
FBlockSize: DWord;
FBlockCount: DWord;
FDirectorySize: DWord;
FBlockMapAddress: DWord;
FStreams: array of TPdbStreamInfo;
function ReadUInt16(const Offset: Int64): Word;
function ReadInt16(const Offset: Int64): SmallInt;
function ReadUInt32(const Offset: Int64): DWord;
function ReadInt32(const Offset: Int64): LongInt;
procedure CheckRange(const Offset, Count: Int64);
procedure ParseSuperBlock;
procedure ParseStreamDirectory;
function StreamBlockCount(const StreamIndex: Integer): Integer;
public
constructor Create(const FileName: string);
function StreamCount: Integer;
function StreamSize(const StreamIndex: Integer): Int64;
function ReadStream(const StreamIndex: Integer): TBytes;
function TryReadPdbInfo(out Info: TPdbInfo): Boolean;
function TryReadDbiInfo(out Info: TPdbDbiInfo): Boolean;
function ReadPublicSymbols: TPdbPublicSymbolArray;
property BlockSize: DWord read FBlockSize;
property BlockCount: DWord read FBlockCount;
property DirectorySize: DWord read FDirectorySize;
property BlockMapAddress: DWord read FBlockMapAddress;
end;
implementation
const
PdbSignature: AnsiString = 'Microsoft C/C++ MSF 7.00';
StreamPdb = 1;
StreamDbi = 3;
NilStreamSize = DWord($FFFFFFFF);
S_PUB32_ST = Word($1009);
S_PUB32 = Word($110E);
function AlignUp(Value, Alignment: Int64): Int64;
begin
if Alignment <= 0 then
raise EPdbError.Create('Invalid alignment');
Result := ((Value + Alignment - 1) div Alignment) * Alignment;
end;
function BytesToAnsiString(const Data: TBytes; Offset, Count: Integer): string;
var
I: Integer;
begin
SetLength(Result, Count);
for I := 0 to Count - 1 do
Result[I + 1] := AnsiChar(Data[Offset + I]);
end;
constructor TPdbFile.Create(const FileName: string);
var
S: TFileStream;
begin
inherited Create;
S := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
SetLength(FData, S.Size);
if S.Size > 0 then
S.ReadBuffer(FData[0], S.Size);
finally
S.Free;
end;
ParseSuperBlock;
ParseStreamDirectory;
end;
procedure TPdbFile.CheckRange(const Offset, Count: Int64);
begin
if (Offset < 0) or (Count < 0) or (Offset + Count > Length(FData)) then
raise EPdbError.CreateFmt('PDB read outside file at offset %d, size %d', [Offset, Count]);
end;
function TPdbFile.ReadUInt16(const Offset: Int64): Word;
begin
CheckRange(Offset, SizeOf(Result));
Move(FData[Offset], Result, SizeOf(Result));
end;
function TPdbFile.ReadInt16(const Offset: Int64): SmallInt;
begin
CheckRange(Offset, SizeOf(Result));
Move(FData[Offset], Result, SizeOf(Result));
end;
function TPdbFile.ReadUInt32(const Offset: Int64): DWord;
begin
CheckRange(Offset, SizeOf(Result));
Move(FData[Offset], Result, SizeOf(Result));
end;
function TPdbFile.ReadInt32(const Offset: Int64): LongInt;
begin
CheckRange(Offset, SizeOf(Result));
Move(FData[Offset], Result, SizeOf(Result));
end;
procedure TPdbFile.ParseSuperBlock;
var
I: Integer;
begin
CheckRange(0, 56);
for I := 1 to Length(PdbSignature) do
if AnsiChar(FData[I - 1]) <> PdbSignature[I] then
raise EPdbError.Create('Not an MSF 7.0 PDB file');
FBlockSize := ReadUInt32(32);
FBlockCount := ReadUInt32(40);
FDirectorySize := ReadUInt32(44);
FBlockMapAddress := ReadUInt32(52);
if (FBlockSize = 0) or (FBlockSize > 1024 * 1024) then
raise EPdbError.CreateFmt('Unsupported PDB block size %d', [FBlockSize]);
if Int64(FBlockCount) * FBlockSize > Length(FData) then
raise EPdbError.Create('PDB block table extends past the file size');
if FBlockMapAddress >= FBlockCount then
raise EPdbError.Create('Invalid PDB stream directory block map address');
end;
procedure TPdbFile.ParseStreamDirectory;
var
DirBlockCount, I, J, Pos, StreamCountValue, BlockCountValue: Integer;
MapOffset, DirOffset: Int64;
DirBlocks: array of DWord;
DirData: TBytes;
function DirReadUInt32: DWord;
begin
if Pos + 4 > Length(DirData) then
raise EPdbError.Create('Truncated PDB stream directory');
Move(DirData[Pos], Result, 4);
Inc(Pos, 4);
end;
begin
DirBlockCount := AlignUp(FDirectorySize, FBlockSize) div FBlockSize;
SetLength(DirBlocks, DirBlockCount);
MapOffset := Int64(FBlockMapAddress) * FBlockSize;
CheckRange(MapOffset, Int64(DirBlockCount) * SizeOf(DWord));
for I := 0 to DirBlockCount - 1 do
begin
DirBlocks[I] := ReadUInt32(MapOffset + I * SizeOf(DWord));
if DirBlocks[I] >= FBlockCount then
raise EPdbError.Create('Invalid stream directory block number');
end;
SetLength(DirData, DirBlockCount * FBlockSize);
for I := 0 to DirBlockCount - 1 do
begin
DirOffset := Int64(DirBlocks[I]) * FBlockSize;
CheckRange(DirOffset, FBlockSize);
Move(FData[DirOffset], DirData[I * FBlockSize], FBlockSize);
end;
SetLength(DirData, FDirectorySize);
Pos := 0;
StreamCountValue := DirReadUInt32;
if StreamCountValue < 0 then
raise EPdbError.Create('Invalid stream count');
SetLength(FStreams, StreamCountValue);
for I := 0 to StreamCountValue - 1 do
FStreams[I].Size := DirReadUInt32;
for I := 0 to StreamCountValue - 1 do
begin
if DWord(FStreams[I].Size) = NilStreamSize then
begin
FStreams[I].Size := -1;
Continue;
end;
BlockCountValue := AlignUp(FStreams[I].Size, FBlockSize) div FBlockSize;
SetLength(FStreams[I].Blocks, BlockCountValue);
for J := 0 to BlockCountValue - 1 do
begin
FStreams[I].Blocks[J] := DirReadUInt32;
if FStreams[I].Blocks[J] >= FBlockCount then
raise EPdbError.CreateFmt('Invalid block number for stream %d', [I]);
end;
end;
end;
function TPdbFile.StreamBlockCount(const StreamIndex: Integer): Integer;
begin
Result := Length(FStreams[StreamIndex].Blocks);
end;
function TPdbFile.StreamCount: Integer;
begin
Result := Length(FStreams);
end;
function TPdbFile.StreamSize(const StreamIndex: Integer): Int64;
begin
if (StreamIndex < 0) or (StreamIndex >= StreamCount) then
raise EPdbError.CreateFmt('Invalid PDB stream index %d', [StreamIndex]);
Result := FStreams[StreamIndex].Size;
end;
function TPdbFile.ReadStream(const StreamIndex: Integer): TBytes;
var
I: Integer;
CopyCount, Remaining: Int64;
SourceOffset, DestOffset: Int64;
begin
if (StreamIndex < 0) or (StreamIndex >= StreamCount) then
raise EPdbError.CreateFmt('Invalid PDB stream index %d', [StreamIndex]);
if FStreams[StreamIndex].Size < 0 then
raise EPdbError.CreateFmt('PDB stream %d is not present', [StreamIndex]);
SetLength(Result, FStreams[StreamIndex].Size);
Remaining := Length(Result);
DestOffset := 0;
for I := 0 to StreamBlockCount(StreamIndex) - 1 do
begin
CopyCount := FBlockSize;
if CopyCount > Remaining then
CopyCount := Remaining;
SourceOffset := Int64(FStreams[StreamIndex].Blocks[I]) * FBlockSize;
CheckRange(SourceOffset, CopyCount);
if CopyCount > 0 then
Move(FData[SourceOffset], Result[DestOffset], CopyCount);
Inc(DestOffset, CopyCount);
Dec(Remaining, CopyCount);
end;
end;
function TPdbFile.TryReadPdbInfo(out Info: TPdbInfo): Boolean;
var
Data: TBytes;
begin
FillChar(Info, SizeOf(Info), 0);
Result := (StreamPdb < StreamCount) and (StreamSize(StreamPdb) >= 24);
if not Result then
Exit;
Data := ReadStream(StreamPdb);
Move(Data[0], Info.Version, SizeOf(Info.Version));
Move(Data[4], Info.Signature, SizeOf(Info.Signature));
Move(Data[8], Info.Age, SizeOf(Info.Age));
Move(Data[12], Info.Guid, SizeOf(Info.Guid));
end;
function TPdbFile.TryReadDbiInfo(out Info: TPdbDbiInfo): Boolean;
var
Data: TBytes;
begin
FillChar(Info, SizeOf(Info), 0);
Result := (StreamDbi < StreamCount) and (StreamSize(StreamDbi) >= 20);
if not Result then
Exit;
Data := ReadStream(StreamDbi);
Move(Data[0], Info.VersionSignature, SizeOf(Info.VersionSignature));
Move(Data[4], Info.VersionHeader, SizeOf(Info.VersionHeader));
Move(Data[8], Info.Age, SizeOf(Info.Age));
Move(Data[12], Info.GlobalSymbolStream, SizeOf(Info.GlobalSymbolStream));
Move(Data[14], Info.PublicSymbolStream, SizeOf(Info.PublicSymbolStream));
Move(Data[16], Info.SymbolRecordStream, SizeOf(Info.SymbolRecordStream));
end;
function TPdbFile.ReadPublicSymbols: TPdbPublicSymbolArray;
var
Dbi: TPdbDbiInfo;
Data: TBytes;
Pos, RecordStart, NameStart, NameEnd, Count: Integer;
RecordLength, RecordKind: Word;
Symbol: TPdbPublicSymbol;
procedure AddSymbol(const Value: TPdbPublicSymbol);
begin
if Count = Length(Result) then
SetLength(Result, Count + 64);
Result[Count] := Value;
Inc(Count);
end;
begin
SetLength(Result, 0);
Count := 0;
if not TryReadDbiInfo(Dbi) then
Exit;
if (Dbi.SymbolRecordStream < 0) or (Dbi.SymbolRecordStream >= StreamCount) then
Exit;
if StreamSize(Dbi.SymbolRecordStream) <= 0 then
Exit;
Data := ReadStream(Dbi.SymbolRecordStream);
Pos := 0;
while Pos + 4 <= Length(Data) do
begin
Move(Data[Pos], RecordLength, SizeOf(RecordLength));
RecordStart := Pos + 2;
if (RecordLength < 2) or (RecordStart + RecordLength > Length(Data)) then
Break;
Move(Data[RecordStart], RecordKind, SizeOf(RecordKind));
if ((RecordKind = S_PUB32) or (RecordKind = S_PUB32_ST)) and (RecordLength >= 14) then
begin
Move(Data[RecordStart + 2], Symbol.Flags, SizeOf(Symbol.Flags));
Move(Data[RecordStart + 6], Symbol.Offset, SizeOf(Symbol.Offset));
Move(Data[RecordStart + 10], Symbol.Segment, SizeOf(Symbol.Segment));
NameStart := RecordStart + 12;
NameEnd := NameStart;
while (NameEnd < RecordStart + RecordLength) and (Data[NameEnd] <> 0) do
Inc(NameEnd);
Symbol.Name := BytesToAnsiString(Data, NameStart, NameEnd - NameStart);
AddSymbol(Symbol);
end;
Pos := RecordStart + AlignUp(RecordLength, 4);
end;
SetLength(Result, Count);
end;
end.