Why developers need symbols
Symbol files are an essential ingredient for debugging our applications. As long as all code is contained in a single Visual Studio solution, the debugger has all information it needs. If we ship our DLLs to some third party, or use third party DLLs in our projects, the associated symbol (PDB) files are needed for efficient debugging.
Without symbols, the debugger has a hard time linking instructions in the IL code generated by the C# compiler to lines in the source code. Visual Studio does a pretty good job at guessing the source lines if we have the sources lying around, but these heuristics are far from perfect. Also, variables that are local to a function lose their name. As the name is not available from the outside no metadata about them is kept in the assembly. Arguably, the situation is not as severe as in native (C++) assemblies, since in .NET most of the metadata is already contained in the assembly.
For debugging PDB files are as important as source code!
More over, with the correct configuration and source server support enabled, the Visual Studio debugger is even able to automatically download the exact version of the source file the was used in the compilation from the git repository.
Configuration in Visual Studio
Before we dive into the technical details, let us see how to configure Visual Studio to consume symbol files. Of course, if we are lucky and the PDF files are deployed alongside of the DLLs, that is they are in the same directory, Visual Studio finds them automatically, and this step is redundant.
First we point Visual Studio to the place where we deposit our symbols files, the symbol server.
Here there are two symbol servers configured. The Microsoft Symbol Servers provide symbols for most of its software, including the .NET framework. Note that activating this source can actually cause a severe delay during startup and running of applications, especially if combined with the Load all modules, unless excluded option. In any case, I strongly ecommend setting a cache directory so that the symbols have to be downloaded only once.
The second server is located somewhere in our file system, possibly a DFS share. We have not yet set up a mechanism for deploying the symbols there. This is usually done as a step in the build pipeline, as described later.
Source Server configuration
Intimately connected with the symbols are the sources, the actual source code, hence the related concept of a source server. The source server delivers, at the request of the debugger, the source code in the version used at compilation. Azure DevOps, can act as a source server for code hosted in a git repository. We will see later how we point the debugger to this location. First, we configure the Visual Studio debugging settings.
- Uncheck Enable Just My Code allows you to debug into third party code
- Check Enable source server support to activate support for Windows PDBs on a symbol server.
- Check Enable Source Link support to activate support for Portable PDBs.
Some information clarifying the difference between the last two options can be found at Source Link Quick Start and Enable source link support & Announcing SourceLink 2.0.
Program Database (PDB) files are the standard symbol files for Windows and .NET. However, there are actually two types of them, Windows PDB and Portable PDB, relevant for different scenarios.
Additional information can be found here
- Publishing and Consuming Symbols and Source for Debugging
- Symbols for Windows debugging (WinDbg, KD, CDB, NTSD)
The classic (proprietary and poorly documented) symbol file format, as produced with classic (non-SDK) projects. They are not cross-platform, i.e. can only be used on Windows. However, tooling support is the best, since they have been around for a long time.
A new format originating with .NET Core, see the documentation on GitHub. The default for projects of the new tooling format (SDK projects aka 2.0 tooling aka dotnet core tooling). They are cross-platform as required by the nature of .NET core, but also incompatible with the old (Windows) format. Also, the tooling is not yet on par with the latter, see Supported scenarios.
SDK projects can also produce classic Windows PDB files, In Visual Studio, Project > Properties > Build > Advanced > DebuggingInformation > Full inserts an XML snippet like below into the .csproj file (note the
<!-- Generate Windows PDB instead of Portable PDB for build on VSTS build server --> <PropertyGroup Condition="'$(BuildingInsideVisualStudio)' != 'true'"> <DebugType>full</DebugType> <DebugSymbols>true</DebugSymbols> </PropertyGroup>
The two formats can be converted into each other with the help of the command line tool Pdb2Pdb. This gives the option to generate both types during the build process.
It is currently not possible to inject Portable PDBs into the (classic) symbol server on a file system, see Index & Publish symbols with core 2.0. If you are stuck with such a server, converting to Windows PDF may be a solution.
Other options for consuming Portable PDF files are
- copy them manually to the folder containing the assemblies
- use Azure DevOps as a symbol server with Azure Artifacts
- embed the PDBs into the DLLs
- add the PDBs into the regular NuGet packages
- provide separate NuGet packages containing the PDBs, see Creating symbol packages.
The last two options apply if you ship a library as a NuGet package.
A symbol server hosts PDB files to be retrieved by the debugger automatically. See Symbol Stores and Symbol Servers for a description and additional tooling for a classic symbol server.
Setting up and maintaining a symbol server comes with some work. A more convenient options is the Azure Artifacts, see also Symbol files (PDBs). The same build task Build: Index Sources & Publish Symbols can be used. It does no indexing in this case though, since this is already done during build.
PDB files contain the absolute path to the source code files from which the DLL was generated. When debugging in the same machine, this enables Visual Studio to locate the original source file on the file system. However, when debugging an assembly that was generated on a different machine (e.g. on the build server), the path will in general not lead to a corresponding source file. Indexing rewrites this path to include information about the source code repository, which may be a Git repository on Azure DevOps. In this way, the debugger (e.g. Visual Studio debugger, or WinDbg) can fetch the exact source code (branch and commit) from which the DLL was generated from Azure DevOps.
The indexing is commonly done during the build on the build server. The task Build: Index Sources & Publish Symbols is used to index the PDB files and publish them to the symbol server. It is included by default in the common .NET build templates on Azure DevOps.
Note that Portable PDBs do not need to (and actually can not) be indexed. You still need to use the same build task, but instead use SourceLink to index the symbols.
Note that here I have also configured the symbols to be published to a symbol server on a file share. The location is the one that we have configured in the Visual Studio debugging options before. As we have discussed, this only makes sense when working with a classic symbol server for Windows PDBs.
Command line tools
In the Windows 10 SDK, there are special tools for inspecting Windows PDB files, see Source Indexing. They need to be installed explicitly in the Windows Control panel: under Programs and Features , select the Windows Software Development Kit – Windows 10.0.xxxxx. In the context menu, select Change > Change > Debugging Tools for Windows. After installation, several command line tools are now available. For instance, the indexing can be checked with the
pdbstr command like this:
C:\> "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\srcsrv\pdbstr" -r -s:srcsrv -p:MySymbolFile.pdb SRCSRV: ini ------------------------------------------------ VERSION=3 INDEXVERSION=2 VERCTRL=Team Foundation Server DATETIME=Sun Apr 28 19:26:13 2019 INDEXER=TFSTB SRCSRV: variables ------------------------------------------ TFS_EXTRACT_TARGET=%targ%\%var5%\%fnvar%(%var6%)%fnbksl%(%var7%) TFS_EXTRACT_CMD=tf.exe git view /collection:%fnvar%(%var2%) /teamproject:"%fnvar%(%var3%)" /repository:"%fnvar%(%var4%)" /commitId:%fnvar%(%var5%) /path:"%var7%" /output:%SRCSRVTRG% %fnvar%(%var8%) TFS_COLLECTION=https://XXX.visualstudio.com TFS_TEAM_PROJECT=B9773313-892A-4A93-BED4-0333CA8B2282 TFS_REPO=95DCA26B-FA15-47DB-91AB-566F4399D7E9 TFS_COMMIT=e0410bc7859eaa7b8817321243c3d7828b0e8a4f TFS_SHORT_COMMIT=e0410bc7 TFS_APPLY_FILTERS=/applyfilters SRCSRVVERCTRL=git SRCSRVERRDESC=access SRCSRVERRVAR=var2 SRCSRVTRG=%TFS_EXTRACT_TARGET% SRCSRVCMD=%TFS_EXTRACT_CMD% SRCSRV: source files --------------------------------------- […]
Be aware that if PDB files are included in NuGet packages which are generated before the indexing step in the build, those PDBs are not indexed, and consequently can not be used to retrieve the source code. Notably the combination of generating NuGet packages with the SDK tooling (
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> in the .csproj file and
<file src="*.pdb" target="lib\net451\"/> in the .nuspec file) with included Windows PDBs (
<DebugType>full</DebugType>) includes non-indexed PDBs into the NuGet package. This can be checked with the
pdbstr command as described above.
A source server provides source code files to a debugger on demand. The debugger gets the name (and version if indexed) of the source file from the symbol file. It is easiest to use Azure DevOps as source server. Except from the above described Index Sources & Publish Symbols build task, no further configuration is needed.
Source Link is the official way of providing source code in connection with Portable PDBs. For NuGet packages generated by the SDK tooling, the configuration is done via NuGet packages in the csproj files, e.g.
<ItemGroup> <PackageReference Include="Microsoft.SourceLink.Vsts.Git" Version="1.0.0-beta2-18618-05" /> </ItemGroup>
There are constantly changes in the configuration and tooling, so this may change in the future. Refer to the official documentation for the most up-to-date procedure.
Source files can also be embedded in PDBs, see Source Embedding This reveals all of the source code for anyone in possession of the PDB file. For propriatory sofware, this is usually not acceptable. However, in combination with embedding the PDBs in the DLL, a completely self contained debugging experience in provided.