OpenVMS Debugger Manual


Previous Contents Index


Chapter 14
Debugging Special Cases

This chapter presents debugging techniques for special cases that are not covered elsewhere in this manual:

14.1 Debugging Optimized Code

By default, many compilers optimize the code they produce so that the program executes faster. With optimization, invariant expressions are removed from DO loops so that they are evaluated only once at run time; some memory locations might be allocated to different variables at different points in the program, and some variables might be eliminated so that you no longer have access to them while debugging.

The net result is that the code that is executing as you debug might not match the source code displayed in a screen-mode source display (see Section 7.4.1) or in a source listing file.

To avoid the problems of debugging optimized code, many compilers allow you to specify the /NOOPTIMIZE (or equivalent) command qualifier at compile time. Specifying this qualifier inhibits most compiler optimization and thereby reduces discrepancies between the source code and executable code caused by optimization.

If this option is not available to you, or if you have a definite need to debug optimized code, read this section. It describes the techniques for debugging optimized code and gives some typical examples of optimized code to show the potential causes of confusion. It also describes some features you can use to reduce the confusion inherent in debugging optimized code.

In order to take advantage of the features that improve the ability to debug optimized code, you need an up-to-date version of your language compiler. For definitive information about the necessary version of your compiler, please see your compiler release notes or other compiler documentation.

Note that about one-third more disk space is needed for debugging optimized code, to accommodate the increased image size.

When debugging optimized code, use a screen-mode instruction display, such as the predefined display INST, to show the decoded instruction stream of your program (see Section 7.4.4). An instruction display shows the exact code that is executing.

In screen mode, pressing KP7 places the SRC and INST displays side by side for easy comparison. Alternatively, you can inspect a compiler-generated machine-code listing.

In addition, to execute the program at the instruction level and examine instructions, use the techniques described in Section 4.3.

Using these methods, you should be able to determine what is happening at the executable code level and be able to resolve the discrepancy between source display and program behavior.

14.1.1 Eliminated Variables

A compiler might optimize code by eliminating variables, either permanently or temporarily at various points during execution. For example, if you try to examine a variable X that no longer is accessible because of optimization, the debugger might display one of the following messages:


%DEBUG-W-UNALLOCATED, entity X was not allocated in memory 
                      (was optimized away) 
 
%DEBUG-W-NOVALATPC, entity X does not have a value at the 
                    current PC 

The following Pascal example shows how this could happen:


PROGRAM DOC(OUTPUT); 
   VAR 
      X,Y: INTEGER; 
   BEGIN 
      X := 5; 
      Y := 2; 
      WRITELN(X*Y); 
   END. 

If you compile this program with the /NOOPTIMIZE (or equivalent) qualifier, you obtain the following (normal) behavior when debugging:


$ PASCAL/DEBUG/NOOPTIMIZE DOC
$ LINK/DEBUG DOC
$ DEBUG/KEEP
   .
   .
   .
DBG> RUN DOC
   .
   .
   .
DBG> STEP
stepped to DOC\%LINE 5 
     5:         X := 5;
DBG> STEP
stepped to DOC\%LINE 6 
     6:         Y := 2;
DBG> STEP
stepped to DOC\%LINE 7 
     7:         WRITELN(X*Y);
DBG> EXAMINE X,Y
DOC\X:  5 
DOC\Y:  2
DBG>

If you compile the program with the /OPTIMIZE (or equivalent) qualifier, because the values of X and Y are not changed after the initial assignment, the compiler calculates X*Y, stores that value (10), and does not allocate storage for X or Y. Therefore, after you start debugging, a STEP command takes you directly to line 7 rather than line 5. Moreover, you cannot examine X or Y:


$ PASCAL/DEBUG/OPTIMIZE DOC
$ LINK/DEBUG DOC
$ DEBUG/KEEP
   .
   .
   .
DBG> RUN DOC
   .
   .
   .
DBG> EXAMINE X,Y
%DEBUG-W-UNALLOCATED, entity X was not allocated in memory 
                      (was optimized away)
