Using GNU's GDB Debugger: Debugging Ncurses Programs

By Peter Jay Salzman


Previous: Debugging A Running ProcessNext: Other Stuff



Ncurses

Activities like printing characters to a screen, moving the cursor, and changing the color of character output are collectively known as screen handling. By its nature, screen handling is very terminal dependent, however, the terminfo and termcap mechanisms were devised to provide terminal independent screen handling. The curses library (a pun on the term "cursor optimization") was created to provide a screen handling API for C programmers. The goal of curses was to provide a fast, portable, and terminal independent C API to handle device dependent terminal codes.

Curses has a very long and twisted history. However, the most commonly used modern implementation of the library is called new curses, or ncurses, for short, which is maintained by . Ncurses is a GNU project released under an MIT style licence and is used under nearly all modern Unixes including GNU/Linux, and Mac OS X. There are now many extensions to ncurses which includes panels, menus and even a full featured widget set: the Curses Development Kit (CDK).



Getting Started

To follow along, download ncurses1.

1   // ncurses1.c
2   #include<ncurses.h>
3   #include<stdlib.h>
4   #include<time.h>
5   
6   unsigned int Seeder(void);
7   int Irand(int low, int high);
8   void Print_A_Character(void);
9   
10  int main(void)
11  {
12       atexit( (void *)endwin );
13       initscr();
14       Seeder();
15  
16       for (int i = 0; i < 500000; ++i)
17            Print_A_Character();
18  
19       return 0;
20  }
21  
22  
23  void Print_A_Character(void)
24  {
25       int x = Irand(1, COLS);
26       int y = Irand(1, LINES);
27       unsigned ascii = Irand('A', 'z');  // ASCII dependent
28       mvaddch(y, x, ascii);
29       refresh();
30  }
31  
32  
33  int Irand(int low, int high)
34  {  
35       return low + (int)( (double)(high-low) * rand()/(RAND_MAX + 1.0) );
36  }
37  
38  
39  unsigned int Seeder(void)
40  {
41       time_t seed;
42       time(&seed);
43       srand((unsigned)seed);
44  
45       return seed;
46  }

Compile and run the program. It should fill your console (or xterm) with characters. It has a bug though: the top row and first column seem to be devoid of characters:

Buggy ncurses program output

Since the probability of that happening is miniscule (and gets smaller with each passing second), there must be a bug in the program.

You need to do a bit more to use GDB with a program that uses ncurses. The problem is that GDB's I/O is intermixed with the program's I/O. Once you get used to it, this is not normally a problem. But when the program performs screen handling, it becomes difficult, if not impossible, to keep track of your debugging session. To see this in action, start GDB on the executable, set a breakpoint at Print_A_Character(), and run the program.

   $ gdb debugging_ncurses
   (gdb) break Print_A_Character 
   Breakpoint 1 at 0x80486fd: file debugging_ncurses.c, line 26.
   (gdb) run
   Starting program: code/ncurses/debugging_ncurses 
   
   Breakpoint 1, Print_A_Character () at debugging_ncurses.c:26
   26              int x = Irand(1, COLS);

Now issue continue 50 a few times. You should see a big mess. Here's what I see:

Image of ncurses mess

Quit GDB when you've had enough. Clearly, we need a way to separate GDB's I/O from the program's I/O when screen handling is done.



Separating the Input/Output

You'll need two terminals (either two consoles or two xterms): One for the program's I/O and another for GDB's I/O. Separating out the two I/O will resolve the problem nicely. I'll be using the word `xterm', but the same thing applies to all non-login terminals like rxvt and eterm, and login terminals like virtual consoles.

  1. Go to the first xterm and find its device file using either tty or who am i. This will be the xterm with GDB's I/O.:
       $ tty
       /dev/pts/1
       $ who am i
       p        pts/1        May 26 12:44 (:0.0)
    
  2. Go to the second xterm and find its device file. This will be the xterm with our program's I/O:
       $ tty
       /dev/pts/4
    
  3. Go back to the first xterm and start a debugging session. Set a breakpoint at Print_A_Character().
       $ gdb debugging_ncurses
       (gdb) break Print_A_Character 
       Breakpoint 1 at 0x80486fd: file debugging_ncurses.c, line 26.
       (gdb) 
    
  4. GDB's tty command instructs GDB to redirect the program's I/O to another terminal. The argument to tty is the device file of the terminal you wish the program I/O to go. In this case, I want the program's I/O to go to the second xterm, pts/4. If you're following along, use whatever device file you obtained in step 2:
       (gdb) tty /dev/pts/4
       (gdb) 
    
  5. Lastly, go to the second xterm (that contains the program's I/O) and tell the shell to sleep for a long time. This is so that anything we type in that window will be sure to go to our program rather than the shell. The amount of time is arbitrary, but pick a time that's longer than you suspect the debugging session will last. This tells the shell to "do nothing" for 100000 seconds:
       $ tty
       /dev/pts/4
       $ sleep 100000
    
  6. Go back to the first xterm which is running GDB and debug to your heart's content. When you're done, you can go back to the program output window and slap it with a control-c to break out of the sleep.


Debugging Ncurses Example

Let's go through a sample debugging session of debugging_ncurses.c. The problem was that the first row and column aren't being printed to. At first guess, we might suspect that the random number generator is at fault.

Under construction