Lexi Cal Nesting
Lexi Cal Nesting
Lexical Nesting
5.1
Chapter Five
Chapter Overview
This chapter discusses nested procedures and the issues associated with calling such procedure and
accessing local variables in nested procedures. Nesting procedures offers HLA users a modicum of built-in
information hiding support. Therefore, the material in this chapter is very important for those wanting to
write highly structured code. This information is also important to those who want to understand how block
structured high level languages like Pascal and Ada operate.
5.2
5.2.1 Scope
Scope in most high level languages is a static, or compile-time concept3. Scope is the notion of when a
name is visible, or accessible, within a program. This ability to hide names is useful in a program because it
is often convenient to reuse certain (non-descriptive) names. The i variable used to control most FOR loops
in high level languages is a perfect example.
The scope of a name limits its visibility within a program. That is, a program has access to a variable
name only within that names scope. Outside the scope, the program cannot access that name. Many programming languages, like Pascal and C++, allow you to reuse identiers if the scopes of those multiple uses
do not overlap. As youve seen, HLA provides scoping features for its variables. There is, however, another
issue related to scope: address binding and variable lifetime. Address binding is the process of associating a
memory address with a variable name. Variable lifetime is that portion of a programs execution during
which a memory location is bound to a variable. Consider the following Pascal procedures:
procedure One(Entry:integer);
var
i,j:integer;
procedure Two(Parm:integer);
var j:integer;
begin
for j:= 0 to 5 do writeln(i+j);
1. Note that C and C++ are not block structured languages. Other block structured languages include Algol, Ada, and Modula-2.
2. Subject, of course, to the limitation that you not reuse the identier within the nested procedure.
3. There are languages that support dynamic, or run-time, scope; this text will not consider such languages.
Page 1375
Chapter Five
Volume Five
if Parm < 10 then One(Parm+1);
end;
begin {One}
for i := 1 to 5 do Two(Entry);
end;
Figure 5.1 shows the scope of identiers One, Two, Entry, i, j, and Parm. The local variable j in procedure Two masks the identier j in procedure One for statement inside procedure Two.
One:
Two:
locals in Two: J, Parm
Globals in Two: I, Entry, One
Figure 5.1
Page 1376
Version: 9/9/02
Lexical Nesting
One(9) parameter
Return Address
Saved EBP Value
"I" Local Variable
Two(9) parameter
Return Address
Saved EBP Value
One(9+1) parameter
Return Address
Saved EBP Value
Two(9+1) parameter
Return Address
Saved EBP Value
Figure 5.2
As you can see, there are several copies of I and J on the stack at this point. Procedure Two (the currently executing routine) would access J in the most recent activation record that is at the bottom of Figure
5.2. The previous instance of Two will only access the variable J in its activation record when the current
instance returns to One and then back to Two.
The lifetime of a variables instance is from the point of activation record creation to the point of activation record destruction. Note that the rst instance of J above (the one at the top of the diagram above) has
the longest lifetime and that the lifetimes of all instances of J overlap.
Page 1377
Chapter Five
Volume Five
A quick, but poorly thought out answer, is to simply index backwards into the stack. After all, you can easily
see in the diagram above that I is at offset eight from Twos activation record. Unfortunately, this is not
always the case. Assume that procedure Three also calls procedure Two and the following statement appears
within procedure One:
If (Entry <5) then Three(Entry*2) else Two(Entry);
With this statement in place, its quite possible to have two different stack frames upon entry into procedure Two: one with the activation record for procedure Three sandwiched between One and Twos activation
records and one with the activation records for procedures One and Two adjacent to one another. Clearly a
xed offset from Twos activation record will not always point at the I variable on Ones most recent activation record.
The astute reader might notice that the saved EBP value in Twos activation record points at the callers
activation record. You might think you could use this as a pointer to Ones activation record. But this scheme
fails for the same reason the xed offset technique fails. EBPs old value, the dynamic link, points at the
callers activation record. Since the caller isnt necessarily the enclosing procedure the dynamic link might
not point at the enclosing procedures activation record.
What is really needed is a pointer to the enclosing procedures activation record. Many compilers for
block structured languages create such a pointer, the static link. Consider the following Pascal code:
procedure Parent;
var i,j:integer;
procedure Child1;
var j:integer;
begin
for j := 0 to 2 do writeln(i);
end {Child1};
procedure Child2;
var i:integer;
begin
for i := 0 to 1 do Child1;
end {Child2};
begin {Parent}
Child2;
Child1;
end;
Just after entering Child1 for the rst time, the stack would look like Figure 5.3. When Child1 attempts
to access the variable i from Parent, it will need a pointer, the static link, to Parents activation record.
Unfortunately, there is no way for Child1, upon entry, to gure out on its own where Parents activation
record lies in memory. It will be necessary for the caller (Child2 in this example) to pass the static link to
Child1. In general, the callee can treat the static link as just another parameter; usually pushed on the stack
immediately before executing the CALL instruction.
Page 1378
Version: 9/9/02
Lexical Nesting
Figure 5.3
To fully understand how to pass static links from call to call, you must rst understand the concept of a
lexical level. Lexical levels in Pascal correspond to the static nesting levels of procedures and functions.
Most compiler writers specify lex level zero as the main program. That is, all symbols you declare in your
main program exist at lex level zero. Procedure and function names appearing in your main program dene
lex level one, no matter how many procedures or functions appear in the main program. They all begin a
new copy of lex level one. For each level of nesting, Pascal introduces a new lex level. Figure 5.4 shows this.
Figure 5.4
During execution, a program may only access variables at a lex level less than or equal to the level of the
current routine. Furthermore, only one set of values at any given lex level are accessible at any one time4 and
those values are always in the most recent activation record at that lex level.
Page 1379
Chapter Five
Volume Five
Before worrying about how to access non-local variables using a static link, you need to gure out how
to pass the static link as a parameter. When passing the static link as a parameter to a program unit (procedure or function), there are three types of calling sequences to worry about:
A program unit calls a child procedure or function. If the current lex level is n, then a child
procedure or function is at lex level n+1 and is local to the current program unit. Note that most
block structured languages do not allow calling procedures or functions at lex levels greater
than n+1.
A program unit calls a peer procedure or function. A peer procedure or function is one at the
same lexical level as the current caller and a single program unit encloses both program units.
A program unit calls an ancestor procedure or function. An ancestor unit is either the parent
unit, a parent of an ancestor unit, or a peer of an ancestor unit.
Calling sequences for the rst two types of calls above are very simple. For the sake of this example,
assume the activation record for these procedures takes the generic form in Figure 5.5.
Figure 5.5
When a parent procedure or function calls a child program unit, the static link is nothing more than the
value in the EBP register immediately prior to the call. Therefore, to pass the static link to the child unit, just
push EBP before executing the call instruction:
<Push Other Parameters onto the stack>
push( ebp );
call ChildUnit;
Of course the child unit can process the static link on the stack just like any other parameter. In this case, the
static and dynamic links are exactly the same. In general, however, this is not true.
If a program unit calls a peer procedure or function, the current value in EBP is not the static link. It is a
pointer to the callers local variables and the peer procedure cannot access those variables. However, as
peers, the caller and callee share the same parent program unit, so the caller can simply push a copy of its
4. There is one exception. If you have a pointer to a variable and the pointer remains accessible, you can access the data it
points at even if the variable actually holding that data is inaccessible. Of course, in (standard) Pascal you cannot take the
address of a local variable and put it into a pointer. However, certain dialects of Pascal (e.g., Turbo) and other block structured
languages will allow this operation.
Page 1380
Version: 9/9/02
Lexical Nesting
static link onto the stack before calling the peer procedure or function. The following code will do this
assuming the current procedures static link is on the stack immediately above the return address:
<Push Other Parameters onto the Stack>
pushd( [ebp+8] );
call PeerUnit;
Calling an ancestor is a little more complex. If you are currently at lex level n and you wish to call an
ancestor at lex level m (m < n), you will need to traverse the list of static links to nd the desired activation
record. The static links form a list of activation records. By following this chain of activation records until it
ends, you can step through the most recent activation records of all the enclosing procedures and functions
of a particular program unit. The stack diagram in Figure 5.6 shows the static links for a sequence of procedure calls statically nested ve lex levels deep.
Lex Level 0
Lex Level 1
Lex Level 2
Lex Level 3
Lex Level 3
Lex Level 4
Lex Level 5
Lex Level 5
Lex Level 5
Figure 5.6
Static Links
If the program unit currently executing at lex level ve wishes to call a procedure at lex level three, it
must push a static link to the most recently activated program unit at lex level two. In order to nd this static
link you will have to traverse the chain of static links. If you are at lex level n and you want to call a procedure at lex level m you will have to traverse (n-m)+1 static links. The code to accomplish this is
// Current lex level is 5. This code locates the static link for,
// and then calls a procedure at lex level 2. Assume all calls are
// near:
<Push necessary parameters>
mov( [ebp+8], ebx );
mov( [ebx+8], ebx );
mov( [ebx+8], ebx );
pushd( [ebx+8] );
call ProcAtLL2;
//
//
//
//
Page 1381
Chapter Five
Volume Five
The Inner procedure accesses global variables at lex level n-1 and n-2 (where n is the lex level of the Inner
procedure). The Middle procedure accesses a single global variable at lex level m-1 (where m is the lex
level of procedure Middle). The following HLA code could implement these three procedures:
procedure Inner; @nodisplay; @noframe;
var
k:int32;
begin Inner;
push( ebp );
mov( esp, ebp );
sub( _vars_, esp );
Page 1382
mov( 3, k );
mov( [ebp+8], ebx );
mov( [ebx-4], eax );
add( k, eax );
mov( [ebx+8], ebx );
add( [ebx-4], eax );
stdout.puti( eax );
stdout.newln();
//
//
//
//
//
//
//
Initialize k.
Static link to previous lex level.
Get js value.
Add in ks value.
Get static link to Outers activation record.
Add in is value to sum.
Display the sum.
Version: 9/9/02
Lexical Nesting
pop( ebp );
ret( 4 );
end Inner;
procedure Middle; @nodisplay; @noframe;
var
j:int32;
begin Middle;
push( ebp );
mov( esp, ebp );
sub( _vars_, esp );
mov( 2, j );
mov( [ebp+8], ebx );
mov( [ebx-4], eax );
add( j, eax );
stdout.put( eax, nl );
//
//
//
//
//
push( ebp );
call Inner;
Initialize j.
Get the static link.
Get is value.
Compute i+j.
Display their sum.
mov( 1, i );
push( ebp );
call Middle;
end Outer;
Note that as the difference between the lex levels of the activation records increases, it becomes less and
less efcient to access global variables. Accessing global variables in the previous activation record requires
only one additional instruction per access, at two lex levels you need two additional instructions, etc. If you
analyze a large number of Pascal programs, you will nd that most of them do not nest procedures and functions and in the ones where there are nested program units, they rarely access global variables. There is one
major exception, however. Although Pascal procedures and functions rarely access local variables inside
other procedures and functions, they frequently access global variables declared in the main program. Since
such variables appear at lex level zero, access to such variables would be as inefcient as possible when
using the static links. To solve this minor problem, most 80x86 based block structured languages allocate
variables at lex level zero directly in the STATIC segment and access them directly.
Page 1383
Chapter Five
Volume Five
The identier Inner is local to the Middle procedure and is not accessible outside Middle (not even
to Outer); similarly, the identier Middle is local to Outer and is not accessible outside Outer. This
information hiding feature lets you prevent other code from accidentally accessing these nested
procedures, just as for local variables.
2.
Before discussing how to use this feature to access non-local variables in a more reasonable fashion
using static links, lets also consider the issue of the static link itself. The static link is really nothing more
than a special parameter to these functions, therefore we can declare the static link as a parameter using
HLAs high level procedure declaration syntax. Since the static link must always be at a xed offset in the
activation record for all procedures, the most reasonable thing to do is always make the stack link the rst
parameter in the list5; this ensures that the static link is always found at offset "+8" in the activation record.
Heres the declarations above with the static links added as parameters:
procedure Outer( outerStaticLink:dword ); @nodisplay; @noframe;
Page 1384
Version: 9/9/02
Lexical Nesting
var
i:int32;
procedure Middle( middleStaticLink:dword ); @nodisplay; @noframe;
var
j:int32;
procedure Inner( innerStaticLink:dword ); @nodisplay; @noframe;
var
k:int32;
begin Inner;
<< Code for the Inner procedure >>
end Inner;
begin Middle;
<< code for the Middle procedure >>
end Middle;
begin Outer;
<< code for the Outer procedure >>
end Outer;
All that remains is to discuss how one references non-local (automatic) variables in this code. As you
may recall from the chapter on Intermediate Procedures in Volume Four, HLA references local variables and
parameters using an address expression of the form "[ebpoffset]" where offset represents the offset of the
variable into the activation record (parameters typically have a positive offset, local variables have a negative
offset). Indeed, we can use the HLA compile-time @offset function to access the variables without having
to manually gure out the variables offset in the activation record, e.g.,
mov( [ebp+@offset( i )], eax );
5. Assuming, of course, that youre using the default Pascal calling convention. If you were using the CDECL or STDCALL
calling convention, you would always make the static link the last parameter in the parameter list.
Page 1385
Chapter Five
Volume Five
reg32 represents any of the 80x86s 32-bit general purpose registers and identier is the non-local identier
you wish to access. HLA substitutes an address expression of the form "[reg32+@offset(identier)]" for this
expression. Given this syntax, we can now rewrite the Inner, Middle, and Outer example in a high level
fashion as follows:
procedure Outer( outerStaticLink:dword ); @nodisplay;
var
i:int32;
procedure Middle( middleStaticLink:dword ); @@nodisplay;
var
j:int32;
procedure Inner( innerStaticLink:dword ); nodisplay;
var
k:int32;
begin Inner;
mov(
mov(
mov(
add(
3, k );
// Initialize k.
innerStaticLink, ebx ); // Static link to previous lex level.
ebx::j, eax );
// Get js value.
k, eax );
// Add in ks value.
end Inner;
begin Middle;
mov( 2, j );
// Initialize j.
mov( middleStaticLink, ebx ); // Get the static link.
mov( ebx::i, eax );
// Get is value.
add( j, eax );
// Compute i+j.
stdout.put( eax, nl );
// Display their sum.
Inner( ebp );
end Middle;
begin Outer;
mov( 1, i );
Middle( ebp );
end Outer;
This example provides only a small indication of the work needed to access variables using static links.
In particular, accessing @ebx::i in the Inner procedure was simplied by the fact that EBX already contained Middles static link. In the typical case, its going to take one instruction for each lex level the code
Page 1386
Version: 9/9/02
Lexical Nesting
traverses in order to access a given non-local automatic variable. While this might seem bad, in typical programs you rarely access non-local variables, so the situation doesnt arrive often enough to worry about.
HLA does not provide built-in support for static links. If you are going to use static links in your programs, then you must manually pass the static links as parameters to your procedures (i.e., HLA will not take
care of this for you). While it is possible to modify HLA to automatically handle static links for you, HLA
provides a different mechanism for accessing non-local variables - the display. To learn about displays, keep
reading...
Lex Level 0
Lex Level 1
Display
0
1
2
3
4
5
6
Lex Level 2
Lex Level 3
Lex Level 3
Lex Level 4
Lex Level 5
????
Lex Level 5
Lex Level 5
Figure 5.7
The Display
Note that the entries in the display always point at the most recent activation record for a procedure at
the given lex level. If there is no active activation record for a particular lex level (e.g., lex level six above),
then the entry in the display contains garbage.
The maximum lexical nesting level in your program determines how many elements there must be in the
display. Most programs have only three or four nested procedures (if that many) so the display is usually
quite small. Generally, you will rarely require more than 10 or so elements in the display.
Page 1387
Chapter Five
Volume Five
Another advantage to using a display is that each individual procedure can maintain the display information itself, the caller need not get involved. When using static links the calling code has to compute and
pass the appropriate static link to a procedure. Not only is this slow, but the code to do this must appear
before every call. If your program uses a display, the callee, rather than the caller, maintains the display so
you only need one copy of the code per procedure.
Although maintaining a single display in the STATIC segment is easy and efcient, there are a few situations where it doesnt work. In particular, when passing procedures as parameters, the single level display
doesnt do the job. So for the general case, a solution other than a static array is necessary. Therefore, this
chapter will not go into the details of how to maintain a static display since there are some problems with
this approach.
Intel, when designing the 80286 microprocessor, studied this problem very carefully (because Pascal
was popular at the time and they wanted to be able to efciently handle Pascal constructs). They came up
with a generalized solution that works for all cases. Rather than using a single display in a static segment,
Intels designers decided to have each procedure carry around its own local copy of the display. The HLA
compiler automatically builds an Intel-compatible display at the beginning of each procedure, assuming you
dont use the @NODISPLAY procedure option. An Intel-compatible display is part of a procedures activation record and takes the form shown in Figure 5.8:
Return Address
Dynamic Link
(previous EBP value)
EBP
Display[0]
Display[1]
.
.
.
Display[n]
Local Variables (if any)
ESP
Figure 5.8
If we assume that the lex level of the main program is zero, then the display for a given procedure at lex
level n will contain n+1 double word elements. Display[0] is a pointer to the activation record for the main
program, Display[1] is a pointer to the activation record of the most recently activated procedure at lex level
one. Etc. Display[n] is a pointer to the current procedures activation record (i.e., it contains the value
found in EBP while this procedure executes). Normally, the procedure would never access element n of Display since the procedure can index off EBP directly; However, as youll soon see, well need the Display[n]
entry to build displays for procedures at higher lex levels.
Page 1388
Version: 9/9/02
Lexical Nesting
One important fact to note about the Intel-compatible display array: its elements appear backwards in
memory. Remember, the stack grows downwards from high addresses to low addresses. If you study Figure
5.8 for a moment youll discover that Display[0] is at the highest memory address and Display[n] is at the
lowest memory address, exactly the opposite for standard array organization. It turns out that well always
access the display using a constant offset, so this reversal of the array ordering is no big deal. Well just use
negative offsets from Display[0] (the base address of the array) rather than the usual positive offsets.
If the @NODISPLAY procedure option is not present, HLA treats the display as a predeclared local
variable in the procedure and inserts the name "_display_" into the symbol table. The offset of the _display_
variable in the activation record is the offset of the Display[0] entry in Figure 5.8. Therefore, you can easily
access an element of this array at run-time using a statement like:
mov( _display_[ -lexLevel*4 ], ebx );
The "*4" component appears because _display_ is an array of double words. lexLevel must be a constant
value that species the lex level of the procedure whose activation record youd like to obtain. The minus
sign prexing this expression causes HLA to index downwards in memory as appropriate for the display
object.
Although its not that difcult to gure out the lex level of a procedure manually, the HLA compile-time
language provides a function that will compute the lex level of a given procedure for you the @LEX function. This function accepts a single parameter that must be the name of an HLA procedure (that is currently
in scope). The @LEX function returns an appropriate value for that function that you can use as an index
into the _display_ array. Note that @LEX returns one for the main program, two for procedures you declare
in the main program, three for procedures you declare in procedures you declare in the main program, etc. If
you are writing a unit, all procedures you declare in that unit exist at lex level two.
The following program is a variation of the Inner/Middle/Outer example youve seen previously in this
chapter. This example uses displays and the @LEX function to access the non-local automatic variables:
program DisplayDemo;
#include( "stdlib.hhf" )
macro Display( proc );
_display_[ -@lex( proc ) * 4]
endmacro;
procedure Outer;
var
i:int32;
procedure Middle;
var
j:int32;
procedure Inner;
var
k:int32;
begin Inner;
mov(
mov(
mov(
add(
4, k );
Display( Middle ), ebx );
ebx::j, eax );
// Get j's value.
k, eax );
// Add in k's value.
Page 1389
Chapter Five
Volume Five
// add in i's value:
mov( Display( Outer ), ebx );
add( ebx::i, eax );
// Display the results:
stdout.puti32( eax );
stdout.newln();
end Inner;
begin Middle;
mov( 2, j );
mov( Display( Outer ), ebx );
mov( ebx::i, eax );
add( j, eax );
stdout.puti32( eax );
stdout.newln();
//
//
//
//
//
Initialize j.
Get the static link.
Get i's value.
Compute i+j.
Display their sum.
Inner();
end Middle;
begin Outer;
mov( 1, i );
Middle();
end Outer;
begin DisplayDemo;
Outer();
end DisplayDemo;
Program 5.1
Assuming you do not attach the @NODISPLAY procedure option to a procedure you write in HLA,
HLA will automatically emit the code (as part of the standard entry sequence) to build a display for that procedure. Up to this chapter, none of the programs in this text have used nested procedures6, therefore there
has been no need for a display. For that reason, most programs appearing in this text (since the introduction
of the @NODISPLAY option) have attached @NODISPLAY to the procedure. It doesnt make a program
incorrect to build a display if you never use it, but it does make the procedure a tiny bit slower and a tiny bit
larger, hence the use of the @NODISPLAY option up to this point.
6. Technically, this statement is not true. Every procedure youve written has been nested inside the main program. However,
none of the sample programs to date have considered the possibility of accessing the main programs automatic (VAR) variables. Hence there has been no need for a display until now).
Page 1390
Version: 9/9/02
Lexical Nesting
Therefore, you may use the instruction for the standard procedure exit code. On an 80386 or earlier processor, the LEAVE instruction is faster than the equivalent move and pop sequence. However, the LEAVE
instruction is slower on 80486 and later processors.
The ENTER instruction takes two operands. The rst is the number of bytes of local storage the current
procedure requires, the second is the lex level of the current procedure. The enter instruction does the following:
// enter( Locals, LexLevel );
push( ebp );
mov( esp, tempreg );
cmp( LexLevel, 0 );
je Lex0;
lp:
dec( LexLevel );
jz Done;
sub( 4, ebp );
pushd( [ebp] );
jmp lp;
Done:
push( tempreg );
Lex0:
As you can see from this code, the ENTER instruction copies the display from activation record to activation
record. This can get quite expensive if you nest the procedures to any depth. Most high level languages, if
they use the ENTER instruction at all, always specify a nesting level of zero to avoid copying the display
throughout the stack.
The ENTER instruction puts the value for the _display_[n] entry at location EBP-(n*4). The ENTER
instruction does not copy the value for display[0] into each stack frame. Intel assumes that you will keep the
main programs global variables in the data segment. To save time and memory, they do not bother copying
the _display_[0] entry. This is why HLA uses lex level one for the main program in HLA the main program can have automatic variables and, therefore, requires a display entry.
Page 1391
Chapter Five
Volume Five
The ENTER instruction is very slow, particularly on 80486 and later processors. If you really want to
copy the display from activation record to activation record it is probably a better idea to push the items
yourself. The following code snippets show how to do this:
// enter( n, 0 );
push( ebp );
mov( esp, ebp );
sub( n, esp );
// enter( n, 1 );
push( ebp );
pushd( [ebp-4] );
lea( ebp, [esp-4] );
sub( n, esp );
//
//
//
//
//
//
//
//
//
push( ebp );
//
pushd( [ebp-4] );
//
pushd( [ebp-8] );
//
pushd( [ebp-12] );
//
lea( ebp, [esp-12] ); //
sub( n, esp );
//
// enter( n, 2 );
push( ebp );
pushd( [ebp-4] );
pushd( [ebp-8] );
lea( ebp, [esp-8] );
sub( n, esp );
// enter( n, 3 );
// enter( n, 4 );
push( ebp );
//
pushd( [ebp-4] );
//
pushd( [ebp-8] );
//
pushd( [ebp-12] );
//
pushd( [ebp-16] );
//
lea( ebp, [esp-16] ); //
sub( n, esp );
//
rec.
rec.
rec.
rec.
// etc.
If you are willing to believe Intels cycle timings, youll nd that the ENTER instruction is almost never
faster than a straight line sequence of instructions that accomplish the same thing. If you are interested in
saving space rather than writing fast code, the ENTER instruction is generally a better alternative. The same
is generally true for the LEAVE instruction as well. It is only one byte long, but it is slower than the corresponding "mov( esp, ebp );" and "pop( ebp );" instructions. The following sample program demonstrates
how to access non-local variables using a display. This code does not use the @LEX function in the interest
of making the lex level access clear; normally you would use the @LEX function rather than the literal constants appearing in this example.
program EnterLeaveDemo;
#include( "stdlib.hhf" )
Page 1392
Version: 9/9/02
Lexical Nesting
procedure LexLevel2;
procedure LexLevel3a;
begin LexLevel3a;
stdout.put( nl "LexLevel3a:" nl );
stdout.put( "esp = ", esp, " ebp = ", ebp, nl );
mov( _display_[0], eax );
stdout.put( "display[0] = ", eax, nl );
mov( _display_[-4], eax );
stdout.put( "display[-1] = ", eax, nl );
end LexLevel3a;
procedure LexLevel3b; noframe;
begin LexLevel3b;
enter( 0, 3 );
stdout.put( nl "LexLevel3b:" nl );
stdout.put( "esp = ", esp, " ebp = ", ebp, nl );
mov( _display_[0], eax );
stdout.put( "display[0] = ", eax, nl );
mov( _display_[-4], eax );
stdout.put( "display[-1] = ", eax, nl );
leave;
ret();
end LexLevel3b;
begin LexLevel2;
stdout.put( "LexLevel2: esp=", esp, " ebp = ", ebp, nl nl );
LexLevel3a();
LexLevel3b();
end LexLevel2;
begin EnterLeaveDemo;
stdout.put( "main: esp = ", esp, " ebp= ", ebp, nl );
LexLevel2();
end EnterLeaveDemo;
Program 5.2
Starting with HLA v1.32, HLA provides the option of emitting ENTER or LEAVE instructions rather
than the discrete sequences for a procedures standard entry and exit sequences. The @ENTER procedure
options tells HLA to emit the ENTER instruction for a procedure, the @LEAVE procedure option tells HLA
to emit the LEAVE instruction in place of the standard exit sequence. See the HLA documentation for more
details.
Page 1393
Chapter Five
5.3
Volume Five
To pass a local variable by value to another procedure, you could use the following code7:
push( LocalVariable );
call proc;
To pass an intermediate variable as a value parameter, you must rst locate that intermediate variables
activation record and then push its value onto the stack. The exact mechanism you use depends on whether
you are using static links or a display to keep track of the intermediate variables activation records. If using
static links, you might use code like the following to pass a variable from two lex levels up from the current
procedure:
mov( [ebp+8], ebx );
mov( [ebx], ebx );
push( ebx::IntVar );
call proc;
Passing an intermediate variable by value when you are using a display is somewhat easier. You could
use code like the following to pass an intermediate variable from lex level one:
mov( _display_[ -1*4 ], ebx );
push( ebx::IntVar );
call proc;
It is possible to use the HLA high level procedure calling syntax when passing intermediate variables as
parameters by value. The following code demonstrates this:
mov( _display_[ -1*4 ], ebx );
proc( ebx::IntVar );
This example uses a display because HLA automatically builds the display for you. If you decide to use
static links, youll have to modify this code appropriately.
7. The non-global examples all assume the variable is at offset -2 in their activation record. Change this as appropriate in your
code.
Page 1394
Version: 9/9/02
Lexical Nesting
//
//
//
//
When using a display, the calling sequence might look like the following:
mov( _display_[ -1*4 ], ebx );
lea( eax, ebx::IntVar );
push( eax );
call proc;
It is possible to use the HLA high level procedure calling syntax when passing parameters by reference,
by value/result, or by result. The following code demonstrates this:
mov( _display_[ -1*4 ], ebx );
proc( ebx::IntVar );
The nice thing about the high level syntax is that it is identical whether youre passing parameters by value,
reference, value/result, or by result.
As you may recall from the chapter on Low-Level Parameter Implementation, there is a second way to
pass a parameter by value/result. You can push the value onto the stack and then, when the procedure returns,
pop this value off the stack and store it back into the variable from whence it came. This is just a special case
of the pass by value mechanism described in the previous section.
8. As you may recall, pass by reference, value-result, and result all use the same calling sequence. The differences lie in the
procedures themselves.
Page 1395
Chapter Five
5.4
Volume Five
The statement "DoCall(xyz);" calls DoCall that, in turn, calls procedure xyz.
Whenever you pass a procedures address in this manner, HLA only passes the address of the procedure
as the parameter value. Upon entry into procedure x via the DoCall invocation, the x procedure rst creates
its own display by copying appropriate entries from DoCalls display. This gives x access to all intermediate
variables that HLA allows x to access.
Keep in mind that thunks are special cases of functions that you call indirectly. However, there is a
major difference between a thunk and a procedure thunks carry around the pointer to the activation record
they intend to use. Therefore, the thunk does not copy the calling procedures display; instead, it uses the
display of an existing procedure to access intermediate variables.
5.5
Page 1396
Version: 9/9/02
Lexical Nesting
proc();
// EAX now contains five...
end MainProc;
Notice that the proc procedure has the @NOFRAME option, so HLA does not emit the standard entry
sequence to build an activation record. This means that upon entry to proc, EBP still points at MainProcs
activation record. Therefore, this code can access the ALocalVar variable by using the syntax ebp::ALocalVar. No other code is necessary.
The drawback to this scheme is that proc may not contain any parameters or local variables (which
would require setting EBP to point at procs activation record). However, if you can live with this limitation,
then this is a useful trick for accessing local variables one lex level up from the current procedure.
5.6
Page 1397
Chapter Five
Page 1398
Volume Five
Version: 9/9/02