Recent

Author Topic: Read/Parse PDB file to get symbol details  (Read 966 times)

tooknox

  • New Member
  • *
  • Posts: 40
Read/Parse PDB file to get symbol details
« on: May 13, 2026, 02:11:02 pm »
Hi Everyone,

Greetings!!

I am trying to extract/parse/read symbol info from MS PDB debug files (using fpc ofc). The idea is to specify the PDB file path and the symbol name to dump all the info related to it. I found MS Debug Help library dbghelp.dll, but that looks steep is there anything else I can try to simplify this?

Thanks in advance
« Last Edit: May 13, 2026, 02:16:13 pm by tooknox »

440bx

  • Hero Member
  • *****
  • Posts: 6531
Re: Read/Parse PDB file to get symbol details
« Reply #1 on: May 13, 2026, 05:10:59 pm »
Disclaimer: I knew the answer to your question but figured I'd let A.I provide the details because there are quite a few worth mentioning.  That means what follows, unlike this sentence and the previous sentence, is A.I generated:

To dump the contents of a Microsoft Program Database (PDB) file in C, the standard approach is to use the Microsoft Debug Interface Access (DIA) SDK, which provides a COM-based API for querying symbols and debugging information.
  • 1. Use the DIA SDK (Official Method)

The DIA SDK is the most reliable way to access PDB data because it abstracts the underlying file format, which Microsoft frequently changes.
  • Acquire Data Source: Create an IDiaDataSource interface via CoCreateInstance using CLSID_DiaSource.
  • Load the PDB: Use the loadDataFromPdb method to open your specific .pdb file.
  • Open a Session: Call openSession to get an IDiaSession, which serves as your main entry point for queries.
  • Enumerate Symbols: Retrieve the global scope via get_globalScope and use findChildren to iterate through functions, types, and other symbols.

Microsoft provides a comprehensive built-in example called DIA2Dump in the Visual Studio installation directory (typically under DIA SDK\Samples\DIA2Dump) that demonstrates exactly how to dump every section of a PDB file.

  • 2. Alternative Tools and Libraries

If you want to avoid writing a full COM application, you can use existing utilities or libraries:
  • PDBDump: A tool that can dump PDB contents to JSON, XML, or SQLite3 formats for easier processing.
  • pdbex: A utility specifically designed to reconstruct C headers from PDB files, which is useful if your goal is to see structure and union definitions.
  • RawPDB: A C++11 library for directly reading PDB files if you need low-level access without the DIA SDK overhead.


No longer A.I text.  the formatting is mine, not A.I's, A.I would have probably done a better job but, it left out a utility that also accesses the PDB called cv2pdb which converts CodeView info found in the PDB into DWARF.  It uses DIA, you may find it instructive for what you're trying to do (in addition to the ones mentioned above whose source is also available - in C or C++ of course.)

I am not aware of a Pascal implementation of .pdb "massaging" or dumping.

HTH.
[/list]
« Last Edit: May 13, 2026, 05:12:50 pm by 440bx »
FPC v3.2.2 and Lazarus v4.0rc3 on Windows 7 SP1 64bit.

tooknox

  • New Member
  • *
  • Posts: 40
Re: Read/Parse PDB file to get symbol details
« Reply #2 on: May 13, 2026, 05:38:09 pm »
MS DIA SDK seems promising because of the COM based API, shouldn't it be possble to call the COM based API from freepascal I mean as long as I am just using a few API functions and declare them properly??

In short should I attempt this in pascal?

440bx

  • Hero Member
  • *****
  • Posts: 6531
Re: Read/Parse PDB file to get symbol details
« Reply #3 on: May 13, 2026, 06:32:29 pm »
In short should I attempt this in pascal?
I cannot think of a reason why not.  Usually, COM Pascal code is cleaner than its C++ counterpart (and no comparison with C's.)

I don't know if there is a port of the DIA COM api... that, you'll have to find out.
FPC v3.2.2 and Lazarus v4.0rc3 on Windows 7 SP1 64bit.

cdbc

  • Hero Member
  • *****
  • Posts: 2814
    • http://www.cdbc.dk
Re: Read/Parse PDB file to get symbol details
« Reply #4 on: May 13, 2026, 07:19:28 pm »
Hi
Yes, you should definitely use DIA com-interfaces, it's sorta like programming with classes ~ Object-Oriented, once you get the hang of it, it's prolly way easier than you think  :D
Regards Benny
If it ain't broke, don't fix it ;)
PCLinuxOS(rolling release) 64bit -> KDE6/QT6 -> FPC Release -> Lazarus Release &  FPC Main -> Lazarus Main

tooknox

  • New Member
  • *
  • Posts: 40
Re: Read/Parse PDB file to get symbol details
« Reply #5 on: May 14, 2026, 09:22:16 am »
@cdbc You were right about it being just objects and methods, I was able to successfully use the msdia.dll com library (still have a few questions at the end though)

Here is what I did,

1. Find the path of msdia140.dll in the msvc installation/DIA SDK folder and register it with regsvr32.dll

