Subroutine
Linkage
When
a subroutine or function is called, control passes to that subroutine but must
return to the instruction immediately following the call when the subroutine
exits.
There
are two main issues in the design of a calling mechanism for
subroutines and functions. These fall
under the heading “subroutine linkage”.
1. How
to pass the arguments to the subroutine.
2. How
to pass the return address to the subroutine so that,
upon completion, it
returns to the correct address.
A
function is just a subroutine that returns a value. For functions, we have one additional issue
in the linkage discussion: how to return the function value.
This
presentation will be a bit historical in that it will pose a number of linkage
mechanisms in increasing order of complexity and flexibility.
We
begin with a simple mechanism based on early CDC–6400 FORTRAN compilers.
We
continue with a slightly more complex mechanism as found on
later CDC–6400 FORTRN compilers.
We
end with a general mechanism that allows the use of recursion.
This is the mechanism used by the Boz–5.
Pass–By–Value
and Pass–By–Reference
Modern
high–level language compilers support a number of mechanisms for passing
arguments to subroutines and functions.
Two
of the most common mechanisms are:
1. Pass
by value, and
2. Pass
by reference.
In
the pass–by–value mechanism, the value of the argument is passed to the
subroutine.
In
the pass–by–reference, it is the address of the argument that is passed to the
subroutine, which can then modify the value and return the new value.
At
this stage in the development of the Boz–5, I have decided not to worry about
the mechanism of argument passing.
The
requirement is that a 32–bit word (value or address) be passed for each
argument, and that the compiler can have a consistent mechanism for handling
the arguments.
Argument
Passing: Version 1
(Based on Early CDC–6400 FORTRAN)
Pass
the arguments in the general–purpose registers.
Register
%R0 continues to be defined as the constant 0.
Registers
%R1 through%R6 are used to pass six arguments.
Register
%R7 is used to return the value of a function.
This
is a very efficient mechanism for passing arguments.
The
problem arises when one wants more than six arguments to be passed.
It
is likely that the desire to use data plotting routines, such as those marketed
by
Calcomp for use with its flat–bed plotters, drove the change from this method.
Argument
Passing: Version 2
(Based on Later CDC–6400 FORTRAN)
In
this design, only two registers would be reserved for subroutine linkage.
%R1 This
points to a memory block containing the number of arguments
and an entry
(value or address) for each of the arguments.
%R7 This
will be reserved for the return value of a function.
This
method allows for passing a large number of arguments.
This
method can be generalized to be compatible with the modern stack–based
protocols.
Calling
Context for the JSR
In
order to understand the full subroutine calling mechanism, we must first
understand its context. We begin with
the situation just before the JSR completes execution.
In
this instruction, we say that EA represents the address of the subroutine to be
called.
The
last step in the execution of the JSR is updating the PC to equal this EA.
Prior
to that last step, the PC is pointing to the instruction immediately following
the JSR.
The
execution of the JSR involves three tasks:
1. Computing
the value of the Effective Address (EA).
2. Storing
the current value of the Program Counter (PC)
so that it can be
retrieved when the subroutine returns.
3. Setting
the PC = EA.
The
effective address will be the address of the first instruction in the
subroutine.
A Simple
Mechanism for Return
(How CDC–6400 FORTRAN did it)
The
simplest method for storing the return address is to store it in the subroutine
itself.
This
mechanism allocates the first word of the subroutine to store the return
address.
If
the subroutine is at address Z in a word–addressable machine such as the Boz–5,
then
Address Z holds the return address.
Address (Z + 1) holds the first executable instruction of the subroutine.
BR *Z An indirect jump on Z is the last instruction of the
subroutine.
Since
Z holds the return address, this affects the return.
This
is a very efficient mechanism.
The
difficulty is that it cannot support recursion.
Example:
Non–Recursive Call
Suppose
the following instructions
100 JSR
200
101 Next
Instruction
200 Holder for
Return Address
201 First
Instruction
Last BR
*200
After
the subroutine call, we would have
100 JSR
200
101 Next
Instruction
200 101
201 First
Instruction
Last BR
*200
The
BR*200 would cause a branch to address 101, thus causing a proper return.
Example 2:
Using This Mechanism Recursively
Suppose
a five instruction subroutine at address 200.
Address
200 holds the return address and addresses 201 – 205 hold the code.
This
subroutine contains a single recursive call to itself that will be executed
once.
Called from First
Recursive First
address 100 Call Return
200 101 200 204 200 204
201 Inst 1 201 Inst 1 201 Inst 1
202 Inst 2 202 Inst 2 202 Inst 2
203 JSR 200 203 JSR
200 203 JSR 200
204 Inst 4 204 Inst 4 204 Inst
4
205 BR * 200 205 BR
* 200 205 BR * 200
Note that the original return
address has been overwritten.
As
long as the subroutine is returning to itself, there is no difficulty.
It will never return to the
original calling routine.
Use a Stack
to Hold Return Addresses
With
the code above, we assume that a stack holds the return addresses.
The
subroutine calls itself SP ® 204 ® 101
First
return SP
® 101
The
subroutine returns to itself.
Second return
The
subroutine returns to the main program.
Our
design will use the following convention.
JSR will
push the return address to the stack.
RET will
pop the return address from the stack.
Implementation
of the Stack Operations
Arbitrarily,
I have decided that the stack grows toward more positive addresses.
Given
this we have two options for implementing PUSH,
each giving rise to a unique implementation of POP.
Option PUSH X POP
Y
1 M[SP] = X SP = SP – 1 // Post–increment on PUSH
SP
= SP + 1 Y = M[SP]
2 SP = SP + 1 Y = M[SP] // Pre–increment on PUSH
M[SP] = X SP
= SP – 1
The constraints on memory
access dictate the first option.
Post–increment on PUSH must be paired
with pre–decrement on POP.
The
operation M[SP] = X corresponds to a memory
write. The latest time at which
this can be done is (E, T2), due to the requirement of a wait cycle before (F,
T0).
If (E,
T2) corresponds to M[SP] = X,
then (E, T3) can
correspond to SP = SP + 1. This does not
affect memory.
Control
Signals for the JSR
Here are the control signals for the complete JSR.
JSR Op-Code = 01110 (Hexadecimal 0x0E)
F, T0: PC ® B1, tra1, B3 ® MAR, READ. //
MAR ¬ (PC)
F, T1: PC
® B1, 1 ® B2, add, B3 ® PC. // PC ¬ (PC) + 1
F, T2: MBR
® B2, tra2, B3 ® IR. //
IR ¬ (MBR)
F, T3: IR
® B1, R ® B2, add,
B3 ® MAR. // Do the indexing.
D, T0: READ.
D, T1: WAIT.
D, T2: MBR
® B2, tra2, B3 ® MAR. // MAR
¬ (MBR)
D, T3: WAIT.
// At
this point, the MAR has the target
address for the subroutine.
// the SP points to the top of the stack.
// the PC contains the return address.
E, T0: PC
® B1, tra1, B3 ® MBR. // Put
return address in MBR
E, T1: MAR
® B1, tra1, B3 ® PC. // Set up for
jump to target.
E, T2: SP
® B1, tra1, B3 ® MAR, WRITE. // Put return
address on stack.
E, T3: SP
® B1, 1 ® B2, add,
B3 ® SP. // Bump SP
Analysis of
Execute Phase of JSR
The goals of JSR are 1) to get the subroutine address into the PC,
and
2) to store the old
value of the PC on the stack,
so
that it can be used for the return.
In order to place the PC on the stack, we must copy PC
® MBR and SP ® MAR.
But note that the MAR contains the address that must
go into the PC.
It cannot be overwritten by the SP until the PC is updated.
E, T0: PC ® B1, tra1,
B3 ® MBR. // Place the old PC into the
MBR
This
saves the old value of the PC into the MBR, from whence it
will be written onto the
stack in (E, T2). This will be the
return address.
E, T1: MAR ® B1, tra1,
B3 ® PC. //
Set up for jump to target.
With the old value of the
PC saved, we can now place the subroutine
address into the PC. Placing an address in the PC causes the
instruction
at that address to be
executed next; the subroutine is started.
E, T2: SP ® B1, tra1,
B3 ® MAR, WRITE. // Put return address on stack.
The stack pointer is used
to address memory and store the old value of
the PC, already stored in
the MBR.
E, T3: SP ® B1, 1 ® B2, add,
B3 ® SP. // The stack
pointer is incremented.
Control
Signals for the RET
This instruction just pops an address from the stack
and places it into the PC.
RET Op-Code = 01010 (Hexadecimal 0x0A)
F, T0: PC ® B1, tra1, B3 ® MAR, READ. //
MAR ¬ (PC)
F, T1: PC
® B1, 1 ® B2, add, B3 ® PC. //
PC ¬ (PC) + 1
F, T2: MBR
® B2, tra2, B3 ® IR. //
IR ¬ (MBR)
F, T3: WAIT
E, T0: SP ® B1, – 1 ® B2, add, B3 ® SP. // Decrement the
SP
E, T1: SP
® B1, tra1, B3 ® MAR, READ. //
Get the return address
E, T2: WAIT.
E, T3: MBR
® B2, tra2, B3 ® PC. // Put return address into PC