Chapter Eight Introduction to Procedures
8.1 Chapter Overview
In a procedural programming language the basic unit of code is the procedure. A procedure is a set of instructions that compute some value or take some action (such as printing or reading a character value). The definition of a procedure is very similar to the definition of an algorithm. A procedure is a set of rules to follow which, if they conclude, produce some result. An algorithm is also such a sequence, but an algorithm is guaranteed to terminate whereas a procedure offers no such guarantee.
This chapter discusses how HLA implements procedures. This is actually the first of three chapters on this subject in this text. This chapter presents HLA procedures from a high level language perspective. A later chapter, Intermediate Procedures, discusses procedures at the machine language level. A whole volume in this sequence, Advanced Procedures, covers advanced programming topics of interest to the very serious assembly language programmer. This chapter, however, provides the foundation for all that follows.
Most procedural programming languages implement procedures using the call/return mechanism. That is, some code calls a procedure, the procedure does its thing, and then the procedure returns to the caller. The call and return instructions provide the 80x86's procedure invocation mechanism. The calling code calls a procedure with the CALL instruction, the procedure returns to the caller with the RET instruction. For example, the following 80x86 instruction calls the HLA Standard Library stdout.newln routine1:call stdout.newln;
The stdout.newln procedure prints a newline sequence to the console device and returns control to the instruction immediately following the "call stdout.newln;" instruction.
Alas, the HLA Standard Library does not supply all the routines you will need. Most of the time you'll have to write your own procedures. To do this, you will use HLA's procedure declaration facilities. A basic HLA procedure declaration takes the following form:procedure ProcName; << Local declarations >> begin ProcName; << procedure statements >> end ProcName;
Procedure declarations appear in the declaration section of your program. That is, anywhere you can put a STATIC, CONST, TYPE, or other declaration section, you may place a procedure declaration. In the syntax example above, ProcName represents the name of the procedure you wish to define. This can be any valid HLA identifier. Whatever identifier follows the PROCEDURE reserved word must also follow the BEGIN and END reserved words in the procedure. As you've probably noticed, a procedure declaration looks a whole lot like an HLA program. In fact, the only difference (so far) is the use of the PROCEDURE reserved word rather than the PROGRAM reserved word.
Here is a concrete example of an HLA procedure declaration. This procedure stores zeros into the 256 double words that EBX points at upon entry into the procedure:procedure zeroBytes; begin zeroBytes; mov( 0, eax ); mov( 256, ecx ); repeat mov( eax, [ebx] ); add( 4, ebx ); dec( ecx ); until( @z ); // That is, until ECX=0. end zeroBytes;
You can use the 80x86 CALL instruction to call this procedure. When, during program execution, the code falls into the "end zeroBytes;" statement, the procedure returns to whomever called it and begins executing the first instruction beyond the CALL instruction. The following program provides an example of a call to the zeroBytes routine:program zeroBytesDemo; #include( "stdlib.hhf" ); procedure zeroBytes; begin zeroBytes; mov( 0, eax ); mov( 256, ecx ); repeat mov( eax, [ebx] ); // Zero out current dword. add( 4, ebx ); // Point ebx at next dword. dec( ecx ); // Count off 256 dwords. until( ecx = 0 ); // Repeat for 256 dwords. end zeroBytes; static dwArray: dword; begin zeroBytesDemo; lea( ebx, dwArray ); call zeroBytes; end zeroBytesDemo; Program 8.1 Example of a Simple Procedure
As you may have noticed when calling HLA Standard Library procedures, you don't always need to use the CALL instruction to call HLA procedures. There is nothing special about the HLA Standard Library procedures versus your own procedures. Although the formal 80x86 mechanism for calling procedures is to use the CALL instruction, HLA provides a HLL extension that lets you call a procedure by simply specifying that procedure's name followed by an empty set of parentheses2. For example, either of the following statements will call the HLA Standard Library stdout.newln procedure:call stdout.newln; stdout.newln();
Likewise, either of the following statements will call the zeroBytes procedure in Program 8.1:call zeroBytes; zeroBytes();
The choice of calling mechanism is strictly up to you. Most people, however, find the HLL syntax easier to read.
8.3 Saving the State of the Machine
Take a look at the following program:program nonWorkingProgram; #include( "stdlib.hhf" ); procedure PrintSpaces; begin PrintSpaces; mov( 40, ecx ); repeat stdout.put( ` ` ); // Print 1 of 40 spaces. dec( ecx ); // Count off 40 spaces. until( ecx = 0 ); end PrintSpaces; begin nonWorkingProgram; mov( 20, ecx ); repeat PrintSpaces(); stdout.put( `*', nl ); dec( ecx ); until( ecx = 0 ); end nonWorkingProgram; Program 8.2 Program with an Unintended Infinite Loop
This section of code attempts to print 20 lines of 40 spaces and an asterisk. Unfortunately, there is a subtle bug that causes it to print 40 spaces per line and an asterisk in an infinite loop. The main program uses the REPEAT..UNTIL loop to call PrintSpaces 20 times. PrintSpaces uses ECX to count off the 40 spaces it prints. PrintSpaces returns with ECX containing zero. The main program then prints an asterisk, a newline, decrements ECX, and then repeats because ECX isn't zero (it will always contain $FFFF_FFFF at this point).
The problem here is that the PrintSpaces subroutine doesn't preserve the ECX register. Preserving a register means you save it upon entry into the subroutine and restore it before leaving. Had the PrintSpaces subroutine preserved the contents of the ECX register, the program above would have functioned properly.
Use the 80x86's PUSH and POP instructions to preserve register values while you need to use them for something else. Consider the following code for PrintSpaces:procedure PrintSpaces; begin PrintSpaces; push( eax ); push( ecx ); mov( 40, ecx ); repeat stdout.put( ' ' ); // Print 1 of 40 spaces. dec( ecx ); // Count off 40 spaces. until( ecx = 0 ); pop( ecx ); pop( eax ); end PrintSpaces;
Note that PrintSpaces saves and restores EAX and ECX (since this procedure modifies these registers). Also, note that this code pops the registers off the stack in the reverse order that it pushed them. The last-in, first-out, operation of the stack imposes this ordering.
Either the caller (the code containing the CALL instruction) or the callee (the subroutine) can take responsibility for preserving the registers. In the example above, the callee preserved the registers. The following example shows what this code might look like if the caller preserves the registers:program callerPreservation; #include( "stdlib.hhf" ); procedure PrintSpaces; begin PrintSpaces; mov( 40, ecx ); repeat stdout.put( ` ` ); // Print 1 of 40 spaces. dec( ecx ); // Count off 40 spaces. until( ecx = 0 ); end PrintSpaces; begin callerPreservation; mov( 20, ecx ); repeat push( eax ); push( ecx ); PrintSpaces(); pop( ecx ); pop( eax ); stdout.put( `*', nl ); dec( ecx ); until( ecx = 0 ); end callerPreservation; Program 8.3 Demonstration of Caller Register Preservation
There are two advantages to callee preservation: space and maintainability. If the callee preserves all affected registers, then there is only one copy of the PUSH and POP instructions, those the procedure contains. If the caller saves the values in the registers, the program needs a set of PUSH and POP instructions around every call. Not only does this make your programs longer, it also makes them harder to maintain. Remembering which registers to push and pop on each procedure call is not something easily done.
On the other hand, a subroutine may unnecessarily preserve some registers if it preserves all the registers it modifies. In the examples above, the code needn't save EAX. Although PrintSpaces changes AL, this won't affect the program's operation. If the caller is preserving the registers, it doesn't have to save registers it doesn't care about:program callerPreservation2; #include( "stdlib.hhf" ); procedure PrintSpaces; begin PrintSpaces; mov( 40, ecx ); repeat stdout.put( ` ` ); // Print 1 of 40 spaces. dec( ecx ); // Count off 40 spaces. until( ecx = 0 ); end PrintSpaces; begin callerPreservation2; mov( 10, ecx ); repeat push( ecx ); PrintSpaces(); pop( ecx ); stdout.put( `*', nl ); dec( ecx ); until( ecx = 0 ); mov( 5, ebx ); while( ebx > 0 ) do PrintSpaces(); stdout.put( ebx, nl ); dec( ebx ); endwhile; mov( 110, ecx ); for( mov( 0, eax ); eax < 7; inc( eax )) do PrintSpaces(); stdout.put( eax, " ", ecx, nl ); dec( ecx ); endfor; end callerPreservation2; Program 8.4 Demonstrating that Caller Preservation Need not Save All Registers
This example provides three different cases. The first loop (REPEAT..UNTIL) only preserves the ECX register. Modifying the AL register won't affect the operation of this loop. Immediately after the first loop, this code calls PrintSpaces again in the WHILE loop. However, this code doesn't save EAX or ECX because it doesn't care if PrintSpaces changes them. Since the final loop (FOR) uses EAX and ECX, it saves them both.
One big problem with having the caller preserve registers is that your program may change. You may modify the calling code or the procedure so that they use additional registers. Such changes, of course, may change the set of registers that you must preserve. Worse still, if the modification is in the subroutine itself, you will need to locate every call to the routine and verify that the subroutine does not change any registers the calling code uses.
Preserving registers isn't all there is to preserving the environment. You can also push and pop variables and other values that a subroutine might change. Since the 80x86 allows you to push and pop memory locations, you can easily preserve these values as well.
8.4 Prematurely Returning from a Procedure
The HLA EXIT and EXITIF statements let you return from a procedure without having to fall into the corresponding END statement in the procedure. These statements behave a whole lot like the BREAK and BREAKIF statements for loops, except they transfer control to the bottom of the procedure rather than out of the current loop. These statements are quite useful in many cases.
The syntax for these two statements is the following:exit procedurename; exitif( boolean_expression ) procedurename;
The procedurename operand is the name of the procedure you wish to exit. If you specify the name of your main program, the EXIT and EXITIF statements will terminate program execution (even if you're currently inside a procedure rather than the body of the main program.
The EXIT statement immediately transfers control out of the specified procedure or program. The conditional exit, EXITIF, statement first tests the boolean expression and exits if the result is true. It is semantically equivalent to the following:if( boolean_expression ) then exit procedurename; endif;
Although the EXIT and EXITIF statements are invaluable in many cases, you should try to avoid using them without careful consideration. If a simple IF statement will let you skip the rest of the code in your procedure, by all means use the IF statement. Procedures that contain lots of EXIT and EXITIF statements will be harder to read, understand, and maintain that procedures without these statements (after all, the EXIT and EXITIF statements are really nothing more than GOTO statements and you've probably heard already about the problems with GOTOs). EXIT and EXITIF are convenient when you got to return from a procedure inside a sequence of nested control structures and slapping an IF..ENDIF around the remaining code in the procedure is not possible.
1Normally you would call newln using the "newln();" statement, but the CALL instruction works as well.
2This assumes that the procedure does not have any parameters.