Code: Text  [Select][+][-]
  1. regsvr32 <base_path>\VC\Tools\MSVC\14.50.35717\bin\Hostx64\x64\msdia140.dll

2. Use the FPC importtl utility to generate the pascal unit for the COM DLL.

https://wiki.freepascal.org/Windows_Programming_Tips#COM_Programming

3. Then this sample code can get for eg the RVA of a symbol in PDB

Code: Pascal  [Select][+][-]
  1. program msdiacom;
  2. uses
  3.   Windows, ComObj, ActiveX, SysUtils, MSDIA140_TLB;
  4.  
  5. // generated MSDIA140_TLB.pas containing declarations using importtl utility
  6. const
  7.   // NameSearchOptions Constants from Microsoft DIA SDK (dia2.h)
  8.   nsNone               = 0;
  9.   nsfCaseSensitive     = 1;
  10.   nsfCaseInsensitive   = 2;
  11.   nsfFNameExt          = 4;
  12.   nsfRegularExpression = 8;
  13.   nsfUndecoratedName   = 16;
  14.  
  15. procedure GetSymbolRVA(const PdbPath, SymbolName: WideString);
  16. var
  17.   DataSource: IDiaDataSource;
  18.   Session: IDiaSession;
  19.   GlobalScope: IDiaSymbol;
  20.   EnumSymbols: IDiaEnumSymbols;
  21.   Symbol: IDiaSymbol;
  22.   RVA: QWORD;
  23.   Fetched: ULONG;
  24. begin
  25.   //RVA := 0;
  26.   CoInitialize(nil);
  27.   try
  28.  
  29.     DataSource := CreateComObject(CLASS_DiaSource) as IDiaDataSource;
  30.  
  31.     if DataSource.loadDataFromPdb(PWideChar(PdbPath)) <> S_OK then
  32.       raise Exception.Create('Failed to load PDB');
  33.  
  34.     DataSource.openSession(Session);
  35.  
  36.     GlobalScope := Session.get_globalScope;
  37.  
  38.     GlobalScope.findChildren(SymTagFunction, PWideChar(SymbolName), nsNone, EnumSymbols);
  39.  
  40.     if EnumSymbols.Next(1, Symbol, Fetched) = S_OK then
  41.     begin
  42.         RVA := Symbol.get_relativeVirtualAddress;
  43.         WriteLn(Symbol.Get_name, ' RVA: ', IntToHex(RVA));
  44.     end;
  45.  
  46.   finally
  47.     CoUninitialize;
  48.   end;
  49. end;
  50.  
  51. begin
  52.   GetSymbolRVA('C:\test.pdb', 'main');
  53. end.
  54.  
  55.  

However, I banged my head for a few hours when I got access violation errors. I had to change the calling convention for get_relativeVirtualAddress function in the importtl generated pas file from stdcall to safecall.

Why didn't it work with stdcall which is technically the correct call conv??
and any way to make importtl utility correct it instead of manual workaround??

Attaching the importtl generated pas file for reference.
« Last Edit: May 14, 2026, 11:00:12 am by tooknox »

Thaddy

  • Hero Member
  • *****
  • Posts: 19267
  • Glad to be alive.