DBG> STEP
stepped to DOC\%LINE 7 
     7:         WRITELN(X*Y);
DBG>

In contrast, the following lines show the unoptimized code at the WRITELN statement:


DBG> STEP
stepped to DOC\%LINE 7 
     7:         WRITELN(X*Y);
DBG> EXAMINE/OPERAND .%PC
DOC\%LINE 7:    MOVL    S^#10,B^-4(FP) 
     B^-4(FP)   2146279292 contains 62914576
DBG> 
 

14.1.2 Changes in Coding Order

Several methods of optimizing consist of performing operations in a sequence different from the sequence specified in the source code. Sometimes code is eliminated altogether.

As a result, the source code displayed by the debugger does not correspond exactly to the actual code being executed.

The following example depicts a segment of source code from a Fortran program as it might appear on a compiler listing or in a screen-mode source display. This code segment sets the first ten elements of array A to the value 1/X.


Line          Source Code 
----          ----------- 
  5            DO 100 I=1,10 
  6            A(I) = 1/X 
  7        100 CONTINUE 

Optimization may produce the following scenario: As the compiler processes the source program, it determines that the reciprocal of X need only be computed once, not 10 times as the source code specifies, because the value of X never changes in the DO loop. The compiler thus may generate optimized code equivalent to the following code segment:


Line          Optimized Code Equivalent 
----          ------------------------- 
  5            TEMP = 1/X 
               DO 100 I=1,10 
  6            A(I) = TEMP 
  7        100 CONTINUE 

Depending on the compiler implementation, the moved code may be associated with the first line of the loop or may retain its original line number (common on Alpha systems).

If a discrepancy occurs, it is not obvious from looking at the displayed source line. Furthermore, if the computation of 1/X were to fail because X is 0, it would appear from inspecting the source display that a division by 0 had occurred on a source line that contains no division at all.

This kind of apparent mismatch between source code and executable code should be expected from time to time when you debug optimized programs. It can be caused not only by code motions out of loops, as in the previous example, but by a number of other optimization methods as well.

14.1.3 Semantic Stepping (Alpha Only)

Semantic stepping (available only on Alpha systems) makes stepping through optimized code less confusing. The semantic-stepping mode complements the traditional step-by-line and step-by-instruction modes. There are two commands for semantic stepping: SET STEP SEMANTIC_EVENT and STEP/SEMANTIC_EVENT.

Semantic Events

One problem of stepping through optimized code is that the apparent source program location "bounces" back and forth with the same line often appearing again and again. Indeed, sometimes the forward progress in STEP LINE mode averages barely more than one instruction per STEP command.

This problem is addressed through annotating instructions that are semantic events. Semantic events are important for two reasons:

A semantic event is one of the following:

It is important to understand that not every assignment, transfer of control, or call is necessarily a semantic event. The major exceptions are as follows:

SET STEP SEMANTIC_EVENT Command

The SET STEP SEMANTIC_EVENT command establishes the default stepping mode as semantic.

STEP/SEMANTIC_EVENT Command

STEP/SEMANTIC_EVENT, or simply STEP when semantic mode is in effect, causes a breakpoint to be set at the next semantic event, whether an assignment, a transfer of control, or a call. Execution proceeds to that next event. Parts of any number of different lines/statements may be executed along the way without interfering with progress. When the semantic event is reached (that is, when the instruction associated with that event is reached but not yet executed), execution is suspended (similar to reaching the next line when STEP/LINE is used).

Example of Semantic Stepping

The comments in the following C program, doct2, point out some considerations for optimization:


#include <stdio.h> 
#include <stdlib.h> 
 
int main(unsigned argc, char **argv) { 
    int w, x, y, z=0; 
 
    x = atoi(argv[1]); 
    printf("%d\n", x); 
 
    x = 5;                     
    y = x;                     
 
    if (y > 2) {               /* always true */ 
        printf("y > 2"); 
        } 
    else { 
        printf("y <= 2"); 
        } 
 
    if (z) {                   /* always false */ 
        printf("z"); 
        } 
    else { 
        printf("not z"); 
        } 
 
    printf("\n"); 
    } 

