CS310 - Spring 2009 Stacks and activation records 4/13/09 Outline - C procedure call example - Machine code issues - LC-3 example and its problems - Stacks - Activation records Example of a C procedure call - The function "SUM" - What happens to the value of R2 during program execution? ------------------------------------------------------ in C: ------------------------------------------------------ main() { int a, b, c; a=7; b=4; c = sum(a); b = b+c; } int sum(int myval) { int i, z; z = 0; for(i=1; i<=myval; i++) { z = z + i; } return z; } LC-3 "equivalent" code - Note: the LC-3 code below is handwritten, not produced by a C compiler ------------------------------------------------------ in LC-3 ASM - What's wrong with this program??? ------------------------------------------------------ .ORIG x3000 MAIN AND R1, R1, #0 ; allocate a to R1 ADD R1, R1, #7 AND R2, R2, #0 ; allocate b to R2 ADD R2, R2, #4 JSR SUM ; parameter passed in R1, returned in R1 ADD R2, R2, R1 ; b = b + c ADD R1, R2, #0 ; Move result to R1 so it can be printed by OUT OUT HALT ;------------------------------------ SUM AND R2, R2, #0 ADD R2, R2, #1 ; initialize R2 as counter i AND R3, R3, #0 ; initialize R3 as accumulator z NOT R1, R1 ADD R1, R1, #1 ; R1 = -(myval) SUM_LOOP ADD R4, R2, R1 BRp LOOP_DONE ADD R3, R3, R2 ADD R2, R2, #1 BR SUM_LOOP LOOP_DONE ADD R1, R3, #0 RET .END - What happens to R2 belonging to MAIN? - What happens if SUM_LOOP calls another function? - What happens if we want to pass a lot of parameters? - Call graph can get quite complicated in a program - procedures can even call themselves (recursion) - A programming system needs to have some assistance in keeping track of where it is - need collection of "return pointers" (R7) back up the call chain - may have local variables along the way... - Big picture - we will use the stack abstraction to create a runtime stack which will allow us to have arbitrary function calling, including recursion. Simple stack abstraction - Let's return to a simple data structure that will help us - the stack - Stack specification - PUSH(val) - result = POP() - Stack implementation - Stack is just a region of memory (consecutive addresses) - it has a beginning and ending address - assume that stack grows from high addresses to low addresses - A stack pointer points to the first free element on the top of the stack - We can keep the stack pointer in R6 - PUSH(val) - assuming that val is in R1 STR R1, R6, 0 ADD R6, R6, -1 - result = POP() ADD R6, R6, 1 LDR R2, R6, 0 - Of course we are missing error checking code for stack over and underflow tomorrow Activation records - If we need to save state when we make a function call, but have a cycle in our call graph (in other words, we don't know how many calls we will make at run time when we are compiling the program or writing the assembly), we need a way to deal with that case. A cycle is where A calls B which calls A, for example, or A calls B calls C calls A. - The solution: a run-time stack, in which the stack grows as we push (add) function call state to it, and it shrinks as function call state is removed (popped) from it. The stack is a LIFO (last in, first out) data structure, as the top of the stack holds the most recent addition. - The structure we use to maintain function state is called an "activation record". We push one activation record on the stack for each function that is called. So long as we don't run out of memory, we can hold a very large number of such records on the stacks (activation records are also commonly called "frames"). - When we call a function (for example function B) from some other function (for example function A, so A is calling B), we name the function performing the call as the "caller" and the function being called as the "callee". It is the caller's responsibility to set up the stack frame for the callee, and then do the actual JSR or JSRR instruction, at which point control is transferred to the callee. Any given function can easily compute how large its activation record is, but a caller can't know the size of the callee's or vice versa. So we will need a protocol. - For our first stack implementation, we will assume that the stack pointer is held in register R6. For our purposes, the pointer always points to the first free element in the activation record of the current function (not the top of the stack, but the start of the activation record at the top of the stack). - If we now use a model in which R6 points to the topmost element (not free element) of the stack: - grows up from high addresses to low addresses - use push/pop semantics - PUSH(val) - assuming that val is in R1 ADD R6, R6, #-1 STR R1, R6, #0 - result = POP() LDR R2, R6, #0 ADD R6, R6, #1 - note the differences between this and the stack implementation above - Note that this stack is a very inexpensive method of allocating and deallocating memory. It works because stack and procedures both behave in a LIFO fashion. What is in an activation record? The entire operating environment for the procedure including: - space for local variables, saved registers - space for passing arguments, returned values - bookkeeping info - Activation records - one per procedure - pushed onto stack when procedure is called - popped off stack when procecure returns - private space for the procedure ... -----Activation Record 2------ -----Activation Record 1------ R6-> Saved register N ...etc... Saved register 2 Saved register 1 ...etc.... Local variable 2 Local variable 1 Frame pointer of caller Return address Return value Parameters (variable) ... -----Activation Record 1------ -----Activation Record 0------ R6-> Saved register N ...etc... Saved register 2 Saved register 1 ...etc.... Local variable 2 Local variable 1 Frame pointer of caller Return address Return value Parameters (variable) ... -----Activation Record 0------ ===================================================== Now the questions are: - How do you find things on the stack? - Use R6 to push/pop at top - Use Frame Pointer (aka dynamic link) in R5 - FP points to first local variable in activation record - Provides an anchor to find passed parameters - Could do it without FP, but it becomes a pain - What are the responsibilities of the caller? - What are the responsibilities of the callee? ===================================================== ... -----Activation Record 2------ -----Activation Record 1------ R6-> Saved register N ...etc... Saved register 2 Saved register 1 ...etc.... Local variable 2 R5-> Local variable 1 Frame pointer of caller Return address Return value Parameters (variable) ... -----Activation Record 1------ -----Activation Record 0------ R6-> Saved register N ...etc... Saved register 2 Saved register 1 ...etc.... Local variable 2 R5-> Local variable 1 Frame pointer of caller Return address Return value Parameters (variable) ... -----Activation Record 0------ Let's build the activation records for main and sum below: ------------------------------------------------------ in C: ------------------------------------------------------ main() { int a, b, c; a=7; b=4; c = sum(a); b = b+c; } int sum(int myval) { int i, z; z = 0; for(i=1; i<=myval; i++) { z = z + i; } return z; } See main_stripped.asm activation records for main and sum 0xcff3: 0000 ; saved register R4 <= R6 (top of stack) 0xcff4: 0000 ; saved register R3 0xcff5: 0004 ; saved register R2 0xcff6: 0007 ; saved register R1 0xcff7: 0000 ; local variable z 0xcff8: 0000 ; local variable i <== R5 0xcff9: cfff ; frame pointer of main 0xcffa: 300a ; return address 0xcffb: 0000 ; return value 0xcffc: 0007 ; passed parameter from main to sum --------------------- 0xcffd: 0000 ; local variable c 0xcffe: 0000 ; local variable b 0xcfff: 0000 ; local variable a 0xd000: 0000 ; frame pointer of "caller to main" ======================================================= Responsibilities of caller Before function call - push arguments onto the stack - call function (JSR) After return - pop return value - protocol will leave return value on top of stack - pop arguments Responsibilities of callee At beginning of call - push spot for return value - push return address (R7) - push callers frame pointer (R5) - set R5 to first free element of stack - push space for local variables - push saved registers At end of call - store return value (relative to R5) - pop saved registers - pop local variables - pop callers frame pointer - pop return address - return - See on-line example code in main.c, main.asm