40. My app works in debug build, but crashes in release. What's the difference between the two?
This is a very common question that arises on the
newsgroups and a lot of people don't fully understand the differences between
debug and release builds, so it's sometimes wrongly answered. The differences
fall into several camps, any one of which can kill you. So, presented in the
order of most likely problem cause -> least likely, these are the major
differences:
Heap Allocator
The debug heap allocator puts guard bytes around
the memory for objects that you allocate on the heap (e.g. using
"new"). If you write beyond the end of an object - a common error in
C/C++ when people forget that arrays are indexed from zero and get their indexes
off by one - then you will probably get away with it in debug mode, but in
release mode you'll definitely cause corruption, and probably a crash sometime
later.... just far enough later to completely confuse you as to the cause.
Whilst we're on the subject of array overwrites,
a dead giveaway for a local (stack) variable overwrite, by the way, is
the function that crashes when it returns. This happens because someone wrote
past the end of an automatic-declared array and corrupted the stack frame. OK,
end of digression.....
ASSERT Behaviour
Many C programming books tell you to "ASSERT
the world". You typically put ASSERT statements in to sanity-check your
code, such as checking that parameters have sensible values, and that handles
are not null. Unfortunately, folks sometimes put code in an ASSERT that they
shouldn't - code which is FUNCTIONAL in nature, i.e. active code for their app.
To take an example:
ASSERT (OpenMyWindow () != NULL);
instead of
hWND = OpenMyWindow();
ASSERT (hWND != NULL);
Code in an ASSERT is omitted in release builds,
so if you wrote the first example, your window would open in debug mode, but not
in release mode. This would be fairly obvious, but of course real-world
instances of this bug tend to be much more subtle....
If you want to use this checking mechanism for
functional code, use the VERIFY macro instead.
Debug Padding
Debug mode pads your code out with large amounts
of its own stuff (compare the size of a debug EXE with the corresponding release
EXE). If you have a wild pointer which scribbles somewhere it shouldn't, there's
a chance that in debug mode it will scribble on something which does no harm.
But you're unlikely to be so lucky in release mode.
Initialization
The debug version of the memory allocator will
initialise memory allocated from the heap (e.g. using "new") with the
magic value 0xCD. The release version of the allocator does not do this -
initial contents are undefined.
NOTE: the guard bytes I mentioned above in the
section on the heap allocator are initialised to 0xFD, and freed memory is
written with the value 0xDD by the debug version of the deallocator.
People frequently have the impression that local
variables are initialised in debug builds : not so. You have the option to
introduce this behaviour for debug builds yourself, by using the /GZ compiler
switch, which will initialise local variables with 0xCC bytes (if you initialise
the variable explicitly, your value of course overwrites the 0xCC). But the /GZ
option is not on by default for debug builds in VC++. /GZ's primary purpose is
to catch uninitialised pointer errors, since the pointer will point to
0xCCCCCCCC when you use it.
Prototypes
If you use MFC ON_MESSAGE macros, then the function prototypes MUST match a
particular style, typically something like :
afx_msg LRESULT <class>::OnMyMessage (WPARAM wParam,
LPARAM lParam);
In debug mode, if your prototype doesn't match the required format, the
compiler will silently fix it and the code will work. The release mode compiler
WON'T fix it and your code can (probably will) go wrong. Wrong in new and
interesting ways :-).
Optimization
Most people will switch the optimizer on for
release builds. But not all optimizations are safe, and this may catch you out
if your code does anything unusual. Remember that you can always selectively
turn optimizing off for a region of code using #pragma directives. You are
unlikely ever to encounter this unless you specifically use aggressive
optimisations (I always use "minimize size", since it gets you all the
safe speed optimisations anyway).
Further Reading
The last resort in this kind of scenario is to run the release version
of your code under the debugger. This IS possible, though the results are
often hard to interpret, and of course your code may fail in the field in
circumstances which you just can't repro back at your desk. But if this
idea floats your boat, the full skinny on how to run release code under the
debugger can be found here