3.4 Activation Records
Whenever you call a procedure there is certain information the program associates with that procedure call. The return address is a good example of some information the program maintains for a specific procedure call. Parameters and automatic local variables (i.e., those you declare in the VAR section) are additional examples of information the program maintains for each procedure call. Activation record is the term we'll use to describe the information the program associates with a specific call to a procedure1.
Activation record is an appropriate name for this data structure. The program creates an activation record when calling (activating) a procedure and the data in the structure is organized in a manner identical to records (see "Records" on page 419). Perhaps the only thing unusual about an activation record (when comparing it to a standard record) is that the base address of the record is in the middle of the data structure, so you must access fields of the record at positive and negative offsets.
Construction of an activation record begins in the code that calls a procedure. The caller pushes the parameter data (if any) onto the stack. Then the execution of the CALL instruction pushes the return address onto the stack. At this point, construction of the activation record continues withinin the procedure itself. The procedure pushes registers and other important state information and then makes room in the activation record for local variables. The procedure must also update the EBP register so that it points at the base address of the activation record.
To see what a typical activation record looks like, consider the following HLA procedure declaration:
procedure ARDemo( i:uns32; j:int32; k:dword ); nodisplay; var a:int32; r:real32; c:char; b:boolean; w:word; begin ARDemo; . . . end ARDemo;Whenever an HLA program calls this ARDemo procedure, it begins by pushing the data for the parameters onto the stack. The calling code will push the parameters onto the stack in the order they appear in the parameter list, from left to right. Therefore, the calling code first pushes the value for the i parameter, then it pushes the value for the j parameter, and it finally pushes the data for the k parameter. After pushing the parameters, the program calls the ARDemo procedure. Immediately upon entry into the ARDemo procedure, the stack contains these four items arranged as shown in Figure 3.3
Figure 3.3 Stack Organization Immediately Upon Entry into ARDemo
The first few instructions in ARDemo (note that it does not have the @NOFRAME option) will push the current value of EBP onto the stack and then copy the value of ESP into EBP. Next, the code drops the stack pointer down in memory to make room for the local variables. This produces the stack organization shown in Figure 3.4
Figure 3.4 Activation Record for ARDemo
To access objects in the activation record you must use offsets from the EBP register to the desired object. The two items of immediate interest to you are the parameters and the local variables. You can access the parameters at positive offsets from the EBP register, you can access the local variables at negative offsets from the EBP register as Figure 3.5 shows:
Figure 3.5 Offsets of Objects in the ARDemo Activation Record
Intel specifically reserves the EBP (extended base pointer) for use as a pointer to the base of the activation record. This is why you should never use the EBP register for general calculations. If you arbitrarily change the value in the EBP register you will lose access to the current procedure's parameters and local variables.
3.5 The Standard Entry Sequence
The caller of a procedure is responsible for pushing the parameters onto the stack. Of course, the CALL instruction pushes the return address onto the stack. It is the procedure's responsibility to construct the rest of the activation record. This is typically accomplished by the following "standard entry sequence" code:
push( ebp ); // Save a copy of the old EBP value mov( esp, ebp ); // Get ptr to base of activation record into EBP sub( NumVars, esp ); // Allocate storage for local variables.If the procedure doesn't have any local variables, the third instruction above, "sub( NumVars, esp );" isn't needed. NumVars represents the number of bytes of local variables needed by the procedure. This is a constant that should be an even multiple of four (so the ESP register remains aligned on a double word boundary). If the number of bytes of local variables in the procedure is not an even multiple of four, you should round the value up to the next higher multiple of four before subtracting this constant from ESP. Doing so will slightly increase the amount of storage the procedure uses for local variables but will not otherwise affect the operation of the procedure.
- Warning: if the NumVars constant is not an even multiple of four, subtracting this value from ESP (which, presumably, contains a dword-aligned pointer) will virtually guarantee that all future stack accesses are misaligned since the program almost always pushes and pops dword values. This will have a very negative performance impact on the program. Worse still, many OS API calls will fail if the stack is not dword-aligned upon entry into the operating system. Therefore, you must always ensure that your local variable allocation value is an even multiple of four.
Because of the problems with a misaligned stack, by default HLA will also emit a fourth instruction as part of the standard entry sequence. The HLA compiler actually emits the following standard entry sequence for the ARDemo procedure defined earlier:
push( ebp ); mov( esp, ebp ); sub( 12, esp ); // Make room for ARDemo's local variables. and( $FFFF_FFFC, esp ); // Force dword stack alignment.The AND instruction at the end of this sequence forces the stack to be aligned on a four-byte boundary (it reduces the value in the stack pointer by one, two, or three if the value in ESP is not an even multiple of four). Although the ARDemo entry code correctly subtracts 12 from ESP for the local variables (12 is both an even multiple of four and the number of bytes of local variables), this only leaves ESP double word aligned if it was double word aligned immediately upon entry into the procedure. Had the caller messed with the stack and left ESP containing a value that was not an even multiple of four, subtracting 12 from ESP would leave ESP containing an unaligned value. The AND instruction in the sequence above, however, guarantees that ESP is dword aligned regardless of ESP's value upon entry into the procedure. The few bytes and CPU cycles needed to execute this instruction pay off handsomely if ESP is not double word aligned.
Although it is always safe to execute the AND instruction in the standard entry sequence, it might not be necessary. If you always ensure that ESP contains a double word aligned value, the AND instruction in the standard entry sequence above is unnecessary. Therefore, if you've specified the @NOFRAME procedure option, you don't have to include that instruction as part of the entry sequence.
If you haven't specified the @NOFRAME option (i.e., you're letting HLA emit the instructions to construct the standard entry sequence for you), you can still tell HLA not to emit the extra AND instruction if you're sure the stack will be dword aligned whenever someone calls the procedure. To do this, use the @NOALIGNSTACK procedure option, e.g.,
procedure NASDemo( i:uns32; j:int32; k:dword ); @noalignstack; var LocalVar:int32; begin NASDemo; . . . end NASDemo;HLA emits the following entry sequence for the procedure above:
push( ebp ); mov( esp, ebp ); sub( 4, esp );3.6 The Standard Exit Sequence
Before a procedure returns to its caller, it needs to clean up the activation record. Although it is possible to share the clean-up duties between the procedure and the procedure's caller, Intel has included some features in the instruction set that allows the procedure to efficiently handle all the clean up chores itself. Standard HLA procedures and procedure calls, therefore, assume that it is the procedure's responsibility to clean up the activation record (including the parameters) when the procedure returns to its caller.
If a procedure does not have any parameters, the calling sequence is very simple. It requires only three instructions:
mov( ebp, esp ); // Deallocate locals and clean up stack. pop( ebp ); // Restore pointer to caller's activation record. ret(); // Return to the caller.If the procedure has some parameters, then a slight modification to the standard exit sequence is necessary in order to remove the parameter data from the stack. Procedures with parameters use the following standard exit sequence:
mov( ebp, esp ); // Deallocate locals and clean up stack. pop( ebp ); // Restore pointer to caller's activation record. ret( ParmBytes ); // Return to the caller and pop the parameters.The ParmBytes operand of the RET instruction is a constant that specifies the number of bytes of parameter data to remove from the stack after the return instruction pops the return address. For example, the ARDemo example code in the previous sections has three double word parameters. Therefore, the standard exit sequence would take the following form:
mov( ebp, esp ); pop( ebp ); ret( 12 );If you've declared your parameters using HLA syntax (i.e., a parameter list follows the procedure declaration), then HLA automatically creates a local constant in the procedure, _parms_, that is equal to the number of bytes of parameters in that procedure. Therefore, rather than worrying about having to count the number of parameter bytes yourself, you can use the following standard exit sequence for any procedure that has parameters:
mov( ebp, esp ); pop( ebp ); ret( _parms_ );Note that if you do not specify a byte constant operand to the RET instruction, the 80x86 will not pop the parameters off the stack upon return. Those parameters will still be sitting on the stack when you execute the first instruction following the CALL to the procedure. Similarly, if you specify a value that is too small, some of the parameters will be left on the stack upon return from the procedure. If the RET operand you specify is too large, the RET instruction will actually pop some of the caller's data off the stack, usually with disastrous consequences.
Note that if you wish to return early from a procedure that doesn't have the @NOFRAME option, and you don't particularly want to use the EXIT or EXITIF statement, you must execute the standard exit sequence to return to the caller. A simple RET instruction is insufficient since local variables and the old EBP value are probably sitting on the top of the stack.
3.7 HLA Local Variables
Your program accesses local variables in a procedure by using negative offsets from the activation record base address (EBP). For example, consider the following HLA procedure (which admittedly, doesn't do much other than demonstrate the use of local variables):
procedure LocalVars; nodisplay; var a:int32; b:int32; begin LocalVars; mov( 0, a ); mov( a, eax ); mov( eax, b ); end LocalVars;The activation record for LocalVars looks like
Figure 3.6 Activation Record for LocalVars Procedure
The HLA compiler emits code that is roughly equivalent to the following for the body of this procedure2:
mov( 0, (type dword [ebp-4])); mov( [ebp-4], eax ); mov( eax, [ebp-8] );You could actually type these statements into the procedure yourself and they would work. Of course, using memory references like "[ebp-4]" and "[ebp-8]" rather than a or b makes your programs very difficult to read and understand. Therefore, you should always declare and use HLA symbolic names rather than offsets from EBP.
The standard entry sequence for this LocalVars procedure will be3
push( ebp ); mov( esp, ebp ); sub( 8, esp );This code subtracts eight from the stack pointer because there are eight bytes of local variables (two dword objects) in this procedure. Unfortunately, as the number of local variables increases, especially if those variables have different types, computing the number of bytes of local variables becomes rather tedious. Fortunately, for those who wish to write the standard entry sequence themselves, HLA automatically computes this value for you and creates a constant, _vars_, that specifies the number of bytes of local variables for you4. Therefore, if you intend to write the standard entry sequence yourself, you should use the _vars_ constant in the SUB instruction when allocating storage for the local variables:
push( ebp ); mov( esp, ebp ); sub( _vars_, esp );Now that you've seen how assembly language (and, indeed, most languages) allocate and deallocate storage for local variables, it's easy to understand why automatic (local VAR) variables do not maintain their values between two calls to the same procedure. Since the memory associated with these automatic variables is on the stack, when a procedure returns to its caller the caller can push other data onto the stack obliterating the values of the local variable values previously held on the stack. Furthermore, intervening calls to other procedures (with their own local variables) may wipe out the values on the stack. Also, upon reentry into a procedure, the procedure's local variables may correspond to different physical memory locations, hence the values of the local variables would not be in their proper locations.
One big advantage to automatic storage is that it efficiently shares a fixed pool of memory among several procedures. For example, if you call three procedures in a row,
ProcA(); ProcB(); ProcC();The first procedure (ProcA in the code above) allocates its local variables on the stack. Upon return, ProcA deallocates that stack storage. Upon entry into ProcB, the program allocates storage for ProcB's local variables using the same memory locations just freed by ProcA. Likewise, when ProcB returns and the program calls ProcC, ProcC uses the same stack space for its local variables that ProcB recently freed up. This memory reuse makes efficient use of the system resources and is probably the greatest advantage to using automatic (VAR) variables.
1Stack frame is another term many people use to describe the activation record.
2Ignoring the code associated with the standard entry and exit sequences.
3This code assumes that ESP is dword aligned upon entry so the "AND( $FFFF_FFFC, ESP );" instruction is unnecessary.
4HLA even rounds this constant up to the next even multiple of four so you don't have to worry about stack alignment.
|