Re: Read/Parse PDB file to get symbol details
« Reply #6 on: May 14, 2026, 10:19:35 am »
This is quite simple, but for basic pdb parsing it is sufficient.
Code: Pascal  [Select][+][-]
  1. unit PdbParser;
  2. { courtesy codex, May 14, 2026, Thaddy de Koning }
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils;
  9.  
  10. type
  11.   EPdbError = class(Exception);
  12.  
  13.   TPdbInfo = record
  14.     Version: DWord;
  15.     Signature: DWord;
  16.     Age: DWord;
  17.     Guid: TGuid;
  18.   end;
  19.  
  20.   TPdbStreamInfo = record
  21.     Size: Int64;
  22.     Blocks: array of DWord;
  23.   end;
  24.  
  25.   TPdbDbiInfo = record
  26.     VersionSignature: LongInt;
  27.     VersionHeader: DWord;
  28.     Age: DWord;
  29.     GlobalSymbolStream: SmallInt;
  30.     PublicSymbolStream: SmallInt;
  31.     SymbolRecordStream: SmallInt;
  32.   end;
  33.  
  34.   TPdbPublicSymbol = record
  35.     Name: string;
  36.     Segment: Word;
  37.     Offset: DWord;
  38.     Flags: DWord;
  39.   end;
  40.  
  41.   TPdbPublicSymbolArray = array of TPdbPublicSymbol;
  42.  
  43.   { TPdbFile parses the MSF container used by Microsoft PDB files.
  44.     It is intentionally small: it reads the stream directory, PDB info stream,
  45.     DBI stream indices, and simple CodeView S_PUB32/S_PUB32_ST records. }
  46.   TPdbFile = class
  47.   private
  48.     FData: TBytes;
  49.     FBlockSize: DWord;
  50.     FBlockCount: DWord;
  51.     FDirectorySize: DWord;
  52.     FBlockMapAddress: DWord;
  53.     FStreams: array of TPdbStreamInfo;
  54.     function ReadUInt16(const Offset: Int64): Word;
  55.     function ReadInt16(const Offset: Int64): SmallInt;
  56.     function ReadUInt32(const Offset: Int64): DWord;
  57.     function ReadInt32(const Offset: Int64): LongInt;
  58.     procedure CheckRange(const Offset, Count: Int64);
  59.     procedure ParseSuperBlock;
  60.     procedure ParseStreamDirectory;
  61.     function StreamBlockCount(const StreamIndex: Integer): Integer;
  62.   public
  63.     constructor Create(const FileName: string);
  64.     function StreamCount: Integer;
  65.     function StreamSize(const StreamIndex: Integer): Int64;
  66.     function ReadStream(const StreamIndex: Integer): TBytes;
  67.     function TryReadPdbInfo(out Info: TPdbInfo): Boolean;
  68.     function TryReadDbiInfo(out Info: TPdbDbiInfo): Boolean;
  69.     function ReadPublicSymbols: TPdbPublicSymbolArray;
  70.     property BlockSize: DWord read FBlockSize;
  71.     property BlockCount: DWord read FBlockCount;
  72.     property DirectorySize: DWord read FDirectorySize;
  73.     property BlockMapAddress: DWord read FBlockMapAddress;
  74.   end;
  75.  
  76. implementation
  77.  
  78. const
  79.   PdbSignature: AnsiString = 'Microsoft C/C++ MSF 7.00';
  80.   StreamPdb = 1;
  81.   StreamDbi = 3;
  82.   NilStreamSize = DWord($FFFFFFFF);
  83.  
  84.   S_PUB32_ST = Word($1009);
  85.   S_PUB32 = Word($110E);
  86.  
  87. function AlignUp(Value, Alignment: Int64): Int64;
  88. begin
  89.   if Alignment <= 0 then
  90.     raise EPdbError.Create('Invalid alignment');
  91.   Result := ((Value + Alignment - 1) div Alignment) * Alignment;
  92. end;
  93.  
  94. function BytesToAnsiString(const Data: TBytes; Offset, Count: Integer): string;
  95. var
  96.   I: Integer;
  97. begin
  98.   SetLength(Result, Count);
  99.   for I := 0 to Count - 1 do
  100.     Result[I + 1] := AnsiChar(Data[Offset + I]);
  101. end;
  102.  
  103. constructor TPdbFile.Create(const FileName: string);
  104. var
  105.   S: TFileStream;
  106. begin
  107.   inherited Create;
  108.   S := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  109.   try
  110.     SetLength(FData, S.Size);
  111.     if S.Size > 0 then
  112.       S.ReadBuffer(FData[0], S.Size);
  113.   finally
  114.     S.Free;
  115.   end;
  116.   ParseSuperBlock;
  117.   ParseStreamDirectory;
  118. end;
  119.  
  120. procedure TPdbFile.CheckRange(const Offset, Count: Int64);
  121. begin
  122.   if (Offset < 0) or (Count < 0) or (Offset + Count > Length(FData)) then
  123.     raise EPdbError.CreateFmt('PDB read outside file at offset %d, size %d', [Offset, Count]);
  124. end;
  125.  
  126. function TPdbFile.ReadUInt16(const Offset: Int64): Word;
  127. begin
  128.   CheckRange(Offset, SizeOf(Result));
  129.   Move(FData[Offset], Result, SizeOf(Result));
  130. end;
  131.  
  132. function TPdbFile.ReadInt16(const Offset: Int64): SmallInt;
  133. begin
  134.   CheckRange(Offset, SizeOf(Result));
  135.   Move(FData[Offset], Result, SizeOf(Result));
  136. end;
  137.  
  138. function TPdbFile.ReadUInt32(const Offset: Int64): DWord;
  139. begin
  140.   CheckRange(Offset, SizeOf(Result));
  141.   Move(FData[Offset], Result, SizeOf(Result));
  142. end;
  143.  
  144. function TPdbFile.ReadInt32(const Offset: Int64): LongInt;
  145. begin
  146.   CheckRange(Offset, SizeOf(Result));
  147.   Move(FData[Offset], Result, SizeOf(Result));
  148. end;
  149.  
  150. procedure TPdbFile.ParseSuperBlock;
  151. var
  152.   I: Integer;
  153. begin
  154.   CheckRange(0, 56);
  155.   for I := 1 to Length(PdbSignature) do
  156.     if AnsiChar(FData[I - 1]) <> PdbSignature[I] then
  157.       raise EPdbError.Create('Not an MSF 7.0 PDB file');
  158.  
  159.   FBlockSize := ReadUInt32(32);
  160.   FBlockCount := ReadUInt32(40);
  161.   FDirectorySize := ReadUInt32(44);
  162.   FBlockMapAddress := ReadUInt32(52);
  163.  
  164.   if (FBlockSize = 0) or (FBlockSize > 1024 * 1024) then
  165.     raise EPdbError.CreateFmt('Unsupported PDB block size %d', [FBlockSize]);
  166.   if Int64(FBlockCount) * FBlockSize > Length(FData) then
  167.     raise EPdbError.Create('PDB block table extends past the file size');
  168.   if FBlockMapAddress >= FBlockCount then
  169.     raise EPdbError.Create('Invalid PDB stream directory block map address');
  170. end;
  171.  
  172. procedure TPdbFile.ParseStreamDirectory;
  173. var
  174.   DirBlockCount, I, J, Pos, StreamCountValue, BlockCountValue: Integer;
  175.   MapOffset, DirOffset: Int64;
  176.   DirBlocks: array of DWord;
  177.   DirData: TBytes;
  178.  
  179.   function DirReadUInt32: DWord;
  180.   begin
  181.     if Pos + 4 > Length(DirData) then
  182.       raise EPdbError.Create('Truncated PDB stream directory');
  183.     Move(DirData[Pos], Result, 4);
  184.     Inc(Pos, 4);
  185.   end;
  186.  
  187. begin
  188.   DirBlockCount := AlignUp(FDirectorySize, FBlockSize) div FBlockSize;
  189.   SetLength(DirBlocks, DirBlockCount);
  190.   MapOffset := Int64(FBlockMapAddress) * FBlockSize;
  191.   CheckRange(MapOffset, Int64(DirBlockCount) * SizeOf(DWord));
  192.  
  193.   for I := 0 to DirBlockCount - 1 do
  194.   begin
  195.     DirBlocks[I] := ReadUInt32(MapOffset + I * SizeOf(DWord));
  196.     if DirBlocks[I] >= FBlockCount then
  197.       raise EPdbError.Create('Invalid stream directory block number');
  198.   end;
  199.  
  200.   SetLength(DirData, DirBlockCount * FBlockSize);
  201.   for I := 0 to DirBlockCount - 1 do
  202.   begin
  203.     DirOffset := Int64(DirBlocks[I]) * FBlockSize;
  204.     CheckRange(DirOffset, FBlockSize);
  205.     Move(FData[DirOffset], DirData[I * FBlockSize], FBlockSize);
  206.   end;
  207.   SetLength(DirData, FDirectorySize);
  208.  
  209.   Pos := 0;
  210.   StreamCountValue := DirReadUInt32;
  211.   if StreamCountValue < 0 then
  212.     raise EPdbError.Create('Invalid stream count');
  213.   SetLength(FStreams, StreamCountValue);
  214.  
  215.   for I := 0 to StreamCountValue - 1 do
  216.     FStreams[I].Size := DirReadUInt32;
  217.  
  218.   for I := 0 to StreamCountValue - 1 do
  219.   begin
  220.     if DWord(FStreams[I].Size) = NilStreamSize then
  221.     begin
  222.       FStreams[I].Size := -1;
  223.       Continue;
  224.     end;
  225.  
  226.     BlockCountValue := AlignUp(FStreams[I].Size, FBlockSize) div FBlockSize;
  227.     SetLength(FStreams[I].Blocks, BlockCountValue);
  228.     for J := 0 to BlockCountValue - 1 do
  229.     begin
  230.       FStreams[I].Blocks[J] := DirReadUInt32;
  231.       if FStreams[I].Blocks[J] >= FBlockCount then
  232.         raise EPdbError.CreateFmt('Invalid block number for stream %d', [I]);
  233.     end;
  234.   end;
  235. end;
  236.  
  237. function TPdbFile.StreamBlockCount(const StreamIndex: Integer): Integer;
  238. begin
  239.   Result := Length(FStreams[StreamIndex].Blocks);
  240. end;
  241.  
  242. function TPdbFile.StreamCount: Integer;
  243. begin
  244.   Result := Length(FStreams);
  245. end;
  246.  
  247. function TPdbFile.StreamSize(const StreamIndex: Integer): Int64;
  248. begin
  249.   if (StreamIndex < 0) or (StreamIndex >= StreamCount) then
  250.     raise EPdbError.CreateFmt('Invalid PDB stream index %d', [StreamIndex]);
  251.   Result := FStreams[StreamIndex].Size;
  252. end;
  253.  
  254. function TPdbFile.ReadStream(const StreamIndex: Integer): TBytes;
  255. var
  256.   I: Integer;
  257.   CopyCount, Remaining: Int64;
  258.   SourceOffset, DestOffset: Int64;
  259. begin
  260.   if (StreamIndex < 0) or (StreamIndex >= StreamCount) then
  261.     raise EPdbError.CreateFmt('Invalid PDB stream index %d', [StreamIndex]);
  262.   if FStreams[StreamIndex].Size < 0 then
  263.     raise EPdbError.CreateFmt('PDB stream %d is not present', [StreamIndex]);
  264.  
  265.   SetLength(Result, FStreams[StreamIndex].Size);
  266.   Remaining := Length(Result);
  267.   DestOffset := 0;
  268.   for I := 0 to StreamBlockCount(StreamIndex) - 1 do
  269.   begin
  270.     CopyCount := FBlockSize;
  271.     if CopyCount > Remaining then
  272.       CopyCount := Remaining;
  273.     SourceOffset := Int64(FStreams[StreamIndex].Blocks[I]) * FBlockSize;
  274.     CheckRange(SourceOffset, CopyCount);
  275.     if CopyCount > 0 then
  276.       Move(FData[SourceOffset], Result[DestOffset], CopyCount);
  277.     Inc(DestOffset, CopyCount);
  278.     Dec(Remaining, CopyCount);
  279.   end;
  280. end;
  281.  
  282. function TPdbFile.TryReadPdbInfo(out Info: TPdbInfo): Boolean;
  283. var
  284.   Data: TBytes;
  285. begin
  286.   FillChar(Info, SizeOf(Info), 0);
  287.   Result := (StreamPdb < StreamCount) and (StreamSize(StreamPdb) >= 24);
  288.   if not Result then
  289.     Exit;
  290.  
  291.   Data := ReadStream(StreamPdb);
  292.   Move(Data[0], Info.Version, SizeOf(Info.Version));
  293.   Move(Data[4], Info.Signature, SizeOf(Info.Signature));
  294.   Move(Data[8], Info.Age, SizeOf(Info.Age));
  295.   Move(Data[12], Info.Guid, SizeOf(Info.Guid));
  296. end;
  297.  
  298. function TPdbFile.TryReadDbiInfo(out Info: TPdbDbiInfo): Boolean;
  299. var
  300.   Data: TBytes;
  301. begin
  302.   FillChar(Info, SizeOf(Info), 0);
  303.   Result := (StreamDbi < StreamCount) and (StreamSize(StreamDbi) >= 20);
  304.   if not Result then
  305.     Exit;
  306.  
  307.   Data := ReadStream(StreamDbi);
  308.   Move(Data[0], Info.VersionSignature, SizeOf(Info.VersionSignature));
  309.   Move(Data[4], Info.VersionHeader, SizeOf(Info.VersionHeader));
  310.   Move(Data[8], Info.Age, SizeOf(Info.Age));
  311.   Move(Data[12], Info.GlobalSymbolStream, SizeOf(Info.GlobalSymbolStream));
  312.   Move(Data[14], Info.PublicSymbolStream, SizeOf(Info.PublicSymbolStream));
  313.   Move(Data[16], Info.SymbolRecordStream, SizeOf(Info.SymbolRecordStream));
  314. end;
  315.  
  316. function TPdbFile.ReadPublicSymbols: TPdbPublicSymbolArray;
  317. var
  318.   Dbi: TPdbDbiInfo;
  319.   Data: TBytes;
  320.   Pos, RecordStart, NameStart, NameEnd, Count: Integer;
  321.   RecordLength, RecordKind: Word;
  322.   Symbol: TPdbPublicSymbol;
  323.  
  324.   procedure AddSymbol(const Value: TPdbPublicSymbol);
  325.   begin
  326.     if Count = Length(Result) then
  327.       SetLength(Result, Count + 64);
  328.     Result[Count] := Value;
  329.     Inc(Count);
  330.   end;
  331.  
  332. begin
  333.   SetLength(Result, 0);
  334.   Count := 0;
  335.   if not TryReadDbiInfo(Dbi) then
  336.     Exit;
  337.   if (Dbi.SymbolRecordStream < 0) or (Dbi.SymbolRecordStream >= StreamCount) then
  338.     Exit;
  339.   if StreamSize(Dbi.SymbolRecordStream) <= 0 then
  340.     Exit;
  341.  
  342.   Data := ReadStream(Dbi.SymbolRecordStream);
  343.   Pos := 0;
  344.   while Pos + 4 <= Length(Data) do
  345.   begin
  346.     Move(Data[Pos], RecordLength, SizeOf(RecordLength));
  347.     RecordStart := Pos + 2;
  348.     if (RecordLength < 2) or (RecordStart + RecordLength > Length(Data)) then
  349.       Break;
  350.  
  351.     Move(Data[RecordStart], RecordKind, SizeOf(RecordKind));
  352.     if ((RecordKind = S_PUB32) or (RecordKind = S_PUB32_ST)) and (RecordLength >= 14) then
  353.     begin
  354.       Move(Data[RecordStart + 2], Symbol.Flags, SizeOf(Symbol.Flags));
  355.       Move(Data[RecordStart + 6], Symbol.Offset, SizeOf(Symbol.Offset));
  356.       Move(Data[RecordStart + 10], Symbol.Segment, SizeOf(Symbol.Segment));
  357.       NameStart := RecordStart + 12;
  358.       NameEnd := NameStart;
  359.       while (NameEnd < RecordStart + RecordLength) and (Data[NameEnd] <> 0) do
  360.         Inc(NameEnd);
  361.       Symbol.Name := BytesToAnsiString(Data, NameStart, NameEnd - NameStart);
  362.       AddSymbol(Symbol);
  363.     end;
  364.  
  365.     Pos := RecordStart + AlignUp(RecordLength, 4);
  366.   end;
  367.   SetLength(Result, Count);
  368. end;
  369.  
  370. end.

