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:foofoo.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.