Your program accesses local variables in a procedure using negative offsets from the activation record base address (EBP). 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
appears in Figure 5-6.
The HLA compiler emits code that is roughly equivalent to the following for the body of this procedure:[84]
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 be:[85]
push( ebp ); mov( esp, ebp ); sub( 8, esp );
This code subtracts 8 from the stack pointer because there are 8 bytes of local variables (two double-word 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.[86] 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 allocates and deallocates storage for local variables, it's easy to understand why automatic (var
) variables do not maintain their values between two calls to the same procedure. Because 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 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, like so:
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.
[84] This ignores the code associated with the standard entry and exit sequences.
[85] This code assumes that ESP is dword aligned upon entry so the and( $FFFF_FFFC, esp );
instruction is unnecessary.
[86] HLA even rounds this constant up to the next even multiple of 4 so you don't have to worry about stack alignment.