Chapter Three Intermediate Procedures
3.1 Chapter Overview
This chapter picks up where the chapter "Introduction to Procedures" in Volume Three leaves off. That chapter presented a high level view of procedures, parameters, and local variables; this chapter takes a look at some of the low-level implementation details. This chapter begins by discussing the CALL instruction and how it affects the stack. Then it discusses activation records and how a program passes parameters to a procedure and how that procedure maintains local (automatic) variables. Next, this chapter presents an in-depth discussion of pass by value and pass by reference parameters. This chapter concludes by discussing procedure variables, procedural parameters, iterators, and the FOREACH..ENDFOR loop.
3.2 Procedures and the CALL Instruction
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 routine:call stdout.newln;
stdout.newln prints a carriage return/line feed sequence to the video display and returns control to the instruction immediately following the "call stdout.newln;" instruction.
The HLA language lets you call procedures using a high level language syntax. Specifically, you may call a procedure by simply specifying the procedure's name and (in the case of stdout.newln) an empty parameter list. That is, the following is completely equivalent to "call stdout.newln":stdout.newln();
The 80x86 CALL instruction does two things. First, it pushes the address of the instruction immediately following the CALL onto the stack; then it transfers control to the address of the specified procedure. The value that CALL pushes onto the stack is known as the return address. When the procedure wants to return to the caller and continue execution with the first statement following the CALL instruction, the procedure simply pops the return address off the stack and jumps (indirectly) to that address. Most procedures return to their caller by executing a RET (return) instruction. The RET instruction pops a return address off the stack and transfers control indirectly to the address it pops off the stack.
By default, the HLA compiler automatically places a RET instruction (along with a few other instructions) at the end of each HLA procedure you write. This is why you haven't had to explicitly use the RET instruction up to this point. To disable the default code generation in an HLA procedure, specify the following options when declaring your procedures:procedure ProcName; @noframe; @nodisplay; begin ProcName; . . . end ProcName;
The @NOFRAME and @NODISPLAY clauses are examples of procedure options. HLA procedures support several such options, including RETURNS (See "The HLA RETURNS Option in Procedures" on page 560.), the @NOFRAME, @NODISPLAY, and @NOALIGNSTACKK. You'll see the purpose of @NOALIGNSTACK and a couple of other procedure options a little later in this chapter. These procedure options may appear in any order following the procedure name (and parameters, if any). Note that @NOFRAME and @NODISPLAY (as well as @NOALIGNSTACK) may only appear in an actual procedure declaration. You cannot specify these options in an external procedure prototype.
The @NOFRAME option tells HLA that you don't want the compiler to automatically generate entry and exit code for the procedure. This tells HLA not to automatically generate the RET instruction (along with several other instructions).
The @NODISPLAY option tells HLA that it should not allocate storage in procedure's local variable area for a display. The display is a mechanism you use to access non-local VAR objects in a procedure. Therefore, a display is only necessary if you nest procedures in your programs. This chapter will not consider the display or nested procedures; for more details on the display and nested procedures see the appropriate chapter in Volume Five. Until then, you can safely specify the @NODISPLAY option on all your procedures. Note that you may specify the @NODISPLAY option independently of the @NOFRAME option. Indeed, for all of the procedures appearing in this text up to this point specifying the @NODISPLAY option makes a lot of sense because none of those procedures have actually used the display. Procedures that have the @NODISPLAY option are a tiny bit faster and a tiny bit shorter than those procedures that do not specify this option.
The following is an example of the minimal procedure:procedure minimal; nodisplay; noframe; noalignstk; begin minimal; ret(); end minimal;
If you call this procedure with the CALL instruction, minimal will simply pop the return address off the stack and return back to the caller. You should note that a RET instruction is absolutely necessary when you specify the @NOFRAME procedure option1. If you fail to put the RET instruction in the procedure, the program will not return to the caller upon encountering the "end minimal;" statement. Instead, the program will fall through to whatever code happens to follow the procedure in memory. The following example program demonstrates this problem:program missingRET; #include( "stdlib.hhf" ); // This first procedure has the NOFRAME // option but does not have a RET instruction. procedure firstProc; @noframe; @nodisplay; begin firstProc; stdout.put( "Inside firstProc" nl ); end firstProc; // Because the procedure above does not have a // RET instruction, it will "fall through" to // the following instruction. Note that there // is no call to this procedure anywhere in // this program. procedure secondProc; @noframe; @nodisplay; begin secondProc; stdout.put( "Inside secondProc" nl ); ret(); end secondProc; begin missingRET; // Call the procedure that doesn't have // a RET instruction. call firstProc; end missingRET; Program 3.1 Effect of Missing RET Instruction in a Procedure
Although this behavior might be desirable in certain rare circumstances, it usually represents a defect in most programs. Therefore, if you specify the @NOFRAME option, always remember to explicitly return from the procedure using the RET instruction.
3.3 Procedures and the Stack
Since procedures use the stack to hold the return address, you must exercise caution when pushing and popping data within a procedure. Consider the following simple (and defective) procedure:procedure MessedUp; noframe; nodisplay; begin MessedUp; push( eax ); ret(); end MessedUp;
At the point the program encounters the RET instruction, the 80x86 stack takes the form shown in Figure 3.1:
Figure 3.1 Stack Contents Before RET in "MessedUp" Procedure
The RET instruction isn't aware that the value on the top of stack is not a valid address. It simply pops whatever value is on the top of the stack and jumps to that location. In this example, the top of stack contains the saved EAX value. Since it is very unlikely that EAX contains the proper return address (indeed, there is about a one in four billion chance it is correct), this program will probably crash or exhibit some other undefined behavior. Therefore, you must take care when pushing data onto the stack within a procedure that you properly pop that data prior to returning from the procedure.
- Note: if you do not specify the @NOFRAME option when writing a procedure, HLA automatically generates code at the beginning of the procedure that pushes some data onto the stack. Therefore, unless you understand exactly what is going on and you've taken care of this data HLA pushes on the stack, you should never execute the bare RET instruction inside a procedure that does not have the @NOFRAME option. Doing so will attempt to return to the location specified by this data (which is not a return address) rather than properly returning to the caller. In procedures that do not have the @NOFRAME option, use the EXIT or EXITIF statements to return from the procedure (See "BEGIN..EXIT..EXITIF..END" on page 740.).
Popping extra data off the stack prior to executing the RET statement can also create havoc in your programs. Consider the following defective procedure:procedure MessedUpToo; noframe; nodisplay; begin MessedUpToo; pop( eax ); ret(); end MessedUpToo;
Upon reaching the RET instruction in this procedure, the 80x86 stack looks something like that shown in Figure 3.2:
Figure 3.2 Stack Contents Before RET in MessedUpToo
Once again, the RET instruction blindly pops whatever data happens to be on the top of the stack and attempts to return to that address. Unlike the previous example, where it was very unlikely that the top of stack contained a valid return address (since it contained the value in EAX), there is a small possibility that the top of stack in this example actually does contain a return address. However, this will not be the proper return address for the MessedUpToo procedure; instead, it will be the return address for the procedure that called MessUpToo. To understand the effect of this code, consider the following program:program extraPop; #include( "stdlib.hhf" ); // Note that the following procedure pops // excess data off the stack (in this case, // it pops messedUpToo's return address). procedure messedUpToo; @noframe; @nodisplay; begin messedUpToo; stdout.put( "Entered messedUpToo" nl ); pop( eax ); ret(); end messedUpToo; procedure callsMU2; @noframe; @nodisplay; begin callsMU2; stdout.put( "calling messedUpToo" nl ); messedUpToo(); // Because messedUpToo pops extra data // off the stack, the following code // never executes (since the data popped // off the stack is the return address that // points at the following code. stdout.put( "Returned from messedUpToo" nl ); ret(); end callsMU2; begin extraPop; stdout.put( "Calling callsMU2" nl ); callsMU2(); stdout.put( "Returned from callsMU2" nl ); end extraPop; Program 3.2 Effect of Popping Too Much Data Off the Stack
Since a valid return address is sitting on the top of the stack, you might think that this program will actually work (properly). However, note that when returning from the MessedUpToo procedure, this code returns directly to the main program rather than to the proper return address in the EndSkipped procedure. Therefore, all code in the callsMU2 procedure that follows the call to MessedUpToo does not execute. When reading the source code, it may be very difficult to figure out why those statements are not executing since they immediately follow the call to the MessUpToo procedure. It isn't clear, unless you look very closely, that the program is popping an extra return address off the stack and, therefore, doesn't return back to callsMU2 but, rather, returns directly to whomever calls callsMU2. Of course, in this example it's fairly easy to see what is going on (because this example is a demonstration of this problem). In real programs, however, determining that a procedure has accidentally popped too much data off the stack can be much more difficult. Therefore, you should always be careful about pushing and popping data in a procedure. You should always verify that there is a one-to-one relationship between the pushes in your procedures and the corresponding pops.
1Strictly speaking, this isn't true. But some mechanism that pops the return address off the stack and jumps to the return address is necessary in the procedure's body.