BOFF Reference Manual

Copyright © 1997, Thinkage Ltd.

 1: Introduction to BOFF
 2: The Modes of BOFF
 3: Values, Names, and Symbols
 4: Expressions
 5: Displaying Memory Locations
 6: Patching
 7: Examining Program Information
 8: Finding Values in Memory
 9: Checkpoints
10: Calling Functions
11: Breakpoints
12: Resuming Execution After Breakpoints
13: Program Faults
14: Tracebacks
15: Stack Manipulation
16: Leaving BOFF
17: Using BOFF
18: BOFF in Patch Mode
19: Bugs and Other Pitfalls
20: The Batch Loader
Appendix A: Binding Strength of Operators for BOFF
Appendix B: The Run Time Environment

1: Introduction to BOFF

BOFF (B Obscure Feature Finder) helps you debug programs that have been compiled by the B, Pascal, or C compilers maintained by Thinkage Ltd. In our descriptions here, we will assume you have a clear understanding of either B or C. Knowing GMAP or YAA is also helpful when using certain features of BOFF, but it is not necessary. It is also helpful to be familiar with the standard run-time environment, as described in "expl b environment manual". Appendix B gives a quick summary of environment information, but the environment manual provides full details.

BOFF lets you get inside your programs. It lets you examine the executable code and look at the values your variables take on. This information should help you figure out why your program doesn't work and what you have to do to correct the problem. With experience, you will develop an intuition for where bugs hide.

Like programming, debugging is an art that takes time to learn. This manual tells you what BOFF can do, but it is not so easy to explain when each feature might be useful. Whenever possible, we will give examples of how BOFF can help you find bugs and correct them. However, we can only scratch the surface -- bugs can hide themselves in more ways than we can possibly describe.

A Note to Pascal Programmers: This manual uses the word "function" to refer to any subprogram called in the usual way. Therefore, Pascal functions and procedures are both considered to be "functions" in the BOFF sense.

2: The Modes of BOFF

BOFF can be used on programs files in several different ways.

Before Program Execution
This is called "Hstar Mode" because it lets you examine an hstar file (core image load module). You can look at the executable code, and at the contents of external and auto variables. In this mode, you can use BOFF to "patch" your load module, i.e. to make changes to data objects and object code before running the changed program. Making changes in this way is often faster than changing your source code and recompiling (although you should incorporate such patches into your source code eventually).

BOFF is able to patch any load module, even if the module was not originally written in B or C. However, it is easier to patch B or C load modules than load modules from other languages, since the B and C compilers prepare special debug tables that BOFF can use to find variables and functions within the module.

During Program Execution
This is called "Run mode". This mode of BOFF "babysits" a program as it executes. For example, BOFF allows you to set "breakpoints" where your program will halt execution temporarily. During this temporary halt, you can check the contents of your variables to make sure they are valid. When you have seen what you want, you can instruct BOFF to resume execution of your program until the next breakpoint. Breakpoints also let you mark sections of code to see if they are ever executed and if so, how often. Finally, Run mode lets you "call" functions in your program and to pass these functions sample parameters. This is just one more way to test parts of your program in a controlled fashion so that you can locate exactly where bugs occur.
After Program Execution
This is called "Abort mode". This mode of BOFF lets you examine the contents of a dump after a program has aborted. One useful feature of Abort mode is BOFF's ability to provide a traceback that shows the function that was being executed at the time of the abort, the function that called that function, the function that called that, and so on back. As with the other BOFF modes, you can examine the contents of your variables and other data objects, displaying those contents in a variety of formats including decimal, octal, ASCII, and string.

In addition to the three modes we have just mentioned, there are several other modes in which BOFF operates: Profile mode, Patch mode, RUNUnit mode, and Evaluate mode. We will discuss these modes in a later section. For the time being, we will concentrate on the ways in which BOFF can be used to examine programs.

Some BOFF commands cannot be used in certain modes. For example, in Hstar mode you cannot get a traceback of what's on the stack, since Hstar mode is used before program execution, when there's nothing on the stack. When we describe commands in the sections to come, we will note those that cannot be used in a particular mode.

3: Values, Names, and Symbols

In this section, we describe the data elements recognized by BOFF. These include constants, variables from the program under examination, and variables which are used exclusively by BOFF. Also discussed here are two special kinds of BOFF constructs.

Simple Constants

BOFF accepts any kind of constant accepted by B or C. Constants should be entered as they would be in a C program (e.g. octal integers must have a leading zero, decimal integers cannot have leading zeroes, etc.). The escape sequences accepted by B (e.g. '*n' for a new-line) are also accepted by BOFF.

In addition to octal and decimal integer constants, BOFF accepts hexadecimal constants. A hex constant is identified with a leading "0X" or "0x", as in 0xFFFF.

External Variables

BOFF obtains the names and locations of your program's external variables from the symbol tables prepared by the loader. External data objects and functions can therefore be referenced by name. There is one catch: the loader symbol tables will not be available in the dump files that are generated when a program aborts. When you are using BOFF in Abort mode, you can use the "Symbols=" option on the command line to obtain the symbol tables from the original load module. This is described in more detail in Section 17.

The loader truncates all external variable names to six characters, and converts from ASCII to BCD (thereby losing the distinction between upper and lower case). For the sake of consistency, BOFF does the same thing. Thus if you enter an external name longer than six characters, it will be truncated. Case distinction is also lost.

In addition to the normal external variables of your program, BOFF defines the variable ".END.". This refers to the highest address available in the BOFF session. If you try to refer to

 .END. + 1

you will get an error message.

Local Symbols

BOFF obtains information about the auto variables of a function by examining local symbol tables prepared by the B, C, and Pascal compilers. NOTE: If your program is compiled with the "-Tables" option, your compiled program will not have symbol tables. You should therefore avoid "-Tables" until your program has been thoroughly debugged.

As with the loader symbol tables, local symbol tables are not usually available in dump files. This means that you should use the "Symbols=" option when invoking BOFF in Abort mode.

There are several different types of local symbols. These are described in the subsections that follow.

Static Functions

A static function is a function whose name is not a global symbol. Static functions arise in two ways.

In Pascal programs, a static subprogram has a static parent. The static parent is the subprogram that contains the definition of the static subprogram. Local symbol tables record a static subprogram's static parent, if any.

Static Objects

Static objects arise in two ways.

Static objects which are not defined inside subprograms are described in a file symbol table. This is like a function's local symbol table, but describes symbols which are local to a source file instead of a function.

A source file is regarded as the static parent of all static functions and objects defined in the file.

B Statement Labels

B statement labels are unusual, in that one label can be assigned to another. The B compiler implements a label by generating a machine instruction which transfers to the memory location immediately following the label. If label X is assigned the value of label Y, X will turn into a machine instruction that transfers to the memory location following Y.

From BOFF's point of view, B statement labels behave like static objects. The symbol names are known only in the function where they are defined, and they refer to an absolute location in memory. Therefore B statement labels are treated in the same way as statics.

Note that C and Pascal labels do not behave like B labels. C and Pascal do not even record labels in the debugging symbol tables.

Objects on the Stack

During execution, a function's argument values and auto variables are stored on a stack. The stack is just a large chunk of memory. A program refers to data objects in the stack by giving (word) offsets from hardware register AR7.

When a program calls a function, the value of AR7 is changed to point to stack space which is not currently in use. The function uses this stack space to hold its local data. During program execution, AR7 points to the stack space of the function that is currently executing. Thus AR7 is usually called the stack pointer.

On the other hand, BOFF is designed to show you any function in your program, not just the one that is currently executing. For this reason, BOFF has two separate stack pointers:

The function whose stack space is indicated by /SP is called the current function. It is the function you are currently examining.

NOTE: functions are only allocated stack space when they are called, and a function gives up its stack space when it terminates. Therefore, only active functions have stack space: the executing function, the function that called the executing function, the caller's caller, and so on. The current function is always one of the active functions, since non-active functions do not have stack space. Section 15 describes commands that will change the current function (i.e. move /SP to point to the stack space of a different active function).

Local data in the current function may be referenced by name, provided there are symbol tables for the function. Local data includes argument values passed to the function (referenced by parameter name) and auto variables declared inside the function.

You cannot refer to local data in a non-active function, since non-active functions have no stack space (and therefore no existing local data). If you want to refer to local data in an active function which is not the current function, use one of the commands from Section 15 to change the current function to the desired function.

If the current function does not have local symbol tables, you can still access its local data. If N is an integer, the symbol "/N" will refer to the Nth machine word of local storage declared in the current function. Function arguments appear first in local storage, followed by local variables.

Therefore, suppose a function takes two integer arguments and allocates one local integer variable. "/1" refers to the first argument value, "/2" refers to the second argument value, and "/3" refers to the local variable.

In B, each function argument occupies one machine word, so "/N" always refers to the Nth function argument. In C, function arguments are at least one word long, but may be longer; therefore, there isn't the same one-to-one correspondence between "/N" symbols and argument values as they appear in the source code.

For example, suppose a C function takes two double precision arguments (which are each two words long). "/1" refers to the first word of the first argument, "/2" refers to the second word of the first argument, "/3" refers to the first word of the second argument, and "/4" refers to the second word of the second argument.

The View Autos Command

In all Abort, Run, and Hstar modes, the command

:va

displays information on the local symbols of the current function (provided local symbol tables are available). Each line of ":va" output has the format

NAME  - LOC X:VALUE

where

NAME
is the symbol name.
LOC
gives the memory address associated with the symbol. The location of an object on the stack is given as a decimal integer indicating the position of the object in the function's local stack storage. For example, the first word of local storage (the beginning of the function's first argument value) is 1; the second word is 2, and so on. The locations of other symbols (e.g. statement labels) are given as octal absolute addresses.
X
is a single character indicating the nature of the symbol. Possible characters are:
A
auto variable.
F
local static function.
L
B statement label.
S
static object.
VALUE
is the current value of the object.

The command

LOCATION:va

displays the local symbols of the function that contains the given LOCATION. The output is similar to the previous form of the command, except that values are not given for the auto symbols. This form of the ":va" command can print the local symbols of any function (active or non-active), provided that the function was compiled to have local symbol tables.