PDBDump:
Code: Pascal  [Select][+][-]
  1. program PdbDump;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. uses
  6.   SysUtils, PdbParser;
  7.  
  8. var
  9.   Pdb: TPdbFile;
  10.   Info: TPdbInfo;
  11.   Dbi: TPdbDbiInfo;
  12.   Symbols: TPdbPublicSymbolArray;
  13.   I, Limit: Integer;
  14.  
  15. begin
  16.   if ParamCount <> 1 then
  17.   begin
  18.     Writeln('Usage: pdbdump <file.pdb>');
  19.     Halt(2);
  20.   end;
  21.  
  22.   try
  23.     Pdb := TPdbFile.Create(ParamStr(1));
  24.     try
  25.       Writeln('PDB MSF 7.0');
  26.       Writeln('Block size: ', Pdb.BlockSize);
  27.       Writeln('Block count: ', Pdb.BlockCount);
  28.       Writeln('Directory size: ', Pdb.DirectorySize);
  29.       Writeln('Streams: ', Pdb.StreamCount);
  30.  
  31.       for I := 0 to Pdb.StreamCount - 1 do
  32.         Writeln('  #', I:3, ' size=', Pdb.StreamSize(I));
  33.  
  34.       if Pdb.TryReadPdbInfo(Info) then
  35.       begin
  36.         Writeln;
  37.         Writeln('PDB info:');
  38.         Writeln('  Version: ', Info.Version);
  39.         Writeln('  Signature: ', Info.Signature);
  40.         Writeln('  Age: ', Info.Age);
  41.         Writeln('  GUID: ', GuidToString(Info.Guid));
  42.       end;
  43.  
  44.       if Pdb.TryReadDbiInfo(Dbi) then
  45.       begin
  46.         Writeln;
  47.         Writeln('DBI info:');
  48.         Writeln('  Version signature: ', Dbi.VersionSignature);
  49.         Writeln('  Version header: ', Dbi.VersionHeader);
  50.         Writeln('  Age: ', Dbi.Age);
  51.         Writeln('  Global symbol stream: ', Dbi.GlobalSymbolStream);
  52.         Writeln('  Public symbol stream: ', Dbi.PublicSymbolStream);
  53.         Writeln('  Symbol record stream: ', Dbi.SymbolRecordStream);
  54.       end;
  55.  
  56.       Symbols := Pdb.ReadPublicSymbols;
  57.       Writeln;
  58.       Writeln('Public symbols found: ', Length(Symbols));
  59.       Limit := Length(Symbols);
  60.       if Limit > 50 then
  61.         Limit := 50;
  62.       for I := 0 to Limit - 1 do
  63.         Writeln('  ', Symbols[I].Segment, ':', HexStr(Symbols[I].Offset, 8), ' ', Symbols[I].Name);
  64.       if Length(Symbols) > Limit then
  65.         Writeln('  ...');
  66.     finally
  67.       Pdb.Free;
  68.     end;
  69.   except
  70.     on E: Exception do
  71.     begin
  72.       Writeln(StdErr, E.ClassName, ': ', E.Message);
  73.       Halt(1);
  74.     end;
  75.   end;
  76. end.
