APPENDIX B

How to use the interpreter

The interpreter is not normally used for the debugging of programs under construction. Its primary application is as a verification tool for almost completed programs. Although the proper operation of the interpreter is obviously a black art, this chapter tries to provide some guidelines.

For the sake of the argument, the source language is assumed to be C, but most hints apply equally well to other languages supported by ACK.

Initial measures

Start with a test case of trivial size; to be on the safe side, reckon with a time dilatation factor of about 500, i.e., a second grows into 10 minutes. (The interpreter takes 0.5 msec to do one EM instruction on a Sun 3/50). Fortunately many trivial test cases are much shorter than one second.

Compile the program into an e.out, the EM machine version of a a.out, by calling em22 (for 2-byte integers and 2-byte pointers), em24 (for 2 and 4) or em44 (for 4 and 4) as seems appropriate; if in doubt, use em44. These compilers can be found in the ACK bin directory, and should be used instead of acc (or normal cc). Alternatively, acc -memNN can be used instead of emNN.

If a C program consists of more than one file, as it usually does, there is a small problem. The acc and cc compilers generate .o files, whereas the emNN compilers generate .m files as object files. A simple technique to avoid the problem is to call em44 *.c if possible. If not, the following hack on the Makefile generally works.

-   Make sure the Makefile is reasonably clean and complete: all calls to the compiler are through $(CC), CFLAGS is used properly and all dependencies are specified.

-   Add the following lines to the Makefile (possibly permanently):
    .SUFFIXES:    .o
    .c.o:
        $(CC) -c $(CFLAGS) $<

-   Set CC to em44 -.c (for example). Make sure CFLAGS includes the -O option; this yields a speed-up of about 15 %.

-   Change all .o to .m (or .k if the -O option is not used).

-   If necessary, change a.out to e.out.

With these changes, make will produce an EM object; esize can be used to verify that it is indeed an EM object and obtain some statistics. Then call the interpreter: int <EM-object-file> [ parameters ] where the parameters are the normal parameters of the program. This should work exactly like the original program, though slower. It reads from the terminal if the original does, it opens and closes files like the original and it accepts interrupts.

Interpreting the results

Now there are several possibilities.

It does all this. Great! This means the program does not do very uncouth things. Now read the file int.mess to see if any messages were generated. If there are none, the program did not really run (perhaps the original cc a.out got called instead?) Normally there is at least a termination message like (Message): program exits with status 0 at "awa.p", line 64, INR = 4124 This says that the program terminated through an exit(0) on line 64 of the file awa.p after 4124 EM instructions. If this is the only message it is time to move to a bigger test case.

On the other hand, the program may come to a grinding halt with an error message. All messages (errors and warnings) have a format in which the sequence "<file name>", line <ln#> occurs, which is the same sequence many compilers produce for their error messages. Consequently, the int.mess file can be processed as any compiler message output.

One such message can be (Fatal error) a.em: trap "Addressing non existent memory" not caught at "a.c", line 2, INR = 16 produced by the abysmal program
main()    {
    *(int*)200000 = 1; }

