Customizing PDB lookup for source information in StackTrace

The System.Diagnostics.StackTrace class in .NET can be used to generate a textual representation of the current callstack.  This is used, for example, by Exception.ToString(). If requested by the caller, StackTrace can include source file locations (file names and line numbers, etc.) for each frame whose module has a PDB file available to the CLR.  PDB files are designed to be used primarily in development-time scenarios, and so the idea here is that when you're developing or testing your application and it spits out an exception (eg. to a log file, or an unhandled exception to the console), it will help you to debug the issue if you can see exactly where in the leaf method the exception was thrown, and where exactly each child function was called (actually it's not technically "exactly" - if JIT optimizations are enabled the results may be approximate, and frames may be missing completely due to inlining).  If you've done much .NET programming, you probably knew all this already. 

The more interesting (and less well documented) question I want to address is where exactly you must place your PDB files for this to work.  The CLR will look next to the corresponding module (DLL or EXE), and also check a few other standard locations (those local paths specified by the _NT_SYMBOL_PATH environment variable for example, and I believe the Windows system directory).  In fact, it's not really the CLR controlling any of this, but the ISymUnmanagedBinder::GetReaderForFile API from diasymreader.dll, which itself is implemented on top of the IDiaDataSource::loadDataForExe API.  Since this support for source locations is designed for development time, when you're generally running binaries you've just built - the PDBs are almost always next to the binaries and this works great.

Occasionally we get requests from people who like this feature but complain that the CLR isn't flexible enough to find their PDB files where they want to put them.  Sometimes this stems from wanting to use this feature for something it wasn't designed, such as shipping PDBs with your product and logging/reporting errors from the field.  For that scenario you're usually MUCH better off using Windows Error Reporting and minidumps.  You generally do not want to ship your PDBs to your customers (they're big, and can make it easier to reverse engineer your code - although this is a much bigger concern for unmanaged C++ code than .NET code).  In other cases, you may want to generate machine-readable stack traces (with module names, method tokens and IL offsets), and then post-process them using PDB files at your location to get source location information.

But, there are a few scenarios where it does really make sense to want more flexibility in how PDBs are located for the StackTraces generated at runtime.  For example, I recently got a request through product support from a customer with a large test environment where they were deploying their actual product.  They keep all their PDBs (1TB+ of them!) on a symbol server, and they would like to be able to use them to generate stack traces with source info without having to deploy PDBs to all their test machines (in the proper directories).  Although the CLR doesn't support this directly, there isn't any reason you can't implement this yourself.  The CLR StackTrace class exposes StackFrame objects which have all the information you need to map back to source addresses given an ISymbolReader instance.  ISymbolReader instances can be created directly (controlling PDB location policy manaually with ISymbolUnmanagedBinder2::GetReaderForFile2) by calling into diasymreader.dll through COM interop.  I've posted sample code for a StackTraceSymbolProvider class that does this here (using MDbg's COM interop wrappers for diasymreader.dll). 

Here's an example of how this code can be used to print out a StackTrace from an Exception while explicitly controlling the directories searched (searchPath is a semi-colon separated list of directories including SRV* entries for symbol servers), and whether things like a symbol server will be checked:

             catch (System.Exception e)
            {
                st = new StackTrace(e, true);
                StackTraceSymbolProvider stsp = new StackTraceSymbolProvider(searchPath,
                    SymSearchPolicies.AllowSymbolServerAccess |
                    SymSearchPolicies.AllowOriginalPathAccess |
                    SymSearchPolicies.AllowReferencePathAccess |
                    SymSearchPolicies.AllowRegistryAccess);

                Console.WriteLine("Custom stack trace:");
                Console.WriteLine(stsp.StackTraceToStringWithSourceInfo(st));
            }

To get this flexibility you have to re-implement some of the formatting done by StackTrace.ToString(), but you might want the flexibility to control this anyway (for example, it's easy to include column numbers in addition to line numbers).  It's non-trivial to wire this all up (especially if you're not familiar with COM-interop), but it's all plumbing really.  Hopefully this sample code will save some of you the hassle of figuring out this plumbing yourself.