As I wrote: it is intentionally simple.
This could be the basis of an IDE plugin. I am happy to do that if required.
« Last Edit: May 14, 2026, 11:36:20 am by Thaddy »
objects are fine constructs. You can even initialize them with constructors.

tooknox

  • New Member
  • *
  • Posts: 40
Re: Read/Parse PDB file to get symbol details
« Reply #7 on: May 14, 2026, 12:25:33 pm »
@Thaddy, thanks for the raw implementation. Will test it out, looks like it will work just fine :)

Also just to update on the MS DIA COM library, I figured it out, the issue is importtl.exe generates correct declarations for [al]most [all] of the functions except those that return HRESULT and take a pointer to a variable to store output.

eg. all C++ signatures like
HRESULT get_relativeVirtualAddress(DWORD* pRetVal);

get converted to
function Get_relativeVirtualAddress: LongWord; stdcall;

which would ofcourse cause stack issue, adding safecall fixes it. I verified (manually), the issue is only with all the Get_xxx functions.

This single sed should fix all of them and we have a nice pascal wrapper for msdia com library. (ofcourse there still may be some corner case)

Code: Bash  [Select][+][-]
  1. sed -E -i.bak '/^[[:space:]]*function[[:space:]]+Get_[A-Za-z_][A-Za-z0-9_]*[[:space:]]*:[[:space:]]*HResult[[:space:]]*;[[:space:]]*stdcall[[:space:]]*;/! s/^([[:space:]]*function[[:space:]]+Get_[A-Za-z_][A-Za-z0-9_]*[[:space:]]*:[[:space:]]*[^;]+;[[:space:]]*)stdcall([[:space:]]*;[[:space:]]*)$/\1safecall\2/I' MSDIA140_TLB.pas
  2.  

