Function Calls, Part 3 (Frame Pointer and Local Variables)

Introduction and Series Recap

  • In Part 1, you looked at the basic structure of code generated surrounding a function call.
  • In Part 2, you looked at calling conventions and studied the code generated for two popular calling conventions, __stdcall and __cdecl.

In this article, you will look at local variables and how they come into play.

Stack Frame

Typical functions have the following: code, arguments, local variables, and return values. Although the code for the function is compiled in and is unchangeable at runtime, the rest are all dynamic entities and as such are all occupants of the stack for a thread.

The data required by a function could be:

  • Arguments for the function: As seen in Parts 1 and 2, the arguments to a function are passed in via stack.
  • Return location to the caller code: As seen in Parts 1 and 2, the return location is pushed on the stack when a call is executed so when the function does return from its body, the processor knows where to continue code execution.
  • Local variables used by the function: You haven't seen this yet, but suffice it to say for now that, this again goes on the stack. The simple reason is that this is a dynamic entity coming into existence and havs meaning only when the function IS called.
  • Temporary data used by the compiler: In addition to variables allocated explicitly in code, the compiler itself may need to wriggle room for its temporary variables. For example, the compiler might need to use a particular register before doing some operation. So, it would push the current contents of the register on to the stack for safekeeping, perform the operation, and pop back the stack to restore the register value.

A stack frame is a block of te stack that contains these data required by a function. Each function call results in a new block of stack memory being set aside and prepared for, so that the function can do its job. The stack frame creation is a process in which both caller and callee take part. The caller starts this by pushing the function arguments (note, only the caller knows what to pass in), followed by a call into the function. From this point, the callee code takes over. It further extends the stack frame and prepares the local variables (note, only the callee knows what variables to use). From that point onwards, the stack frame is set up for the function body to operate on.

Each function call results in a creation of a stack frame (with the minimum being the address to return to). So, if funcA calls funcB and funcB calls funcC, three stack frames are set up one on top of the another. When a function returns, its frame becomes invalid. A well-behaved function acts only on its own stack frame and does not trespass on another's. Once it starts going out of its legal bounds, unpredictable and disastrous behavior can occur. It is to be noted that, when funcA calls into say funcB, the stack frame for the funcA is frozen—it doesn't grow or shrink anymore because the control has been passed to funcB (again, if funcB is well behaved and is using only its OWN stack frame).

However, in the context of funcB, the stack frame can be dynamic; it can grow and shrink depending on what is happening inside the function. This means the topmost stack frame is dynamic in nature and it could be contracting and expanding as the function executes. In any case, it can at the least contract to the position in the stack it was in when the function body was just started. If it shrinks any further, it is basically trespassing on the caller established stack frame and the consequences can be bad.

This is pictorially shown below. The scenario is funcA calling into funcB which in turn calls into funcC.

  • Block 1 is how the stack frame is when the processor EIP is within the body of funcA. Note the solid nature of the lower boundary, indicating anything below that is forbidden as far as funcA goes. Similarly, note the dotted boundary at the top of the frame, indicating there is a scope for dynamic expansion and contraction of this top edge as the function is executing. For a moment note that, because this is the topmost function that is executing in that thread, any memory upwards is available to do anything (of course, within limits of the stack size for the thread).
  • Block 2 represents how the stack frames look like when the processor EIP is within the body of funcB. Note now the stack frame for funcA has been frozen at the state when it made a call into funcB. This is indicated red and has a solid boundary at the top indicating, when funcB does return, the stack pointer MUST be at that point.
  • Block 3 represents how the stack frames look when the processor EIP is within the body of funcC. Note now, compared to block 2, the stack frame boundary of funcB is at a lower point than in block 2. This is representative of the fact that, during the execution of func, the compiler might have had code that resulted in a some pushing and some popping of data on the stack. So, at the time funcC was called, it was at a different stack pointer location than it was when block 2 snapshot was taken.
  • Blocks 4 and 5 show how the stack frames are invalidated and are restored to their original state when funcC and funcB have just returned respectively.
  • In all this, it is to be noted that although the red region indicated as forbidden, is not really forbidden by the compiler or the processor. It is just a boundary that you have to visualize such that, if for some reason there happens to be a trespass, there is a problem with the code.

To summarize, stack frames are set up to facilitate the function bodies. A call to a function is preceded by the caller code preparing the stack to push parameters, followed by the callee code preparing the stack for its local variables. When the callee code returns, it has to make sure the stack pointer is back at the same value when its code was entered in the first place. This makes sure the stack frame is restored.

