Chapter 7: Run Time Environments: 7.1: Storage Organization
Chapter 7: Run Time Environments: 7.1: Storage Organization
enter f(1) exit f(1) exit f(3) enter f(2) exit f(2) exit f(4) enter f(3) enter f(2) exit f(2) enter f(1) exit f(1) exit f(3) exit f(5) main ends
int a[10]; int main(){ int i; for (i=0; i<10; i++){ a[i] = f(i); } } int f (int n) { if (n<3) return 1; return f(n-1)+f(n-2); }
We can make the following observation about these procedure calls. 1. If an activation of p calls q, then that activation of q terminates no later than the activation of p. 2. The order of activations (procedure calls) corresponds to a preorder traversal of the call tree. 3. The order of de-activations (procedure returns) corresponds to postorder traversal of the call tree. 4. If execution is currently in an activation corresponding to a node N of the activation tree, then the activations that are currently live are those corresponding to N and its ancestors in the tree. They were called in the order given by the rootto-N path in the tree and the returns will occur in the reverse order.
3. Saved status from the caller, which typically includes the return address and the machine registers. The register values are restored when control returns to the caller. 4. The access link is described below. 5. The control link connects the ARs by pointing to the AR of the caller. 6. Returned values are normally (but not always) placed in registers. 7. The first few parameters are normally (but not always) placed in registers. The diagram on the right shows (part of) the control stack for the fibonacci example at three points during the execution. In the upper left we have the initial state, We show the global variable a, although it is not in an activation record and actually is allocated before the program begins execution (it is statically allocated; recall that the stack and heap are each dynamically allocated). Also shown is the activation record for main, which contains storage for the local variable i. Below the initial state we see the next state when main has called f(1) and there are two activation records, one for main and one for f. The activation record for f contains space for the parameter n and and also for the result. There are no local variables in f. At the far right is a later state in the execution when f(4) has been called by main and has in turn called f(2). There are three activation records, one for main and two for f. It is these multiple activations for f that permits the recursive execution. There are two locations for n and two for the result.
1. Values computed by the caller are placed before any items of size unknown by the caller. This way they can be referenced by the caller using fixed offsets. One possibility is to place values computed by the caller at the beginning of the activation record (AR), i.e., near the AR of the caller. The number of arguments may not be the same for different calls of the same function (so called varargs, e.g. printf() in C). 2. Fixed length items are placed next. These include the links and the saved status. 3. Finally come items allocated by the callee whose size is known only at run-time, e.g., arrays whose size depends on the parameters. 4. The stack pointer sp is between the last two so the temporaries and local data are actually above the stack. This would seem more surprising if I used the book's terminology, which is top_sp. Fixed length data can be referenced by fixed offsets (known to the intermediate code generator) from the sp. The top picture illustrates the situation where a pink procedure (the caller) calls a blue procedure (the callee). Also shown is Blue's AR. Note that responsibility for this single AR is shared by both procedures. The picture is just an approximation: For example, the returned value is actually the Blue's responsibility (although the space might well be allocated by Pink. Also some of the saved status, e.g., the old sp, is saved by Pink. The bottom picture shows what happens when Blue, the callee, itself calls a green procedure and thus Blue is also a caller. You can see that Blue's responsibility includes part of its AR as well as part of Green's. Calling Sequence 1. The caller evaluates the arguments. (I use arguments for the caller, parameters for the callee.) 2. The caller stores the return address and the (soon-to-be-updated) sp in the callee's AR. 3. The caller increments sp so that instead of pointing into its AR, it points to the corresponding point in the callee's AR. 4. The callee saves the registers and other (system dependent) information. 5. The callee allocates and initializes its local data.
6. The callee begins execution. Return Sequence 1. The callee stores the return value near the parameters. Note that this address can be determined by the caller using the old (soon-to-be-restored) sp. 2. The callee restores sp and the registers. 3. The callee jumps to the return address. Note that varagrs are supported.
Data obtained by malloc/new have hard to determine lifetimes and are stored in the heap instead of the stack. Data, such as arrays with bounds determined by the parameters are still stack like in their lifetimes (if A calls B, these variables of A are allocated before and released after the corresponding variables of B).
It is the second flavor that we wish to allocate on the stack. The goal is for the (called) procedure to be able to access these arrays using addresses determinable at compile time even though the size of the arrays (and hence the location of all but the first) is not know until the program is called and indeed often differs from one call to the next. The solution is to leave room for pointers to the arrays in the AR. These are fixed size and can thus be accessed using static offsets. Then when the procedure is invoked and the sizes are known, the pointers are filled in and the space allocated. A small change caused by storing these variable size items on the stack is that it no longer is obvious where the real top of the stack is located relative to sp. Consequently another pointer (call it real-top-of-stack) is also kept. This is used on a call to tell where the new allocation record should begin.
return;
int f (int y) { g(y); return y+1; } printf("The answer is %d\n", f(x)); return 0;
The program compiles without errors and the correct answer of 11 is printed. So we can use C (really the GCC, et al extension of C).
am surprised to see a regression from 1e to 2e, so make sure I have not missed something in the cases below. 1. N(D)>N(R). The only possibility is for D to be immediately declared inside R. Then when compiling the call from R to D it is easy to include code to have the access link of D point to the AR of R. 2. N(D)N(R). This includes the case D=R, i.e., a direct recursive call. For D to be in the scope of R, there must be another procedure P enclosing both D and R, with D immediately inside P, i.e., N(D)=N(P)+1 and N(R)=N(P)+1+k, with k0.
3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. P() { D() {...} P1() { P2() { ... Pk() { R(){... D(); ...} } ... } } }
Our goal while creating the AR for D at the call from R is to set the access link to point to the AR for P. Note that this entire structure in the skeleton code shown is visible to the compiler. Thus, the current (at the time of the call) AR is the one for R and if we follow the access links k+1 times we get a pointer to the AR for P, which we can then place in the access link for the being-created AR for D. When k=0 we get the gcc code I showed before and also the case of direct recursion where D=R.
7.3.8: Displays
Basically skipped. In theory access links can form long chains (in practice nesting depth rarely exceeds a dozen or so). A display is an array in which entry i points to the most recent (highest on the stack) AR of depth i.
As this program continues to run it will require more and more storage even though is actual usage is not increasing significantly. 6. Dangling References. The programmer forgets that they did a deallocate.
7. allocate X 8. use X 9. deallocate X 10. 100,000 lines of code not using X
11.
use X
7.5.2: Reachability
Skipped.