Contrast the following two examples, which show stepping by line and stepping by semantic event through the optimized doct2 program:

Notice that the semantic stepping behavior is much smoother and more straightforward than the stepping-by-line example. Further, semantic stepping results in stopping at significant points of the program. In general, semantic stepping significantly reduces or eliminates the confusion of "bouncing" around the code nonsequentially, which characteristically happens with stepping by line through optimized code. Although some reordering of the source program may be done to take advantage of better execution characteristics, generally the flow is from top to bottom.

The granularity of stepping is different between stepping by line and stepping semantically. Sometimes it is greater, sometimes smaller. For example, a statement that would by its semantic nature constitute a semantic event will not show up with semantic stepping if it has been optimized away. Thus, the semantic region will span across several lines, skipping the line that has been optimized away.

14.1.4 Use of Registers

A compiler might determine that the value of an expression does not change between two given occurrences and might save the value in a register. In such cases, the compiler does not recompute the value for the next occurrence, but assumes the value saved in the register is valid.

If, while debugging a program, you use the DEPOSIT command to change the value of the variable in the expression, the corresponding value stored in the register might not be changed. Thus, when execution continues, the value in the register might be used instead of the changed value in the expression, which will cause unexpected results.

In addition, when the value of a nonstatic variable (see Section 3.4.3) is held in a register, its value in memory is generally invalid; therefore, a spurious value might be displayed if you enter the EXAMINE command for a variable under these circumstances.

14.1.5 Split-Lifetime Variables

In compiling with optimization, the compiler sometimes performs split-lifetime analysis on a variable, "splitting" it into several independent subvariables that can be independently allocated. The effect is that the original variable can be thought to reside in different locations at different points in time --- sometimes in a register, sometimes in memory, and sometimes nowhere. It is even possible for the different subvariables to be simultaneously active.

On Alpha processors, in response to the EXAMINE command, the debugger tells you at which locations in the program the variable was defined. When the variable has an inappropriate value, this location information can help you determine where the value of the variable was assigned. (The /DEFINITIONS qualifier enables you to specify more or fewer than the default five locations.)

Split-lifetime analysis applies only to scalar variables and parameters. It does not apply to arrays, records, structures, or other aggregates.

Examples of Split-Lifetime Processing

The following examples illustrate the use of split-lifetime processing. For the first example, a small C program, the numbers in the left column are listing line numbers.


385 doct8 () { 
386 
387     int i, j, k; 
388 
389     i = 1; 
390     j = 2; 
391     k = 3; 
392 
393     if (foo(i)) { 
394         j = 17; 
395         } 
396     else { 
397         k = 18; 
398         } 
399 
400     printf("%d, %d, %d\n", i, j, k); 
401 
402     } 

When compiled, linked, and executed for debugging, the optimized program results in this dialogue:


$ run doct8


   .
   .
   .
     DBG> step/into 
     stepped to DOCT8\doct8\%LINE 391 
        391:     k = 3; 
     DBG> examine i 
     %W, entity 'i' was not allocated in memory (was optimized away) 
     DBG> examine j 
     %W, entity 'j' does not have a value at the current PC 
     DBG> examine k 
     %W, entity 'k' does not have a value at the current PC 

Note the difference in the message for the variable i compared to j or k. The variable i was not allocated in memory (registers, core, or otherwise) at all, so there is no point in ever trying to examine its value again. By contrast, j and k do not have a value "at the current PC" here; somewhere later in the program they will.