Attaching the modified pascal wrapper here, in case someone wants to test things out. Some constants might seem missing but those come from dia header file, I have included a few in my example and others are mostly integers you can pass directly. or will attach them later.

Edit: Added the constants, don't know what they all do but they sure are there now.
« Last Edit: May 14, 2026, 02:14:02 pm by tooknox »

Thaddy

  • Hero Member
  • *****
  • Posts: 19267
  • Glad to be alive.
Re: Read/Parse PDB file to get symbol details
« Reply #8 on: May 14, 2026, 03:59:35 pm »
(Note in my version you can change all occurrences of "SmallInt" to "Word" You can also ignore possible warnings that that gives you)
objects are fine constructs. You can even initialize them with constructors.

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12898
  • FPC developer.
Re: Read/Parse PDB file to get symbol details
« Reply #9 on: May 14, 2026, 05:26:43 pm »
Importtl operates on the COM interface description in the DLL, not the direct C++ prototype.

It could be that the importtl unit ignores some atttribute that signals the safecall signature, or the TLB is simply wrong and the C++ prototype was manually fixed. Probably it needs debugging to see what is what.

I tried to do so, and I have two dozen instances of that DLL on my system, but they won't register (dllregisterserver failed with 0x80070005) with regsvr32 (even in automation directories)

