Debugging GUI Programs

These days, users are accustomed to having their application programs come with graphical user interfaces (GUIs). They are, of course, just programs, so general debugging principles apply, but special considerations do come into play.

GUI programming consists largely of calls to a library to perform various operations on the screen. There are many, many such libraries in widespread use. We obviously can't cover them all, and in any case the principles are similar.

Accordingly, we've chosen the simplest example, the curses library. It's so simple that many people might not consider it a GUI at all—one student called it "a text-based GUI"—but it will get the point across.

The curses library enables the programmer to write code that will move the cursor around the screen, change colors of characters or change them to reverse video, insert and delete text, and so on.

For example, text editors such as Vim and Emacs are programmed in curses. In Vim, hitting the j key will make the cursor move down one line. Typing dd will result in the current line being erased, the lines below it moving up one line each, and the lines above it remaining unchanged. These actions are achieved by calls to the functions in the curses library.

In order to use curses, you must include this statement in your source code

#include <curses.h>

and you must link in the curses library:

gcc -g sourcefile.c -lcurses

Let's take the code below as an example. It runs the Unix ps ax command to list all the processes. At any given time, the line at which the cursor currently lies will be highlighted. You can move the cursor up and down by hitting the u and d keys, and so on. See the comments in the code for a full list of commands.

Don't worry if you haven't used curses before, as the comments tell what that library does.

// psax.c; illustration of curses library

// read this code in a "top-down" manner:  first these comments and the global
// variables, then main(), then the functions called by main()

// runs the shell command 'ps ax' and saves the last lines of its output,
// as many as the window will fit; allows the user to move up and down
// within the window, with the option to kill whichever process is
// currently highlighted

// usage:  psax

// user commands:

//    'u':  move highlight up a line
//    'd':  move highlight down a line
//    'k':  kill process in currently highlighted line
//    'r':  re-run 'ps ax' for update
//    'q':  quit
// possible extensions: allowing scrolling, so that the user could go
// through all the 'ps ax' output, not just the last lines; allow
// wraparound for long lines; ask user to confirm before killing a
// process

#define MAXROW 1000
#define MAXCOL 500

#include <curses.h>  // required

WINDOW *scrn; // will point to curses window object

char cmdoutlines[MAXROW][MAXCOL];  // output of 'ps ax' (better to use
                                   // malloc())
int ncmdlines,  // number of rows in cmdoutlines
    nwinlines,  // number of rows our "ps ax" output occupies in the
                //  xterm (or equiv.) window
    winrow,  // current row position in screen
    cmdstartrow,  // index of first row in cmdoutlines to be displayed
    cmdlastrow;   // index of last row in cmdoutlines to be displayed

// rewrites the line at winrow in bold font
highlight()
{
   int clinenum;
   attron(A_BOLD);  // this curses library call says that whatever we
                    // write from now on (until we say otherwise)
                    // will be in bold font
   // we'll need to rewrite the cmdoutlines line currently displayed
   // at line winrow in the screen, so as to get the bold font
   clinenum = cmdstartrow + winrow;
   mvaddstr(winrow,0,cmdoutlines[clinenum]);
   attroff(A_BOLD);  // OK, leave bold mode
   refresh();  // make the change appear on the screen
}
// runs "ps ax" and stores the output in cmdoutlines
runpsax()
{
   FILE *p; char ln[MAXCOL]; int row,tmp;
   p = popen("ps ax","r");  // open UNIX pipe (enables one program to read
                            // output of another as if it were a file)
   for (row = 0; row < MAXROW; row++)  {
      tmp = fgets(ln,MAXCOL,p);  // read one line from the pipe
      if (tmp == NULL) break;  // if end of pipe, break
      // don't want stored line to exceed width of screen, which the
      // curses library provides to us in the variable COLS, so truncate
      // to at most COLS characters
      strncpy(cmdoutlines[row],ln,COLS);
      cmdoutlines[row][MAXCOL-1] = 0;
   }
   ncmdlines = row;
   close(p);  // close pipe
}

// displays last part of command output (as much as fits in screen)
showlastpart()
{
   int row;
   clear();  // curses clear-screen call
   // prepare to paint the (last part of the) 'ps ax' output on the screen;
   // two cases, depending on whether there is more output than screen rows;
   // first, the case in which the entire output fits in one screen:
   if (ncmdlines <= LINES)  { // LINES is an int maintained by the curses
                              // library, equal to the number of lines in
                              // the screen
      cmdstartrow = 0;
      nwinlines = ncmdlines;
   }
   else  { // now the case in which the output is bigger than one screen
      cmdstartrow = ncmdlines - LINES;
      nwinlines = LINES;
   }
   cmdlastrow = cmdstartrow + nwinlines - 1;
   // now paint the rows to the screen
   for (row = cmdstartrow, winrow = 0; row <= cmdlastrow; row++,winrow++)
      mvaddstr(winrow,0,cmdoutlines[row]);  // curses call to move to the
                                            // specified position and
                                            // paint a string there
   refresh();  // now make the changes actually appear on the screen,
               // using this call to the curses library
   // highlight the last line
   winrow--;
   highlight();
}
// moves cursor up/down one line
updown(int inc)
{
   int tmp = winrow + inc;
   // ignore attempts to go off the edge of the screen
   if (tmp >= 0 && tmp < LINES)  {
      // rewrite the current line before moving; since our current font
      // is non-BOLD (actually A_NORMAL), the effect is to unhighlight
      // this line
      mvaddstr(winrow,0,cmdoutlines[winrow]);
      // highlight the line we're moving to
      winrow = tmp;
      highlight();
   }
}
// run/re-run "ps ax"
rerun()
{
   runpsax();
   showlastpart();
}
// kills the highlighted process
prockill()
{
   char *pid;
   // strtok() is from C library; see man page
   pid = strtok(cmdoutlines[cmdstartrow+winrow]," ");
   kill(atoi(pid),9);  // this is a UNIX system call to send signal 9,
                       // the kill signal, to the given process
   rerun();
}

