3.8. The Program Debugger

Debugging programs written at the assembly language level presents a challenge to the programmer, in part because of the effort required to insert temporary diagnostics.

Figure 3-4. Output from the nm command for SQUARES
L> nm bin/squares
0000000000000000 a *ABS*
60000000000007e0 ? _DYNAMIC
6000000000000798 ? _GLOBAL_OFFSET_TABLE_
6000000000000930 G _IO_stdin_used
6000000000000780 ? __CTOR_END__
6000000000000778 ? __CTOR_LIST__
6000000000000790 ? __DTOR_END__
6000000000000788 ? __DTOR_LIST__
6000000000000960 A __bss_start
6000000000000750 D __data_start
40000000000005c0 t __do_global_ctors_aux
40000000000004a0 t __do_global_dtors_aux
6000000000000960 B __dso_handle
                 w __gmon_start__
6000000000000940 G __ia64_app_header
6000000000000758 D __libc_ia64_register_backing_store_base
                 U __libc_start_main@@GLIBC_2.2
6000000000000960 A _edata
6000000000000968 A _end
4000000000000640 ? _fini
40000000000002b0 ? _init
4000000000000400 T _start
6000000000000750 W data_start
4000000000000590 t done
6000000000000938 g dtor_ptr
4000000000000520 t first
4000000000000520 T main
6000000000000760 d sq1
6000000000000768 d sq2
6000000000000770 d sq3
L>

Debugging aids are available with most programming environments; we shall use only the simplest such tools in command-line mode, the adb debugger for HP-UX and the gdb debugger for Linux and HP-UX. We present a subset of commands and techniques for exploration of our sample programs.

Preventing bugs and logic flaws through careful design and reasoning is a laudable goal for any programmer. Nevertheless, few final programs are developed without passing through an awkward debugging phase to fix the inevitable errors. Learning to use at least the simplest capabilities of a debugger can greatly increase productivity.

The command-line debuggers provide interactive environments that will allow you to experiment with your programs as you develop them. We shall illustrate how to use the debugger for obtaining simple formatted output.

3.8.1. Capabilities of Debugger Programs

A debugger allows you to execute your program interactively through the control capabilities of the debugger program itself. A typical debugger includes facilities such as the following:

  • State examination and modification. You can examine variables and change their values at will. This can save development time, since you can try a simple modification without editing the source file.

  • Execution control. You can restrain your program from runaway execution by stepping along, one or a few statements at a time. You can modify the flow of execution by skipping instructions, by branching to a different point in the program, or by calling procedures.

  • Breakpoints and watchpoints. You can define a breakpoint at a specific statement where execution should be paused in order to allow you to examine registers or memory. You can define a watchpoint for a variable in order to pause automatically whenever that variable is modified.

Specific debugger commands are listed in Table 3-6. Both debuggers offer considerable built-in assistance through an interactive help command.

Usually the commands for a debugger may be abbreviated—e.g., simply p for print (gdb). The debugger may use different naming conventions for the hardware registers Gr0 … Gr127 and Fr0 … Fr127. For instance, the gdb debugger expects $r2 and $f2 (Linux) or $gr2 and $fr2 (HP-UX), respectively, in contrast to r2 and f2 in the assembly language source file. The leading $ character avoids conflict with user-defined symbols that begin with the letters r, g, or f.

An address ADDR can be specified numerically, by the name of a general register containing the address, or by the C-style &SYMB where SYMB is a symbolic address. The address ADDR can also be specified as an expression containing any of these.

The next two sections illustrate some gdb and adb debugger commands. Study most carefully the one that corresponds to the facilities to which you have access.

3.8.2. Running SQUARES using gdb (Linux® and HP-UX®)

A debugger is most useful when it has access to the symbolic names used in a program. The gcc assembler and linker include basic symbolic information in the executable file by default. A good strategy is to use break commands that ask the debugger to pause when execution reaches certain labeled points in the program.

Table 3-6. Selected Debugger Commands for Itanium Programming Environments
gdbadbAction taken
Examining and modifying memory locations
x/ngf &SYMBOLADDR/njFDisplays n double-precision values
x/ngx &SYMBOLADDR/njxDisplays n quad word values (hexadecimal)
x/ngd &SYMBOLADDR/njdDisplays n quad word values (decimal)
x/nwx &SYMBOLADDR/ngxDisplays n double word values (hexadecimal)
x/nwd &SYMBOLADDR/ngdDisplays n double word values (decimal)
x/nhx &SYMBOLADDR/nexDisplays n word values (hexadecimal)
x/nhd &SYMBOLADDR/nedDisplays n word values (decimal)
x/nbx &SYMBOLADDR/nbxDisplays n byte values (hexadecimal)
x/nbd &SYMBOLADDR/nbdDisplays n byte values (decimal)
x/ni &SYMBOLADDR/niInterprets as n (gdb) or 3 n (adb) instructions
x/nc &SYMBOLADDR/ncShows as a string of n characters
x/s &SYMBOLADDR/sShows a null-terminated string
set {long} &SYMBOL=EXPChanges a stored quad word value
Examining and modifying variables and registers
print/x EXP Prints evaluated expression (hexadecimal)
print/d EXP Prints evaluated expression (decimal)
set REG=EXP Changes a register value
 ra or r or fPrints all (ra) or general (r) or floating (f) registers
