What to do for debugging

Started by
10 comments, last by Zipster 9 years, 2 months ago

A few days ago, I wanted to test some initial systems I made and see how they would perform on a computer that wasn't my dev pc.

So I handed out some EXEs to my friends, first friend everything runs perfectly. Second friend instant crash! I thought maybe it has something to do with graphics drivers, so they tested it on a different computer... Boom crash again! Having no idea why I added more debug and checkpoint statements. After a hour or so I finally had enough statements to pin point the problem, figured out I never set a GLEW boolean to true

Then this got me thinking about debugging. What happens if something like this happens again? I don't want to go back and add a ton of statements every time a problem happens. Normally I would add some global debug variable to switch debugging on/off, but when I think about it that can't be that great of a solution right? I mean, you'ed have a if check for every block of debug statements. That's got to hurt performance to do all those checks, especially if you have a lot

So I'm wondering what do you guys do? Or am I doing it?

Advertisement

It depends on the programming language you use. It is normal to have at least two builds, one debug build and one release build. In C/C++ they differ mainly by defining a pre-processor variable like _DEBUG and switching off some optimizations and leaving debug symbols in the binaries in the debug build, while having no _DEBUG and doing aggressive optimization and stripping debug symbols in the release build. Using the _DEBUG variable with stuff like #if defined _DEBUG ... #endif allows to have pieces of code only in the debug build. If the project gets bigger, you may introduce a finer control than a global _DEBUG. So you can restrict embedded debug support to specific section / sub-systems / whatever.

One thing that is useful in this sense is the assertion checking (in C/C++ the assert(...) function). There is a paradigm called "design by contract", that verbally means "if the state is A when invoking me, then I guarantee that the resulting state is B". To verify this, assertions called "requirements" or "pre-conditions" at the beginning of functions can be checked, and assertions called "ensurements" or "post-conditions" at the end of functions can be checked. Notice that this means not to wait until a problem occurs. It is meant to be part of the programming process, giving you a tool to feel certain that things work as expected.

Of course, all this helps only if you give your friends / testers the debug build, and if the debug will log the problem before it crashes the program.

In addition to haegarr's observations, another method for providing debug information is logging.

If your compiler supports ANSI compliant macros, there are macros such as __LINE__, __FILE__, __TIME__, etc., that can be logged during execution.

You can define a global function (ugh) such as:


void Log(int line, char* file)
{
#ifdef _DEBUG
   std::cout << line << " " << file << "\n";
   // alternatively, output to a log file
#endif
}
// sprinkle your code with calls to Log(), primarily at the entrance to routines of interest.
bool RenderSomething()
{
   Log( __LINE__, __FILE__);
   ...
}


If a fatal error occurs, at a minimum, you'll have the line# and file name where the last log call was executed. Doing nothing in the log call for a Release mode exe adds only a minimum of overhead, and, eventually if desired, "Log( ... )" can be globally searched and deleted from your code.

EDIT: corrected argument type in Log().

Please don't PM me with questions. Post them in the forums for everyone's benefit, and I can embarrass myself publicly.

You don't forget how to play when you grow old; you grow old when you forget how to play.

The overhead of checking your global flag isn't that great, provided you don't do it too often.

But don't make the mistake of doing other expensive processing, even if the debug flag is off. Don't format complicated debug strings, then pass them to a debug function to be ignored. Instead, do the check first, before the formatting.

---

You might also want to consider enabling post-mortem debugging in your app. How to do this depends on the environment, but it's usually possible to collect some kind of crash dump, and ask the users to send this back, which lets you see what went wrong in a production environment that you can't necessarily replicate. A lot of (e.g. PC) games do this.

It's best to collect crash dumps from executables you distribute. You can then load these crash dumps with the PDBs generated _at the same time you built the executable_ into Visual Studio quite easily (meaning that you must keep your PDBs around for any executable - or DLL - that you distribute).

You could generate minidumps yourself using code like that illustrated in CodeProject: Post-Mortem Debugging Your Application with Minidumps and Visual Studio .NET.

Another option is Google Breakpad. It not only generates the dumps but also helps provide a way of automatically collecting those crash dumps with minimal (or no) user interaction, which will be helpful once you're distributing to builds to more than a couple friends.

With these options, you can collect crashes from any user without any runtime overhead. The executable does not need to be built in Debug mode though it does need to have symbols enabled (which has no runtime overhead).

The problem with adding logging is that you very probably didn't add all the logging you need to debug the problem. Then you're having to continuously deliver updated executables to your long-suffering users, who are continously sending you slightly different log outputs in the hope that the problem is fixed.

With a crash dump, you can immediately inspect the back trace and see exactly where the crash occurred, inspect the stack (to a degree, anyway, since it _is_ a Release mode executable) and even some parts of the program's memory. Basically, you can load up the crash right into your debugger just as if the crash had happened on your own machine, with a few limitations.

--

On top of all this, you can split up your logging, asserts, and so on a bit further. You don't need just Debug mode with everything turned on and Release with all the logging turned off. You can and having some logging statements enabled even in your final production builds. e.g. you might have macros like DEBUG_LOG_IF and RELEASE_LOG_IF or ASSERT and ALWAYS_ASSERT or whatever suits your fancy. A number of high-performance, flexible logging libraries already provide APIs of that sort.

It's not uncommon for games to have a Debug build (no optimizations, all logging enabled, etc.), an Optimized build (full optimizations, all logging enabled) and a Release build (all optimizations, limited logging). You might even have more intermediate build types that e.g. enable "excessive" logging for some modules of the engine to help pinpoint bugs there by turning off all optimizations in that module (so debugging is easy) while keeping optimizations on everywhere else so the game runs well.

