CS222 Lecture: Procedures; The VAX Procedure-Calling Standard; 10/12/91
Recursion Revised 1/27/99
Materials:
1. Handout: ARGUMENTS.MAR
2. Transparencies: VAX Procedure Call Frame; H & P Figures 3.12, 3.13
I. Introduction
- ------------
A. As we know, any large program should be written as a collection of
independent routines which call and are called by one another.
B. Further, the preparation of programs can be made much simpler by
making use of LIBRARIES of pre-written routines.
1. Some of your labs have made use of a library of routines
to do simple IO tasks: RNUM, PNUM, etc.
2. Compilers for HLL's often translate statements into calls to a
support library - e.g. Pascal write, read etc. are implemented by
such routines.
C. Today, we begin to consider how routines are implemented at the
machine level. We must consider issues like:
1. Routine call/return mechanisms
2. Passing of parameters
3. Handling of values returned by functions
4. Preservation of register values when both the caller and the
called routine use the same registers
5. Handling of recursion.
D. On the VAX, some of these issues are handled directly by the hardware,
while others are handled by following certain conventions in software.
1. In particular, we will become familiar with a document called "The VAX
Procedure Calling Standard" which prescribes conventions that software
should follow when using procedures.
a. This standard is followed by all system software (e.g. the VMS
run-time library and system services.)
b. It is followed by all VAX language compilers. Adherence to these
conventions allows procedures written in one programming language
to call procedures written in another; in particular, we will
learn how Pascal routines can call MACRO procedures and vice-versa.
c. Procedures within a user-written program that call one another do
not have to adhere to this standard - but generally should.
2. For generality, we will also look at how some of these issues are
handled by machines other than the VAX - in particular, MIPS.
II. Routine call/return mechanisms
-- ------- ----------- ----------
A. The VAX hardware provides two distinct mechanisms to allow routines to
call other routines.
1. The subroutine mechanism.
2. The procedure mechanism.
B. We have already met the VAX subroutine mechanism.
1. It uses 4 instructions: 3 for calling a routine and 1 for return
a. Call instructions: BSBB, BSBW, JSB
b. Return instruction: RSB
2. It does not provide any special support for parameters. (The IO
library routines we have used have passed parameters through registers
R0 and/or R1.)
C. The procedure mechanism is much more powerful and general.
1. It uses 3 instructions: 2 for calling a routine and 1 for return
a. Call instructions: CALLS, CALLG
b. Return instruction: RET
2. It provides significant support for parameters - and should normally
be used whenever a routine must have parameters. (Earlier examples
used subroutines for IO to avoid having to introduce the full
procedure mechanism too early.)
3. It is also normally the mechanism to be used when routines in one
module call routines in another module. (Again, the IO library forms
an exception.)
4. Finally, it is the mechanism that MUST be used when a routine written
in one language calls a routine written in another language.
D. The MIPS call/return mechanism makes use of two instructions:
1. jal - jump and link - saves the address of the next instruction in
a designated register (typically $31) and jumps to the start of a
procedure.
2. jr - jump register - jumps to a location whose address is contained
in a register (again, typically $31).
III. Parameter-Passing
--- -----------------
A. Two basic issues:
1. WHERE are parameters passed?
2. WHAT information is passed when a parameter is passed?
B. Some options for the WHERE issue.
1. Parameters can be passed through fixed locations in memory
Example: The RSTS/E operating system - used on the PDP-11 - requires
that parameters to system routines be passed in one or both
of two fixed regions in memory called FIRQB (address 402
octal and up) and XRB (begins just after FIRQB)
Problems:
a. The caller needs to know a lot of detail about the routine it calls.
b. Recursion is difficult - the parameters for one call have to be
"moved out of the way" to make room for another.
2. The CPU registers - e.g. first parameter in R0, second in R1 etc.
Example: The IO library used in some of our examples.
Problems:
a. The number of parameters is limited by the number of registers.
b. Before a procedure can call another, it must first move its own
parameters out of the way.
MIPS uses this approach, with 4 or 8 registers dedicated by convention
to this purpose ($4 .. $7 or $4 .. $11 - two different sets of
conventions). If a given procedure requires more parameters, one of
the other approaches must be used for the rest.
3. On the stack - the caller pushes a series of parameters onto the
run-time stack.
a. This mechanism is used by many CPU's.
b. It fully supports flexibility in calling other procedures from
within a procedure - including recursively.
c. It is one of two options commonly used on the VAX.
4. The VAX approach is based on the use of PARAMETER LISTS.
a. A parameter list is a series of longwords in adjacent memory
cells. The AP register (R12) points to the first of these.
i. The first longword holds (in its low order byte), a COUNT of
the number of parameters. This allows routines to take a
variable number of parameters.
Note: This value must lie in the range 0..255. The high order
24 bits of this longword are not used and must be zero.
ii. The remaining longwords hold the individual parameters.
Example: call procedure foo with actual parameters 1 and 17.
The parameter list would look like this:
_________________
(AP): | (zero) | 2 |
|---------------|
| 1 |
|---------------|
| 17 |
-----------------
b. The parameter list can be constructed in one of two ways:
i. It can be created at compile time in the caller's data area.
When the procedure is called, the AP is loaded with the
address of this list.
ii. It can be created at run time by pushing the parameters on
the run-time stack. (Note: the parameters must be pushed in
the order LAST, SECOND TO LAST .. FIRST, COUNT because the
stack grows from high addresses toward low.) When the procedure
is called, the AP is loaded with the address of the top of the
stack (which holds the count.)
iii. The first approach is more efficient when the parameters are
known at compile-time - e.g. when the parameters are constants
or the addresses of variables. The second approach must be used
if the items to be included in the parameter list are not known
until run-time.
c. The two different VAX CALL instructions reflect these two
options.
i. CALLG is used when the parameter list can be set up ahead of
time.
Example:
C: foo(1, 17);
MACRO: ; In data area:
FOO_PARAMS: .LONG 2
.LONG 1
.LONG 17
; Actual call:
CALLG FOO_PARAMS, FOO
The hardware places the address of the parameter
list in AP, and then calls the procedure
ii. CALLS is used when the parameter list is to be created on
the stack. Example:
C: foo(x, y);
MACRO: PUSHL Y
PUSHL X
CALLS #2, FOO
The hardware completes the parameter list by
pushing the count longword (derived from #2), and
then copies SP into AP before calling the procedure.
iii. When a procedure is called using CALLS, the RET instruction
pops its argument list from the stack automatically. (The CALLS
instruction sets a flag to indicate that CALLS was used, and
the RET instruction checks this flag and then (if it set) uses
the count field in the argument list to determine how many
longwords to remove.)
C. Some options for the WHAT is passed issue
1. Recall that Pascal supports two types of parameters:
a. Value - The procedure receives a COPY of the actual parameter. Any
changes made to it do not affect the original belonging to the
caller.
b. Var (reference) - The procedure receives the ADDRESS of the caller's
actual parameter. Any changes made by the procedure do affect the
original. (Also - as a consequence - the actual parameter must be
a variable or a component of a variable - it cannot be an expression
that is evaluated at run time.
2. The following examples show how each type of parameter COULD be
handled. (VAX Pascal doesn't actually do it this way, but many other
Pascal implementations do.)
a. Value parameters:
Pascal: procedure foo(a: integer);
begin
b := a + 1;
...
end;
...
foo(x);
VAX MACRO: .ENTRY FOO, ^M<>
ADDL3 #1, 4(AP), B
...
RET
PUSHL X
CALLS #1, FOO
MIPS: .ent foo
.globl foo
foo:
addi $2, $4, 1 ; $2 used as temporary
sw $2, b
...
lw $4, x
jal foo
b. Var parameters:
Pascal: procedure foo(var a: integer);
begin
b := a + 1;
...
end;
...
foo(x);
VAX MACRO: .ENTRY FOO, ^M<>
ADDL3 #1, @4(AP), B ; Note deferred mode
...
RET
PUSHAL X ; Note push ADDRESS
CALLS #1, FOO
or Alternate code for caller:
PLIST: .LONG 1
.ADDRESS X
...
CALLG PLIST, FOO
MIPS: .ent foo
.globl foo
foo:
lw $2, 0($4) ; $2 used as temp - value of a
addi $2, $2, 1
sw $2, b
...
addiu $4, $0, x ; $4 gets ADDRESS of x
jal foo
3. The VAX calls these two mechanisms CALL BY IMMEDIATE VALUE and CALL
BY REFERENCE (respectively).
a. Immediate value is limited to use with parameters that are either a
longword or can be converted to longwords.
i. Byte or word must be widened (using CVTxx or MOVZxx)
ii. Immediate value cannot be used with D, G, or H float or arrays
or records.
b. Reference can be used with any data type, since an ADDRESS is always
a longword.
c. VAX Pascal actually uses call by reference for both value and var
parameters, and other Pascals may use call by reference for value
parameters when the actual parameter is a structured type (array,
etc.) In this case, it becomes the task of the CALLED procedure to
construct a private copy before proceeding.
Example: VAX Pascal implementation of the example above example
when the parameter of foo is NOT a var parameter:
MACRO: .ENTRY FOO, ^M<>
PUSHL @4(AP) ; Private copy
ADDL3 #1, -4(FP), B
...
RET
PUSHAL X
CALLS #1, FOO
Note: When the actual parameter is a large array, this making of a
local copy can consume a lot of time and space. This is why
it is more efficient to make large arrays var parameters.
4. The VAX Procedure Calling Standard sepcifies a third mechanism: CALL
BY DESCRIPTOR.
a. This is most often used for complex structures - e.g. character
strings whose lengths may vary, or conformant array parameters.
(A conformant array parameters is one which may accept actual
parameter arrays having different bounds - e.g. in Pascal:
procedure foo(x: array[lo..hi: integer] of real)
This could be called with any one-dimensional array of reals as the
actual parameter.
b. The following is the general format of a descriptor:
_________________________________
| CLASS | DTYPE | Length |
|-------------------------------|
| Pointer |
|-------------------------------|
(Additional items as needed)
i. CLASS indicates the type of descriptor - e.g. string, array, etc.
ii. DTYPE indicates the data type of the individual elements of the
array or string
iii. Length indicates the overall length of a character string, or the
length of an individual element of an array.
iv. Pointer is the address of the actual data item.
v. Additional items are used for arrays to give bounds information.
c. Three different types of descriptors can be used for character
strings:
i. Fixed-length string: the size of the string cannot be altered
by the procedure receiving it.
CLASS = DSC$K_CLASS_S
DTYPE = DSC$K_DTYPE_T
ii. Dynamic string: the size of the string can be altered by
allocating new space for it and altering the length and pointer
fields in the descriptor. (The old space should also be
recycled.)
CLASS = DSC$K_CLASS_D
DTYPE = DSC$K_DTYPE_T
iii. Variable string: the size of the string can be altered within
a FIXED allocation. The length field gives the MAXIMUM
length, and the string is immediately preceeded by a word giving
the CURRENT length.
CLASS = DSC$K_CLASS_VS
DTYPE = DSC$K_DTYPE_VT
Note: pointer points to a structure that looks like this:
Current length (word)
Body of string
...
d. Array descriptors can get complex. We will only touch on some of
the fields:
_________________________________
| CLASS | DTYPE | Length |
|-------------------------------|
| Pointer |
|-------------------------------|
| DIMCT | AFLAGS | (ignore) |
|-------------------------------|
| ARSIZE |
|-------------------------------|
| A0 |
|-------------------------------|
| M1 |
|-------------------------------|
| M2 |
|-------------------------------|
...
|-------------------------------|
| Mn |
|-------------------------------|
| L1 |
|-------------------------------|
| U1 |
|-------------------------------|
| L2 |
|-------------------------------|
| U2 |
...
|-------------------------------|
| Ln |
|-------------------------------|
| Un |
---------------------------------
i. CLASS is DSC$K_CLASS_A
ii. DTYPE indicates the data type of the elements, and length gives
the size of an individual element. (The Procedure Calling
Standard gives a table of DTYPE values for various standard
types - e.g. DSC$K_DTYPE_L is longword integer; and length would
be 4 for this type.)
iii. Pointer, as always, points to the beginning of the space
allocated for the array.
iv. DIMCT is the number of dimensions (e.g. 1 for a vector, 2 for
a two-dimensional array, etc.)
v. AFLAGS contains individual bits that indicate various features.
For most purposes, the two bits set will be
<1 @ DSC$V_FL_COEFF> + <1 @ DSC$V_FL_BOUNDS>
vi. ARSIZE is the total size of the array.
vii. A0 is the address of element [0, 0, 0 ... ,0]. (Note that
this may not actually lie within the space allocated for the
array.)
viii. M1, M2 .. are the multiplicative coefficients for each
dimension - i.e. (Ui - Li + 1)
ix. L1, U1, L2, U2 .. are the lower and upper bounds for each
dimension.
x. The address of element [i1, i2, i3 .. in] is calculated as
A0 + ([[[I1 * M2] + I2] * M3 ... ] * Mn + In) * Length
e. When a parameter is passed by descriptor, what goes into the
argument list is the ADDRESS of the descriptor.
Example: To pass a variable-length string with maximum length 16
whose initial value is "Hello, world" to procedure foo.
(Note that .ASCID cannot be used since the string is not
a fixed-length string.)
DESC: .WORD 16
.BYTE DSC$K_DTYPE_VT
.BYTE DSC$K_CLASS_VS
.ADDRESS HELLO
HELLO: .WORD 12
.ASCII "Hello, world"
.BLKB 4 ; (Remainder of 16 bytes.)
PUSHAB DESC
CALLS #1, FOO
f. Some examples of call by descriptor:
i. A routine to print a variable length string (passed by
descriptor) by calling the PLINE routine we have used for labs:
.ENTRY PSTRING, ^M<>
MOVL 4(AP), R0 ; Address of descriptor -> R0
MOVL 4(R0), R0 ; Pointer to string -> R0
MOVZWL (R0)+, R1 ; Current length -> R1;
; R0 left at body
JSB PLINE
RET
ii. A routine to read into a variable length string (passed by
descriptor) by calling the RLINE routine we have used for
projects:
.ENTRY RSTRING, ^M<>
MOVL 4(AP), R0 ; Address of descriptor -> R0
MOVZWL (R0), R1 ; Max length -> R1
MOVL 4(R0), R0 ; Pointer to string -> R0
MOVAB 2(R0), R0 ; Address of string body -> R0
JSB RLINE
MOVL 4(AP), R0 ; Address of descriptor -> R0
MOVW R1, @4(R0) ; Fill in current length field
RET
5. An example: HANDOUT ARGUMENTS.MAR
IV. Returning Values from a Function to its Caller
- --------- ------ ---- - -------- -- --- ------
A. We have discussed the ideas of passing parameters into a procedure or
function. In the case of a function, we need some mechanism to allow
it to return a value to its caller.
B. On most machines, including the VAX and MIPS, the convention is to return
a function value via a designated CPU register.
1. On the VAX, the Procedure Calling Standard specifies that R0 is used
for byte, word, or longword-sized values; and R0 and R1 together are
used for quadword-sized values (e.g. double-precision real.)
2. On MIPS, $2 (and $3, if needed) are used in this same way.
3. How do we handle function values that may be larger what a register
(or register pair) can hold - e.g. arrays?
a. Many programming languages do not allow such results to begin with.
b. However, some do. A typical implementation is to pass an extra,
hidden reference parameter that points to an area in the caller's
data space set aside to receive the value.
Example: VAX Pascal lets a function return a string as a value.
Such functions are called with an extra initial parameter
that points to a temporary created by the caller to
receive the result. In addition, the function returns
the address of this temporary as its value in R0.
V. Preservation of Information Belonging to the Caller
- ------------ -- ----------- --------- -- --- ------
A. In general, we want procedures to be modular, with no unintended side
effects.
1. Parameter passing provides a controlled means of communication.
2. Global variables - rightly used - can do the same.
B. One possible set of surprise interactions we want to avoid is interaction
through the registers.
1. As a general rule, any register used by both a procedure and its
caller must be saved at the time of the call and restored to its
original value when the procedure returns.
2. This can be handled in one of two ways:
a. We can require that the caller save any registers it is currently
using just before calling the procedure. This would allow the
procedure to freely use any registers it needs.
e.g. Assume that the caller is currently using R3, R4:
PUSHL R3
PUSHL R4
CALL ---
MOVL (SP)+, R4
MOVL (SP)+, R3
b. We could require that the procedure save and later restore any
registers it intends to use.
e.g. Assume that the procedure needs R2, R5:
.ENTRY ---
PUSHL R2
PUSHL R5
...
MOVL (SP)+,R5
MOVL (SP)+,R2
RET
3. The VAX uses the latter approach, and the CALL_ instruction provides
hardware support for saving/restoring registers automatically.
a. The first word of a procedure is an entry mask (which we have thus
far been simply setting to zero.) Each bit in the mask corresponds
to one register that might need to be saved upon procedure entry
and restored upon procedure exit, as follows:
^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
| | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | save r0
| | | | | | | | | | | | | | | save r1
| | | | | | | | | | | | | | save r2
| | | | | | | | | | | | | save r3
| | | | | | | | | | | | save r4
| | | | | | | | | | | save r5
| | | | | | | | | | save r6
| | | | | | | | | save r7
| | | | | | | | save r8
| | | | | | | save r9
| | | | | save r10
| | | | save r11
(to be discussed
later)
b. Upon procedure call, the entry mask is scanned and the specified
registers are pushed on the stack. In addition, AP, FP, and PC
are ALWAYS pushed (which is why the bits that would correspond
to these registers in the entry mask can be used for something
else.) A copy of the entry mask is also saved on the stack.
c. Upon procedure return, the hardware scans the saved entry mask
and pops the specified registers, along with AP, FP, and PC.
d. Note that the entry mask contains bits to specify saving and
restoring R0 and R1. However, the convention specified by the
Procedure Calling Standard is that these bits are never set -
i.e. R0 and R1 are never saved and restored. (The procedure
may use them freely; the caller must not assume that their
values will be preserved.) This correlates with the usage of R0 and
possibly R1 for the return value of functions.
e. As we have already seen, the .ENTRY directive serves to both create
a global label for a procedure and set up its entry mask. MACRO
uses the syntax ^M< registers > to specify an entry mask.
Example: Procedure foo uses registers r0, r3, r5, and r7
.ENTRY FOO, ^M<R3, R5, R7>
-- note omission of R0
f. We have noted that 4 bits in the entry mask are not used to
specify registers, since the registers they would specify are
handled in a special way.
i. Two are used to specify the setting of the DV and IV overflow
trap enable bits in the PSL when the procedure starts
executing.
ii. Two (the two nearest the middle) are required to be zero.
4. MIPS uses a mixed approach.
a. By convention, certain registers use the callee save convention -
if a called procedure alters them, it must save their original
values upon entry and restore them upon exit.
i. The following registers are required to be preserved across
calls:
$16 .. $23
Note: These registers are often referred to by the alternate
names s0 .. s7 - standing for "saved register" ...
ii. In addition, certain other registers have special uses that
in effect require a form of callee save:
$28 .. $31 - to be discussed below
b. Most of the other registers are not preserved by the callee; if
the caller needs them, it must preserve them:
c. In either case, registers that need to be saved are pushed on the
stack, and are later popped from the stack by the same routine that
pushed them (caller or callee, as the case may be.)
VI. Recursion
-- ---------
A. One especially interesting kind of procedure is a recursive one.
Recursive procedures are interesting because several activations of
the same procedure may be active at the same time.
Example: function fact(n: integer): integer;
begin
if n = 0 then fact := 1 else fact := n * fact(n-1)
end;
If this function is called by fact(6), at one point there will be
seven copies of it active: fact(6), fact(5), fact(4), ... fact(0).
B. Of course, each activation of the procedure must have its own private
copies of its parameters, return address, local variables, etc.
1. The natural data structure for managing this is the stack.
2. The conventional approach is to allocate space for local variables
on the stack upon procedure entry.
a. On the VAX, a special register called the frame pointer (FP) is set
to point to the top of the stack at procedure entry by the CALL
instruction. As a result, anything pushed onto the stack by the
procedure can then be referenced by NEGATIVE offsets relative to FP.
Example: A procedure has two integer local variables i and j.
Upon entry: SUBL #8, SP
To reference i, use: -8(FP)
To reference j, use: -4(FP)
Because of the way the VAX RET instruction works, anything pushed
on the stack above FP is automatically discarded when it is
executed.
b. A very similar approach is used on MIPS, except that the programmer
must manage the frame pointer register ($30 also known as $fp)
explicitly and must both allocate and deallocate the space
on the stack.
VII. Call Frames
--- ---- ------
A. We have seen that various items are pushed on the stack as part of the
procedure call conventions - e.g. the return address from the procedure,
registers that need to be saved, local variables, and perhap parameters.
The net effect of all this pushing is to create a run-time data structure
called as STACK FRAME or CALL FRAME. The exact form this structure
takes, and the presence of special hardware support for it, depends on
the CPU.
B. On the VAX, most of the work of constructing a call grame is done by the
CALL instructions, and most of the work of destroying it is done by
the RET instruction. The FP register (R13) always points to the call
frame of the currently executing procedure. A VAX call frame always has
the following structure:
TRANSPARENCY: VAX Procedure Call Frame.
C. As one would expect on a RISC, a MIPS call frame is constructed by
a series of explicit instructions created by the programmer or compiler,
and is destroyed by another series of explicit instructions. One
consequence of this is that the structure of a call frame is less
rigidly defined. Typically, it has the following structure:
TRANSPARENCY: MIPS Procedure Call Frame (H & P Figure 3.12)
[Note: the GNU compiler puts $fp at the bottom of the frame, where
$sp points - not at the top as shown in transparency]
Creation of a frame like this requires a protocol like the following:
1. Procedure entry protocol:
Subtract size needed for frame from $sp
Save registers as necessary, including old value of $fp *
Copy $sp to $fp
* $ra and argument registers must be saved if procedure intends
to call other procedures
2. Procedure exit protocol
Restore registers that were saved
Add size of frame to $sp
Return
D. At this point, it might be helpful to review how MIPS uses its registers.
TRANSPARENCY: H & P Figure 3.13
VIII. Inter-Language Calls Between Pascal and VAX MACRO
---- -------------- ----- ------- ------ --- --- -----
A. We are now prepared to discuss how procedures written in Pascal and
procedures written in MACRO can interface to one another. (Similar
principles will govern other sorts of inter-language calls.)
B. To call a MACRO procedure from Pascal, include an external declaration
in the Pascal program, using appropriate foreign-mechanism specifiers
to specify how arguments are to be passed. (The MACRO routine is
"the boss" on this.)
Example: The following is the declaration for a function that takes
an array passed by descriptor, two integers passed by value,
and two integers passed by reference, returning a boolean:
function MOUSE(%descr Maze: array[lr..hr: integer; lc..hc: integer] of char;
%immed StartRow, StartCol: integer;
%ref CheeseRow, CheeseCol: integer): boolean; external;
C. To call a Pascal procedure from MACRO, preceed the procedure declaration
in the Pascal program by [global], and note that the MACRO caller must
obey Pascal's calling conventions - e.g.
1. Most parameters are passed by reference.
2. Conformant arrays and strings are passed by descriptor.
Example: The following is the declaration for a procedure that takes
a conformant array parameter (passed by descriptor) and
two integers (passed by reference).
[global]
procedure Draw_Mouse(var Maze: array[lr..hr: integer; lc..hc: integer] of char;
CurrentRow, CurrentCol: integer);
Copyright ©1999 - Russell C. Bjork