Debug symbols are one of the key elements of an effective debugging session. Debug symbols keep a mapping of the source code to the generated EXE or DLL binary; this allows the debugger to set breakpoints at source code locations and display rich debugging information when a breakpoint is hit. Debug symbols are stored in program database (PDB) files, and these are generated in Visual C++ 2008 for both debug and release by default. The project setting for debug file generation is located in the Linker | Debugging section, as shown in Figure 1.
Figure 1: Controlling Debug File Generation
For most small-team development, ensuring that all the projects that make up a solution are generating debug information is typically all that is required to ensure a good debugging experience. As the size of the development team grows and as bugs become more interesting, better debugging symbol support is required. For large teams, having all the projects required to build an application within the same solution will eventually become too time consuming from a build perspective. For DLLs that change infrequently and for DLL dependencies built by other sub-teams, excluding the project for these DLLs from the solution makes sense.
The problem with excluding DLLs from a solution occurs when a breakpoint needs to be set in the external project or when the call stack contains significant calls from the binary dependency. Unless the PDB file also has been copied over with the DLL, breakpoints from functions from within the DLL are difficult to set and the call stack will be missing the function names. While manually building the required DLL to generate the PDB file is not usually an overly difficult task, this process can become tedious when it needs to be repeated frequently.
The solution to this problem is to locate all the PDB files from common DLLs onto a network share and get Visual Studio to download automatically and cache the PDBs. Prior to Visual Studio 2005, this was a difficult exercise that involved using the Microsoft Symbol Server DLL (SymSrv.dll) from the Debugging Tools for Windows toolkit and setting up some Windows environmental variables (Knowledge Base Article 311503 details the procedure). Starting with Visual Studio 2005 and continuing with Visual Studio 2008, the download of debug symbols from an intranet server can be achieved easily by using the Visual Studio settings shown in Figure 2.
Figure 2: Download Debug Symbols from an Intranet Server
The dialog allows one or more PDB locations to be specified, and optionally allows a path to be provided where the debug symbols can be cached locally. As with a local PDB file, Visual Studio and the symbol server will only download and cache a PDB if it matches the DLL that has been loaded by the process. Locally cached files are stored with a check-sum in the folder path, so multiple PDB files that correspond to many different builds of a binary can be stored locally at the same time.
PDB files store the full path of the source code files that were used to build the binary, and if the files are in a different location on the machine where the binary is being debugged, Visual Studio will display a prompt asking for the location of the source code file. It is generally a good idea to ensure that all the developers within a team are using the same folder and drive structure to store the source code files that they pull down from the source repository to avoid path mismatches and a number of other similar issues.
Even with the best development environment setup, it is possible that partway through a debugging session, it will become apparent that the debug symbols for a particular DLL haven’t been loaded. If it is simple to reproduce the application state in the debug session, uploading the PDB file to the symbol server and restarting the debugger will generally be the best option. However, if reaching the same point in the debugging session would take a fair amount of time and effort, it is possible to manually load the PDB file from the Modules window, as shown in Figure 3.
Figure 3: Manually Loading Debug Symbols
Microsoft Symbol Server
For application-level bugs that are typically the result of incorrectly implemented business logic, working out the operation system calls or .NET Framework interactions that influenced the code’s execution isn’t generally required. For more interesting defects, such as multi-threading problems, memory corruption, or memory leaks, working out the operating system and framework calls that are occurring is a critical clue in working out the cause of the problem. For MFC and ATL applications, diagnosing framework problems isn’t too difficult because the source code ships with Visual C++, and the debug builds of these frameworks make stepping through the source code relatively easy.
For the .NET Framework and Windows operating system, the situation isn’t as straightforward; debug symbols for these binaries do not ship with the operating system. Microsoft maintains a publicly accessible symbol server at http://msdl.microsoft.com/download/symbols. This URL is not accessible via a browser, but when it is specified in the Visual Studio debugger options as shown in Figure 4, debug symbols for a range of Microsoft products, including Windows and the .NET Framework, will be downloaded. The settings shown in Figure 4 also will cache the files locally in the c:symbols folder. To confirm the setting has been applied correctly, the first time a debug session has been started and the Microsoft Symbol Server is contacted, the dialog shown in Figure 5 will appear.
Figure 4: Downloading Microsoft Debug Symbols
Figure 5: License Agreement Dialog
With these symbols loaded, the name of functions that have been called from operating system DLLs are visible. Figure 6 shows the before and after Call Stack view of a native application.
Figure 6: Before and After With Debug Symbols
The difference in information about the method that was called in kernel32.dll is quite apparent; rather than simply showing the address of the function called, the name of the method and the length in bytes of the parameters that the method takes is shown.
With complex Frameworks such as .NET, having debug information is even more valuable. Figure 7 shows the before and after views of the .NET finalizer thread. Without debug information, trying to work out what the thread is doing is quite difficult, but with debug symbols loaded, the meaningful function names instantly give an indication of the purpose of the thread.
Figure 7: .NET Application with Debug Symbols
Effectively managing and loading debug symbols is an important skill for a developer to have. Newer versions of Visual Studio make debug symbol management extremely easy, and once the small amount of time to correctly set up and debug symbol loading has been invested, the power of knowing exactly what is happening in all the DLLs loaded within a process will rapidly become apparent.
About the Author
Nick Wienholt is an independent Windows and .NET consultant based in Sydney. He is the author of Maximizing .NET Performance and co-author of A Programmers Introduction to C# 2.0 from Apress, and specialises in system-level software architecture and development, with a particular focus of performance, security, interoperability, and debugging.
Nick is a keen and active participant in the .NET community. He is the co-founder of the Sydney Deep .NET User group and writes technical articles for Australian Developer Journal, ZDNet, Pinnacle Publishing, CodeGuru, MSDN Magazine (Australia and New Zealand Edition) and the Microsoft Developer Network. An archive of Nick’s SDNUG presentations, articles, and .NET blog is available at www.dotnetperformance.com.
In recognition of his work in the .NET area, he was awarded the Microsoft Most Valued Professional Award from 2002 through 2007.