Hi @atripp,
I understand that it will not be simple to find the possible issue in the huge MicrosoftPdbFile class. As first step it would be nice if you could compare the code of my colleague with your class and if this not help than you could possibly share the class code with me so that we could verify this.
The PdbFile class of my colleague is the following:
namespace Pdbreader;
// Adapted and ported from https://github.com/symstore/symstore/blob/master/symstore/pdb.py
/*
* Copyright(c) 2019 Lennart Blanco
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using System.Buffers.Binary;
public class PdbInvalidSignatureException : Exception
{
public PdbInvalidSignatureException() : base("Invalid PDB signature.") { }
public PdbInvalidSignatureException(string message) : base(message) { }
public PdbInvalidSignatureException(string message, Exception inner) : base(message, inner) { }
}
/// <summary>
/// This class supports reading Windows PDB files and extracting the GUID and Age found
/// inside to create the GUID used by the Symbol server path.
/// </summary>
public class PdbFile
{
private static ReadOnlySpan<byte> ExpectedSignature =>
[
0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x20, 0x43, 0x2F,
0x43, 0x2B, 0x2B, 0x20, 0x4D, 0x53, 0x46, 0x20, 0x37, 0x2E, 0x30, 0x30,
0x0D, 0x0A, 0x1A, 0x44, 0x53, 0x00, 0x00, 0x00
];
/// <summary>
/// Parses a PDB file at the given path, extracting its GUID and age.
/// </summary>
/// <param name="filepath">The full path to the <c>.pdb</c> file to read.</param>
/// <exception cref="PdbInvalidSignatureException">
/// Thrown if the file does not begin with the expected MSF 7.00 signature.
/// </exception>
public PdbFile(string filepath)
{
using var fs = new FileStream(filepath, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(fs);
Span<byte> sig = stackalloc byte[ExpectedSignature.Length];
var bytesRead = reader.Read(sig);
if (bytesRead < ExpectedSignature.Length || !sig.SequenceEqual(ExpectedSignature))
{
throw new PdbInvalidSignatureException();
}
var pageSize = reader.ReadInt32();
reader.ReadInt32(); // free page map index
reader.ReadInt32(); // pages used
var rootDirSize = reader.ReadInt32();
reader.ReadInt32(); // reserved
var root = new Root(reader, pageSize, rootDirSize);
// --- Read PDB stream (stream index 1) ---
var pdbStreamData = root.ReadStream(1);
using var pdbMs = new MemoryStream(pdbStreamData);
using var pdbBr = new BinaryReader(pdbMs);
pdbBr.ReadInt32(); // Version
pdbBr.ReadInt32(); // Signature/Timestamp
var pdbAge = pdbBr.ReadInt32();
var guidBytes = pdbBr.ReadBytes(16);
Guid = new Guid(guidBytes);
// --- Read DBI stream (stream index 3) ---
var dbiStreamData = root.ReadStream(3);
if (dbiStreamData.Length >= 12)
{
using var dbiBr = new BinaryReader(new MemoryStream(dbiStreamData));
dbiBr.ReadInt32(); // Signature
dbiBr.ReadInt32(); // Version
Age = dbiBr.ReadInt32();
}
else
{
Age = pdbAge;
}
}
/// <summary>
/// Get the Guid found in the PDB file
/// </summary>
public Guid Guid { get; }
/// <summary>
/// The age of the PDB, used to match it against its corresponding binary.
/// Sourced from the DBI stream where available, falling back to the PDB info stream.
/// </summary>
public int Age { get; }
/// <summary>
/// Returns the symbol store lookup key for this PDB in the format <c>{GUID}{Age}</c>,
/// e.g. <c>3844DBD05CC449FEB97320D39D627B2F1</c>.
/// </summary>
public string ToStoreFormat() => $"{Guid:N}".ToUpperInvariant() + Age;
}
internal class Root
{
private readonly BinaryReader _reader;
private readonly int _pageSize;
private readonly int _size;
private uint[]? _rootPages;
private uint? _cachedNumStreams;
private int[]? _cachedStreamSizes;
private const int HeaderSize = 32; // signature
private const int HeaderInts = 5; // pageSize, fpmIdx, pagesUsed, rootDirSize, reserved
public Root(BinaryReader reader, int pageSize, int size)
{
_reader = reader;
_pageSize = pageSize;
_size = size;
}
private static int PageCount(int size, int pageSize) => (int)Math.Ceiling((double)size / pageSize);
private uint[] GetRootPages()
{
if (_rootPages is not null) return _rootPages;
var numPages = PageCount(_size, _pageSize);
_reader.BaseStream.Seek(HeaderSize + 4 * HeaderInts, SeekOrigin.Begin);
var numIndexPages = PageCount(numPages * 4, _pageSize);
var indexPages = new uint[numIndexPages];
for (var i = 0; i < numIndexPages; i++)
{
indexPages[i] = _reader.ReadUInt32();
}
var rootPageData = new byte[numIndexPages * _pageSize];
for (var i = 0; i < indexPages.Length; i++)
{
_reader.BaseStream.Seek(indexPages[i] * _pageSize, SeekOrigin.Begin);
var chunk = _reader.ReadBytes(_pageSize);
Buffer.BlockCopy(chunk, 0, rootPageData, i * _pageSize, chunk.Length);
}
_rootPages = new uint[numPages];
using var br = new BinaryReader(new MemoryStream(rootPageData));
for (var i = 0; i < numPages; i++)
{
_rootPages[i] = br.ReadUInt32();
}
return _rootPages;
}
/// <summary>Reads <paramref name="length"/> bytes starting at <paramref name="start"/> within
/// the root directory stream (not an arbitrary stream).</summary>
private byte[] ReadFromRoot(int start, int length)
{
var pages = GetRootPages();
var result = new byte[length];
var destOffset = 0;
var startPage = start / _pageSize;
var startByte = start % _pageSize;
_reader.BaseStream.Seek(pages[startPage] * _pageSize + startByte, SeekOrigin.Begin);
var toRead = Math.Min(length, _pageSize - startByte);
Buffer.BlockCopy(_reader.ReadBytes(toRead), 0, result, destOffset, toRead);
destOffset += toRead;
length -= toRead;
startPage++;
while (length > 0)
{
_reader.BaseStream.Seek(pages[startPage] * _pageSize, SeekOrigin.Begin);
toRead = Math.Min(_pageSize, length);
Buffer.BlockCopy(_reader.ReadBytes(toRead), 0, result, destOffset, toRead);
destOffset += toRead;
length -= toRead;
startPage++;
}
return result;
}
private uint GetNumStreams() =>
_cachedNumStreams ??= BinaryPrimitives.ReadUInt32LittleEndian(ReadFromRoot(0, 4));
private int[] GetAllStreamSizes()
{
if (_cachedStreamSizes is not null) return _cachedStreamSizes;
var count = (int)GetNumStreams();
var raw = ReadFromRoot(4, count * 4);
_cachedStreamSizes = new int[count];
for (var i = 0; i < count; i++)
{
_cachedStreamSizes[i] = BinaryPrimitives.ReadInt32LittleEndian(raw.AsSpan(i * 4, 4));
}
return _cachedStreamSizes;
}
private uint[] GetStreamPages(int streamIndex)
{
var sizes = GetAllStreamSizes();
var numStreams = (int)GetNumStreams();
if (streamIndex >= numStreams)
{
throw new IndexOutOfRangeException($"Stream index {streamIndex} out of range (count: {numStreams}).");
}
var pagesOffset = 4 + 4 * numStreams;
for (var i = 0; i < streamIndex; i++)
{
var sz = sizes[i];
if (sz > 0)
{
pagesOffset += PageCount(sz, _pageSize) * 4;
}
}
var streamSize = sizes[streamIndex];
if (streamSize <= 0) return []; // nil or empty stream
var numPages = PageCount(streamSize, _pageSize);
var rawPages = ReadFromRoot(pagesOffset, 4 * numPages);
var pages = new uint[numPages];
for (var i = 0; i < numPages; i++)
{
pages[i] = BinaryPrimitives.ReadUInt32LittleEndian(rawPages.AsSpan(i * 4, 4));
}
return pages;
}
/// <summary>Reads the full contents of a numbered MSF stream across all its pages.</summary>
public byte[] ReadStream(int streamIndex)
{
var sizes = GetAllStreamSizes();
if (streamIndex >= sizes.Length)
{
return [];
}
var streamSize = sizes[streamIndex];
if (streamSize <= 0)
{
return [];
}
var pages = GetStreamPages(streamIndex);
var result = new byte[streamSize];
var remaining = streamSize;
var destOffset = 0;
foreach (var page in pages)
{
_reader.BaseStream.Seek(page * _pageSize, SeekOrigin.Begin);
var toRead = Math.Min(_pageSize, remaining);
var chunk = _reader.ReadBytes(toRead);
Buffer.BlockCopy(chunk, 0, result, destOffset, toRead);
destOffset += toRead;
remaining -= toRead;
}
return result;
}
}
and he wrote this simple test program for this:
namespace Pdbreader;
public static class PdbReader
{
public static void Main(string[] args)
{
if (args.Length != 1)
{
Console.WriteLine("Usage: PdbReader <pdb_file>");
return;
}
try
{
var path = args[0];
var filename = Path.GetFileName(path);
var pdb = new PdbFile(path);
Console.WriteLine($"GUID: {pdb.ToStoreFormat()}");
Console.WriteLine($"Symstore path should be: <root>/{filename}/{pdb.ToStoreFormat()}/{filename}");
// The following will not work with windows pdbs, only the portable ones
//var metaStr = PortableMetaData.GetGuidString(args[0]);
//Console.WriteLine($"Meta-GUID: {metaStr}");
}
catch (Exception e)
{
Console.WriteLine($"Exception: {e}");
}
}
}
Best regards