Flow control
run:rStarts execution of the program
stepi n or si n,n:sExecutes only n machine instructions
continue:cResumes execution to the next breakpoint
quitqExits from the debugger
Breakpoints and watchpoints
break LABELLABEL:bPuts a breakpoint at the labeled line
watch VAR Sets a watchpoint for modification of the variable
delete nLABEL:dRemoves a breakpoint or watchpoint

A successful assembling, linking, and gdb debugging session for the SQUARES program would proceed as follows (spaces and tabs have been adjusted for a clearer presentation):

L> gcc -Wall -O0 -o bin/squares squares.s
L> gdb bin/squares
[messages deleted here]
(gdb) break done
Breakpoint 1 at 0x4000000000000590
(gdb) run
Starting program: /home/user/bin/squares

Breakpoint 1, 0x4000000000000590 in done ()
(gdb) p/x sq1
$1 = 0x1
(gdb) p/x sq2
$2 = 0x4
(gdb) p/x sq3
$3 = 0x9
(gdb) q
The program is running.  Exit anyway? (y or n) y
L>

The debugger is instructed to pause execution at done. The run command initiates debugger-controlled execution of the program. When the instruction pointer reaches the address corresponding to done, the debugger returns to its interactive prompt, (gdb).

The three squares should have been stored in memory. Now we can issue additional debugger commands to assess how the program has worked. Here we chose one of the simplest methods, the print command, abbreviated as p. We see the expected answers for the squares of the first three integers.

The print command of gdb can print only the value of one evaluated expression at a time. If we had a lot of computed values, we could examine contiguous information units using the x command instead:

(gdb) x/3g &sq1
0x6000...0760 <sq1>:       0x0000...0001      0x0000....0004
0x6000...0770 <sq3>:       0x0000...0009
(gdb)

We have used zero suppression (...) to make this example fit the book margins. Notice that the debugger agrees with the nm command on the addresses corresponding to the storage cells declared in the SQUARES program.

3.8.3. Running SQUARES using adb (HP-UX)

A successful assembling, linking, and adb debugging session for the SQUARES program would proceed as follows (spaces and tabs have been adjusted for a clearer presentation):

H> cc_bundled +DD64 -o bin/squares squares.s
H> adb bin/squares
adb> done:b
adb> :r
Process 735 Thread 120552 Execed
Breakpoint 1 set at address 0x4000000000000ad0
main + 0x70:
>       adds             r8=0,r0
        nop.f            0
        nop.b            0;;
Hit Breakpoint 1 at address 0x4000000000000ad0
adb> sq1/jx
sq1:
                0x1
adb> sq2/jx
sq2:
                0x4
adb> sq3/jx
sq3:
                0x9
adb> q
H>

Note that some commands to adb begin with or contain a colon. A breakpoint is set at the line marked with the label done. The program is then allowed to run until execution reaches the label done. The printed results are clearly correct for an algorithm that computes a table of squares of the first few integers.

3.8.4. Examples of Debugger Commands

We present here some further illustrations of useful gdb commands. These scarcely scratch the surface of what can be accomplished using any reasonably versatile debugger.

Watchpoint example

We could have used a watchpoint to determine when the SQUARES program has stored a value for sq3. Here are suitable command gdb sequences:

L> gdb bin/squares
[messages deleted here]
(gdb) watch sq3
Hardware watchpoint 1: {<data variable, no debug info>} ...
(gdb) run
Starting program: /home/user/bin/squares
Hardware watchpoint 1: {<data variable, no debug info>} ...
Hardware watchpoint 1: {<data variable, no debug info>} ...
Hardware watchpoint 1: {<data variable, no debug info>} ...

Old value = 0
New value = 9
0x4000000000000581 in main ()
(gdb)

The debugger executes the program instructions, and pauses when the specified variable sq3 has been modified. Although seemingly trivial here, watchpoints can be invaluable with a bigger program when you may have developed a psychological “blind spot” and cannot see how, when, or where a quantity is modified. Note, however, that the debugger may operate much more slowly with watchpoints than with breakpoints.

Single-step example

We could have used single-stepping to follow the progress of SQUARES after noting that register r20 successively takes on the values of the computed squares:

L> gdb bin/squares
[messages deleted here]
(gdb) display/i $pc
(gdb) display/x $r20
(gdb) break main
Breakpoint 1 at 0x4000000000000520
(gdb) break done
Breakpoint 2 at 0x4000000000000590
(gdb) run
Starting program: /home/user/bin/squares