Often the effects are more subtle, however. The program
main()    {
    int *a, b = 777;
    b = *a; } produces the following five warnings (in far less than a second): (Warning 47, #1): Local data pointer expected at "t.c", line 4, INR = 17 (Warning 61, cont.): Actual memory is undefined at "t.c", line 4, INR = 17 (Warning 102, #1): Returned function result too small at "<unknown>", line 0, INR = 21 (Warning 43, #1): Local integer expected at "exit.c", line 11, INR = 34 (Warning 61, cont.): Actual memory is undefined at "exit.c", line 11, INR = 34 The one about the function result looks the most frightening, but is the most easily solved: main is a function returning an int, so the start-up routine expects a (four-byte) integer but gets an empty (zero-byte) return area.

Note: The experts are divided about this. The traditional school holds that main is an int function and its result is the return code; this leaves them with two ways of supplying a return code: one as the parameter of exit() and one as the result of main. The modern school (Berkeley 4.2 etc.) claims that return codes are supplied exclusively by exit(), and they have an exit(0) in the start-up routine, just after the call to main(); leaving main() through the bottom implies successful termination.

We shall satisfy both groups by
main()    {
    int *a, b = 777;
    b = *a;
    exit(0); } This results in (Warning 47, #1): Local data pointer expected at "t.c", line 4, INR = 17 (Warning 61, cont.): Actual memory is undefined at "t.c", line 4, INR = 17 (Message): program exits with status 0 at "exit.c", line 11, INR = 33 which is pretty clear as it stands.

Using stack dumps

Let's, for the sake of argument and to avoid the fierce realism of 10000-line programs, assume that the above still does not give enough information. Since the error occurred in EM instruction number 17, we should like to see more information around that moment. Call the interpreter again, now with the shell variable AT set at 17: int AT=17 t.em (The interpreter has a number of internal variables that can be set by assignments on the command line, like with make.) This gives a file called int.log containing the stack dump of 150 lines presented at the end of this chapter.

Since dumping is a subfacility of logging in the interpreter, the formats of the lines are the same. If a line starts with an @, it will contain a file-name/line-number indication; the next two characters are the subject and the log level. Then comes the information, preceded by a space. The text contains three stack dumps, one before the offending instruction, one at it, and one after it; then the interpreter stops. All kinds of other dumps can be obtained, but this is default.

For each instruction we have, in order:

-   an @x9 line, giving the position in the program,

-   the messages, warnings and errors from the instruction as it is being executed,

-   dump(s), as requested.

The first two lines mean that at line 4 in file t.c the interpreter performed its 16-th instruction, with the Program Counter at 30 pointing at opcode 180 in the text segment; the instruction was an LOL (LOad Local) with the operand -4 derived from the opcode. It copies the local at offset -4 to the top of the stack. The effect can be seen from the subsequent stack dump, where the undefined word at addresses 2147483568 to ...571 (the variable a) has been copied to the top of the stack at 2147483560 (copying undefined values does not generate a warning). Since we used the em44 compiler, all pointers and ints in our dump are 4 bytes long. So a variable at address X in reality extends from address X to X+3.
Note that this is not the offending instruction; this stack dump represents the situation just before the error.

The stack consists of a sequence of frames, each containing data followed by a Return Status Block resulting from a call; the last frame ends in top-of-stack. The first frame represents the stack when the program starts, through a call to the start-up routine. This routine prepares the second stack frame with the actual parameters to main(): argc at 2147483596, argv at 2147483600 and environ at 2147483604.

The RSB line shows that the call to main() was made from procedure 0 which has 0 locals, with PC at 16, an LB of 2147483608 and file name and line number still unknown. The code in the RSB tells how this RSB was made; possible values are STP (start-up), CAL, RTT (returnable trap) and NRT (non-returnable trap).

The next frame shows the local variable(s) of main(); there are two of them, the pointer a at 2147483568, which is undefined, and variable b at 2147483564, which has the value 777. Then comes a copy of a, just made by the LOL instruction, at 2147483560. The following line shows that the Function Return Area (which does not reside at the end of the stack, but just happens to be printed here) has size 0 and is presently undefined. The stack dump ends by showing that the Actuals Base is at 2147483596 (pointing at argc), the Locals Base at 2147483572 (pointing just above the local a), the Stack Pointer at 2147483560 (pointing at the undefined pointer), the line count is 4 and the file name is "t.c".

(Notice that there is one more stack frame than one would probably expect, the one above the start-up routine.)

The Function Return Area could have a size larger than 0 and still be undefined, for example when an instruction that does not preserve the contents of the FRA has just been executed; likewise the FRA could have size 0 and be defined nevertheless, for example just after a RET 0 instruction.

All this has set the scene for the distaster which is about to strike in the next instruction. This is indeed a LOI (LOad Indirect) of size 4, opcode 169; it causes the message warning: Local data pointer expected [stack.c: 242] and its continuation warning cont.: Actual memory is undefined (detected in the interpreter file stack.c at line 242; this can be useful for sorting out dubious semantics). We see that the effect, as shown in the third frame of this stack dump (at instruction number 17) is somewhat unexpected: the LOI has fetched the value 4 and stacked it. The reason is that, unfortunately, undefinedness is not transitive in the interpreter. When an undefined value is used in an operation (other than copying) a warning is given, but thereafter the value is treated as if it were zero. So, after the warning a normal null pointer remains, which is then used to pick up the value at location 0. This is the place where the EM machine stores its current line number, which is presently 4.

The third stack dump shows the final effect: the value 4 has been unstacked and copied to variable b at 2147483564 through an STL (STore Local) instruction.

Since this form of logging dumps the stack only, the log file is relatively small as dumps go. Nevertheless, a useful excerpt can be obtained with the command grep 'd1' int.log This extracts the Return Status Block lines from the log, thus producing three traces of calls, one for each instruction in the log:
 d1 >> RSB: code = STP, PI = uninit, PC = 0, LB = 2147483644, LIN = 0, FIL = NULL
 d1 >> RSB: code = CAL, PI = (0,0), PC = 16, LB = 2147483608, LIN = 0, FIL = NULL
 d1 >> AB = 2147483596, LB = 2147483572, SP = 2147483560, HP = 848, LIN = 4, FIL = "t.c"
 d1 >> RSB: code = STP, PI = uninit, PC = 0, LB = 2147483644, LIN = 0, FIL = NULL
 d1 >> RSB: code = CAL, PI = (0,0), PC = 16, LB = 2147483608, LIN = 0, FIL = NULL
 d1 >> AB = 2147483596, LB = 2147483572, SP = 2147483560, HP = 848, LIN = 4, FIL = "t.c"
 d1 >> RSB: code = STP, PI = uninit, PC = 0, LB = 2147483644, LIN = 0, FIL = NULL
 d1 >> RSB: code = CAL, PI = (0,0), PC = 16, LB = 2147483608, LIN = 0, FIL = NULL
 d1 >> AB = 2147483596, LB = 2147483572, SP = 2147483564, HP = 848, LIN = 4, FIL = "t.c" Theoretically, the pertinent trace is the middle one, but in practice all three are equal. In the present case there isn't much to trace, but in real programs the trace can be useful.

Errors in libraries

Since libraries are generally compiled with suppression of line number and file name information, the line number and file name in the interpreter will not be updated when it enters a library routine. Consequently, all messages generated by interpreting library routines will seem to originate from the line of the call. This is especially true for the routine malloc(), which, from the nature of its business, often contains dubitable code.

A usual message is: (Warning 43, #1): Local integer expected at "buff.c", line 18, INR = 266 (Warning 64, cont.): Actual memory contains a data pointer at "buff.c", line 18, INR = 266 and indeed at line 18 of the file buff.c we find:
    buff = malloc(buff_size = BFSIZE); This problem can be avoided by using a specially compiled version of the library that contains the correct LIN and FIL instructions, or, less elegantly, by including the source code of the library routines in the program; in the latter case, one has to be sure to have them all.

Unavoidable messages
Some messages produced by the logging are almost unavoidable; sometimes the writer of a library routine is forced to take liberties with the semantics of EM.

Examples from C include the memory allocation routines. For efficiency reasons, one bit of an pointer in the administration is used as a flag; setting, clearing and reading this bit requires bitwise operations on pointers, which gives the above messages. Realloc causes a problem in that it may have to copy the originally allocated area to a different place; this area may contain uninitialised bytes.
@x9 "t.c", line 4, INR = 16, PC = 30 OPCODE = 180 @L6 "t.c", line 4, INR = 16, DoLOLm(-4)
 d2
 d2 . . STACK_DUMP[4/4] . . INR = 16 . . STACK_DUMP . .
 d2 ----------------------------------------------------------------
 d2 ADDRESS BYTE ITEM VALUE SHADOW
 d2 2147483643 0 (Dp)
 d2 2147483642 0 (Dp)
 d2 2147483641 0 (Dp)
 d2 2147483640 40 [ 40] (Dp)
 d2 2147483639 0 (Dp)
 d2 2147483638 0 (Dp)
 d2 2147483637 3 (Dp)
 d2 2147483636 64 [ 832] (Dp)
 d2 2147483635 0 (In)
 d2 2147483634 0 (In)
 d2 2147483633 0 (In)
 d2 2147483632 1 [ 1] (In)
 d1 >> RSB: code = STP, PI = uninit, PC = 0, LB = 2147483644, LIN = 0, FIL = NULL
 d2
 d2 ADDRESS BYTE ITEM VALUE SHADOW
 d2 2147483607 0 (Dp)
 d2 2147483606 0 (Dp)
 d2 2147483605 0 (Dp)
 d2 2147483604 40 [ 40] (Dp)
 d2 2147483603 0 (Dp)
 d2 2147483602 0 (Dp)
 d2 2147483601 3 (Dp)
 d2 2147483600 64 [ 832] (Dp)
 d2 2147483599 0 (In)
 d2 2147483598 0 (In)
 d2 2147483597 0 (In)
 d2 2147483596 1 [ 1] (In)
 d1 >> RSB: code = CAL, PI = (0,0), PC = 16, LB = 2147483608, LIN = 0, FIL = NULL
 d2
 d2 ADDRESS BYTE ITEM VALUE SHADOW
 d2 2147483571 undef
 d2 | | | | | |
 d2 2147483568 undef (1 word)
 d2 2147483567 0 (In)
 d2 2147483566 0 (In)
 d2 2147483565 3 (In)
 d2 2147483564 9 [ 777] (In)
 d2 2147483563 undef
 d2 | | | | | |
 d2 2147483560 undef (1 word)
 d2 FRA: size = 0, undefined
 d1 >> AB = 2147483596, LB = 2147483572, SP = 2147483560, HP = 848, \
                        LIN = 4, FIL = "t.c"
 d2 ----------------------------------------------------------------
 d2 @x9 "t.c", line 4, INR = 17, PC = 31 OPCODE = 169 @w1 "t.c", line 4, INR = 17, warning: Local data pointer expected [stack.c: 242] @w1 "t.c", line 4, INR = 17, warning cont.: Actual memory is undefined @L6 "t.c", line 4, INR = 17, DoLOIm(4)
 d2
 d2 . . STACK_DUMP[4/4] . . INR = 17 . . STACK_DUMP . .
 d2 ----------------------------------------------------------------
 d2 ADDRESS BYTE ITEM VALUE SHADOW
 d2 2147483643 0 (Dp)
 d2 2147483642 0 (Dp)
 d2 2147483641 0 (Dp)
 d2 2147483640 40 [ 40] (Dp)
 d2 2147483639 0 (Dp)
 d2 2147483638 0 (Dp)
 d2 2147483637 3 (Dp)
 d2 2147483636 64 [ 832] (Dp)
 d2 2147483635 0 (In)
 d2 2147483634 0 (In)
 d2 2147483633 0 (In)
 d2 2147483632 1 [ 1] (In)
 d1 >> RSB: code = STP, PI = uninit, PC = 0, LB = 2147483644, LIN = 0, FIL = NULL
 d2
 d2 ADDRESS BYTE ITEM VALUE SHADOW
 d2 2147483607 0 (Dp)
 d2 2147483606 0 (Dp)
 d2 2147483605 0 (Dp)
 d2 2147483604 40 [ 40] (Dp)
 d2 2147483603 0 (Dp)
 d2 2147483602 0 (Dp)
 d2 2147483601 3 (Dp)
 d2 2147483600 64 [ 832] (Dp)
 d2 2147483599 0 (In)
 d2 2147483598 0 (In)
 d2 2147483597 0 (In)
 d2 2147483596 1 [ 1] (In)
 d1 >> RSB: code = CAL, PI = (0,0), PC = 16, LB = 2147483608, LIN = 0, FIL = NULL
 d2
 d2 ADDRESS BYTE ITEM VALUE SHADOW
 d2 2147483571 undef
 d2 | | | | | |
 d2 2147483568 undef (1 word)
 d2 2147483567 0 (In)
 d2 2147483566 0 (In)
 d2 2147483565 3 (In)
 d2 2147483564 9 [ 777] (In)
 d2 2147483563 0 (In)
 d2 2147483562 0 (In)
 d2 2147483561 0 (In)
 d2 2147483560 4 [ 4] (In)
 d2 FRA: size = 0, undefined
 d1 >> AB = 2147483596, LB = 2147483572, SP = 2147483560, HP = 848, \
                        LIN = 4, FIL = "t.c"
 d2 ----------------------------------------------------------------
 d2 @x9 "t.c", line 4, INR = 18, PC = 32 OPCODE = 229 @S6 "t.c", line 4, INR = 18, DoSTLm(-8)
 d2
 d2 . . STACK_DUMP[4/4] . . INR = 18 . . STACK_DUMP . .
 d2 ----------------------------------------------------------------
 d2 ADDRESS BYTE ITEM VALUE SHADOW
 d2 2147483643 0 (Dp)
 d2 2147483642 0 (Dp)
 d2 2147483641 0 (Dp)
 d2 2147483640 40 [ 40] (Dp)
 d2 2147483639 0 (Dp)
 d2 2147483638 0 (Dp)
 d2 2147483637 3 (Dp)
 d2 2147483636 64 [ 832] (Dp)
 d2 2147483635 0 (In)
 d2 2147483634 0 (In)
 d2 2147483633 0 (In)
 d2 2147483632 1 [ 1] (In)
 d1 >> RSB: code = STP, PI = uninit, PC = 0, LB = 2147483644, LIN = 0, FIL = NULL
 d2
 d2 ADDRESS BYTE ITEM VALUE SHADOW
 d2 2147483607 0 (Dp)
 d2 2147483606 0 (Dp)
 d2 2147483605 0 (Dp)
 d2 2147483604 40 [ 40] (Dp)
 d2 2147483603 0 (Dp)
 d2 2147483602 0 (Dp)
 d2 2147483601 3 (Dp)
 d2 2147483600 64 [ 832] (Dp)
 d2 2147483599 0 (In)
 d2 2147483598 0 (In)
 d2 2147483597 0 (In)
 d2 2147483596 1 [ 1] (In)
 d1 >> RSB: code = CAL, PI = (0,0), PC = 16, LB = 2147483608, LIN = 0, FIL = NULL
 d2
 d2 ADDRESS BYTE ITEM VALUE SHADOW
 d2 2147483571 undef
 d2 | | | | | |
 d2 2147483568 undef (1 word)
 d2 2147483567 0 (In)
 d2 2147483566 0 (In)
 d2 2147483565 0 (In)
 d2 2147483564 4 [ 4] (In)
 d2 FRA: size = 0, undefined
 d1 >> AB = 2147483596, LB = 2147483572, SP = 2147483564, HP = 848, \
                        LIN = 4, FIL = "t.c"
 d2 ----------------------------------------------------------------
 d2