main()
{
   char c;
   // window setup; next 3 lines are curses library calls, a standard
   // initializing sequence for curses programs
   scrn = initscr();
   noecho();  // don't echo keystrokes
   cbreak();  // keyboard input valid immediately, not after hit Enter
   // run 'ps ax' and process the output
   runpsax();
   // display in the window
   showlastpart();
   // user command loop
   while (1)  {
      // get user command
      c = getch();
      if (c == 'u') updown(-1);
      else if (c == 'd') updown(1);
      else if (c == 'r') rerun();
      else if (c == 'k') prockill();
      else break;  // quit
   }
   // restore original settings
   endwin();
}

Running the program, you'll find that the picture looks all right, but when you hit the u key to make the cursor go up a line, it doesn't work correctly, as you see in Figure 6-1.


The output of ps ax is in ascending order by process number, yet suddenly you see process 2270 being displayed after 7162. Let's track down the bug.

A curses program is a debugging book author's dream, because it forces the programmer to use a debugging tool. The programmer cannot use printf() calls or cout statements to print out debugging information, because that output would be mixed in with the program output itself, causing hopeless chaos.

So start up GDB, but there is one extra thing we must do, related to that last point. We must tell GDB to have the program execute in a different terminal window than the one that GDB is running in. We can do that with GDB's tty command. First, we go to another window, in which the program I/O will be done, and run the Unix tty command there to determine the ID for that window. In this case, the output of that command tells us that the window is terminal number dev/pts/8, so we type

(gdb) tty /dev/pts/8

in the GDB window. From now on, all keyboard input and screen output for the program will be in the execution window.

One last thing before we start: We must type something like

sleep 10000

in the execution window, so that our keyboard input to that window will go to the program, rather than to the shell.

Next, set a breakpoint at the beginning of the function updown(), since the error occurs when we try to move the cursor up. We then type run, and the program will begin to execute in the execution window. Hit the u key in that window, and GDB will stop at the breakpoint.

(gdb) r
Starting program: /Debug/psax
Detaching after fork from child process 3840.

Breakpoint 1, updown (inc=-1) at psax.c:103
103     {  int tmp = winrow + inc;

First, let's confirm that the variable tmp has the right value.

(gdb) n
105        if (tmp >= 0 && tmp < LINES)  {
(gdb) p tmp
$2 = 22
(gdb) p LINES
$3 = 24

The variable winrow shows the current location of the cursor within the window. That location should be the very end of the window. lines has the value 24, so winrow should be 23, since the numbering starts at 0. With inc equal to -1 (since we were moving the cursor up, not down), the value of tmp shown here, 22, is confirmed.

Now let's go to the next line.

(gdb) n
109           mvaddstr(winrow,0,cmdoutlines[winrow]);
(gdb) p cmdoutlines[winrow]
$4 = " 2270 ?        Ss     0:00 nifd -n\n", '\0' <repeats 464 times>

Sure enough, there it is, the line for process 2270. We quickly realize that the line

mvaddstr(winrow,0,cmdoutlines[winrow]);

in the source code should be

mvaddstr(winrow,0,cmdoutlines[cmdstartrow+winrow]);

Once we fix that, the program runs fine.

When we are done, press CTRL-C in the execution window, so as to kill the sleep command and make the shell usable again.

Note that if something goes wrong and the program finishes prematurely, that execution window may retain some of the nonstandard terminal settings—for example, cbreak mode. To fix this, go to that window and press CTRL-J, then type the word reset, then press CTRL-J again.

First note that in building your project, you need to tell Eclipse to use a -lcurses flag in the makefile, the procedure for which was shown in Chapter 5.

Here too you will need a separate execution window. You can do this when you set up your debug dialog. After setting up a run dialog as usual and selecting Run | Open Debug Dialog, we'll take a slightly different path from what we've done up to now. In Figure 6-3, note that in addition to the usual choice, C/C++ Local Application, there is also the option C/C++ Attach to Local Application. The latter means that you want Eclipse to make use of GDB's ability to attach itself to an already running process (discussed in Chapter 5). Right-click C/C++ Attach to Local Application, select New, and proceed as before.


When you start an acual debug run, first start your program in a separate shell window. (Don't forget that the program probably resides in your Eclipse workspace directory.) Then select Run | Open Debug Dialog as you usually do the first time you go through a debug run; in this case, Eclipse will pop up a window listing processes and asking you to choose the one to which you wish GDB to attach. This is shown in Figure 6-4, which shows that your psax process has ID 12319 (notice the program running in another window, partially hidden here). Click that process, then click OK, leading to the situation depicted in Figure 6-5.

In that figure, you can see that we stopped during a system call. Eclipse informs you that it does not have source code for the current instruction, but this is to be expected and is no problem. Actually, this is a good time to set breakpoints in the source file, psax.c. Do so, and then click the Resume icon. Eclipse will run until it hits the first breakpoint, and then you can debug as usual.