Accessing Variables at Runtime
In this set of notes we will consider how three different kinds of
variables: local, global, and non-local, are accessed at runtime.
For the purposes of this discussion, we will define these three
categories as follows:
Local variables (including parameters) are stored in the activation
record of the method in which they are declared.
They are accessed at runtime using an offset from the frame pointer (FP).
Since we're assuming that "up" in the stack means a lower
address, these offsets will be negative numbers.
For example, given this code:
What are the offsets for each of the parameters and local variables in
the following methods?
As noted above, global variables are stored in the static data area.
Using MIPS code, each global is stored in a word labeled with the name
of the variable, and the variable is accessed at runtime using its name.
(Since we will be using the SPIM simulator, and since SPIM has some
reserved words that can also be used as Simple variable names, you
will need to add an underscore to global variable names to prevent
clashes with SPIM reserved words.)
For example, if the Simple source code includes:
Recall that we are using the term "non-local variable" to refer
to two situations:
First, let's consider an example (Pascal) program that includes accesses to
non-local variables (the nesting levels of the procedures are given for
later reference):
Trace the execution of this program, drawing the activation records
for the main program and for each called procedure.
Each time a non-local variable is accessed, determine which activation
record it is stored in.
Notice that the relative nesting levels of the variable's declaration and
its use does not tell you how far down the stack to look for
the activation record that contains the non-local variable.
In fact, this number changes for different calls (e.g., due to recursion).
The idea behind the use of access links is as follows:
In this case, the calling procedure sets up the called procedure's access link
by pushing the value of the
FP just before the call, since the FP points to its access link.
For example, when Q is calling R, here is a picture of the stack just
after R's access link has been set up by Q:
To illustrate this situation, consider two cases from the example code:
R calls P, and Q calls itself.
Recall that the nesting structure of the example program is:
When Q is about to call itself, the stack looks like this:
Trace the execution of the example program again.
Each time a procedure is called, determine which case applies in terms
of how to set up the called procedure's access link.
Then use the appropriate algorithm to find the value of the new access
link, and draw the new AR with its access link.
Each time a non-local variable x is accessed, make sure that you find
its activation record by following i - j access links (where i is the nesting
level of the procedure that uses x
and j is the nesting level of the procedure that declares x).
To use an access link:
The motivation for using a display is to avoid the runtime overhead of
following multiple access links to find the activation record that
contains a non-local variable.
The idea is to maintain a global "array" called the display.
Ideally, the display is actually implemented using registers (one for each
array element) rather than an actual array;
however, it can also be implemented using an array in the static data area.
The size of the display is the maximum nesting depth of a procedure in the
program (which is known at compile time).
The display is used as follows:
To maintain the display, a new field (called the "save-display" field)
is added to each activation record.
The display is maintained as follows:
Trace the execution of the running example program, assuming that a display
is used instead of access links.
Each time a non-local variable x is accessed, make sure that you
understand how to find its AR using the display.
Recall that under dynamic scoping, a use of a non-local variable corresponds
to the declaration in the "most recently called, still active" method.
So the question of which non-local variable to use can't be determined at
compile time. It can only be determined at run-time.
There are two ways to implement access to non-locals under dynamic scope:
"deep access" and "shallow access", described below.
Using this approach, given a use of a non-local variable, control links
are used to search back in
the stack for the most recent AR that contains space for that variable.
Note that this requires that it be possible to tell which variables are
stored in each AR;
this is more natural for languages that are interpreted rather
than being compiled (which was indeed the case for languages that used
dynamic scope).
Note also that the number of control links that must be followed cannot
be determined at compile time;
in fact, a different number of links may be followed at different times
during execution, as illustrated by the following example program:
Trace the execution of the program given above.
Note that method P includes a use of non-local variable x.
How many control links must be followed to find the AR with space
for x each time P is called?
Introduction
Our discussion will include information specific to the last programming
assignment (the code generator);
i.e., how to generate MIPS code to access these three kinds of variables.
Local Variables
void P(int x) {
int a, b;
...
}
and assuming that activation records do not include an access link field
or space for saved registers,
P's activation records will be organized as follows:
<--- Stack Pointer
| |
|_______________|
b: | |
|_______________|
a: | |
|_______________|
| Control Link |
|_______________|
| Return Addr |
|_______________|
x: | |
|_______________| <--- Frame Pointer
Assuming 4 bytes for each pointer and each integer, here are the offsets
for each of P's locals:
The following MIPS code loads the values of a and b into
registers t1 and t2, respectively:
lw t1, -12(FP) # load a
lw t2, -16(FP) # load b
To be able to generate this code at compile time (e.g., to process
a statement like x = a + b), the offset of each local variable
must be known by the compiler.
Therefore, the offset of each local variable from the Frame Pointer
should be stored as an attribute of the variable in the symbol table.
This can be done as follows:
Note that the "current offset" counter is only set to zero at the start
of a method, not at the start of a nested block, so each variable
declared (somewhere) in the method has its own, unique offset in that
method's AR.
void P1(int x, int y) {
int a, b, c;
...
while (...) {
int a, w;
}
}
void P2() {
int x, y;
...
if (...) {
int a;
...
}
else {
int b, c;
...
}
}
Global Variables
static int g; // static class field; a global variable
The following code would be generated to reserve space in the static
data area for variable g:
.data
_g: .word 1
And the following code would be generated to access the value of variable g:
lw t1, _g # load contents of g into t1
Non-Local Variables
Note that in languages (like C and C++) that permit the same name to be
used in nested blocks within a method, we might also use the term "non-local
variable" to refer to a use of a variable in one block that was declared in
an enclosing block.
However, this is not an interesting case in terms of runtime access.
All of a method's variables (regardless of which block they are declared in)
are stored in the method's activation record, and are accessed using
offsets from the frame pointer, as discussed above.
Static Scoping
+ program MAIN;
| var x: integer;
|
| + procedure P;
| (2) write(x);
| +
|
| + procedure Q;
| | var y: integer = x;
| |
| | + procedure R;
| | | x = x + 1;
| | | y = y + x;
(1) (2) (3) if y<6 call R;
| | | call P
| | +
| |
| | call R;
| | call P;
| | if x < 5 call Q;
| +
|
| x = 2;
| call Q;
+
Method #1: Access Links
Here's a snapshot of the stack when the example program given above is running,
after R calls P (showing only the access links and the local variables
in the ARs):
____________
P | |------------+ <-- FP
|==========| |
R | |----------+ |
|==========| | |
-- maybe some more ARs for Q here ---
|==========| | |
y:| | | |
Q |----------| | |
| |------+ <-+ |
|==========| | |
y:| | | |
Q |----------| | |
| |--+ | |
|==========| | | |
x:| | | | |
MAIN |----------| | | |
| null |<-+ <-+ <--+
|__________|
To access the value of x from procedure R, access links must be followed.
The number of links that must be followed is:
(level # of R) - (level # of decl of x)
So the code for y := x in procedure R would be as shown below
(Note: we assume that the access link field is the first field in the
AR; i.e., is pointed to by the FP.
We also assume that both variable x and variable y are at offset -12 in
their ARs.
This is because the access link is at offset 0, the return address is at
offset -4, and the control link is at -8; since x and y are the first local
variables in their respective program/procedure, they are each at offset
-12 in their respective ARs.)
= 3 - 1 = 2
lw t0, FP // no links followed yet; t0 holds ptr to R's access link field
lw t0, (t0) // 1 link: t0 holds ptr to Q's AR's access link field
+-> lw t0, (t0) // 2 links: t0 holds ptr to main's AR's access link field
| lw t0, -12(t0) // t0 holds value of x
| sw t0, -12(FP) // y := x
|
This code would be repeated if we needed to follow more links.
How to set up access links:
There are two cases:
Case 1:
The calling procedure's level is less than the called procedure's level
(i.e., the called procedure is nested directly inside
the calling procedure, because if it's not, it's invisible and it can't
be called).
In this case, the called procedure's access link should point to the access
link field of the calling procedure's AR.
This case occurs in the example when Q calls R.
<-- SP
|----------|
| |------+
|==========| |
y:| | |
Q |----------| |
| |--+ <-+ <-- FP
|==========| |
x:| | |
MAIN |----------| |
| null |<-+
|__________|
Case 2:
The calling procedure's level is greater than or equal to the called
procedure's level.
In this case, the called procedure's access link should point to an AR that
is already on the stack.
The value for the new access link is found by starting with the value of
the calling procedure's access link field (which is pointed to by the FP),
and following X-Y access links, where:
X = calling procedure's level
The following code loads the value of the calling procedure's access link
field into register t0:
Y = called procedure's level
lw t0(FP)
If X == Y (i.e., no links should be followed to find the value of the
new access link), then the value of t0 should simply be pushed
onto the stack.
If X is greater than Y, then the code:
lw t0(t0)
should be generated X - Y times, before pushing the value of t0.
+--
|
| +--
| |
| P|
| |
| +--
|
MAIN | +--
| |
| | +--
| | |
| Q| R|
| | |
| | +--
| |
| +--
|
+--
When R is about to call P, the stack looks like this:
|==========|
R | |----------+
|==========| |
-- maybe some more ARs for Q here ---
|==========| |
y:| | |
Q |----------| |
| |------+ <-+
|==========| |
y:| | |
Q |----------| |
| |--+ |
|==========| | |
x:| | | |
MAIN |----------| | |
| null |<-+ <-+
|__________|
Since P is nested inside MAIN, P's access link should point to MAIN's AR
(i.e., the bottom AR on the stack).
R's nesting level is 3 and P's nesting level is 2.
Therefore, we start with the value of R's access link (the pointer to Q's
AR) and follow one link.
This retrieves the values of Q's access link, which is (as desired) a
pointer to MAIN's AR.
This is the value that will be pushed onto the stack (copied into P's AR
as its access link).
|==========|
y:| |
Q |----------|
| |--+
|==========| |
-- maybe some more ARs for Q here ---
|==========|
|==========| |
x:| | |
MAIN |----------| |
| null |<-+
|__________|
The access link in the new AR for Q should be the same as the access link
in the current AR for Q; namely, it should be a pointer to the AR for MAIN.
This value is found by starting with the value of Q's access link (a pointer
to MAIN's AR) and following zero links (because X = Y = 1).
Summary
Follow i - j links to find the AR with space for non-local x,
where i is the nesting level of the procedure that uses x and j is the
nesting level of the procedure that declares x.
To set up an access link:
Method #2: The Display
To illustrate this, refer back to our running example program, outlined below:
+--
|int x;
|
| +--
| |
| P|...x...
| |
| +--
|
MAIN | +--
| | int y;
| |
| | +--
| | |...x...y...
| Q| R|call R
| | |call P
| | +--
| |
| | call R
| | call P
| | call Q
| +--
|
| call Q
+--
Below are two pictures comparing the use of access links with the use
of a display.
Both show the same moment at runtime.
USING ACCESS LINKS USING A DISPLAY
____________ ----------
P | |------------+ P | |<--+
|==========| | |========| | +--+
R | |----------+ | R | |<------| | [2]
|==========| | | |========| | +--+
-- maybe some more ARs for Q here --- +---| | [1]
|==========| | | |========| +--+
y:| | | | y:| | +---| | [0]
Q |----------| | | Q |--------| | +--+
| |------+ <-+ | | | |
|==========| | | |========| | DISPLAY
y:| | | | y:| | |
Q |----------| | | Q |--------| |
| |--+ | | | | |
|==========| | | | |========| |
x:| | | | | x:| | |
MAIN |----------| | | | MAIN |--------| |
| null |<-+ <-+ <--+ | |<--+
|__________| ----------
STACK STACK
This process is illustrate below, showing how the display and the save-display
fields are updated when R calls P (only the local variable and save-display
fields of the ARs are shown).
Before R calls P:
----------------
________ ____________
[2] | |--\ R | |
|______| -------------->| |
[1] | |--\ |==========|
|______| \ y:| |
[0] | |--\ \ Q |----------|
|______| \ ------------>| |
\ |==========|
\ x:| |
\ MAIN |----------|
---------->| |
|==========|
After R calls P:
---------------
________
[2] | |--\ ------------
|______| \ P | |---+
[1] | |----\------------>| | |
|______| \ |==========| |
[0] | |--\ \ R | | |
|______| \ ---------->| | |
\ |==========| |
\ x:| | |
\ Q |----------| |
\ | |<--+
\ |==========|
\ y:| |
\ MAIN |----------|
------>| |
|----------|
Comparison: Access Links vs The Display
Dynamic Scoping
Deep Access
void P() { write x; }
void Q() {
x = x + 1;
if (x < 23) Q();
else P();
}
void R() {
int x = 20;
Q();
P():
}
void main() {
int x = 10;
R();
P();
}