<<

Course Notes 1.2 Fall 2001 94.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!

Copyright ã Trevor W. Pearce, October 11, 2000 For use in the 94.201 course only – not for distribution outside of the Department of Systems and Engineering, Carleton University, Ottawa, Canada Course Notes 1.2 Fall 2001 94.201 Page 2 of 9

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 ++ 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

The augmented DISPLAY function is a further generalization of the previous DISPLAY function. Signed decimal format can still be displayed by passing the format value = 0; but the augment function also handles 3 additional display formats.

There are several ways in which parameter information can be communicated (i.e. passed) from the caller to the subroutine:

Copyright ã Trevor W. Pearce, October 11, 2000 For use in the 94.201 course only – not for distribution outside of the Department of Systems and Computer Engineering, Carleton University, Ottawa, Canada Course Notes 1.2 Fall 2001 94.201 Page 3 of 9

Global Variables: A 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.

Whether a parameter is passed by value or reference can be specified in the prototype used to describe the subroutine. As a POLICY in this course, let's assume that the default is to pass by value, and that reference parameters will be prefixed with "&". For example, consider the prototype: void SQUARE( int X, int & XSquared ) The purpose of the SQUARE subroutine is to calculate X*X, and to return the result in the variable XSquared. The parameter X is passed by value, and the parameter XSquared is passed by reference. When setting up to call the subroutine, the caller must supply an integer value as the X argument, and a reference to (i.e. the address of) an integer variable as the XSquared argument.

Copyright ã Trevor W. Pearce, October 11, 2000 For use in the 94.201 course only – not for distribution outside of the Department of Systems and Computer Engineering, Carleton University, Ottawa, Canada Course Notes 1.2 Fall 2001 94.201 Page 4 of 9

Parameter Passing on the Stack

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.

Following a call to a subroutine, the stack has the following configuration:

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

Copyright ã Trevor W. Pearce, October 11, 2000 For use in the 94.201 course only – not for distribution outside of the Department of Systems and Computer Engineering, Carleton University, Ottawa, Canada Course Notes 1.2 Fall 2001 94.201 Page 5 of 9

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

To illustrate passing parameters by value and by reference, consider an implementation of the SQUARE subroutine. Recall the prototype: void SQUARE( int X, int & XSquared )

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

One coding solution might be:

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

Copyright ã Trevor W. Pearce, October 11, 2000 For use in the 94.201 course only – not for distribution outside of the Department of Systems and Computer Engineering, Carleton University, Ottawa, Canada Course Notes 1.2 Fall 2001 94.201 Page 6 of 9

MOV AX, [BP + 4] ; AX := X ; MUL performs unsigned multiply, so need absolute value of X CMP AX, 0 ; if X < 0 then negate X JGE Positive NEG AX

Positive: ; AX now contains the positive magnitude of X ; perform unsigned multiply MUL AX ; DX:AX := AX * AX (16-bit multiply!)

; now save result MOV BX, [BP + 6] ; BX := address of XSquared variable MOV [BX], AX ; [XSquared] := X * X

; now restore registers used ; NB: restore in reverse of order saved! POP BX POP DX POP AX POP BP ; don’t forget BP too!

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:

; push parameters (right-to-left in prototype!) MOV AX, Z ; get address of Z PUSH AX ; & XSquared argument = reference to Z PUSH WORD PTR [Y] ; X argument = value of Y CALL SQUARE ADD SP, 4 ; clear arguments from stack - returns stack to previous form

Copyright ã Trevor W. Pearce, October 11, 2000 For use in the 94.201 course only – not for distribution outside of the Department of Systems and Computer Engineering, Carleton University, Ottawa, Canada Course Notes 1.2 Fall 2001 94.201 Page 7 of 9

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 )

One coding solution might be:

SQUARE_OK: ; standard entry code PUSH BP MOV BP, SP

; NOTE: previous version saved AX here – no need to save AX since ; this version returns a value in AX!

PUSH DX ; DX is used in the multiply PUSH BX ; BX is used for register indirect access to XSquared

MOV AX, [BP + 4] ; AX := X CMP AX, 0 ; if X < 0 then negate X JGE Positive NEG AX

Positive: ; AX now contains the positive magnitude of X MUL AX ; DX:AX := AX * AX (16-bit multiply!)

; now save result MOV BX, [BP + 6] ; BX := address of XSquared variable MOV [BX], AX ; [XSquared] := X * X

Copyright ã Trevor W. Pearce, October 11, 2000 For use in the 94.201 course only – not for distribution outside of the Department of Systems and Computer Engineering, Carleton University, Ottawa, Canada Course Notes 1.2 Fall 2001 94.201 Page 8 of 9

; NEW CODE HERE! Check for a valid (16-bit) integer result and ; return appropriate value in AX

; if DX not = 0, then overflow (result requires more than 16 bits!) CMP DX, 0 JNE OverFlow ; must also check that (signed) AX is a positive integer (can't have a negative ; answer!) CMP AX, 0 JL OverFlow

;answer is OK! MOV AX, 1 ; non-zero return-value indicates success JMP Restore

OverFlow: ;answer not OK MOV AX, 0 ; zero return-value indicates overflow

Restore: ; END OF NEW CODE ; now restore registers used NB: restore in reverse of order saved! POP BX ; IMPORTANT: code did not save and restore AX! POP DX ; restoring original value of AX here would POP BP ; overwrite the return-value!

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: Parameters may be passed by value (assumed as default) or by reference (annotate reference parameters with "&" in prototype). Reference parameters are implemented by passing the referenced variable's address as the argument.

POLICY: Each subroutine will be responsible for saving and restoring the values of all registers that it uses.

Copyright ã Trevor W. Pearce, October 11, 2000 For use in the 94.201 course only – not for distribution outside of the Department of Systems and Computer Engineering, Carleton University, Ottawa, Canada Course Notes 1.2 Fall 2001 94.201 Page 9 of 9

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: if a subroutine is declared to be a function (i.e. it has a return-value), then the AX register will be used to return the return-value argument.

POLICY: The caller is responsible for removing arguments from the stack after the return from a subroutine invocation.

Copyright ã Trevor W. Pearce, October 11, 2000 For use in the 94.201 course only – not for distribution outside of the Department of Systems and Computer Engineering, Carleton University, Ottawa, Canada