UMBC CMSC 313 -- Subprograms and the Stack Previous | Next


Subprograms and the Stack

So far the stack has been for storing the return address or saving the contents of registers before calling a procedure. There are some other things we can do with it: One simple answer covers the problems. Each procedure in execution is assigned a stack frame, a fixed-sized block of memory on the stack for parameters, return address, local variables and register storage. When a procedure is called, its stack frame is pushed onto the stack. The routine itself may push and pop temporary storage on the stack. Then,if this procedure is calls other procedures, the material for the first procedure will stay on the stack and each other procedure will adds its material to the stack. Then as every procedure terminates it must remove its material off the stack.

The ebp (base pointer register points to a fixed location in the middle of the stack from of the currently executing procedure and the stack pointer esp points to the end of the procedure's temporary storage.

Because ebp is an index register, the parts of the stack frame can be referenced relative to its contents by adding or subtracting a displacement. (Displacements are always referencing words or double words, but the value is measured in bytes!) These displacements are going to be the same for each and every use of a particular procedure. That way we we are use age as tht way we we are use age as the third dword parameter, it will always be [bp + 12]. Since parameters are can be both words and dwords, you must keep track of the true displacement. Additional, the esi and edi registers can be used.

The calling procedure must push the parameters on the stack before making the procedure call. Then the called procedure must save and reset the ebp, reserve space for the local variables, save the appropriate registers, and do its work. Then it must restore things in reverse order:

mySub:                         ;  Start of procedure  
         push  ebp
         mov   ebp, esp
         sub   esp, n          ; reserve n bytes of local storage
         push  dword [reg1]    ; save registers
         push  dword reg2

             ; do some processing 

         pop   dword reg2
         pop   dword reg1
         add   esp, n          ; just the opposite 
         mov   esp, ebp
         pop   ebp
         ret                   ; we are done.

This results in memory that looks like this:

When working with C functions, some of them have a variable number of parameters, where each time the function can have a different (variable) number of arguments. Printf is an example of this. It looks like:

printf(a1, ..., an); where n is the number of one-word parameters. They are pushed in the order of reverse order that they are encounter: an, ..., a1 After returning from such a call, there will be a statement like:
add esp, n   ; where n is two times the number of  2-byte parameters,
             ; plus four times the number of 4-byte parameters.

There are two kinds of parameters, call by value and call by reference. We have been using call by value when we do the push. Since the values are not retained, whatever changes that are made to them are lost. Notice that this is the default for C for everything except for arrays. Since the size of anything being pushed is in units of 16 bits or 32 bits, when the parameter is a character, it is promoted to 16-bits.

Both recusion and reentrancy are also solved with this method, because every call means that a separate frame are being manipulated. Each call becomes independent!

Procedures should be small, simple, and only do one thing. When possible, subprograms should not do any I/O. Then libraries can be built up and only the current outer driver has to worry about whether to use the keyboard, CRT, modem, disk file, etc. The inner procedures are independent of the I/O constraints that change with most programs.


Previous | Next

©2004, Gary L. Burt