Yet another option (that can be combined with all of the previous ones; none of these are exclusionary choices) is to allow efficient runtime selection of logging. A macro like:

#define LOG_IF(predicate, message, ...) do{ if (!!(predicate)) ::LogFunctionPrintf(__FILE__, __LINE__, __FUNCTION__, (message), __VA_ARGS__); }while(false)
Would call the LogFunctionPrintf with location information only if predicate is set. This predicate can just check for a simple global boolean (among other things) so that graphics logging (for example) is only enabled if gLogGraphics is set. Fancier macros and support systems can automate the enable/disable support by modules, vastly improve on the awfulness of the C printf syntax and feature set, allow a "log only once" feature, allow a "break first time this is hit if in a debugger" feature, and so on. Various existing logging libraries support these features, or you can add them yourself if you're into that.

Sean Middleditch – Game Systems Engineer – Join my team!

I use log files and crash dumps.

Log Files

Very good when starting the game as there is often problems there with drivers and everything. I prefer to log each subsystem. Example.


Init Keyboard - Done
Init Audio - Done
Init OpenGl - 

Gives a good hint that something is off with the opengl system as it died before the Done log message smile.png. While the game is running i avoid logging anything but major events such as changed level and return to main menu.

Crashdumps

When everything crash and burn it's nice to have a crashdump to fall back on. As SeanMiddleditch said you need to keep the pdb's for each version you release and setup some sane way of tagging them with version numbers. Everyone will swear by their mothers grave that they used the latest build and they will be wrong so it's best to have a version number in the crashdump filename dry.png . When you open a crashdump (in visual studio) you get the callstack of the crash and some of the variables used in the call. Not a direct answer but it makes it a bit more easy to guess what is wrong cool.png .

I have an example of a crash system i have done. Really old code for visual studio 2008 and not so sure it works anymore but you can always have a look and get some ideas. Check it out at http://spinningcubes.com/?page_id=958. That system is made to include the pdb's in the build so artists and other developer can get direct crash messages. But it should also be possible to toggle on so it writes down a minidump only.

@spinningcubes | Blog: Spinningcubes.com | Gamedev notes: GameDev Pensieve | Spinningcubes on Youtube

As for knowing when to log it, remember to check every return value.

Many people, beginners especially, will pull code from tutorials and guides where the code is meant to be an example with everything going right.

Whenever your function returns a status code you should check and verify it is correct. If incorrect, drop a log entry and handle the response.

Some people are afraid of all the if(status!=0) blocks after each major command, and if you're among them you can use the likely/unlikely flags on GCC or PGO optimizations

It's best to collect crash dumps from executables you distribute. You can then load these crash dumps with the PDBs generated _at the same time you built the executable_ into Visual Studio quite easily (meaning that you must keep your PDBs around for any executable - or DLL - that you distribute).

The problem with adding logging is that you very probably didn't add all the logging you need to debug the problem. Then you're having to continuously deliver updated executables to your long-suffering users, who are continously sending you slightly different log outputs in the hope that the problem is fixed.

With a crash dump, you can immediately inspect the back trace and see exactly where the crash occurred, inspect the stack (to a degree, anyway, since it _is_ a Release mode executable) and even some parts of the program's memory. Basically, you can load up the crash right into your debugger just as if the crash had happened on your own machine, with a few limitations.


I definitely want to add crash dumps to my system to make my future testing easier.
With C++ I'm actually really fuzzy when it comes exceptions/crashes. I pretty much have no experience in this area sad.png

I understand what crash dumps are and I know that PDB files are generated with my build (my IDE is visual studio 2013).
But I still do not understand to actual use them. Also what if I want to run my application on OSX or Linux, will those PDB files work?
When I think about it I'm not actually sure how to actually generate a crash dump wacko.png

I mean it can't be as easy as:


 
void main
{
     try
     {
          /*
           Normal game code. Everything. Update Loop, Render loop, Input code, etc
          */
     }
     catch(exception &e)
     {
           /*Generate Crash dump*/
          /* Create a new file and write all the exception things */
     }
}
 


I understand what crash dumps are and I know that PDB files are generated with my build (my IDE is visual studio 2013).
But I still do not understand to actual use them. Also what if I want to run my application on OSX or Linux, will those PDB files work?
When I think about it I'm not actually sure how to actually generate a crash dump

It won't work in OSX or Linux because they don't use Windows executables. (Well, they can if you are using WINE, but it still works the same.)

It is almost as easy as you wrote.

Create your exception handler with the signature: LONG WINAPI MyCrashHandler(EXCEPTION_POINTERS * ExceptionInfo);

You can put whatever popups and useful information you want, but make sure it calls MiniDumpWriteDump(). That function will write your minidump file.

Then in your main function, register your function with ::SetUnhandledExceptionFilter(MyCrashHandler);

That will capture most stuff, but there are a few situations where the system terminates your program without calling it, like fatally corrupting your heap.


That will capture most stuff, but there are a few situations where the system terminates your program without calling it, like fatally corrupting your heap.

I believe I mentioned this in another thread, but we usually set up a separate launcher that starts the game and attaches itself as a debugger so it can catch the exceptions and write the mini dumps. It's a little more work to get up and running, but being able to catch those extra tricky bugs where the game itself is indeed in a bad state, is totally worth it. Plus you prevent someone from attaching their own debugger to the game, which isn't a huge deterrent for a determined hacker but it's low-hanging fruit that comes for free with the crash dump collector :)

This topic is closed to new replies.

Advertisement