Notes On Subroutines: Address. This Is Not Difficult To Implement, Since The Address of The Next Sequential
Notes On Subroutines: Address. This Is Not Difficult To Implement, Since The Address of The Next Sequential
201 Page 1 of 9
Notes on Subroutines
Programs are often required to perform the same operation many times. For example,
applications involving integers are often required to display integer values. The code
fragment that performs an operation must be executed each time the operation is to be
performed. One way to achieve this would be to duplicate the code fragment at each point
it is needed. This could work, but might make the program very large. A better solution
might be to load one copy of the code fragment into memory as a subroutine, and to
provide instructions that allow a program to invoke (i.e. transfer control to) the
subroutine whenever the operation is to be performed. At first glance, a simple JMP to
the subroutine might seem appropriate; however, the JMP instruction does not include
any mechanism to allow control to be returned to the point in the program from which the
subroutine was invoked. Subroutines are such a widely used programming concept, that
processors include special instructions to support the implementation of subroutines.
The p86 processor includes the CALL (invoke) and RET (return) instructions to support
subroutines. The execution of the instructions relies on the presence of a stack to hold the
return address while the subroutine is executing. Programs wishing to use subroutines
must be sure that a stack has been set up using the SP register.
The CALL instruction is similar to the JMP instruction, except that the address of the
instruction that follows the CALL instruction is pushed onto the stack as part of the
CALL execution. The value that is pushed onto the stack is referred to as the return
address. This is not difficult to implement, since the address of the next sequential
instruction (i.e. the return address) is present in the IP register immediately after fetching
the CALL instruction. The CALL instruction uses relative addressing (as does the JMP
instruction). The execution of the CALL instruction can be summarized as:
PUSH IP
JMP target
where: target is the relative offset from the instruction after the CALL to the first
instruction of the subroutine.
The RET instruction returns execution control to the instruction immediately after the
CALL instruction that invoked the subroutine. This is accomplished by popping the
return address off of the stack and into the IP register. After executing the RET
instruction, the next instruction to be executed will be fetched from the return address.
The RET instruction will only succeed in accomplishing this objective if the return
address is the value on the top of the stack at the time the RET is executed. Subroutines
must be careful to ensure that this condition is satisfied!
There are many details associated with subroutines and parameter passing, and there may
be several possible ways to deal with the details. By following programming POLICY,
the details are handled consistently, which results in programs that are easier to construct,
understand and modify.
Parameters
Parameters allow information to be communicated between the caller (i.e. the code that is
invoking the subroutine) and the callee (i.e. the subroutine). Parameters allow subroutines
to implement a wider range of operations, which typically results in fewer subroutines
being needed in a program. Furthermore, subroutines that have been generalized are
often more easily re-used in other applications.
For example, a DISPLAY subroutine might be written to display 16-bit integer values in
signed ASCII-decimal form. A C++ prototype for the subroutine would be written:
void DISPLAY ( int X )
The intention of the subroutine is to display the supplied integer value (represented by the
parameter X). An invocation of the subroutine must supply a value for X. When
referring to the general description of the subroutine, X is a parameter. When
considering a specific invocation of the subroutine, the particular value supplied for X is
referred to as the argument.
Subroutines are based on assumptions about the purpose of the subroutine. For example,
the DISPLAY subroutine described above assumes that the value is to be displayed in
signed decimal form. The subroutine might be further generalized by adding an
additional parameter that is used to specify the display form. For example, suppose that
the prototype for the DISPLAY function was augmented to become:
void DISPLAY ( word X, int format )
where: format is an integer value used to represent how the value should be
displayed
format = 0: signed decimal
format = 1: unsigned decimal
format = 2: hexadecimal
format = 3: binary
There are several ways in which parameter information can be communicated (i.e.
passed) from the caller to the subroutine:
Global Variables: A global variable is one that is shared by both the caller and the
subroutine. The caller communicates information to the subroutine by writing a value
into the shared variable before calling the subroutine. The subroutine obtains the
information by reading the variable. The variable is referred to as being "global" because
the variable's static name (i.e. address) is known both to the caller and the subroutine.
The use of global variables is not (generally) considered to be good programming
practice; however, we will see in the 94.203 course that there are important cases where
global variables are the only practical mechanism for communicating between program
components.
Registers : Registers may be used to pass information. The caller places arguments in
relevant registers before calling the subroutine, and the subroutine assumes that the
relevant registers contain arguments. There are two limitations to this approach: First,
there are a limited number of registers, and this in turn limits the number of parameters
that may be passed via this mechanism. Second, assembly-level programming involves
the use of the registers to accomplish program objectives, and therefore, parameters
passed in registers must often be saved to memory (perhaps the stack?) to allow the
registers to be used by the subroutine.
Stack: Parameters may be passed on the stack. The caller must push arguments onto the
stack prior to calling the subroutine, and the subroutine must retrieve the arguments from
the stack.
In this course, we will use the following POLICY: parameters will be passed on the
stack. (Passing parameters on the stack is discussed in much more detail below!)
In addition to the issue of the physical location (global, register, stack) used to pass
parameters, there is also the issue of whether each parameter is passed by value or by
reference. When passing by value, a copy of the relevant information is passed. When
passing by reference, a reference to a variable containing the relevant information is
passed. Passing parameters by reference is often implemented by passing the address of
the variable.
When passing parameters on the stack, the caller must push the relevant arguments before
calling the subroutine.
An immediate question that arises is the order in which the parameters are pushed onto
the stack. In this course, we will use the following POLICY: each subroutine will be
documented by (at least) a C++-like prototype of the subroutine, and the parameters will
be pushed onto the stack in right-to-left order of appearance in the prototype. For
example, recall the prototype:
void SQUARE( int X, int & XSquared )
The XSquared parameter is the rightmost, and the X parameter is the leftmost (as they
appear in the prototype); therefore, when calling the SQUARE subroutine, the XSquared
argument would be pushed onto the stack before pushing the X argument.
SP return address
arguments
A simple way for the subroutine to access the arguments would be to use register indirect
addressing based on the SP value; however, the SP register is not one of the registers
permitted for this addressing mode (recall that only BX, BP, SI and DI are permitted).
For reasons that will become clear in 94.203, the BP register is used to access the
arguments. To use the BP register to access the arguments, the value of SP must be
copied to BP.
Since BP might contain a value important to the caller's objective, the subroutine's use of
BP raises the question of the responsibility for protecting register values. One solution is
to have the caller save all important register values before calling the subroutine, and then
restore the values after the subroutine returns. Another solution is to have the subroutine
save the values of all registers that it uses, and then restore the values just before
returning to the caller. In this course, we will adopt the POLICY in which the subroutine
is responsible for saving and restoring register values (typically on the stack). Since the
subroutine uses BP, it must save BP before copying the SP value. Therefore, all
subroutines that have parameters should start with the following standard entry code:
PUSH BP
MOV BP, SP
save other registers used
The standard entry code sets up the BP register to access the parameters. Following the
execution of the standard entry code, the stack frame associated with the subroutine
invocation has the following format:
SP saved reg’s
BP old BP
stack frame
return address
arguments
Immediately after the execution of the standard entry code, the stack frame would look
like:
BP old BP
BP + 2 return address
BP + 4 value of X
BP + 6 address of XSquared
SQUARE:
; standard entry code
PUSH BP
MOV BP, SP
PUSH AX ; AX and DX are used in the multiply
PUSH DX
PUSH BX ; BX is used for register indirect access to XSquared
RET
The above implementation does not account for cases where the result overflows 16-bit
integer capacity. A variation that accounts for this is considered later.
The responsibility for removing arguments from the stack has not yet been considered.
The subroutine might remove the arguments before returning, or the caller might remove
them after control is returned from the subroutine. In this course, we will adopt the
POLICY of having the caller remove the arguments after the return from the subroutine.
A program fragment wishing to call the SQUARE subroutine must first push appropriate
arguments onto the stack, invoke SQUARE, and then remove the arguments from the
stack. Suppose that a caller has an integer variable Y, and it is desired to store Y2 in the
variable Z. The fragment associated with the call might look like:
In a high-level language like C++, there are two mechanisms that can be used to
communicate information from the subroutine back to the caller. One mechanism is to
accept a reference parameter and to modify the value stored in the referenced variable (as
was done in the SQUARE example above). The second mechanism is to define the
subroutine as a function that returns a value. The SQUARE subroutine prototype begins
with "void" indicating that the subroutine is not a function and does not return a value
using the second mechanism. To implement the function return-value mechanism, high-
level languages often use a dedicated register to pass the return-value back to the caller.
In this course, we will adopt the POLICY of using the:
AL register to return 8-bit values from functions, and the
AX register to return 16-bit values from functions.
To illustrate returning a value from a function, recall that the SQUARE subroutine did
not account for cases where the squaring operation might exceed the capacity of 16-bit
(signed, 2's complement) integers. Suppose that the prototype were changed to return an
integer value, such that returning the value = 0 indicates failure, while returning any non-
zero value indicates success (note: C/C++ assume that boolean values are implemented
with 0 representing "false", while any non-zero value represents "true"). The revised
prototype might be:
int SQUARE_OK( int X, int & XSquared )
SQUARE_OK:
; standard entry code
PUSH BP
MOV BP, SP
; NEW CODE HERE! Check for a valid (16-bit) integer result and
; return appropriate value in AX
;answer is OK!
MOV AX, 1 ; non-zero return-value indicates success
JMP Restore
RET
SUMMARY
This discussion has covered a lot of material. Some of the highlights are briefly
summarized below:
The CALL and RET instructions use the stack to save/restore the return address.
POLICY: C++-like prototypes are used to document the function name, whether there is
a return value, and the types of the parameters.
POLICY: Each subroutine will be responsible for saving and restoring the values of all
registers that it uses.
POLICY: Parameters are passed on the stack. When setting up for a call, the caller
pushes them in right-to-left order of appearance in the prototype. Subroutines will use
standard entry code (before saving other registers) to set up the BP register to access
parameters.
POLICY: The caller is responsible for removing arguments from the stack after the
return from a subroutine invocation.