Using a debugger

By Macoy Madson. Published on .

I was quite annoyed by the top comment on this thread being "just use printf". This post is a more fleshed-out version of my comment on that thread.

Debuggers are drastically better than just using prints for many problems. An hour of research on this now will save you countless hours solving hard problems later.

In short, debuggers let you run your program in "human time" rather than ultra-fast computer time, giving you time to carefully inspect program state, control flow, and your assumptions.

This article serves as a stepping-off place for learning about debuggers.

Debugger programs

The best graphical debugger is, without contest, Microsoft Visual Studio. You can debug C, C++, and C# programs with it (and maybe more). In this article I will refer to it as "MSVS". MSVS is the industry-standard C/C++ debugger for game development. Most graphical debuggers will feel familiar to MSVS.

Note that VSCode also has debugging capabilities, but it isn't nearly as streamlined or powerful as MSVS.

On Linux, GDB is the top dog. It's a command-driven debugger, but there are many front-ends that you can use to get a MSVS-like experience. VS Code has a GDB front-end. I use Emacs and GUD to help with debugging (run gdb to start; I also prefer running gdb-many-windows after gdb has started).

For interpreted languages, there are often debuggers built specifically for them. For example, JavaScript in a web browser can be debugged via the "Developer Tools" inspector/debugger. Android Studio provides a Java debugger for Android apps. Search "your language" + "debugger" and you should find tools that provide interactive debugging features. Not all languages have interactive debuggers; this deficiency should be considered when deciding on a language to use.

Core features

Here is a list of features you should search and get familiar with using. Nearly every interactive debugger should support these fundamental features. You may need to look at your debugger's documentation. I assume an MSVS-like debugger for this article.

Breakpoints

Breakpoints insert a special instruction that halts your program at that point in the code. You place a breakpoint as close to the problem as you can, so you can inspect program state.

Stepping

Stepping is when you advance the program by one "step", usually one line of code. This is very useful because you can go line by line through your program, checking your watches/hovering your mouse over variables to see how the data is changing. "Step Into" means go into the function being called and step inside it. "Step Over" means don't step into that function, just run it and step to the next line. You usually Step Over functions you know are working/irrelevant to the problem.

Continue

Resumes the program execution. If you still have breakpoints, they may be hit again for the next iteration, game frame, etc.

Watches

Watches are added in MSVS via right-clicking a variable/statement and selecting "Add Watch". This displays the variable and its current value when the program is stopped.

You can modify watch statements to show you fields, the address of things, etc. by using the same syntax as the programming language, e.g. myVar in watch, add &myVar.field and it'll show you the address of field instead. Hovering over a variable with your mouse usually shows a pop-up that you can use to explore all the fields and substructs in a variable.

In MSVS, you can use e.g. myVar,3 if myVar is an array to display 3 elements in the watch. Use myvar-3 to decrement its address by 3. GDB has its own syntax for arrays, so refer to their documentation.

Advanced features

The above features form the core toolset of every interactive debugger. There are some advanced features that help solve trickier problems. I recommend you search e.g. "How to use conditional breakpoints in visual studio" and add these valuable tools to your toolset.

Data breakpoints

These take a memory address and a size. If memory at that address is changed by any code, the breakpoint will fire immediately after the change. This solves questions like "who the hell is changing this variable?" and assists with discovering things like memory stomps etc. It also helps narrow down tons of stepping, because you can set a data breakpoint to only notify you when there is change.

Conditional breakpoints

These only break if a condition you specify is met. In MSVS, you create these by right-clicking on an existing breakpoint and selecting "Add Condition". You can do e.g., bIsAttacking == true and the breakpoint will only be hit in that game state. This also saves you a lot of unnecessary stepping, if e.g. the thing only happens in certain states.

Moving the instruction pointer

In MSVS, you can drag around the little arrow pointing to the current line the debugger is stopped at. This allows you to repeat code so you can step through it again, or skip over blocks that aren't working yet.

Inspecting Memory

MSVS has a "Memory" window that lets you see the raw memory at arbitrary addresses. This can be useful when working with optimized code, among other things.

Assembly

For compiled languages, debuggers will often allow you to inspect and step at the assembly level. This is especially useful when working on program optimization.

Gotchas

Some things you need to be aware of, especially for C/C++ debugging:

Suggested workflows

McConnell's Code Complete recommends stepping through any new code you add as a habit. This helps spot easy problems by walking through things line by line and making sure your assumptions/understanding of the program are correct. It greatly reduces the amount of iteration you have to do. I like doing this instead of getting my hopes up that a new block of code will work the very first time, which is unrealistic for most non-trivial implementation tasks.

John Carmack recommends occasionally stepping through an entire frame to get a better understanding of your game's execution:

An exercise that I try to do every once in a while is to "step a frame" in the game, starting at some major point like common->Frame(), game->Frame(), or renderer->EndFrame(), and step into every function to try and walk the complete code coverage. This usually gets rather depressing long before you get to the end of the frame. Awareness of all the code that is actually executing is important, and it is too easy to have very large blocks of code that you just always skip over while debugging, even though they have performance and stability implications.

Prints are still useful, sometimes

There are some problems that printf may be the better tool for:

Other debugging techniques

Visualization is also a valuable technique:

Conclusion

There are some programmers who don't believe debuggers are very useful (Walter Bright, the creator of the D programming language, for example). I think this is silly. There's no harm in learning to use a debugger, especially when even the most rudimentary use can be hugely valuable. It's another tool suitable for some subset of problem debugging, much like prints.