Breakpoint 1, 0x4000000000000520 in main ()
2: /x $r20 = 0x2000000000044000
1: x/i $ip  0x4000...0520 <main>:    [MMI] mov r21=1;;
(gdb) stepi
0x4000000000000521 in main ()
2: /x $r20 = 0x2000000000044000
1: x/i $ip  0x4000...0521 <main+1>:        mov r22=2
(gdb) stepi
0x4000000000000522 in main ()
2: /x $r20 = 0x2000000000044000
1: x/i $ip  0x4000...0522 <main+2>:        nop.i 0x0;;
(gdb) stepi
0x4000000000000530 in main ()
2: /x $r20 = 0x2000000000044000
1: x/i $ip  0x4000...0530 <main+16>: [MMI] mov r20=1;;
(gdb) stepi
0x4000000000000531 in main ()
2: /x $r20 = 0x1
1: x/i $ip  0x4000...0531 <main+17>:       addl r14=-56,r1
(gdb) stepi
0x4000000000000532 in main ()
2: /x $r20 = 0x1
1: x/i $ip  0x4000...0532 <main+18>:       nop.i 0x0;;
(gdb) stepi
0x4000000000000540 in main ()
2: /x $r20 = 0x1
1: x/i $ip  0x4000...0540 <main+32>: [MMI] st8 [r14]=r20;;
(gdb) stepi
0x4000000000000541 in main ()
2: /x $r20 = 0x1
1: x/i $ip  0x4000...0541 <main+33>:       add r21=r22,r21
(gdb) stepi
0x4000000000000542 in main ()
2: /x $r20 = 0x1
1: x/i $ip  0x4000...0542 <main+34>:       nop.i 0x0;;
(gdb) stepi
0x4000000000000550 in main ()
2: /x $r20 = 0x1
1: x/i $ip  0x4000...0550 <main+48>: [MMI] add r20=r21,r20;;
(gdb) stepi
0x4000000000000551 in main ()
2: /x $r20 = 0x4
1: x/i $ip  0x4000...0551 <main+49>:       addl r14=-48,r1
(gdb) cont
Continuing.

Breakpoint 2, 0x4000000000000590 in done ()
2: /x $r20 = 0x9
1: x/i $ip  0x4000...0590 <done>:    [MMI] mov r8=r0;;
(gdb)

Here we have asked the debugger to step through the program from main onwards and print out both the current value in register r20 and the mnemonic form of the next instruction indicated by the instruction pointer. When we saw r20 change from 1 to 4, we then ceased single-stepping and allowed the program to continue until the next interrupting event, which was the breakpoint set at done.

We do not need to retype the stepi command each time because gdb responds to pressing the return key on an empty line by repeating the most recent command.

Program disassembly

The capability of a debugger's print command to interpret stored instructions back into human-readable mnemonic form is tantamount to being able to disassemble, or reverse-engineer, the program. The gdb debugger has a useful synonym for print/i that takes two labels (or related expressions) to specify the start and end points for display of program instructions:

(gdb) disas main done
Dump of assembler code from 0x4000...0520 to 0x4000...0590:
0x4000000000000520 <main>:      [MMI]      mov r21=1;;
0x4000000000000521 <main+1>:               mov r22=2
0x4000000000000522 <main+2>:               nop.i 0x0;;
0x4000000000000530 <main+16>:   [MMI]      mov r20=1;;
0x4000000000000531 <main+17>:              addl r14=-56,r1
0x4000000000000532 <main+18>:              nop.i 0x0;;
0x4000000000000540 <main+32>:   [MMI]      st8 [r14]=r20;;
0x4000000000000541 <main+33>:              add r21=r22,r21
0x4000000000000542 <main+34>:              nop.i 0x0;;
0x4000000000000550 <main+48>:   [MMI]      add r20=r21,r20;;
0x4000000000000551 <main+49>:              addl r14=-48,r1
0x4000000000000552 <main+50>:              nop.i 0x0;;
0x4000000000000560 <main+64>:   [MMI]      st8 [r14]=r20;;
0x4000000000000561 <main+65>:              add r21=r22,r21
0x4000000000000562 <main+66>:              nop.i 0x0;;
0x4000000000000570 <main+80>:   [MMI]      add r20=r21,r20;;
0x4000000000000571 <main+81>:              addl r14=-40,r1
0x4000000000000572 <main+82>:              nop.i 0x0;;
0x4000000000000580 <main+96>:   [MFB]      st8 [r14]=r20
0x4000000000000581 <main+97>:              nop.f 0x0
0x4000000000000582 <main+98>:              nop.b 0x0;;
End of assembler dump.
(gdb)

This information is equivalent to a segment of an assembly listing (Figure 3-2) for the instructions from main up to, but not including, done. The notations [MMI] and [MFB] refer to the templates for instruction bundles. We will discuss those later in this book.

Other capabilities

Symbolic debuggers usually offer many additional capabilities, but these tend to differ from environment to environment, and depend upon particular capabilities of the hardware and operating system. We have introduced enough material to demonstrate how to obtain printed output without taking on program segmentation, the use of system routines, or other complications at this stage.

The capabilities and features of the adb debugger furnished with a basic installation of HP-UX are more limited than those of gdb. In particular, it lacks a way to inspect the contents of just one or a select few registers. To have used the ra, r, and f commands for showing register contents in examples like those in this section would have produced voluminous output ill-suited for presentation in a book.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.93.168