Thaddy

  • Hero Member
  • *****
  • Posts: 19267
  • Glad to be alive.
Re: Read/Parse PDB file to get symbol details
« Reply #10 on: May 14, 2026, 06:29:43 pm »
Run regsvr32 as administrator.

I also note CreateComObject is used (late bound) instead of the expected CoCreateInstance or CoCreateInstanceEx (early bound). That is either / or: these are different models.
If you want to use late bound, the whole shebang of initializing COM and using tlb's is not necessary.
You can just as well use CreateOleObject...
« Last Edit: May 14, 2026, 06:53:12 pm by Thaddy »
objects are fine constructs. You can even initialize them with constructors.

tooknox

  • New Member
  • *
  • Posts: 40
Re: Read/Parse PDB file to get symbol details
« Reply #11 on: May 14, 2026, 07:05:09 pm »
@marcov,

Did some testing and it seems only the function which take a pointer to out var as the ONLY argument are affected.

For eg, the functions like these are declared correctly

Code: Pascal  [Select][+][-]
  1. function openSession(out ppSession:IDiaSession):HRESULT;stdcall;

but a function that takes say pointer to int as argument to store the result gets declared as a function returning Int with no argument.

Moreover since all of these functions are getter, there is a corresponding property which will again cause access violation.

All other types of methods/functions get declared perfectly.

I used the msdia140.dll distributed with MSVC toolchain and ran regsvr32 as admin.

@Thaddy thanks for pointing out the early/late binding. I just happen to land on the only freepascal wiki page regarding COM DLL and importtl felt convenient with everything properly defined. CreateComObject came after I kept getting errors and tried things in desparation  %)

