Why does Visual Studio require debugger symbol files to *exactly* match the binary files that they were built with?

Recently a coworker of mine lost the symbol file for one of his binaries.  Because he needed to debug that binary, getting those symbols back was of utmost importance since debugging without them is near impossible.  He decided to try and use a symbol file from a previous build whose sources exactly matched.  Much to his chagrin he found that Visual Studio refused to load them.  He asked me:

"Why is it that Visual Studio cannot load a symbol file for a binary that was built with the exact same set of source files?" 

Before I answer that, I'll take the question a step further:

"Surely if I build a component twice, consecutively, with no changes to source in-between, I certainly will end up with two identical copies of that component, right?"

Surprisingly, the answer to this question is: "no, that is not necessarily true".  The most obvious reason for this is the fact that an internal time-stamp will be different.  But even disregarding that, the answer is still the same, the actual layout of the code could be different.

The reason is that compiler writers are far more interested in generating correctly functioning code and generating it quickly than ensuring that whatever is generated is laid out identically on your hard drive.  Due to the numerous and varied methods and implementations for optimizing code, it is always possible that one build ended up with a little more time to do something extra or different than another build did.  Thus, the final result could be a different set of bits for what is the same functionality.

 

Here's a very simple example to demonstrate.  Imagine that the component you're building consists of a function and a variable.

Does it matter whether the resulting file contents looks like this?

0000: MyFunc()
0020: gGlobalVariable

or this?

0000: gGlobalVariable
0004: MyFunc()

Functionally, it doesn't matter at all, but for the debugger it's HUGELY important. In fact, getting it wrong can wreak havoc on your debugging session.  In this example, what if you used the symbol file from build #1 to try and determine the value of the global variable when running with the component built by build #2?  The debugger would consult the symbol file and return the value referenced by address 0020.  Unfortunately, the global variable isn't at that address in component build #2.  Rather some some value that makes up the instruction stream for MyFunc() is there.

A debugger depends on knowing what the internal layout of the component is.  So, I can now answer my colleague's original question:

"Because both the Visual Studio and your sanity depends on it."

 

 

PS: In practice, you are not likely to see a difference in compiler/linker output from build to build.  So in theory, it is possible to make use of mismatched symbols.  However, compiler/linker determinism is not guaranteed, our debugger cannot depend on it.  Furthermore, we would much rather not take the support cost of allowing some sort of override.  Therefore, if you find that you absolutely, positively, have to try and use a symbol file that doesn't match you can use WinDbg.  I'll leave getting it to work as an exercise for the reader, since you really, really don't want to do this. :)