Local Symbol Names

BOFF distinguishes between upper and lower case characters in local symbol names. However, BOFF truncates all local symbol names to eight characters. This can cause difficulties. Suppose a function declares two symbols named "variable1" and "variable2" and suppose that "variable1" comes before "variable2" in the function's local symbol table. If you try to refer to "variable2", BOFF will truncate this to "variable" and search through the function's local symbol table for the first symbol that begins with "variable". Thus you might type "variable2", but you will get information for "variable1".

To get around this problem, use the ":va" to get the full names and locations of all local symbols in the function. With this information, you can find out where "variable2" is stored in the function's stack space and then use the "/N" address form to refer to the location of "variable2".

BOFF Variables

BOFF lets you define BOFF variables in addition to the variables in your program. BOFF variables have names consisting of a slash (/) followed by one to six characters, as in

/NAME

Characters can be upper or lower case letters, digits, or the underscore. The first character of a BOFF variable's name cannot be a digit. BOFF does not distinguish between the case of characters in names; therefore

/NAME    /name    /Name    /naMe

and so on all refer to the same variable.

The normal assignment operator is used to assign a value to a BOFF variable. For example, consider

/ADDR = &(/3);

Since "/3" refers to the third word of local storage for the current function, &(/3) refers to the address of that word. Once you have assigned this address to the variable /ADDR, you can refer to the contents of the storage location with the expression

*(/ADDR)

This works, even if you change /SP and change the current function. /ADDR will still refer to "/3" for the original function. Of course, this is only useful as long as the original function is active.

BOFF variables can also be used to keep counts of events (e.g. breakpoints), to save information, or to be passed as arguments to functions which are called in the way described in Section 10.

When a BOFF variable is first referenced, it is initialized to zero. If an existing BOFF variable is set to zero, the variable is "forgotten" (i.e. it disappears). In other words, a BOFF variable only exists when it has a non-zero value.

Predefined BOFF Variables

BOFF maintains a number of its own variables to refer to important program values. These are

/A      the A register
/Q      the Q register
/X0-7   index registers 0 through 7
/AR0-7  EIS address registers 0 through 7
/IC     the instruction counter
/I      the indicator register
/E      the exponent register
/SP     the stack pointer for the current function
        (as described earlier)
/CP     the current co-routine pointer
        (can be changed by the ":c" command)
/RD     the current B read unit
        (only applicable in Run mode)
/WR     the current B write unit
        (only applicable in Run mode)
/       the address of the future word
        (see Section 5)
/.      the address of the current word
        (see Section 5)
/..     the address of the past word
        (see Section 5)
/...    the address of the "past-past" word
        (see Section 5)
/_      the value of the most recently
        evaluated expression
/__     the value of the second most recently
        evaluated expression

BOFF also has several predefined variables which do not start with a slash (/).

IC      the value of the word that
        the instruction counter points to
.       the value of "/." (the current word)
..      the value of "/.." (the past word)
...     the value of "/..." (the past-past word)

Notice that these all correspond to /NAME variables. For example, /IC is the value of the instruction counter, while IC is the value in the memory location to which the instruction counter points. An assignment like

/IC = VALUE

changes the value of the instruction counter itself, while

IC = VALUE

changes the value of the memory location pointed at by the instruction counter.

NOTE: If your program defines its own symbol "ic", your program's definition takes precedence over BOFF's variable. BOFF only interprets a name as a BOFF variable if the name cannot be interpreted in any other way.

Context Expressions

Readers who know GMAP may be interested in looking at the machine code generated by a particular statement. When local symbol tables are available in a program, the construct

NAME{EXPRESSION}

can be used to find machine instructions corresponding to particular source code lines. For example,

main{20}

generally refers to the first machine instruction compiled for line 20 in the source file that defined the function "main". There are some important caveats when using this kind of expression:

As a result of these rules,

main{20}

always refers to an instruction in "main", even if line 20 is not in "main". source code for the function "main". Also, the expression always refers to the first machine instruction generated for a source code line.

If the expression in

NAME{EXPRESSION}

can only be an Rvalue, the value of the expression is taken to be a line number. This is the case in the example we have just discussed; in "main{20}", 20 can only be an Rvalue.

(Recall that an Rvalue is an expression that could be used on the right hand side of an assignment, while an Lvalue is an expression that could be used on the left hand side. Thus quantities such as "a", "vec[i]", and "*(&main+4)" are Lvalues while "a+1", "&a", and "{lda x}" are not.)

If the expression in the brace brackets can be an Lvalue, the situation is different. In this case, the NAME before the braces gives a context and the EXPRESSION gives the actual reference. For example, consider

main{i}

where "i" is an auto variable in "main". The result of this expression is the value of the variable "i" in "main". Similarly,

&main{i}

is the address of the variable "i".

If another function "func" defined its own variable named "i",

func{i}

would refer to the variable "i" defined in "func" instead of the one in "main".

Caveats: The expressions we have described in this section set the context of your BOFF session (which is why the expressions are called context expressions). Effectively, these expressions tell BOFF to use the local symbol tables of the given function when trying to resolve symbol references. Thus

func{i}

