Complete roguelike tutorial using C++ and libtcod - extra 2: debugging
Complete roguelike tutorial using C++ and libtcod -originally written by Jice Text in this tutorial was released under the Creative Commons Attribution-ShareAlike 3.0 Unported and the GNU Free Documentation License (unversioned, with no invariant sections, front-cover texts, or back-cover texts) on 2015-09-21. |
---|
|
Sooner or later (and always sooner than expected), you will do something wrong in your code and the result will be a sudden, brutal, impolite crash of your application.
This article will show you how to debug the application with a command line debugger. I know it's easier to use an IDE and I might add a dedicated article on debugging with CodeLite. But you don't always have an IDE available. Your favorite IDE might not be ported to some obscure platform you want your game to run on. gdb is always a useful addition in any C/C++ developer's swiss army knife.
Let's crash
For example, let's do some minor change to the Engine constructor and move the map creation before the player creation :
map = new Map(80,45);
player = new Actor(40,25,'@',TCODColor::white);
actors.push(player);
This seems totally inoffensive. Let's recompile the game as usual :
Windows :
> g++ src/*.cpp -o tuto -Iinclude -Llib -ltcod-mingw -static-libgcc -static-libstdc++
Linux :
> g++ src/*.cpp -o tuto -Iinclude -L. -ltcod -ltcodxx -Wl,-rpath=.
Now if you run the game, you should see the game window disappearing as soon as it opens. No error message, no nice stack trace. What the hell ? Well welcome to native application coding.
Adding debugging information
To be able to understand what's happening, we need a debugger. And for the debugger to be able to track what the program is doing, we need to add information in the .exe. With g++ you do that with the -g flag.
We'll also go further and use the libtcod library that also contains debugging information. That means adding -debug to the library names in the compilation command :
Let's recompile with debugging information :
Windows :
> g++ src/*.cpp -o tuto -Iinclude -Llib -ltcod-mingw-debug -static-libgcc -static-libstdc++ -g
Linux :
> g++ src/*.cpp -o tuto -Iinclude -L. -ltcod_debug -ltcodxx_debug -Wl,-rpath=. -g
Ok, now our game is bigger, probably slightly slower, but we can see what it's doing while running.
Smashing the bug
Let's load the game into GNU's debugger :
> gdb ./tuto ... Reading symbols from tuto.exe...done. (gdb)
Now we have gdb's prompt. Let's run the game (type 'r' or 'run')
(gdb) r Starting program: tuto.exe 24 bits font. key color : 0 0 0 character for ascii code 255 is colored Using SDL renderer... Program received signal SIGSEGV, Segmentation fault. 0x00401cc9 in Map::createRoom (this=0x9149240, first=true, x1=47, y1=14, x2=56, y2=22) at src/Map.cpp:75 engine.player->x=(x1+x2)/2;
First we can see some messages printed by libtcod on the game's standard output (24 bits font and so on). Then it happens.
Program received signal SIGSEGV, Segmentation fault. This scary SIGSEGV word means that the application received a SIGnal because there was a SEGmentation Violation. Seems pretty bad. Segmentation violation is a cuss word to say that you tried to access some part of the memory you are not allowed to. How is that possible ? Well, at this stage you should know that C has pointers, integer variables storing a memory address. Since you can put any value in a pointer variable, it can point to any part of the memory, including parts that do not belong to your program or that do not exist. For example :
int *pi = NULL;
*pi = 0;
Here we're trying to put the value 0 at address NULL (== 0). The very first byte of memory. No need to say, this part of the memory is locked by the OS and no user program can write there. This code will trigger a SIGSEGV.
Back to our game. gdb kindly provides the faulty line :
0x00401cc9 in Map::createRoom (this=0x9149240, first=true, x1=47, y1=14, x2=56, y2=22) at src/Map.cpp:75
It happened in Map.cpp in the function createRoom at line 75 :
engine.player->x=(x1+x2)/2;
Using a pointer dereference like (*ptr) or ptr->... is a typical case where a SIGSEGV can occur. Let's see the value of the player pointer :
(gdb) print engine.player $1 = (Actor *) 0x0
0x0 is not a smiley. It's the hexadecimal value for 0. Our pointer is NULL. No surprise trying to dereference it leads to a crash. Remember that when we build the map, we place the player in the center of the first room. The problem is that since we moved the map creation before the player creation, we can't do that anymore.
Ok let's revert our change.
player = new Actor(40,25,'@',TCODColor::white);
actors.push(player);
map = new Map(80,45);
libtcod's debugging library
Now we'll see something different. In the Engine::render function, add this line just before the TCODConsole::flush() call :
TCODConsole::root->putChar(100,100,'X');
TCODConsole::flush();
We're trying to print a character outside of the console. If you compile this with the release version of libtcod, everything will run smoothly, except that you won't see the X on the screen. That's because the release version of libtcod ignores silently calls with bad parameters to keep the game from crashing.
The debug version of libtcod is not as kind. Try to run the game :
> ./tuto
24 bits font.
key color : 0 0 0
character for ascii code 255 is colored
Using SDL renderer...
Assertion failed: dat != ((void *)0) && (unsigned)(x) <
(unsigned)dat->w && (unsigned)(y) < (unsigned)dat->h,
file src/console_c.c, line 253
This time, the game exited with a failed assertion. That means that some libtcod function was called with bad parameter values.
Using gdb to handle failed assertions
Let's run again with gdb :
> gdb tuto ... Reading symbols from tuto.exe...done.
gdb on Linux automatically stops the code execution when an assertion fails. Unfortunately, Mingw's version doesn't. We have to add the breakpoint by hand :
(gdb) break _assert Function "_assert" not defined. Make breakpoint pending on future shared library load? (y or [n]) y
OK, now let's run the game.
(gdb) r Starting program: tuto.exe 24 bits font. key color : 0 0 0 character for ascii code 255 is colored Using SDL renderer... Breakpoint 1, 0x65ea273c in _assert () from libtcod-mingw-debug.dll
The breakpoint stopped the game execution when the failed assertion was triggered. At this point, you can examine the call stack to see where it happened :
(gdb) where #0 0x65ea273c in _assert () from libtcod-mingw-debug.dll #1 0x65e43f7a in TCOD_console_put_char (con=0x3e3fb0, x=100, y=100, c=88, flag=TCOD_BKGND_DEFAULT) at src/console_c.c:253 #2 0x65e70ed4 in TCODConsole::putChar (this=0x3e3f70, x=100, y=100, c=88, flag=TCOD_BKGND_DEFAULT) at src/console.cpp:186 #3 0x00401a50 in Engine::render (this=0x422060) at src/Engine.cpp:82 #4 0x00401ed8 in main () at src/main.cpp:11
The assertion failed in libtcod's TCOD_console_put_char function, that was called by the C++ wrapper TCODConsole::putChar. More interesting is where in our code it occurred : in Engine.cpp, line 82.
The debugger is now stopped inside the _assert function. Let's tell him that we would rather focus on the third function in the stack.
(gdb) frame 3 #3 0x00401a50 in Engine::render (this=0x422060) at src/Engine.cpp:82 TCODConsole::root->putChar(100,100,'X');
Ahaaah ! Here is the culprit.
gdb quick sheet
The main (only?) commands you need when debugging are :
break / clear : add / remove a breakpoint
condition / cond : add a condition to a breakpoint
step / s : execute the next line, entering inside function calls
next / n : execute the next line, stepping over function calls
finish : execute until the end of the current function
cont / c : continue execution
frame / up / down : navigate in the call stack
print : display variables content
A few tricks :
- when entering a function/method name you can press TAB twice to use autocomplete. For example : break Engine::<TAB><TAB>
- pressing Enter at the gdb prompt repeat the last command. Convenient when you're doing step by step.
But if you want to become a gdb ninja, here is a more complete list.