Local Variables

In Parts 1 and 2, you saw how function arguments are passed. These are passed via the stack. To be precise, the caller pushes these on the stack and then issues a call instruction with the address of function to be called. If the stack contents are checked right before the function code is executed, you see that the top most on the stack is the return address, followed by the function arguments. That was the data from the caller. How about callee data? Specifically, how about the function local variables? These also have a lifetime as good as that for the function body, so they are perfect candidates to be allocated on the stack. This in fact is the case and the compiler DOES allocate local variable space right on the stack. This is one of the first things the callee code generated by the compiler will do. It will allocate enough data to accomodate all local variables within a function body. The following figure shows this setup. Note the division of responsibility of stack frame setup by different parts of the code.

The picture above is a representation of the stack frame setup for a function call. It is interesting to note that the return address location acts as a kind of anchor point for accessing function data. Note that, to access the arguments, the function body will have to traverse down (higher addresses) from the location where the return address is stored, and to access the local variables, the function body will have to traverse up the stack (lower addresses) relative to the location where the return address is stored. In fact, typical compiler generated code for the function will do exactly this. The compiler dedicates a register called EBP for this (Base Pointer). Another name for the same is frame pointer. The compiler typically, as the first thing for the function body, pushes the current EBP value on to the stack and sets the EBP to the current ESP. This means, once this is done, in any part of the function code, argument 1 is EBP+8 away, argument 2 is EBP+12(decimal) away, local variables are EBP-n away.

Tidbit: If Frame Pointer Omission is enabled for compilation, the compiler will not use a dedicated register for the frame pointer. This means EBP can be used like any other general purpose registers by the compiler. The downside to this is that the generated code is more complicated because, due to the lack of a fixed pointer acting as frame of reference, the compiler needs to keep recalculating where each of the arguments and local variables are as the function code proceeds depending on what registers are being touched.

Call Stack Reconstruction Using Frame Pointers

If Frame pointers, in other words dedicated EBP, are used, there is an interesting side effect. Note that the first thing a compiler-generated code would do on entering a function is to push the current EBP on the stack. This EBP value that was pushed is in fact the frame pointer of the calling function. Once this is pushed, the EBP value is the frame pointer for the called function. So, if you stop execution in a function and check the EBP value, it will be the frame pointer to the function. If you check the contents of the frame pointer location, the contents are actually the frame pointer for the previous function (in other words, the calling function). And if you take the value in that location and check what that is pointing to, it will be the frame pointer of the function that called it and so on. So, by using the EBP values pushed on the stack, you can, traverse through all the frames step by step. Interesting isn't it? So, at any given time, you can trace back and see what all the frames were, which indirectly means, you know what parameters were passed to each of the functions before it stopped execution at where you are. Take some time to digest this.

Now, wouldn't it be cool if you could, in addition to what parameters were passed, know the functions that were called? I mean, what good is knowing the parameters if you don't know what functions they are meant for? Well, it turns out, that information is available too. Referring to the figure above, you see that the caller's return address is at frame pointer+4 bytes away. Bingo. You can construct the whole story now. If you stop execution at any time (say funcC), you know from the current EBP what function called you (because you have the return address to funcB pushed at EBP+4). You also know where the stack frame of the function that called us is (in other words, the stack frame of funcB is at EBP+0). From this, you know the return address that your caller in turn has to return to (in other words, you know that funcB has to return to funcA). From the stack frame of funcB, you know what parameters were passed into funcB and so on. All this is pictorially shown below:

Function Calls, Part 3 (Frame Pointer and Local Variables)

Getting Down into the Disassembly View

