Wintellect Blogs

Correctly Creating Native C++ Release Build PDBs

8 Mar , 2014  

In my ongoing discussion of PDB files and debugging, Sa Li had a great question:

Should the final release native program also generate the pdb files before being published?

If switching on the pdb in link setting, what kind of info would be added into the .exe executable file?

I found our program grows a lot after turning on this flag. Does that have anything which can reveal some sensitive secrete of our program?

If you’ve read any of my books that discussed native debugging, I’ve been shouting that you must build PDB files for all builds. Of course, if you are paid by the hour don’t build PDB files so debugging is massively harder and you have guaranteed employment. There’s nothing like debugging yourself a new car. Just kidding!

One of the issues that you bump into with debugging release build native C++ applications are the compiler optimizations. For example, many local variables disappear because instead of putting them on the stack, as what happens in debug builds, the code generator puts them in registers instead. Additionally, release builds aggressively inline calls to functions so the code generator puts the body of the function directly in the calling method. Once you get used to the compiler’s patterns, and know a bit of assembly language, it’s not too hard to figure out what’s going on when debugging release build code. If you have never debugged release builds, I’d highly recommend the outstanding book, Advanced Windows Debugging, by Mario Hewardt and Daniel Pravat to get you started.

What I want to cover in this article are the exact switches necessary to properly create native C++ release build PDBs without screwing up your application so I can answer Sa’s question. The switches I will show you have nothing to do with optimizations and PDB file creation does not affect optimizations.

The first switch to set is on the compiler, CL.EXE and it’s /Zi. This switch will put the debug information in the .OBJ file so the linker can put it into the final PDB. You’ll set this switch in the project’s C/C++ property page as shown below.

The next three switches apply to the linker, LINK.EXE. The first, /DEBUG, tells the linker that you want to build create a PDB file for this build. Here’s the property page:

There’s a small problem with the /DEBUG switch. Turning it on tells the linker that you are doing a non-optimized build so /DEBUG implicitly turns on the /INCREMENTAL and essentially creates a debug build, though the compiler optimizations would apply (but not the link time code generation optimizations). What this means to you is that the linker links in “fast mode” so if you have an OBJ that has 300 functions in it, but you only reference (i.e., use) one of those functions, the linker puts all 300 functions into the output binary. Yes, that means 299 dead functions and a really bloated binary.

This “throw everything into the output binary” is one of the reasons your debug builds are so much bigger than your release builds. The Microsoft optimization technologies are extremely good, but not that good!

Because you only want those functions you actually reference in the output binary, you need to tell that to the linker with the /OPT:REF switch, which is set in the Optimization section of the linker as below:

The final linker switch you need to set is a an interesting little gem: /OPT:ICF. This turns on COMDAT folding. Wow! There’s a term you don’t hear every day. This is a nice little compiler optimization where the linker will look for functions that have identical assembly language code and only generate one of them. The first time most people here about COMDAT folding it throws them for a loop. However, when you consider how many functions, especially STL templates that are simple and identical, this COMDAT folding can help you slim down your binaries. For a more detailed discussion, see Raymond Chen’s examples and why on rare occasions optimized builds step into the “wrong” function with this switch turned on. If you looked closely at the Optimization section of the screen shot above, you’ll see the /OPT:ICF option is right below the /OPT:REF option.

In Sa’s case, I bet the reason their release build binaries are growing because they aren’t setting the /OPT:REF and /OPT:ICF switches. The only information added to a native C++ binary with these switches on is the debug directory in the output binary. As I mentioned in the first article in this series, you can peer into your Portable Executable files with the DUMPBIN program:

PS > dumpbin /headers foo.dll
// Output clipped for clarity.
Debug Directories
Time Type Size RVA Pointer
——– —— ——– ——– ——–
4A831A79 cv 4A 0000A5C0 97C0
Format: RSDS, {9FCACFCD-1503-4B25-A2EA-6009EAFC83BA}, 1, c:\foo\foo.pdb
// Output clipped for clarity.

I hope that clears things up for Sa and everyone else still working on native C++ applications. While .NET gets all the coverage these days, a huge amount of the Windows world still works because of those applications. Do you have any other questions about PDBs or debugging? Ask away by sending me an email (john @ this company’s domain) or through a question on the blog.

  • Anonymous
  • Anonymous

    Migrating some native C++ projects from VS6 to VS8. In VS6 the release builds have /Zi and /OPT:REF. The PDB files are included in the install package.

    Using the same options with VS8, the PDB grows in size by 300%, a signicant impact to the install package.

    First guess was that VS6 stripped public symbols in NDEBUG builds, but VS8 did not.

    Tried the setting you recommend here and the size of the PDB (*_STRIPPED) files in VS8 dropped to half of the size of VS6. Wow, what is going on here.

  • Anonymous

    Excellent article. Must check these setting in our projects :)

    [q]Finally, I hope that was a typo when you wrote “VS8.” Visual Studio 2005 is ancient. If you’re moving to a VS version, Visual Studio 2008 is much better and faster.[/q]
    … Dunno. We use VS2005sp1 (99% C++) and I have tried 2008express on my private PC. Can’t see too much difference, really. Was your comment targeted at .Net or do you also think VS2008 is a major improvement when developing C++ only?


  • Anonymous

    Except that VS2008 seems to be a worse IDE overall. I’m sure the compiler team made some significant improvements, but the IDE hangs more, crashes more, Intellisense barely works at all, etc. In my opinion it is much worse than VS2005 which is much worse than VS6. The trend continues with VS2010.

  • Anonymous

    thanks for the insightful article.
    I have the following PDB related problem:
    I ported an application from VS.NET to VS2010. That application contains a stack dumper and an external reader that matches the dump file with the corresponding PDB.
    – First the PDB file grew from 13 Mb to 55 Mb; yes I have the settngs that you mention all set. However, the real problem is:
    – Matching the addresses from the dump with the PDB file, both created with the VS2010 version, returns wrong function names. It seems that either the addresses as generated by the stack dumper are wrong (why?), or the matching via SymGetSymFromAddr() fails (why?) – or both.
    Any hint? Any hidden change in VS2010 that breaks this code?
    Any suggestion is greatly appreciated.

    Kind regards,

  • Anonymous

    Just solved the problem. The dump-reading tool was compiled with VS2003. After porting it to VS2010 things work fine now.

    Kind regards,