tells BOFF that it should be using the local symbol tables of "func" when looking up symbols. However, context expressions do NOT change the value of /SP (the stack pointer). Therefore, you will get very confusing results if /SP is pointing towards the stack space for function F1, but you use a context expression which sets the context to function F2. In this situation, you will get correct results when you look at static variables for F2 (since local symbol tables contain absolute addresses for statics), but incorrect results when you look at autos (since autos are referenced as offsets from /SP, and /SP is not pointing at F2's stack space).

For this reason, you should be somewhat careful when using a context expression. On the other hand, there are times when BOFF itself gets lost (i.e. its context function is not the same as the function whose stack space is referenced by /SP). In this case, an appropriate context expression can correct BOFF's context and set things right.

Resolving References

Suppose your current function is named F1, and suppose you type in a command referring to a symbol X. BOFF must figure out what X refers to. It follows these steps.

  1. First, BOFF looks for a definition of X in the local symbol table of F1. If it is found, X is local to F1.
  2. Next, BOFF looks for a definition of X in the global symbol table. If it is found, X is an external name.
  3. Next, BOFF figures out which function (if any) contains the current word (i.e. "/.", the memory location most recently referenced by a BOFF command). This does not have to be the current function -- the current function is always an active function, but BOFF lets you examine any function in the program. Once BOFF has figured out which function this is (call it F2), BOFF determines if the symbol X is defined as a static in F2's local symbol table or in any of F2's static parents (including the file that contains the definition of F2). If X is found, it is taken to be a static.
  4. If X is not local, global, or static, BOFF interprets the symbol as a BOFF variable.

In the search for a static X, BOFF may lose track of your context. For example, suppose you are examining memory locations in a function F and ask to look at a static X defined in an outer scope. BOFF will look at F's statics, then the statics in the file symbol table. In so doing, BOFF will lose track of the symbol table where it began. If you ask to look at the static X outside F, then try to look at a local variable Y inside F, BOFF may not be able to get back to the right local symbol table.

You can avoid this kind of trouble by giving the context explicitly, as in

F{Y}

If FS is a static function defined or referenced inside a function FG,

FG{FS}{Y}

refers to the symbol Y inside FS, and FS is located by looking at FG's local symbol tables.

Machine Instructions

The construct

{GMAP command}

is a one-word constant containing the machine code generated by the GMAP assembler statement. For example, the GMAP instruction

lda 100,dl

could be represented with

{lda 100,dl}

As another example,

tra *+2

could be represented with

{tra /.+2}

The symbol "/." stands for the current word, and is explained in Section 5. BOFF does not understand GMAP's "*" notation.

The DRL names defined with the macro T.DRL (in GMAP) are accepted in a {COMMAND} construction. MME names are also recognized.

B and BOFF do not interpret names in the same way as GMAP. GMAP considers the value of a symbol to be its address; in BOFF, the value of a symbol is the contents of an address. In GMAP, you might write

tsx1 main

to transfer to the routine MAIN. In BOFF, you would have to say

{tsx1 &main}

to get the same effect. If you left out the "&", BOFF would get the first word of MAIN, and consider the contents of that word as the transfer address.

At present, there is no easy way to get the offsets of auto variables into a {COMMAND} construction. Instructions like

{lda p}
{lda p,,7}

do not work when "p" is an auto variable. The simplest way to get the offset of an auto variable at present is to do a ":va" and then to use the decimal offset that ":va" displays.

Artificial Externals

BOFF's ":n" command lets you define "artificial" external variables.

ADDRESS:n NAME

associates NAME with the given ADDRESS. ADDRESS may be an expression.

Variables created with ":n" are indistinguishable from external variables defined in your program. This is useful in a number of instances. For example,

FG{FS}{Y}:n Y

associates the simple name "Y" with the given expression. From this point onward, you can just use "Y" to refer to the given location.

Artificial variables also come in handy when you are working with a program that doesn't contain the normal symbol tables. Defining a few artificial symbols will likely make it easier to locate code and data when you are looking at parts of your program.

The ":n" command has space to define at least 20 artificial symbols, and often it has room for even more. To redefine a name, simply issue an ":n" command that associates the name with a new memory location.

C Pointers

BOFF was originally created for debugging B programs. Since then, it has been adapted for debugging C and Pascal programs as well. The major difference between B code and C/Pascal code is the way the languages use pointers. B stores pointer values in the bottom 18 bits of the machine word, while C and Pascal place pointer values in the top 20 bits.

BOFF handles the difference in this way: if the bottom 16 bits are zero and the top 18 bits are not, BOFF assumes it is a C pointer (top 20 bits). Otherwise, BOFF assumes it is a B pointer (bottom 18 bits). In other words, a pointer value is taken as a C/Pascal pointer if possible.

This has one unfortunate side effect. When B pointers are used,

*(a+3)   a[3]   3[a]

are all equivalent. If "a" is a C pointer, "*(a+3)" is different from the other two because the 3 is added to the bottom of the word instead of the top half.

C programmers should also note that BOFF handles pointer arithmetic according to the rules of B. When you add 1 to a C pointer, the resulting address depends on the pointer type. For example, if you add 1 to a char pointer in C, the resulting address refers to the next byte; if you add 1 to a double pointer, the resulting address refers to the original word address, plus 2.

However, in B and in BOFF, pointers always behave like an "int" pointer in C. When you add 1 to a B/BOFF pointer, the resulting address points to the next machine word, regardless of the type of data that the pointer pointed to.

4: Expressions

Most B operators are defined in BOFF, and expressions using these operators can be evaluated. Currently, the following operators are NOT defined:

The logical operators:
&& || ?:
The assignment operators:
+= -= *= /= %= &= |= ^= >>= <<=

BOFF variables, auto variables, external variables, constants, and function calls can be freely intermixed in BOFF expressions.

Many of the BOFF commands described in later sections require Lvalues as operands.

NOTE: The "!" character at the beginning of a line tells BOFF to treat the rest of the line as a system call, as in

!expl boff summary

Therefore, expressions that begin with the logical-NOT operator "!" should be enclosed in parentheses to avoid ambiguity. Type

(!a)

instead of

!a

The "!" operator has a second special meaning.

A ! B

is equivalent to

(&A)[B]

when used inside BOFF. For example,

main!20

refers to the 20th word of "main". This is also how you address an element of a C array. For example,

array!3

is element 3 of the vector "array".

The same kind of expression can be used to reference elements in C arrays. However, remember that BOFF's pointer arithmetic works like B, not C. For example, suppose "array" is a char array in a C program.

array!1

refers to the word containing elements 4 through 7 of "array".

Differences Between C Expressions and BOFF

As noted previously, BOFF is based on the B programming language. Because B and C have many similarities, C programmers will not have much trouble using BOFF; however, there are some differences that should be observed.

By the time BOFF sees a program, all information about the type of data objects has been lost (since the program has been compiled). This means that BOFF cannot interpret the sense of an expression by looking at the type of the operands.

For example, consider how the expression

X+Y

is treated in a C program.

BOFF, on the other hand, does not associate types with operands. Therefore

X+Y

is always interpreted as integer addition. If X and Y are floating point values, you must use B's floating point addition operator "#+", as in

X #+ Y

If X is integer and Y is floating point, you must convert X to floating point with the "#" operator, then perform floating point addition. The expression would be written

(#X) #+ Y

As another example of how BOFF's typelessness differs from C, consider

A[N]

If this appeared in a C program, the type of elements in the array A would be significant. The expression would refer to the Nth value of the given type that occurred after the start of the array A. If the elements are single characters, it would be the Nth byte; if the elements are double precision numbers, it would be the Nth double-word. Thus the size of elements in A would determine the location of "A[N]".

Since BOFF does not know the declared size of elements in A,

A[N]

is always interpreted as the Nth machine word after the beginning of A. This principle also affects the behavior of "A!B".

The binding order of operators differs slightly between BOFF and C. The most important difference is that the binary operators "<<", ">>", "&", "^", and "|" all come after the arithmetic operations (*, /, %, +, -) in precedence in BOFF. In C, they come before. C users must remember that BOFF uses B's order of evaluation.

Appendix A lists the meanings and precedence rules for all BOFF operators. The B Reference manual gives more details.

Pointers

BOFF's "&" operator obtains the address of an object and stores it in the lower half of a machine word. In this respect, "&" behaves like the B version of the operator, not C. The unary "%" operator obtains a C pointer. For example,

%X

obtains the address of X and puts it in the top half of the machine word.

String Constants

BOFF lets you use string constants in BOFF commands, but all such strings are transient -- they disappear once the command is executed. Therefore, string constants can only be used in function calls, as in

strcat(S,"abc")

"strcat" is a function that adds the contents of one string to the end of an existing string. In the above example, the characters "abc" are added to the end of the string S. The string constant "abc" will disappear when the operation has been completed, but the change to S will be preserved.

Remember that when a string is specified as a function argument, the function receives a pointer to the string. B pointers are passed in the lower half of a word; C and Pascal pointers are passed in the upper half. Therefore, when a string is specified as a function argument, BOFF must guess whether the function was written in B, or in C or Pascal. Most of the time, BOFF has enough information to guess correctly.

If BOFF guesses incorrectly, the situation is tricky to fix. Essentially, you have to use "malloc" (or "getvec") to obtain dynamic storage, use "strcat" to store the string in that storage, then shift the pointer to that storage into the appropriate half of a machine word.

5: Displaying Memory Locations

In any of BOFF's modes, the command

EXPRESSION\FORMAT

will display the value of the EXPRESSION in one or more formats. FORMAT is a string of characters indicating the desired display formats.

If EXPRESSION is an Lvalue, BOFF's output will be the address of the location being displayed and the contents of that address. If EXPRESSION is an Rvalue, BOFF will just display the expression's value (in all the desired formats).

Each available format is represented by a single character, and the FORMAT string can contain several such characters. Possible characters are listed below.

a (ASCII)
displays the contents of a word as four ASCII characters. Unprintable characters are represented by a tilde (~).
b (BCD)
displays a word as a BCD character constant.
c (ASCII character constant)
displays a word as an ASCII character constant. This differs from the "a" format in two ways: first, the "c" format encloses its output in single quotes, while the "a" format does not; second, the "c" format does not print leading NUL bytes ('*0' in B, '\0' in C). Therefore, a byte containing "*0*0*0A" would be displayed as 'A' in "c" format and ~~~A in "a" format.
d (Decimal integer)
displays a word as a decimal integer.
e (Double precision)
displays a pair of words as a double precision floating point number.
f (Single precision)
displays a single word as a single precision floating point number.
h (Hexadecimal bytes)
displays a word as four bytes, with the value of each byte expressed as two hexadecimal digits. The hexadecimal digits represent the bottom eight bits of the byte. If the top (ninth) bit of the byte is 0, the hex digits are preceded by a space; if the bit is 1, the hex digits are preceded by a dot. Therefore ".FF" is octal 777; " FF" is 377. (This format is useful when examining packed decimal data.)
i (Instruction)
treats a word as a machine instruction and gives a GMAP equivalent.
l (Memory Location)
displays the address of the word being examined, the nearest externals before and after the word, and the line number of the source statement that contained the word (when applicable).

If the location is in "free space" (space used for dynamic memory allocation), BOFF will print out "Free" and the addresses of the start and end of the free block that contains the location. See Section 15 for more about free blocks.

If the location is in the stack, there are several possibilities.

Note: the stack space for a function overlaps with the stack space of its caller. This means that certain locations in the stack are shared by two functions. Usually, BOFF will attribute stack locations to the highest appropriate function (furthest from "main"). However, at the entry point of a function (before AR7 has been "bumped"), BOFF will attribute local data in the callee to the caller.

If a location is in the area of memory used by BOFF itself, BOFF will print out "In BOFF" as part of the display.

If a location is in the "heap" of free memory at the end of a program, BOFF will print out "In Heap". Note that the stack and the free list are part of the heap; therefore, BOFF will occasionally say that a location is in the heap when it is in the stack or the free list, and vice versa.

o (Octal Integer)
displays a word as an octal integer.
s (String)
interprets EXPRESSION as a pointer to a string. BOFF will print out the string (up to the first NUL byte, '*0' in B, '\0' in C).
u (Unsigned integer)
displays a word as an unsigned decimal integer.
x (Hexadecimal Word)
displays a word as nine hexadecimal digits.

As an example, the command

/a\doc

displays the contents of the A register (/a) in decimal, octal, and ASCII character constant format. The command

string\os

displays the contents of the variable "string" in octal form, and then uses those contents as a pointer to a string. This string will be displayed starting at the given address and ending at the first NUL character found.

The "." character can be used in FORMAT to cause a line break in BOFF output. For example,

100\do

prints out

         100 000000000144

but

100\d.o

prints

         100
000000000144

Display Formats with :va and :vu

You can specify display formats after ":va" and ":vu" commands. For example,

:va\d

displays auto variables in decimal format rather than the default octal.

The Current Word

"/." is a BOFF variable called the current word. The current word can be thought of as the word that you are currently examining. For example, at the end of a display command, the current word is set to the last word displayed, provided that the last value displayed was an Lvalue. If the last value displayed was an Rvalue, the current word is not changed. Thus the command

0100\d

simply displays the decimal equivalent of the octal constant "0100". The command

*(0100)\d

sets the current word to memory location 000100 and then displays the contents of that location in decimal format.

"/.." is a BOFF variable called the past word. The past word is just the value "/." had before its most recent change.

"/..." is a BOFF variable called the past-past word. This is the value the past word had before its most recent change. Thus when the current word is changed, there is a type of "cascade" effect:

/...  gets the value of /..
/..   gets the value of /.
/.    changes to the new value

The symbols "/.", "/..", and "/..." are all regarded as Rvalues. For example, if the current word is the external variable "x", "/." is

&(x)

Because of this, a command like

/..\od

does not change the value of the current word. It merely displays the address of the former current word in octal and decimal formats. In the same way, displaying the special symbols described in Section 3 ("/a", "/q", "/ic", etc.) does not affect the current word. (A command like

*(/x1)\o

does change the current word, since "*(/x1)" is an Lvalue.)

The symbols "/.", "/..", and "/..." are all interpreted as memory addresses. For example, "/." stands for the address of the current word. The symbols ".", "..", and "..." stand for the contents of the associated words. For example, "." stands for the contents of the address given by "/.".

"/ " is a BOFF variable called the future word. After a simple display command like

x\ao

the future word is set to the address of the word immediately after the current word. If you now enter a command consisting only of a carriage return (which we will write as <cr>), BOFF will set the current word to the future word, and then display the new current word in the most recently specified display format(s). For example,

vec[0]\d
<cr>
<cr>

starts by printing "vec[0]" in decimal format. The future word is set to the word that follows "vec[0]" (i.e. "vec[1]"). The first <cr> line sets the current word to "vec[1]", displays "vec[1]" in decimal format, and then sets the future word to "vec[2]". The second <cr> line sets the current word to "vec[2]", displays "vec[2]" in decimal format, and then sets the future word to "vec[3]". In this way, you can walk through words in memory without a lot of typing.

The future word is regarded as an Rvalue, just as "/.", "/..", and "/..." are.

Display Modes with Indirection

In addition to the display formats we have already described, there are several special formats that let you display several different addresses with one Display command. They do this by changing the Display command's effective address.

The effective address is similar to the current word. However, the current word is the last word that was actually displayed on the terminal; the effective address can be changed without displaying anything, and therefore the current word and the effective address can sometimes differ. Whenever a value is displayed, the current word is set to the effective address, and the two will match up once again.

When special formats are used, the future word is usually set to the Display command's last effective address, whether that address was displayed or not. However, if the last effective address is the same as the original address, the future word is set to the next address in memory. This whole process should become clearer in the examples given in the individual FORMAT descriptions below.

p (B Pointer)
sets the new effective address to the address stored in the lower 18 bits of the current effective address. Thus the command
bptr\opd

displays the contents of the external variable "bptr" in octal format, sets the new effective address to the one given in the bottom half of "bptr", then displays the contents of the new effective address in decimal format. At the end of this instruction, the future word is set to "*(bptr)", i.e. the word that "bptr" points to. Since this is the last word displayed, the current word would be set to "*(ptr1)" too.

m (C Pointer)
sets the new effective address to the address stored in the upper 18 bits of the current effective address. Thus the command
instr\imd

displays "instr" as an assembler instruction, sets the new effective address to the one given in the top half of "instr", then displays the contents of the new effective address in decimal form. At the end of this instruction, the future word is set to

*(instr >> 18)

Since this is the last word displayed, the current word is set to this location too.

+ (Increment by one)
sets the effective address to EXPRESSION+1. Thus the command
begin\o+o+o+

displays "begin", "begin+1", and "begin+2" in octal format. At the end of this instruction, the effective address would have the value of "begin+3", because of the final "+". Thus the future word would be set to "begin+3" while the current word would be set to the last word displayed, "begin+2".

- (Decrement by one)
sets the effective address to EXPRESSION-1. Thus the command
end\s-s-s-

displays strings pointed to by "end", "end-1", and "end-2". At the end of this instruction, the future word is set to "end-3" and the current word is set to "end-2".

r (Reset)
resets the effective address back to the address of the current word. Thus the command
ptr\opdr

uses the octal format to display the value of "ptr", then uses decimal format to display the value that "ptr" points to. The final "r" resets the effective address (and therefore the current word) to "ptr". Since this is the same as the original address, the future word is set to the word after "ptr".

Display formats are remembered from one display command to the next. Thus if you say

main\i
func

the first word of "main" will be displayed in instruction format, and then the first word of "func" will be displayed in the same format.

The EXPRESSION in a display command is optional. A command with the form

\FORMAT

prints the current word in the given display formats. For example,

hello[0]\d
\s

displays "hello[0]" in decimal and string formats. A display command of this form does not change the current word.

As we noted earlier, a null line (just a carriage return) is a special kind of display command. In this case, the current word is set to the future word and then displayed using the most recently specified FORMAT. Thus

vec[0]\d+d+d+
<cr>
\o

will first display "vec[0]", "vec[1]", and "vec[2]" in decimal format. The final "+" in the specified display modes sets the effective address and the future word to "vec[3]". The following carriage return will set the current word to "vec[3]" and then display "vec[3]", "vec[4]", and "vec[5]". The future word will be set to "vec[6]". The final "\o" will display the current word "vec[5]" in octal format. ("vec[5]" is the current word because it was the last one displayed.)

Displaying More Than One Word

To display the contents of more than one word at a time, you can use the command

EXPRESSION,COUNT\FORMAT

In this form of the display command, EXPRESSION must be an Lvalue. The COUNT tells BOFF repeat the display command

EXPRESSION\FORMAT

COUNT times. Before each repetition, the current word is set to the future word. Thus

main,10\i

prints words "main" to "main!9" in GMAP instruction format. The command

tree,3\po+

is one way to step through three nodes of a tree structure. Each node of the tree contains an integer (displayed in octal) followed by a B pointer to the next node. "tree" itself is a pointer to the first node of the tree.

COUNT may have a negative value, as in

main,-20\i

This is interpreted as

main!-20,20\i

and BOFF will display the 20 words leading up to "main". In general,

LOCATION,-COUNT

is treated as

LOCATION!-COUNT,COUNT

Display Mode Registers

FORMAT strings may be stored in display format registers. There are five display format registers, numbered 0 through 4. To store a format in a register, use the command

\nFORMAT

where "n" is the number of the register that should hold the format string. For example,

\2cbo

stores the format "cbo" in format register 2. The command also makes "cbo" the current default format, and all display commands will use this format until a new format is specified.

If a display command specifies a FORMAT, but no register to contain that format, the FORMAT string is stored in register 0.

To summarize how format registers work, consider the following:

main\i
\2po
ptr
main+1\0
ptr2\2

Remember that the default display format will be the format most recently used in any display command.

Format registers may appear in the middle of another FORMAT string. For example, if you set

\4m

the format "\o4o" is equivalent to "\omo". If you use this mode in a display command, register 4 will be unchanged but register 0 will be set to "o4o", the most recent display mode used.

Final Notes about the Display Command

The full syntax of the display command may use all the elements we have discussed in this section. The form is

[EXPRESSION] [,COUNT] \ [n] [FORMAT]

Thus a command like

vec.ch,4\3om

will display four words of memory, moving through a chain of pointers in the upper half of words beginning at "vec.ch". The words will be displayed in octal. In the process, format register 3 is set to "om". When the display process is finished, the current word will be whatever the effective address was at the end of the fourth display.

As a final note, the single character '^' (caret) followed by a carriage return displays the memory location immediately preceding the current word, using the current default FORMAT.

6: Patching

To change the contents of a location in a file or program, use the command

EXPR1=EXPR2

EXPR1 must be an Lvalue, the address of some word in memory. EXPR2 becomes the new value of EXPR1.

One of the most common uses of patching is to change the initialization value of an external variable before running a program. For example,

here = 1

will give the external variable "here" the value 1. Note that there is no point in using this method to initialize auto variables, because you don't know where the auto variable will be stored on the stack.

If EXPR1 is not given, BOFF uses the last memory location that was displayed or patched. For example,

var=10
=010

sets "var" to a decimal 10, then changes it to an octal 010 (decimal 8).

Register values may be set by assigning to the appropriate BOFF variable. For example,

/AR1 = 0

assigns 0 to AR1.

7: Examining Program Information

BOFF has a number of commands that provide information about your program and your BOFF session. These are listed below.

:va (View Autos)
was described in Section 3.
:ve (View Externals)
lists the names of the external symbols (variables and function names) of a program.
:vu (View User variables)
lists all BOFF variables (of the form /NAME).
:vc (View Co-routines)
decodes a co-routine header (see Section 15).
:vb (View Breakpoints)
displays all the breakpoints which are currently set in your program (see Section 11).
:vf (View Formats)
lists the contents of all the active display format registers. See Section 5 for more about display format registers.
:x (EXamine registers)
displays the contents of all hardware registers. In Run mode, the register values are given as they were when the program reached the current breakpoint. In Abort mode, the register values are given as they were at the time the program aborted. ":x" also gives the limits and name of the QSTAR element. This is particularly useful when examining GCOS modules.

8: Finding Values in Memory

To find a specific value in memory, use the command

ADDRESS :f EXPRESSION\FORMAT

Starting at ADDRESS, BOFF will search for a word in memory which is equal to the value of EXPRESSION. When a matching word is found, it is displayed in the given FORMAT(s). The "\FORMAT" may be omitted, in which case the current display formats are used. As an example,

main:f {tsx1 &func}

finds the first location in "main" where the given TSX1 instruction occurs.

A second form of the command is

ADDRESS :fa EXPRESSION\FORMAT

This is like the previous form, except that it displays all matching words that are found after ADDRESS. (":f" just displays the first matching word.) For example,

main:fa {tsx1 &func}

finds all the occurrences of the given TSX1 instruction, after the beginning of "main". This may include occurrences of the instruction in other functions if the program has functions that follow "main" in memory.

The Find commands can also be written

ADDRESS :f  EXPRESSION,MASK\FORMAT
ADDRESS :fa EXPRESSION,MASK\FORMAT

where MASK is an integer expression. When a MASK is given, ":f" and ":fa" only compare memory locations to EXPRESSION in bit positions where MASK is zero. Remember it this way: 0-bits in MASK are "holes", and BOFF "looks through the holes" to compare memory locations to EXPRESSION. For example,

main:f 'ab',0777777000000

starts at "main" and finds the first word that contains 'ab' in its lower half.

*(0):fa %var,0777777

finds all words with the address of "var" in their upper half.

If no ADDRESS is specified in a Find command, the default is the current word plus one. If neither a MASK nor an EXPRESSION is specified, BOFF uses the MASK and EXPRESSION from the most recent Find command. If an EXPRESSION is specified but there is no MASK, BOFF uses a default MASK of zero and checks all the bits of each word.

The ":f" and ":fa" commands both set the current word "/." to the last word found during the memory search.

You may interrupt a search at any time by pressing BREAK.

9: Checkpoints

In Run mode, it is sometimes desirable to save the current program image before making a change. This process is called checkpointing.

The command

:s FILENAME

saves the current image of both your program and BOFF in the given file.

:r FILENAME

restores this image and returns your session to the point when it was saved.

The FILENAME may be omitted in both ":s" and ":r" commands. If it is, BOFF will use the FILENAME from the most recent save or restore. If a name is omitted in the first ":r" or ":s" command of a session, the default name "*boffed*" is used.

A program image is saved in system loadable format (H*). This means that you can start up a saved session from system level with

go:FILENAME

10: Calling Functions

The Run mode of BOFF lets you invoke any function in your program. Arguments are usually passed in the same way that arguments are passed in the original programming language. However, C and Pascal programmers must remember that pointers are special cases. You can pass any kind of pointer (B, C, or Pascal) to a standard library routine because library functions accept all pointer types. However, you cannot pass B pointer to a user-defined C routine, since the languages store pointers in different parts of the word.

This can catch you off guard if you are trying something like

func(&A);

to pass an address to a C function. Remember that "&" is the B address operator. To get a C pointer, you need the "%" operator, as in

func(%A)

Similarly, array names must be converted manually when passing them to a C function. For example, if your C program has the declaration

int A[10];

and A is passed as an argument to function "g" in BOFF, you must write

g(%A);

A function may be passed any valid BOFF expression as an argument. If a string constant is passed as an argument, the string will be put into temporary storage which will vanish once the function has finished execution. Some sample function calls are given below.

printf("%s %d",string,*(012) );
myfunc(vec, 0777 + 12, /a);
/wunit = open("rvanwinkle/stuff", "r");

Use of the Stack

When a function is called in this manner, BOFF uses a separate stack space to allocate storage for local variables. This leaves the program's stack intact for future use. Because the stack is not the program's real stack, calls to RESET or other non-local gotos will not work during the course of function execution. All breakpoints in the program are disabled while a function is executing in this manner.

When a function is invoked in this way, the I/O units are set to the values found in the BOFF variables "/rd" and "/wr". If the function execution changes these current I/O units, "/rd" and "/wr" will be set to the new values. In general, the I/O units in your program can be freely changed by assigning "/rd" and "/wr" new values, by calls to OPEN, and so on.

There is one exception to this freedom of changing I/O units at breakpoints. If you halt execution by pressing BREAK, it is possible that you will stop your program while it is in the middle of a library I/O routine like ACC.FIL or OPEN. It is best to avoid changing the I/O units when you are down in the heart of the I/O package like this, since it can have unpleasant side effects.

In this situation, it is best to define a breakpoint somewhere inside your own program rather than the library function, and then to resume execution without changing anything. It will be much safer to change your I/O units when execution halts at the breakpoint you have defined in your program than to make changes while inside the I/O routines. (The process of setting breakpoints is described in Section 11).

Hints For Using Function Calls

The ability to invoke functions is very useful in the debugging process. It allows you to pass sample arguments to your own functions to see how your functions behave. After a function has finished execution, you can use the display commands to see what variables have been changed by the function. You can also use this function-calling ability to determine how your function handles exceptional cases: when an argument falls outside a certain range of values, when a passed string is null, etc.

We have already mentioned that when a function is called, the program stack is not affected. This does not mean that calling a function in this way leaves the program completely unchanged. For example, the function may make changes to external variables.

If you would like to call a function in this way and yet not affect the state of your program, it is best to make a checkpoint save with ":s" before the function call. Once the function is finished execution and you have examined the function's effects, you can restore the previous state of your program with ":r".

While ":s" and ":r" let you avoid the problem of changing the state of your program with this kind of function call, it is still possible that the function may write out to a file and thereby foul up that file's contents. One way to get around this is to call OPEN to redirect the write unit to a junk file, and then to return the unit to the real file after the function has finished execution.

11: Breakpoints

Breakpoints are used in Run mode to halt a program temporarily. When the program is stopped in this way, you can examine memory locations using the display commands. You can also use the ":t" command to get a traceback of the functions which are currently on the stack -- see Section 14 for details.

Simple Breakpoints

The simplest way to set a breakpoint is a line of the form

LOCATION:b

where LOCATION can be any Lvalue. The breakpoint is set at the location indicated by LOCATION. If and when the program tries to execute the instruction at LOCATION, program execution will stop. You can then use BOFF commands (e.g. display commands or ":f") to examine the state of your program at this moment.

When a program stops at a breakpoint, BOFF prints out

breakpoint : LOCATION

There are three possible forms in which the LOCATION may be displayed.

EXTERNAL
If the breakpoint is at a location that is associated with an external name, BOFF prints the name.
EXTERNAL!OFFSET
If the breakpoint is close to an external name, BOFF prints the name and the (positive or negative) offset from that name. OFFSET is a word offset, given in decimal.
OCTAL_LOC
If the breakpoint is not close to a known external name, BOFF just prints the absolute address (in octal).

To resume program execution after a breakpoint, use the command

:p

(where "p" stands for "Proceed"). Program execution will proceed until the next breakpoint is triggered.

BOFF automatically sets a breakpoint at the first word of "main". Therefore a program in Run mode will always stop after program set-up to give you a chance to examine the program before execution begins. This is the best time to set other breakpoints.

If a simple breakpoint is set at a LOCATION, the program will break every time execution reaches that location. Later in this section, we will show how to set up temporary breakpoints (which disappear the first time they are triggered) and how to delete existing breakpoints.

Commands Executed at Breakpoints

Breakpoints can also be set with a command of the form

LOCATION:b {COMMANDS}

where COMMANDS is a list of one or more BOFF commands. These COMMANDS are executed as soon as the breakpoint is triggered. For example,

func:b {var\do}

sets a simple breakpoint at the start of the function "func". If the program calls "func", the breakpoint will be triggered and BOFF will execute the command

var\do

This will display the contents of "var" in decimal and octal.

When several COMMANDS are specified, they are separated with semicolons. For example, suppose that many functions in your program call "prtn" to print a string called "str". You wish to break when the function "outerr" calls "prtn", but you do not want to break if "prtn" is called by any other routine. The command

outerr:b  {prtn:b {str\s} ; :p}

first sets a breakpoint at "outerr". When that break is encountered, BOFF will execute the two commands

prtn:b {str\s}
:p

This sets a breakpoint at "prtn", then resumes execution once more using ":p". In this way, the program only breaks briefly, then resumes execution. When "prtn" is called (presumably by "outerr"), the new breakpoint will be triggered. At this time, "str" will be printed in string format, as dictated by the

str\s

instruction specified when the "prtn" breakpoint was set.

As shown above, the COMMANDS in a Breakpoint command can contain nested braces. However, COMMANDS cannot contain string or character constants that contain brace brackets. For example,

X:b { a = '}' }

will result in an error. The brace brackets enclosing COMMANDS may be omitted if you choose.

Breakpoint Options

Up until now, we have been describing simple breakpoints. There are a number of other forms of the Breakpoint command. These all take the form

LOCATION:bC

where "C" is a character that indicates an option for setting the breakpoint.

:bd
deletes the breakpoint at LOCATION.
:bda
deletes all breakpoints currently set in the program.
:bf
will print the usual line describing the breakpoint, and a second traceback line. See Section 14 for the contents of a traceback.
:br
sets a return breakpoint. This breakpoint will be triggered at the point when the current function finishes execution. (The current function is the function that is executing or about to begin executing when the ":br" breakpoint is set.) The breakpoint is triggered just after the current function returns. /SP will point to the storage of the returning function, while AR7 will point to the storage of the caller. At a "return" breakpoint, BOFF prints
FNAME (ARGLIST) returns VALUE

where FNAME is the function name, ARGLIST is the list of arguments which were passed to the function, and VALUE is the value returned by the function.

A LOCATION may not be specified in a ":br" command, since the breakpoint is always set for the point where the current function returns.

:bq
sets a quiet breakpoint. BOFF will not print out the usual information line when this breakpoint is triggered.
:bt
sets a temporary breakpoint at LOCATION. When this breakpoint is triggered, it is automatically deleted. Thus, the program will not break the next time execution reaches LOCATION.

Many of the above options may be combined. For example,

func:bft

creates a temporary breakpoint which will print out a traceback line in addition to the information.

If two breakpoints are set for the same location, the resulting breakpoint is set with the options given in both definitions. For example,

exit:bq
exit:bf

sets a breakpoint at "exit" which prints a traceback line but does not print the normal breakpoint header.

COMMANDS may be specified for most of the special breakpoints described above. Make sure you use brace brackets to enclose the COMMANDS to avoid confusion. If two different COMMANDS strings are specified for the same breakpoint (by different breakpoint commands), only the last one is used.

Ignoring Breakpoints

BOFF can be instructed to ignore a breakpoint a specific number of times before finally breaking. To do this, put an integer COUNT after the LOCATION, as in

NAME,COUNT:b

BOFF will ignore the breakpoint COUNT times, then break at the specified LOCATION every time it is reached. If there are two breakpoint definitions set for the same location and both definitions have COUNT fields, BOFF will use the COUNT of the second definition.

A COUNT may be specified for special breakpoints too (e.g. ":bt", ":bq").

The maximum value that can be given for a COUNT is 32,767. BOFF does not warn you if a COUNT exceeds this number; BOFF simply takes the bottom 15 bits of the COUNT you specify.

Breakpoint Notes

BOFF takes control from your program at defined breakpoints and whenever the BREAK key is pressed. BOFF also gains control if your program calls the function ".boff". If a program calls ".boff" when it is not executing in Run mode under BOFF, the call to ".boff" has no effect.

It is possible for BOFF to come across an unexpected breakpoint. This happens when the program being monitored moves a breakpoint from one place in its code to another. BOFF is unable to handle such a condition since it has no way of determining where the breakpoint came from or what instruction should be in the position where the breakpoint is now. Needless to say, a program which moves around pieces of its executable code is very, very sick.

A ":br" breakpoint is inserted in source code at the point just after a function returns, but the stack pointer (/SP) is restored to point to the function's stack space. This can lead to surprising results. Suppose F is a function that is called in the statement

if (...) F(...)

A return breakpoint would be put into the code at the machine instruction immediately following the call to F. If this is a permanent breakpoint, the breakpoint will be triggered every time that machine instruction is reached. This will happen, even if the condition of the if is false and F is never called -- the breakpoint appears after the call to F. You can avoid this confusing situation with an instruction of the form

F:bq { :brt; :p }

This sets a (permanent) quiet breakpoint at the beginning of F. Each time this breakpoint is triggered, a temporary (one-shot) return breakpoint is set for F and then program execution is resumed. The program will break when F returns, but then the breakpoint disappears until the next time F is called. In this way, the breakpoint is only set when F has actually been called.

12: Resuming Execution after Breakpoints

We have already noted that

:p

resumes execution after a breakpoint, from the location where the breakpoint occurred.

If you want to resume execution from a different location, set the BOFF variable "/ic" (instruction counter) to the address where you wish to begin, and then do a ":p". Naturally, you must be careful when changing the instruction counter. The program's stack is set up for a particular function, and if you try to resume execution with a different function you will get into trouble.

You may also find problems with ":p" if you have changed your program's image by calling program functions. As mentioned in Section 10, it is usually a good idea to do a checkpoint save before you alter your program in any drastic way.

BOFF will not let you resume execution from an unexpected breakpoint. If you really want to continue executing, you can change the instruction counter and resume from a different location.

In addition to the forms of the ":p" command described above, there is also a conditional form.

:p EXPRESSION

only resumes execution after a breakpoint provided that the given EXPRESSION has a non-zero Rvalue. This form is particularly useful when included in the {COMMANDS} in a breakpoint definition. For example,

putcha:bq {:p (/1 != 'a'); :t}

sets a quiet breakpoint at the function "putcha". The commands associated with this breakpoint are

:p (/1 != 'a')
:t

At the breakpoint, if the first word of local storage (/1) is not equal to 'a', BOFF will perform the ":p" and program execution will resume immediately. If this word is 'a', the ":p" command will not be executed. BOFF will perform the ":t" command (described in Section 14), then wait for further input from the terminal.

As another example of the conditional ":p", consider how you would set a breakpoint that was triggered after 100,000 passes through a particular location. You could NOT say

LOCATION,100000:b

because the maximum COUNT in this situation is 32,767. To get around this problem, you could write

/J = 100000
LOCATION:bq {/J=/J-1; :p (/J>0)}

This sets a quiet breakpoint at the given LOCATION. Each time it goes off, it decrements the value of /J by 1. When /J reaches zero, the conditional ":p" is not executed and program execution halts.

It turns out that the above implementation is very slow, since it has to stop and do special breakpoint handling 100,000 times. It is much faster to have "/J" jump down in larger steps. For example, you might write

/J = 100000
LOCATION:bq {/J=/J-25000; /.,25000:bq ; :p (/J>0)}

Again, /J is used to count the number of passes through the breakpoint. However, the instruction

/.,25000:bq

sets a COUNT of 25,000 for the breakpoint at the current LOCATION, so that it will be 25,000 iterations before the breakpoint goes off. At that time, the full set of commands associated with the LOCATION will be executed. /J is decremented by 25,000, another COUNT of 25,000 is set, and so on. In this way, the instructions inside the braces are only executed every 25,000 iterations, making the process much quicker.

13: Program Faults

If a program running under BOFF in Run mode generates a hardware fault, BOFF gains control after TSS writes out an abort file. From this point, you can either proceed in Run mode or move into Abort mode to examine the abort file.

If a fault occurs during normal program execution, the hardware registers will represent the state of the program at the time of the fault. However, a fault could also occur when you are making a test call to a function while stopped at a breakpoint (as described in Section 10). In this case, the hardware registers will represent the state of the program at the time of the breakpoint, not at the time of the fault. The reasoning is that a test call to a function is something outside normal program execution and should not affect the state of the currently executing program.

14: Tracebacks

Tracebacks can be obtained in Run mode and Abort mode. In Run mode, a traceback tells which function is current, which function called the current function, which function called that function, and so on backwards. In Abort mode, the traceback tells which function was current at the time of the abort, and again traces back through the sequence of callers.

The general form of the traceback command is

NUMBER:t

where NUMBER is the number of levels to trace back. If no NUMBER is specified, the default is 15. If NUMBER is greater than the number of functions currently on the stack, the traceback will stop at the lowest level function. (This will be a standard library function, called before "main" to set up the environment for your program.)

Entries on the traceback list have the format

SP  FNAME (ARGS) at {LINE_NUM}   Returns to LOCATION

where

SP
is the octal value of the stack pointer SP for the function FNAME.
FNAME
is the name of an active function.
ARGS
are the current values of the arguments that were passed to the function.
LINE_NUM
is the source line number where the function call occurred. In a traceback like
A (ARGS)
B (ARGS) at {LINE}

the given LINE indicates where B called A. For the function at the top of the stack, LINE_NUM gives the line that was executing when the breakpoint occurred.

LOCATION
is the memory location to which a function intends to return.

Note that the traceback gives the current values of the function's arguments, not the values at the time the function was called. BOFF has no way of knowing if these arguments have changed since the function was invoked.

If a function contains wrap-up traps (described in the B Environment Manual), the traceback will contain the notation

Trapped to LOCATION

where LOCATION is the location of the routine that is executed by the first wrap-up trap.

15: Stack Manipulation

When you first begin a BOFF session in Abort mode, the current function will be the function that was executing at the time of the abort, and /SP will point to that function's stack space. When a breakpoint is triggered in Run mode, the current function will be the function that was executing at the time the breakpoint was triggered and /SP will point to that function's stack space. An exception is made for return breakpoints -- in that case, the current function will be the function that just returned and /SP will point to the returning function's stack space.

The command

N:d

moves /SP N functions down the stack (usually closer to "main"). For example,

1:d

points /SP towards the stack space for the function that called the current function. This then becomes the new current function. The value N may be omitted in ":d", in which case the default for N is 1.

N:u

moves /SP N functions up the stack (usually away from "main"). Again, if N is not specified, the default is one.

BOFF will not let you move farther down the stack than the program set-up function or farther up the stack than the function in which the current breakpoint occurred (in Run mode) or in which the abort occurred (in Abort mode). If you try to go up or down too far, BOFF will stop at either the top of the stack or at the program set-up function.

":d" and ":u" change the context of the program at the same time that they change /SP. Therefore when you use ":d" or ":u" to get a new current function, BOFF will use that function's local symbol tables as the context for resolving symbol references.

The ":d" and ":u" commands are the usual ways to change the value of /SP. You can also assign a value directly to /SP, as in

/SP = 0

but this is only for people who know what they're doing. For example, the assignment changes the stack pointer but does not change the program's context. If /SP does not point at a function's normal stack space, strange behavior should be expected.

Co-Routine Stacks

If your program uses co-routines, each active co-routine will have its own stack. The command

LOCATION:c

changes from one co-routine stack to another. The given LOCATION should indicate the FCV of the active co-routine whose stack you wish to examine. For more information on co-routines, see "expl b coroutine manual".

The ":c" command will not change to another co-routine unless the given location "looks like" a reasonable FCV. In other words, BOFF checks values at the given LOCATION to see if they could possibly be found in a valid FCV. These tests do not guarantee that the FCV is valid, but they reduce the likelihood of mistakes.

Viewing a Co-Routine Stack

The ":vc" command displays information about the current coroutine. This information has the format

cp VAL1, sp VAL2, Entry LOCATION
Stack base LOC1, end LOC2, high point LOC3
Stack usage: Current W1, Max W2, Out of W3

where

VAL1
is the current octal value of the co-routine pointer (/CP).
VAL2
is the current octal value of the stack pointer (/SP).
LOCATION
is the entry point of the co-routine. This will be printed in one of the formats used for printing LOCATIONs at simple breakpoints (see Section 11).
LOC1
is the octal address of the beginning of the co-routine stack.
LOC2
is the octal address of the absolute end of the co-routine stack.
LOC3
is the highest octal address that has been used in the stack so far in program execution.
W1
is the number of words currently being used in the stack.
W2
is the maximum of number of words that have been used in the stack so far in program execution.
W3
is the size of the stack (in words).

The ":vc" command also checks the validity of a co-routine stack. This means that it checks for stack overflow, and conditions like the stack pointer going out of range or invalid subroutine linkage areas.

The Free List

The ":vc" command also checks the validity of the free list. The free list is a linked list of blocks of memory that are being used for dynamically allocated data (e.g. memory obtained through "malloc" in a C program, "getvec" in a B program, or "new" in a Pascal program).

Each block in the free list begins with a control word. The top half of this word is a pointer to the control word of the next block in the list. The bottom half of the control word is the length (in words) of that block.

A program uses the external variable VEC.CH to indicate the first block in the free list. VEC.CH has the same form as a block control word. The end of the free list is indicated by a control word with the value -1.

To be valid, the free list must satisfy the following criteria:

If either of the above criteria do not hold, the free list is invalid. ":vc" will report the problem and display the last control word in the list that appears to be valid. Possible error messages include the following:

Bad cp OCTAL_VALUE
Co-routine pointer does not point to addressable memory.
Even stack pointer at OCTAL_VALUE
The value of a stack pointer should always be an odd number.
Invalid free list at OCTAL_VALUE
The free list pointer at the given location is invalid.
Invalid function entry address OCTAL_VALUE
The function entry address for the co-routine is not a valid memory location.
Previous stack pointer OCTAL_VALUE not with stack
Recall that a function always saves the stack pointer of its caller. When this message appears, BOFF has discovered that this saved stack pointer does not actually point at stack space.
Stack overflow - need NUMBER words
A program needs more stack space than it has. NUMBER is a decimal integer giving the number of additional words of stack space that are needed.
Stack pointer OCTAL_VALUE not with stack
The value of the stack pointer is not in the stack space.
Stack pointer off bottom of stack at OCTAL_VALUE
The stack pointer points below the beginning of the stack space.

16: Leaving BOFF

To get out of any mode of BOFF, you can use the command

:q

BOFF will terminate execution by calling the function "exit()". "exit" automatically flushes I/O buffers and closes all open files.

In Run mode, the command

:qq

terminates BOFF immediately. "exit" is not called and no I/O buffers are flushed. Any files that are currently accessed will remain in the AFT.

":qq" can be used in all other BOFF modes too, but it is the same as ":q".

If your program appears to be in an infinite loop, you can usually stop it by pressing BREAK. This returns control to BOFF so that you can enter debugging instructions or quit. However, there is one situation which can make it difficult to BREAK your program in an infinite loop. If there is a breakpoint of the form

LOCATION:b {:p}

in the loop, BOFF will continually be running into the breakpoint, handling the break, then resuming execution again. During the time that BOFF is handling the breakpoint, it ignores all external interrupts, including the BREAK key.

In a typical infinite loop, the execution time between hitting breakpoints is very small in comparison with the time BOFF spends dealing with each breakpoint. Therefore when you press BREAK, the odds are good that the BREAK will be received while BOFF is ignoring external interrupts instead of when BOFF is executing your program. Therefore pressing BREAK will usually be ignored.

The only way to avoid this kind of problem is to avoid setting the given type of breakpoint. Perhaps you might set a count at the breakpoint instead of using the "{:p}" construct.

17: Using BOFF

This section is intended to show new BOFF users some of the ways that BOFF can help you debug your programs. Naturally, we can only scratch the surface of this, but we hope that this introduction will help people get started with the BOFF system.

To make things as straightforward as possible, we will take a step-by-step approach showing how you might use BOFF to get a program working.

Compilation

Compile your program and get rid of any compilation errors. Do not use the "-Tables" option when compiling, because this option prevents the compiler from preparing a number of local symbol tables which BOFF can use. When the program is finally working, "-Tables" eliminates these tables so that your program takes up less space, but there's no point in doing that until you're sure there are no bugs left.

The First Run

Let's assume that you're an optimist and that you believe there's a chance your program will work the first time. You run your load module with a command like

go:.h

Very likely your program will abort with some sort of fault. Now is the time to use BOFF in Abort mode.

To invoke BOFF in Abort mode, you might use

boff abort +s

The "abort" keyword says that you want to use BOFF in Abort mode. The "+s" option indicates that the original load module was named ".h".

BOFF has to know where to find the original load module because the module contains the loader and local symbol tables. The abort file itself does not contain these tables because the tables are usually released once the program is loaded. If your original load module was not named ".h", you can specify the real name with the option

s=FILENAME

(see "expl boff abort" for other variations in the command line that invokes Abort mode BOFF).

Once you enter BOFF, you can use the traceback command ":t" to find out which function was executing at the time of the abort. You can examine the contents of the function's auto variables using the display commands. You can check the auto variables of any other function on the stack too: just use the ":d" or ":u" command to make the function current, and then you can examine its variables.

You can look at the values of external variables too. If you use ":ve" to see what external variables are defined for your program, you may find many names you don't recognize. These are probably external names used by the library functions you have called; such names usually contain a "." to make them easily recognizable.

Quite often, it is enough for you to look at your variables to understand what's going wrong. You can see that a vector index is too big, or that a variable contains garbage, or that a pointer doesn't point where it ought to. Once you realize what's gone wrong, you should be able to fix it and run your program again.

Run Mode

Even if you get rid of all conditions that cause program aborts, your program still may not do what you want it to. For example, infinite loops do not necessarily cause aborts, but they are certainly undesirable. Many times too, a program comes to a normal termination but the results are incorrect.

Using BOFF in Run mode may help to get rid of these bugs. To start running a program under Run mode BOFF, use the command line

boff run FILE "COMMAND_LINE"

where FILE is the name of your load module and COMMAND_LINE is the command line that you would normally use to invoke the program. BOFF will load your program and halt automatically at the beginning of "main". If you wish, you can use the display commands right here at the beginning to check that your external variables contain what you expect.

More likely though, you will want the program to do something before you start looking around inside. Thus you will want to set a number of breakpoints inside your program so that it will stop occasionally to give you a chance to see what's going on.

For example, you might put a breakpoint at the start of every major function in your program. This gives you a chance to examine the arguments that the function is passed. Breakpoints at the beginning of a function also allow you to examine the contents of the auto variables of the calling function too. Remember, you can only look at a function's auto variables when that function is on the stack. Furthermore, there is no point in looking at the auto variables of a function which is just about to start execution -- since local variables cannot be initialized, they will only contain garbage at the beginning of execution.

When you have examined whatever variables you want, you can use the ":p" command to resume execution of your program. Execution will continue until the next breakpoint (or program termination).

While stopped at a breakpoint, you might want to set a ":br" breakpoint which will cause a break when the current function returns to its caller. Again, at a breakpoint like this, you have the chance to see what each function variable contains.

It is difficult to be more specific about the sort of things you might want to do in Run mode. Essentially you can stop the program at any point and see what is going on. You can stop at the beginning of functions, and you can stop at any label inside a function; however, you can only set breakpoints at labels when the function is on the stack, since label names are local, not external.

If you suspect your program is in an infinite loop, you can press BREAK to stop it. A traceback at this point will tell you exactly where the loop has occurred.

Patching

When you have found an error in your program, the way you fix it depends on the size of the problem. If the error is major, you will have to go back and recompile your program. However, some types of problems can be patched "on the fly".

For example, B, C, and Pascal programs have an external variable named ".stack" which indicates the maximum amount of stack space that the program is allowed to use. If your program is aborting because it is running out of stack space, you can patch ".stack" to allow more space. This avoids recompiling and relinking.

To make the patch, invoke BOFF in Hstar mode with

boff hstar FILE

where FILE is the name of your load module. The command

 .stack\d

displays the current (decimal) value of ".stack". By default, this is 500 (giving 500 words of stack space). If you decide to double your stack space, you could use the command

 .stack = 1000

If you now run the program again, you will have twice as much stack space. You should eventually recompile your source code to reflect the patches you make, but patching can be a lot faster than recompiling an entire program.

While all this available information doesn't guarantee that you can find and fix your bug, it certainly increases your chances of solving the problem. With practice, you will get the knack of using BOFF to locate errors and also develop an instinct for sniffing out where errors are likely to occur. One of the best ways to build up a familiarity with BOFF is to write a small uncomplicated program and then just use BOFF to play around with it for a while. This will make things that much easier when you come to use BOFF on your first major program.

Internal I/O BOFF

The standard version of BOFF does all its I/O using the I/O routines of the program under examination. This has its advantages and disadvantages. The main advantage is that it reduces the amount of memory BOFF takes up, since it doesn't have to have I/O routines of its own.

One disadvantage is that BOFF will die a horrible death if anything ever happens to the program's I/O routines. (As an experiment, see what happens to Run mode BOFF when you set the first word of PRINTF to zero.) Another disadvantage is that the standard version of BOFF cannot be used with programs that do not contain the standard I/O routines.

Internal I/O BOFF is a version of BOFF which has its own I/O routines. It is about 6K bigger than the standard version of BOFF, but it does get around the standard version's disadvantages. For example, you can use Internal I/O BOFF on practically any load module, including ones that were not produced by the B, C, or Pascal compilers.

Internal I/O has several disadvantages:

To obtain the Internal I/O version of BOFF, just specify the "+Internalio" option on the BOFF command line. This option can be specified for Hstar mode, Abort mode, and Run mode.

BOFF automatically invokes its "+Internalio" option if your program lacks certain routines necessary for normal BOFF (e.g. "printf"). (NOTE: if your program does not have external symbol tables, BOFF will not be able to find the necessary routines. Therefore Internal I/O BOFF will be invoked, even if your program actually contains the required routines.) Internal I/O BOFF is also invoked automatically if the program being examined was compiled under a compiler release that doesn't match the BOFF release.

Evaluate Mode in BOFF

There are several modes for using BOFF that we have not yet discussed. One of these is known as Evaluate mode. This mode of BOFF is used to evaluate expressions and display their results in various formats using the standard display command.

The easiest way to use the Evaluate mode of BOFF is with a command of the form

boff eval EXPRESSION\FORMAT

This will display the value of EXPRESSION using specified FORMAT. You can use this in a number of ways.

Evaluate mode BOFF will accept most of the expressions accepted by other modes of BOFF. Because the expressions are not evaluated within the context of an actual program, it will not evaluate operations which are related to positions in memory. Thus it will not accept indirection (&) or addressing (*) operators, subscripts, etc. Similarly, it will not accept display modes like "\p", "\+", "\s", and so on.

The expressions to be evaluated may contain BOFF variables if you wish. One BOFF variable which is particularly convenient is "/_" which refers to the value of the last expression evaluated. "/_" is remembered from one BOFF expression call to the next; thus

boff eval 2*4\d
boff eval /_+2\d

will print out "8" and "10" respectively. The system will not remember your particular value of "/_" indefinitely; however, "/_" should be remembered throughout your current terminal session and will certainly be available to you if you make several calls to BOFF in close succession.

Interactive Evaluate Mode

If you are going to use BOFF to do a number of evaluations, it is best to use BOFF's interactive Evaluate mode. To enter Evaluate mode interactively, you simply say

boff eval

without specifying any expressions on the command line. BOFF will then begin to accept expressions interactively, reading a line from the terminal, evaluating the expression(s), and displaying the result(s) in the desired formats. It will then wait for the user to type in a new expression.

Variables used in these expressions do not need the usual "/" character preceding their names (although you can have names of this form if you want). Apart from this, interactive Evaluate mode BOFF handles expressions in the same way as it handles expressions specified on the command line. To leave this mode of BOFF, use the standard ":q" command.

18: BOFF in Patch Mode

Up until this point, we have been talking about using BOFF on program files. It is possible to use BOFF on any file by invoking the subsystem with the command line

boff patch FILENAME

where FILENAME is the name of the file you wish to examine.

When examining a random file that is not a program file, some of the usual BOFF commands are meaningless. For example, the ":ve" command will not work (unless you specify a symbols file, as described in "expl boff symbols"). The ordinary display command does work however, as does the patching command.

Another command that is useful in Patch mode is the Origin Offset command. This command has the form

EXPRESSION:o

The value of EXPRESSION is used as an offset into the file. This offset is considered to be the "origin point" inside the file. All addresses read in or printed out will be interpreted as offsets from the origin point. For example,

6*320:o

sets the offset to the sixth llink of the file. The display command

*0\o

will then display the zero'th word of that llink in octal form. The command ":o" without an expression displays the current Origin Offset in words and llinks.

The ":o" command is not available when using BOFF on program files. While there are a few times when this facility might be useful in one of the program modes, there are many times when it would be a liability. For example, when displaying a data object with the "m", "p", or "s" display formats, there will be times when you want the resulting address to be relative to an Origin and times when you would want it to be taken as an absolute address. There is no obvious way to decide on the correct way to interpret offsets and addresses when working with program files, so the ":o" is not available in those modes.

Checksums

When patching Hstars and other files, it is useful to be able to calculate checksums. The command to do this is

LOCATION,COUNT:k N

where

LOCATION
is the start of the area to be checksummed. If this is not specified, location 0 is assumed.
COUNT
is the length (in words) of the area to be checksummed. If this is not specified, the default is one word.
N
is 0, 1, or 2. If N is 0, the final carry is not added to the checksum. If N is 1, the final carry is added. If N is 2, the final carry is added, and if there is a carry from this final addition, that carry is also added.

The result of a checksum operation is left in the BOFF variable "/_".

When calculating a checksum for an entire file, the usual value of N is 0. When performing "piecemeal" checksums (on parts of programs), N should usually be 2.

19: Bugs and Other Pitfalls

When BOFF is being used in Hstar mode to patch system loadable files (Hstars, Qstars, and ** files), the file is modified as soon as the "=" operators is used, but the checksum of the file is not recalculated until the ":q" command is given. This means that the patched file will have a bad format if the BOFF session terminates abnormally. This bad checksum can be fixed by re-entering BOFF, patching any word to the value it already has, and then terminating BOFF normally. The checksum will be recalculated properly at this time.

The function that handles the format of floating point numbers is usually not loaded with a program or with Run mode BOFF unless the program contains floating point operations. If you attempt to use a floating point constant when the handler is not available, BOFF will print an error message. To get around this problem, you must recompile your program with the

Use=.float

option in the command line. You can also avoid the problem by using "Internal I/O" BOFF (which has its own floating point routines).

20: The Batch Loader

The loader that runs in batch does not leave external symbol tables in the final load module. This causes a number of problems if you want to use BOFF on such a load module.

One solution is to get the loader to produce a load map. With a small amount of editing, this load map can be used as input for the "Symbol=file" option of the Run mode command line. The load map will show where all the program's external symbols are located, and BOFF can work from there.

If modules of a program are compiled with local symbol tables (i.e. the "-Tables" option was NOT specified), the local symbol tables will be present in a load module, even if the global symbol tables are not. Unfortunately, without the global symbol tables, BOFF can't find the local symbol tables.

To find the local symbol tables, BOFF needs a definition for the global symbol ".ENTRY". If you have some way to determine the location of ".ENTRY", you can use the command

LOCATION:n .entry

to associate the name .ENTRY with the given location, and from this point onward, BOFF will be able to find the local symbol tables. Here are some hints on finding .ENTRY.

Note that it is also a good idea to establish locations for .VNTRY and .ZNTRY if possible, because these symbols are frequently used.

Appendix A: Binding Strength of Operators for BOFF

Operators are listed from highest to lowest binding strength; there is no order within groups. Operators of equal strength bind as indicated, either left to right (denoted by [LR]) or right to left (denoted by [RL]).

[LR]   name const primary[expr] primary(arglist) (expr)
[RL]   ++ -- * & % - ! ~ #- # ## (unary)
[LR]   >> <<
[LR]   &
[LR]   ^
[LR]   |
[LR]   * / % #* #/ (binary)
[LR]   + - #- #+
[LR]   == != > < <= >= #== #!= #> #< #<= #>=
[RL]   =

Below we give a brief explanation of how the operators work.

++L    increments the Lvalue L by 1.
       Result is new value of L.
L++    has a result equal to the current value of L.
       After result is obtained, L is incremented by 1.
--L    decrements the Lvalue L by 1.
       Result is new value of L.
L--    has a result equal to the current value of L.
       After result is obtained, L is decremented by 1.
*R     treats the Rvalue as an address.
       Result is the Lvalue indicated by the address.
&L     obtains the address of the Lvalue L,
       as a B pointer.
%L     obtains the address of the Lvalue L,
       as a C or Pascal pointer.
-R     numeric negative of R
       (treated as integer).
!R     logical negation of R (1 if R is integer 0,
                              0 otherwise).
~R     bitwise complement of R.
#-R    numeric negative of R
       (treated as floating point value).
#I     floating point value equal to integer I
       (e.g. #3 is 3.0).
##FP   integer part of floating point number FP
       (e.g. ##3.7 is 3).
A>>B   shifts integer value A right by B bits.
A<<B   shifts integer value A left by B bits.
A&B    obtains bitwise AND of A and B.
A^B    obtains bitwise exclusive OR of A and B.
A|B    obtains bitwise inclusive OR of A and B.
A*B    A multiplied by B (integer).
A/B    A divided by B (integer).
A%B    remainder of A divided by B (integer).
A#*B   A multiplied by B (floating point).
A#/B   A divided by B (floating point).
A+B    A plus B (integer).
A-B    A minus B (integer).
A#-B   A minus B (floating point).
A#+B   A plus B (floating point).
A==B   compare A and B for (integer) equivalence.
       Result is 1 if equal, 0 otherwise.
A!=B   compare A and B for (integer) non-equivalence.
       Result is 1 if not equal, 0 otherwise.
A>B    Result is 1 if A is greater than B (integer),
                 0 otherwise.
A<B    Result is 1 if A is less than B (integer),
                 0 otherwise.
A>=B   Result is 1 if A is greater than or equal to B
                 (integer), 0 otherwise.
A<=B   Result is 1 if A is less than or equal to B
                 (integer), 0 otherwise.
A#==B  Result is 1 if A equals B (floating point),
                 0 otherwise.
A#!=B  Result is 1 if A is not equal to B (floating point),
                 0 otherwise.
A#>B   Result is 1 if A is greater than B (floating point), 
                 0 otherwise.
A#<B   Result is 1 if A is less than B (floating point),
                 0 otherwise.
A#>=B  Result is 1 if A is greater than or equal to B
                 (floating point), 0 otherwise.
A#<=B  Result is 1 if A is less than or equal to B
       (floating point), 0 otherwise.
L=R    Assigns Rvalue R to Lvalue L.
       Result of expression is value of R.

Appendix B: The Run Time Environment

In this appendix, we give a brief introduction to the run-time environment. Most technical details are omitted -- all we want to do is give you a feel for how a program works.

The Stack

The stack is a large piece of memory that is used for local data storage. The stack register is the hardware register AR7 which is used when referencing data stored in the stack. For example, suppose a function F has a local variable X. F might "know" that X is located at the 10th word after the value stored in AR7. Therefore, every time F wants to obtain the value of X or assign a value to X, F gets the value of AR7 and adds 10 to it, then uses the result as the location of X.

This means that the value of the stack register entirely determines the memory space that a function uses for its local (auto) variables. Different values for AR7 mean that functions use different areas of memory for local data.

When a function F1 is called, the value of AR7 is bumped (incremented) to point to an area of the stack which is not currently in use. When a new function F2 is called, the value of AR7 is bumped again to point beyond the end of F1's storage, so that the stack space for F2 comes immediately after the stack space of its caller.

When F2 finishes execution, the value of AR7 is restored to its previous value (pointing to F1's stack space) so that F1 can access its local data through AR7.

Note that this set-up allows for recursion. If a function F calls itself, the stack register is bumped so that the stack space used by the second activation of F is separate from the stack space for the first activation. In this way, the two activations of F do not affect each other's local (auto) storage.

Stack Organization

The stack space used by a function F is called its stack frame. Contents in the stack frame always appear in the same order.

  1. The first two machine words in the frame are called the linkage area. These words are used when F calls another function. They store information about the function call (e.g. the address to which the function should return).
  2. The next word in the frame holds the stack register value in F's caller. When F returns, this value will be assigned to AR7 to restore the caller's expected state.
  3. Next comes space for the argument values passed to F. There is space allocated for all the argument values. Most of the argument values are stored in this space by F's caller, not by F itself. However, the caller passes the first two argument words to F in the AQ register (to make the argument passing process quicker). F can then store these arguments in the allocated space or use the values directly.
  4. Next comes space for the local (auto) variables of F. Space is allocated in the order in which variables are declared.
  5. Finally, the stack space contains an area for temporary data objects (used to hold intermediate values when evaluating expressions).

The address in AR7 is actually word (b) above (the stack location that holds the caller's stack pointer). Therefore the words in the linkage area are at offsets -2 and -1 from the value in AR7.

Function Calls

The caller begins a function call by calculating the amount of stack space it is currently using. This lets it calculate a stack bump value which is the value that must be added to AR7 when the function call occurs (to point to unused stack space).

Next, the function calculates argument values and stores them in the temporary section of its stack frame. This is the end of the caller's stack frame, and will be the beginning of the callee's stack frame; therefore, when the callee takes over, the argument values will be in their proper place in the callee's frame.

The actual call to a function F uses the following code (in the caller):

tsx1   F
zero   STBUMP,NARGS

where STBUMP is the stack bump value and NARGS is the number of words of argument values that are being passed. (Explanation of GMAP: the TSX1 instruction sets X1 to point to the next word of data, then jumps to the code for F. The ZERO line is a word with STBUMP in its upper half and NARGS in its lower half.)

The first word of a B function is a transfer instruction to the second word of the function. This is because you can assign one B function "value" to another B function -- see the environment manual for more details.

The first and second words of a C or Pascal function (and the second and third words of a B function) are

tsx0   .ENTRY
zero   FRAMESIZE,DBGPTR

where

.ENTRY
is the name of a routine which performs most of the operations necessary for finishing the function call.
FRAMESIZE
is the maximum amount of stack space that this function will use.
DBGPTR
points to debugging information for the function.

The .ENTRY routine saves call/return information in the caller's stack and then bumps the stack register value by the amount specified as STBUMP (in the caller). The .ENTRY routine also makes sure that the stack area has enough space to hold the amount of memory indicated by FRAMESIZE. The routine returns to the word following the FRAMESIZE,DBGPTR word.

Since the stack register AR7 is bumped by the .ENTRY routine, the register points to the caller's stack frame at the very beginning of function execution. In BOFF, /SP will indicate the stack frame of the callee, but AR7 will indicate the stack frame of the caller.

Function Return

A function that is ready to return to its called begins by setting up its result. Integer results are returned in the Q register. Floating point results are returned (in double precision) in the EAQ register. Longer results (e.g. C structures or Pascal records) are returned on the stack. The result value is written over the function's local data, starting at the argument values that the function was passed. In this case, much of the function's local data may be lost as it is overwritten.

Next, the returning function calls a routine named ".RETRN". From the current stack frame, .RETRN obtains the AR7 value of the caller and assigns this value to AR7. (This restores the caller's stack register.) The .RETRN routine then obtains the return address from the caller's linkage area and transfers to this address. This returns control to the caller, which then proceeds with normal execution.