Okay. It's time to get started on some hands-on work.

  • Fire up Visual Studio 2005. Choose Win32 as the project type and choose the Win32 Console Application template. Enter a name, say callstack. Click finish to create the project.
  • Press Alt+F7 to invoke the project properties. Now, to make learning easy, you turn off certain settings that cause the compiler to emit some code that will make it harder to understand the core concepts for this article. I will try to address the implications of these settings at a future time.
    • Go to Configuration Properties->C/C++->General. Herein, set Debug Information Format to Program Database(/Zi).
    • Go to Configuration Properties->C/C++->Code Generation. Herein, set Basic Runtime Checks to Default.
    • Go to Configuration Properties->Linker->General. Herein, set Enable Incremental Linking to No.
    • Hit OK.
  • Modify code to what you see below and put a breakpoint on Line 10 (place a caret on Line 10 and press F9 to insert a breakpoint):
  • [step1.png]

  • Press F7 to do a build.
  • Press F5 to start debugging. The program execution stops at Line 10 now.
  • Press Alt+5. This now brings up the registers window.
  • Press Alt+6. This brings up a memory watch window.
  • Place a caret on Line 10, right-click, and choose go to disassembly.
  • In the disassembly view, right-click again and make sure you have the following checked:
    • Show Address
    • Show Source Code
    • Show Code Bytes
  • Now, start analying the portions of the code. The code generated for each of the functions funcA, funcB, and funcC is like below:
  • [frmsetup.png]

    In the picture above, note how the DBP value is immediately pushed onto the stack on function entry for all three functions (circled in red). The value that is being pushed is the frame pointer value for the previous function and the current function is quickly saving it away before touching it. The frame pointer then is set to the current stack position by the second instruction. Similarly, just before issuing the ret instruction, the function body pops out the frame pointer which it saved on entry. So, from this point onwards, when the function returns to the caller, the EBP value for the caller is back to what it was.
  • Now, take a look at the portions circled in black.
  • [locvar.png]

    Note how the stack now is expanded by 8 bytes to make place for the local variables. Each int on a 32 bit system occupies 4 bytes of memory. Because funcC and funcB have 2 local int variables, you see the code generated to make space for them immediately before any code begins to execute. This code, however, is absent from the funcA body! Well, it doesn't have any local variables, so that makes perfect sense.
  • Next, take a look at the portions circled pink.
  • [frmptr.png]

    Note how from now on, EBP is used as a frame of reference to get to each local variable; in other words, the first local variable is at EBP-4 and the second one is at EBP-8. If there was code like below in funcC, which accesses the argument,
    void funcC(int c1, int c2)
    {
       int ca1 = 7;
       int ca2 = 8;
       c1 = 9;
       c2 = 10;
       return;
    }
    
    the disassembly would have these lines of code
       10:   c1 = 9;
    00401014 C7 45 08 09 00 00 00 mov    dword ptr [ebp+8],9
       11:   c2 = 10;
    0040101B C7 45 0C 0A 00 00 00 mov    dword ptr [ebp+0Ch],0Ah 
    
    Note how this time EBP+n values are used to access the data on the other side of the frame pointer. This demonstrates how the EBP acts as the anchor point to access the local variables and the function arguments.
  • Continuing further, look at the gray circled block below:
  • [restore.png]

    This code is to restore the ESP to back to where it was before the function was entered; in other words, it is simply undoing the stack expansion that it did to accomodate local variables. Again, this piece of code is missing for funcA because it does not have any local variables.
  • Finally, to complete the analysis, the highlighted portions below is the structure of call to function which you saw in Part 2. Note the arguments being pushed; the call instruction is being done followed by the cleanup code mandated by __cdecl calling convention.
  • [calls.png]

  • If at this breakpoint, if you were to launch memory window using Alt+6 combination and typed in EBP-8 for address, you would see the stack layout like below. See how each entry on the stack reveals the history of the calls that led to it.
  • [stksnap.png]

    Tidbit: The analysis of the stack that you just did is central to troubleshooting application crashes, where, in most cases all you have is the stack snapshot at the time of the stack. With this picture, it is often possible to trace the calls that have been made that led to the crash. This is on condition that the stack is okay. If the stack is corrupted for some reason, that too can be confirmed to some extent if one inspects the stack and sees wierd inconclusive data on the stack which goes against the normal program flow theory.

Summary

To summarise, this is what you have learned so far.

  • You learnt how the local variables are allocated on the stack.
  • The compiler uses a frame pointer to access local variables and function arguments.
  • Each function call results in creation of a frame. The frames stack one on top of another with each function call.
  • You learned how, when you have a snapshot of the stack at a given time, by using the EBP register, you can decipher the function calls that were made leading up to the point at which the snapshot was taken. This ability to back trace from the current point is of tremendouse help in debugging, especially post-mortem debugging.

References



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • IBM Worklight is a mobile application development platform that lets you extend your business to mobile devices. It is designed to provide an open, comprehensive platform to build, run and manage HTML5, hybrid and native mobile apps.

  • Agile methodologies give development and test teams the ability to build software at a faster rate than ever before. Combining DevOps with hybrid cloud architectures give teams not just the principles, but also the technology necessary to achieve their goals. By combining hybrid cloud and DevOps: IT departments maintain control, visibility, and security Dev/test teams remain agile and collaborative Organizational barriers are broken down Innovation and automation can thrive Download this white paper to …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds