I want to write check to proof that given assembly has valid pdb file near it and that the checksums stored in the pdb match the source checksums.
The check used to be called as unit test on our CI server to prevent publishing binaries if the build server config was broken (e.g. different git branch was used to build the code).
If possible, it should accept MethodInfo
as input parameter. I want to do double-check and verify that the source lines for a method are estimated ones.
The first part, reading the source for the specific method is pretty simple. Here's a sample.
The hard part is checking the checksums of all documents in the PDB. The standard System.Diagnostics.SymbolStore.SymDocument.GetCheckSum()
throws NotImplementedException
so I definitely need to use something else.
Any suggestion?
UPD. Trying to clarify:)
I'm interested in checking that source files' checksums are matching to the checksums stored in PDB file, the first part (checking that pdb matches binary) is done already.
I'm looking for some API for this task as I do not want to parse console output. Also, I'd prefer not to install any additional SDKs on our CI servers as it'll take additional time to proof nothing was broken.
If you need a programmatic way to do it, you can use the Microsoft.DiaSymReaderPackage (version 1.0.7.0), and you need to declare a couple other unmanaged interfaces as well. The checkum is computed easily from the files bytes using one of two algorithms: SHA1 or MD5.
Here is a sample console application that displays information on its own source:
class Program
{
static void Main(string[] args)
{
ValidateChecksums(Path.GetFullPath("ConsoleApplication1.exe"));
}
// this needs a reference to Microsoft.DiaSymReader
// or redefine its interfaces manually from here https://github.com/dotnet/roslyn/tree/master/src/Dependencies/Microsoft.DiaSymReader
public static void ValidateChecksums(string filePath)
{
if (filePath == null)
throw new ArgumentNullException(nameof(filePath));
var dispenser = (IMetaDataDispenser)new CorMetaDataDispenser();
var import = dispenser.OpenScope(filePath, 0, typeof(IMetaDataImport).GUID);
var binder = (ISymUnmanagedBinder)new CorSymBinder_SxS();
ISymUnmanagedReader reader;
binder.GetReaderForFile(import, filePath, null, out reader);
int count;
reader.GetDocuments(0, out count, null);
if (count > 0)
{
var docs = new ISymUnmanagedDocument[count];
reader.GetDocuments(count, out count, docs);
foreach (var d in docs)
{
var doc = new SymDocument(d);
Console.WriteLine(doc.Url);
if (doc.Checksum.SequenceEqual(doc.ComputeChecksum()))
{
Console.WriteLine(" checksum is valid.");
}
else
{
Console.WriteLine(" checksum is not valid.");
}
}
}
}
}
And a sample helper class, native interfaces and coclasses.
public class SymDocument
{
// guids are from corsym.h
public static readonly Guid CorSym_SourceHash_MD5 = new Guid(0x406ea660, 0x64cf, 0x4c82, 0xb6, 0xf0, 0x42, 0xd4, 0x81, 0x72, 0xa7, 0x99);
public static readonly Guid CorSym_SourceHash_SHA1 = new Guid(0xff1816ec, 0xaa5e, 0x4d10, 0x87, 0xf7, 0x6f, 0x49, 0x63, 0x83, 0x34, 0x60);
public SymDocument(ISymUnmanagedDocument doc)
{
if (doc == null)
throw new ArgumentNullException(nameof(doc));
int len;
doc.GetUrl(0, out len, null);
if (len > 0)
{
var urlChars = new char[len];
doc.GetUrl(len, out len, urlChars);
Url = new string(urlChars, 0, len - 1);
}
doc.GetChecksum(0, out len, null);
if (len > 0)
{
Checksum = new byte[len];
doc.GetChecksum(len, out len, Checksum);
}
Guid id = Guid.Empty;
doc.GetChecksumAlgorithmId(ref id);
ChecksumAlgorithmId = id;
}
public string Url { get; private set; }
public byte[] Checksum { get; private set; }
public Guid ChecksumAlgorithmId { get; private set; }
public byte[] ComputeChecksum()
{
HashAlgorithm algo;
if (ChecksumAlgorithmId == CorSym_SourceHash_MD5)
{
algo = MD5.Create();
}
else if (ChecksumAlgorithmId == CorSym_SourceHash_SHA1)
{
algo = SHA1.Create();
}
else
throw new NotSupportedException();
try
{
return algo.ComputeHash(File.ReadAllBytes(Url));
}
finally
{
algo.Dispose();
}
}
}
[ComImport, Guid("0A29FF9E-7F9C-4437-8B11-F424491E3931")]
internal class CorSymBinder_SxS // from corsym.h
{
}
[ComImport, Guid("E5CB7A31-7512-11d2-89CE-0080C792E5D8")]
internal class CorMetaDataDispenser // from cor.h
{
}
[Guid("7DAC8207-D3AE-4c75-9B67-92801A497D44"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMetaDataImport // from cor.h
{
// we don't need to use what's inside
}
[Guid("809c652e-7396-11d2-9771-00a0c9b4d50c"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMetaDataDispenser // from cor.h
{
void _VtblGap0_1(); // skip 1 method
IMetaDataImport OpenScope([MarshalAs(UnmanagedType.LPWStr)] string szScope, int dwOpenFlags, [MarshalAs(UnmanagedType.LPStruct)] Guid riid);
}
If the Guid matches, you should be certain that the source matches. I use the chkmatch command line tool to compare the age and Guid. (See: http://www.debuginfo.com/tools/chkmatch.html)
You can read the age and GUID from PDB using Microsoft CCI or Mono.Cecil, and read the same information from the RSDS resource in a PE file.
If the age and GUID match, consider that PDB and PE file does indeed match.
The match can further be verified with symchk tool.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With