Stepping one more line results in this:


     DBG> step 
     stepped to DOCT8\doct8\%LINE 385 
        385: doct8 () { 

This looks like a step backward --- a common phenomenon in optimized (scheduled) code. (This problem is dealt with by "semantic stepping mode," discussed in Section 14.1.2.) Continuing to step results in this:


     DBG> step 5 
     stepped to DOCT8\doct8\%LINE 391 
        391:     k = 3; 
     DBG> examine k 
     %W, entity 'k' does not have a value at the current PC 
     DBG> step 
     stepped to DOCT8\doct8\%LINE 393 
        393:     if (foo(i)) { 
     DBG> examine j 
     %W, entity 'j' does not have a value at the current PC 
     DBG> examine k 
     DOCT8\doct8\k:  3 
         value defined at DOCT8\doct8\%LINE 391 

Here j is still undefined, but k now has a value, namely 3. That value was assigned at line 391.

Recall from the source that j was assigned a value before k (at line 390), but that has yet to show up. Again, this is common with optimized (scheduled) code.


     DBG> step 
     stepped to DOCT8\doct8\%LINE 390 
        390:     j = 2; 

Here the value of j appears. Thus:


     DBG> examine j 
     %W, entity 'j' does not have a value at the current PC 
     DBG> step 
     stepped to DOCT8\doct8\%LINE 393 
        393:     if (foo(i)) { 
     DBG> examine j 
     DOCT8\doct8\j:  2 
         value defined at DOCT8\doct8\%LINE 390 

Skipping ahead to the print statement at line 400, examine j again.


     DBG> set break %line 400 
     DBG> g 
     break at DOCT8\doct8\%LINE 400 
        400:     printf("%d, %d, %d\n", i, j, k); 
     DBG> examine j 
     DOCT8\doct8\j:  2 
         value defined at DOCT8\doct8\%LINE 390 
         value defined at DOCT8\doct8\%LINE 394 

Here there is more than one definition location given for j. Which applies depends on which path was taken in the IF clause. If a variable has an apparently inappropriate value, this mechanism provides a means to take a closer look at those places, and only those, where that value might have come from.

You can use the SHOW SYMBOL/ADDRESS command to display the split-lifetime information for a symbol, as in the following example:


DBG> show symbol/address j
     data DOCT8\doct8\j 
       between PC 131128 and 131140                      
         PC definition locations are at: 131124          
         address: %R3 
       between PC 131144 and 131148                      
         PC definition locations are at: 131140          
         address: %R3 
       between PC 131152 and 131156                      
         PC definition locations are at: 131124          
         address: %R3 
       between PC 131160 and 131208                      
         PC definition locations are at: 131124, 131140  
         address: %R3

The variable j has four lifetime segments. The PC addresses are the result of linking the image, and the comments relate them to line numbers in the source program.

On Alpha systems, the debugger tracks and reports which assignments and definitions might have provided the displayed value of a variable. This additional information can help you cope with some of the effects of code motion and other optimizations --- effects that cause a variable to have a value coming from an unexpected place in the program.

EXAMINE/DEFINITIONS Command (Alpha Only)

For a split-lifetime variable, the EXAMINE command not only displays the value of the active lifetime, it also displays the lifetime's definition points. The definition points are places where the lifetime could have received an initial value (if there is only one definition point, then that is the only place.)

There is more than one definition point if a lifetime's initial value can come from more than one place. In the previous example when the program is suspended at the printf, examining j results in the following:


DBG> examine j
DOCT8\doct8\j:  2 
    value defined at DOCT8\doct8\%LINE 390 
    value defined at DOCT8\doct8\%LINE 394

Here, the lifetime of j has two definition points, because the value could have come from either line 390 or line 394, depending on whether or not the expression at line 393 was TRUE.

By default, up to five definition locations are displayed when the contents of a variable are examined. You can specify the number of definition locations to display with the /DEFINITIONS=n qualifier, as in the following example:


DBG> EXAMINE/DEFINITIONS=74 FOO

Note that the minimum abbreviation is /DEFI.

If you want a default number of definitions other than five, you can use a command definition like the following:


DBG> DEFINE/COMMAND E = "EXAMINE/DEFINITIONS=100"

If the /DEFINITIONS qualifier is set to 100, and the split-lifetime variable examined has 120 definition points, the debugger displays the 100 as specified, and then reports:


there are 20 more definition points   
 


Previous Next Contents Index