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.
There are other ways we could handle the problem of separating GDB output from the program's output. For instance, we could start the program's execution first, then fire up GDB in another window, attaching it to the running program.
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.
What about DDD? Again, you'll need a separate window for execution of the program. Arrange this by choosing View | Execution Window, and DDD will pop up an execution window. Note that you don't type the sleep
command into that window, as DDD does that for you. The screen will now appear as in Figure 6-2.
Set breakpoints as usual, but remember to type the program input in the execution window.
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.