Eitherway would be great to have importtl fixed (if it's a bug/issue)

« Last Edit: May 14, 2026, 07:32:25 pm by tooknox »

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12898
  • FPC developer.
Re: Read/Parse PDB file to get symbol details
« Reply #12 on: May 14, 2026, 11:45:56 pm »
@marcov,

Did some testing and it seems only the function which take a pointer to out var as the ONLY argument are affected.

For eg, the functions like these are declared correctly

Code: Pascal  [Select][+][-]
  1. function openSession(out ppSession:IDiaSession):HRESULT;stdcall;

I looked at IDiaSymbol::relativeVirtualAddress with a typelib viewer and this is what it says:

Quote
[id(0x0000000a), propget, helpstring(".")]
HRESULT relativeVirtualAddress([out, retval] unsigned long* pRetVal);

So it has "out" and "retval" as properties, as well as a HRESULT returnvalue, which IMHO is a sign that the TLB is ok.  But maybe because it is not a function, but a property, somehow that transformation goes wrong. The relevant code for properties seems to be way shorter than for functions (around line importtl: 805)

Code: Pascal  [Select][+][-]
  1. if FD^.invkind=INVOKE_PROPERTYGET then
  2.             begin
  3.             s:=s+format('   function Get_%s%s : %s; %s;'#13#10,[sMethodName,sPropParam2,sType,sConv]);
  4.             with aPropertyDefs[findProperty(FD^.memid)] do
  5.               begin
  6.               bget:=true;
  7.               name:=sMethodName;
  8.               sgtype:=sType;
  9.               sorgname:=BstrName;
  10.               sdoc:=BstrDocString;
  11.               sParam:=sPropParam;
  12.               sDefault:=sl;
  13.               end;      
  14.  

Which has no handling for those attributes, which would involve some checking in FD^.lprgelemdescParam[0].paramdesc.wParamFlags and PARAMFLAG_FREFVAL and PARAMFLAG_FOUT. The code would probably look like somewhere around line 585:

Code: Pascal  [Select][+][-]
  1.  if ((FD^.lprgelemdescParam[FD^.cParams-1].paramdesc.wParamFlags and (PARAMFLAG_FRETVAL or PARAMFLAG_FOUT)) <>0) then
  2.           begin
  3.           delete(sType,1,1); //out parameters are always defined as pointer
  4.           if assigned(FD^.lprgelemdescParam[FD^.cParams-1].tdesc.lptdesc) then
  5.             iType:=FD^.lprgelemdescParam[FD^.cParams-1].tdesc.lptdesc^.vt;
  6.           end;    
  7.  

But that is all my uneducated guess of course. It might be worth filing a bug.
« Last Edit: May 15, 2026, 12:02:01 am by marcov »

tooknox

  • New Member
  • *
  • Posts: 40
Re: Read/Parse PDB file to get symbol details
« Reply #13 on: May 15, 2026, 07:07:32 am »
@marcov

You are absolutely correct, I got some basic understanding of importtl code (with help of AI) and I think a something basic like this should fix it. The checks can be/should be done around the lines you mentioned but just for simplicity I have added extra functions.

Code: Pascal  [Select][+][-]
  1. function IsOutRetValParam(AIndex: Integer): Boolean;
  2. var
  3.   Flags: Word;
  4. begin
  5.   Result := False;
  6.  
  7.   if FD = nil then
  8.     Exit;
  9.  
  10.   if AIndex < 0 then
  11.     Exit;
  12.  
  13.   if AIndex >= FD^.cParams then
  14.     Exit;
  15.  
  16.   Flags := FD^.lprgelemdescParam[AIndex].paramdesc.wParamFlags;
  17.  
  18.   Result :=
  19.     ((Flags and PARAMFLAG_FOUT) <> 0) and
  20.     ((Flags and PARAMFLAG_FRETVAL) <> 0);
  21. end;
  22.  
  23. function PropertyGetNeedsSafecall: Boolean;
  24. begin
  25.   Result := False;
  26.  
  27.   if FD = nil then
  28.     Exit;
  29.  
  30.   if FD^.invkind <> INVOKE_PROPERTYGET then
  31.     Exit;
  32.  
  33.   if FD^.callconv <> CC_STDCALL then
  34.     Exit;
  35.  
  36.   // Dispatch and dual interfaces already use safecall in importtl.
  37.   // Line 607-610 already have this
  38.   // if not (bIsDispatch or ((TA^.wTypeFlags and TYPEFLAG_FDUAL)=TYPEFLAG_FDUAL)) then
  39.   //      sConv:='stdcall'
  40.   //    else
  41.   //      sConv:='safecall';
  42.  
  43.   if bIsDispatch then
  44.     Exit;
  45.  
  46.   if (TA^.wTypeFlags and TYPEFLAG_FDUAL) <> 0 then
  47.     Exit;
  48.  
  49.   // Need at least one parameter so there can be a retval parameter.
  50.   if FD^.cParams <= 0 then
  51.     Exit;
  52.    
  53.   // the retval would be the final parameter.
  54.   if not IsOutRetValParam(FD^.cParams - 1) then
  55.     Exit;
  56.  
  57.   Result := True;
  58. end;
  59.  
  60. // And then have INVOKE_PROPERTYGET check the extra condition
  61. if FD^.invkind = INVOKE_PROPERTYGET then
  62. begin
  63.   if PropertyGetNeedsSafecall then
  64.     s := s + format(' function Get_%s%s : %s; safecall;'#13#10,
  65.                     [sMethodName, sPropParam2, sType])
  66.   else
  67.     s := s + format(' function Get_%s%s : %s; %s;'#13#10,
  68.                     [sMethodName, sPropParam2, sType, sConv]);
  69.  

This is definitely a bug cuz I see safecall getting handled for other conditions just fine. BTW before I open a bug, this would go in main FPC-source work items right?
« Last Edit: May 15, 2026, 07:23:56 am by tooknox »

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12898
  • FPC developer.
Re: Read/Parse PDB file to get symbol details
« Reply #14 on: May 15, 2026, 11:50:04 am »
@marcov

You are absolutely correct, I got some basic understanding of importtl code (with help of AI) and I think a something basic like this should fix it. The checks can be/should be done around the lines you mentioned but just for simplicity I have added extra functions.

...

This is definitely a bug cuz I see safecall getting handled for other conditions just fine. BTW before I open a bug, this would go in main FPC-source work items right?

I also think it is a bug, but it is maybe a rare situation, where a property getting/setter is not a pure getter/setter. I don't know how we are supposed to deal with that (keep in mind if you change the signature of the getter function, then "property" might no longer work. And we can't use some wrapper function because it is an interface. (helper?). If it can deal with the safecall form, that would work though.

I tried to import using Delphi, but it crashed. 
« Last Edit: May 15, 2026, 12:32:22 pm by marcov »

 

TinyPortal © 2005-2018