Last Updated: 2015-06-17 Wed 16:26

Function Call Stack Examples

Table of Contents

1 Function Call Stack Demonstrations

This document goes through several examples of how the function call stack works in C. The function call stack (often referred to just as the call stack or the stack) is responsible for maintaining the local variables and parameters during function execution. It is often only alluded to in textbooks but plays a central role in the execution of C programs (and just about every other modern programming language too).

Understanding how the call stack works explains how functions actually "work" including the following.

  • The difference between function arguments and variables used to call functions
  • Why several variables with the same name but residing in different function can co-exist
  • Why there are certain limitations on what functions can do
  • Why local variables that are not initialized might have any value contained in them.

Warning

The call stack is a dynamic entity: its contents and size change, growing as functions are called and shrinking as functions complete. This makes it a little difficult to explain with static text and pictures just as describing how a program which uses a loop can be difficult to describe due to the things changing in each iteration of a loop. This document makes a stab at showing stack behaviors but it will take some patient reading and analysis.

Assumptions

  • ints are 4 bytes (32-bits) and doubles are 8 bytes (64-bits). This is the case on many modern platforms but may vary somewhat and is not guaranteed by the C standard.
  • The starting address of the first stack frame is chosen arbitrarily. In most cases, it is not important where it starts, only to understand the mechanics of how it grows and shrinks.

Suggestion

Open up the source code associated with this document in a different program or browser window. View the stack behavior in the browser and follow along in source code as steps are taken.

Side-by-side Viewing

Figure 1: Viewing source code and this document side-by-side

2 Example 1: Simple Calls

Source Code simple_calls.c

 1: #include <stdio.h> 
 2: 
 3: int mogrify(int a, int b){
 4:   int tmp = a*4 - b / 3;                  // First line of mogrify (mogrify)
 5:   return tmp;                             // (mogrify_return)
 6: }
 7: double truly_half(int x){
 8:   double tmp = x / 2.0;                   // First line of turly_half (truly_half)
 9:   return tmp;                             // (truly_half_return)
10: }
11: int main(){
12:   int a = 7, y = 17;                      // First line of main (main)
13:   int mog = mogrify(a,y);                 // Call to mogrify (mogrify_call)
14:   printf("Done with mogrify\n");          // (first_print)
15: 
16:   double x = truly_half(y);               // Call to truly_half (truly_half_call)
17:   printf("Done with truly_half\n");       // (second_print)
18: 
19:   a = mogrify(x,mog);                     // (mogrify2)
20: 
21:   printf("Results: %d %lf\n",mog,x);      // (last_print)
22:   return 0;                               // (main_return)
23: }

Compile and run

lila [stack-demo-code]% gcc simple_calls.c
lila [stack-demo-code]% ./a.out
Done with mogrify
Done with truly_half
Results: 23 8.500000

Call Stack behavior

Like all programs, control for the program starts in the main() function with the first line. main() has 3 local variables: a,y which are ints and x which is a double. The initial state of the stack is as follows.

main() called

Method Line Var Value Addr Notes
main() 12 a ? 1024  
    y ? 1028  
    mog ? 1032  
    x ? 1036  

Notice that at the beginning of running main (line 12), stack space is allocated for all of its local variables but none of them have defined values yet. C programs do not guarantee local variables are initialized to anything and it is a mistake to use a variable value without first ensuring it has a well-defined value.

After moving ahead one line in main, locals a,y have defined values. This probably constitutes several low-level machine/assembly instructions.

One line of main() executed

Method Line Var Value Addr Notes
main() 13 a 7 1024  
    y 17 1028  
    mog ? 1032  
    x ? 1036  

At line 13 a different function is invoked. Control is suspended in main until the function mogrify completes. Calling a function pushes another stack frame onto the call stack which has enough space for the arguments to the function any local variables as shown below.

mogrify() called

Method Line Var Value Addr Notes
main() 13 a 7 1024  
    y 17 1028  
    mog ? 1032  
    x ? 1036 double variable
mogrify() 4 a 7 1044  
    b 17 1048  
    tmp ? 1052  

Notice that the new stack frame starts almost immediately after the frame for main. There may be a small amount of additional space required to deal with return values or register saves at the low level, but in our high-level view this doesn't matter too much and we will always start the stack frames as close to each other as possible.

Notice also that the difference in the locations is 8 bytes: this is because the variable x in main is a double so takes 8 bytes of space whereas all the other variables seen so far take 4 bytes as they are integers.

Control now starts at the beginning of mogrify but will eventually return to main at which point it will resume execution on line 13 completing that line and moving forward.

Finally, notice that the parameters a,b to mogrify have values defined. This is as a result of main calling the function and passing actual values in for those parameters. The local variable tmp does not yet have known value associated with it as the first line of mogrify has not yet executed.

mogrify() first line executed

Method Line Var Value Addr Notes
main() 13 a 7 1024  
    y 17 1028  
    mog ? 1032  
    x ? 1036 double variable
mogrify() 5 a 7 1044  
    b 17 1048  
    tmp 23 1052  

After executing one line of mogrify, the local variable tmp takes on a value due to its assignment. Make sure you understand the integer arithmetic behind this assignment (17 / 3 is 5 remainder 2 in integer division).

mogrify() second line (return) executed

Method Line Var Value Addr Notes
main() 13 a 7 1024  
    y 17 1028  
    mog 23 1032  
    x ? 1036 double variable

The second line of mogrify is to return a value to the calling function. This has two effects.

  • The return value is stored wherever it was intended from the calling function: line 13 of main stores the value in variable mog.
  • Returning pops the stack frame for mogrify off the call stack leaving it in the state above.

Control now resumes in main by advancing one line forward.

Method Line Var Value Addr Notes
main() 14 a 7 1024  
    y 17 1028  
    mog 23 1032  
    x ? 1036 double variable

Executing a printf

Method Line Var Value Addr Notes
main() 14 a 7 1024  
    y 17 1028  
    mog 23 1032  
    x ? 1036 double variable
printf() library call fmt ?? 1044 4-byte pointer
    ?? 7 1048  

printf is like other functions in that it will push another stack frame onto the stack with space for its arguments and local variables. printf is a little unique in that it is a variadic function: it can take any number of arguments so long as the arguments coincide with the format string. Analyzing the code for printf is not pertinent to gaining a basic understanding of the function call stack so we'll presume printf does its business, puts something on the screen like

Done with mogrify

and returns which pops its stack frame. Control resumes in main at the next relevant line of code.

Second function call

Method Line Var Value Addr Notes
main() 16 a 7 1024  
    y 17 1028  
    mog 23 1032  
    x ? 1036 double variable

At line 16, another function is called which pushes another stack frame onto the call stack.

truly_half() called

Method Line Var Value Addr Notes
main() 16 a 7 1024  
    y 17 1028  
    mog 23 1032  
    x ? 1036 double variable
truly_half() 8 x 17 1044  
    tmp ? 1048 double variable

When truly_half is called, its stack frame is pushed on below main; notice that the memory addresses for its local variables are identical to previous function calls to mogrify and printf. Space on the stack is re-used by subsequent function calls. This has implications for the values of variables that are not assigned values which we may discuss later.

The first line of truly_half assigns tmp as follows.

truly_half() first line executed

Method Line Var Value Addr Notes
main() 16 a 7 1024  
    y 17 1028  
    mog 23 1032  
    x ? 1036 double variable
truly_half() 9 x 17 1044  
    tmp 8.5 1048 double variable

Executing the next line of truly_half returns this value to assign the local variable x in main and pops the stack frame of truly_half off the call stack.

Control returns to main

Method Line Var Value Addr Notes
main() 17 a 7 1024  
    y 17 1028  
    mog 23 1032  
    x 8.5 1036 double variable

In main another result is printed

Done with truly_half

and control advances. Line 19 calls mogrify again with different arguments

mogrify called again

Method Line Var Value Addr Notes
main() 19 a 7 1024  
    y 17 1028  
    mog 23 1032  
    x 8.5 1036 double variable
mogrify() 4 a 8 1044 caste to int
    b 23 1048  
    tmp ? 1052  

Control in main is suspended at a different location than the first call to mogrify (line 19 this time) but control begins again in mogrify at its first line (line 4).

Notice that the first argument to mogrify in this call is a little interesting: it was passed as a double with value 8.5 but appears as an int with value 8. The compiler has automatically inserted low-level instructions to caste the 8-byte floating point value to a 4-byte integer value. Most of the time this is nice convenience which saves programmers the trouble of writing such code but it can create subtle bugs if the programmer did not intend for such a conversion to happen.

mogrify first line executed again

Method Line Var Value Addr Notes
main() 19 a 7 1024  
    y 17 1028  
    mog 23 1032  
    x 8.5 1036 double variable
mogrify() 5 a 8 1044 caste to int
    b 23 1048  
    tmp 25 1052  

mogrify second line executed again

Method Line Var Value Addr Notes
main() 19 a 25 1024  
    y 17 1028  
    mog 23 1032  
    x 8.5 1036 double variable

mogrify completes assigning its result to local variable a in main and popping its stack frame off.

The program completes by printing the final results

Results: 23 8.500000

and returning 0. Two things to note here:

  • main is also a function which returns meaning its stack frame will be popped off and control will be returned to the mysterious and powerful C runtime system which is responsible for setting up main to run in the first place.
  • The integer return value from main is passed back to whatever entity ran the program in the first place. Unix refers to this value as the exit status of a program. The unix convention is that a 0 exit status indicates everything went normally for the program while a non-zero return corresponds in some way to an error that occurred. Shell programs can access the return codes of programs to detect, for instance, that gcc did not succeed in compiling some code or that the search program grep did not find anything matching search terms.

3 Example 2: In-class callstack.c

Source Code

 1: #include <stdio.h>
 2: 
 3: double f1(double x){
 4:   return x+1.0;                 // (f1_1)
 5: }
 6: 
 7: double f2(double x){
 8:   double tmp = f1(x);           // (f2_1)
 9:   double z = f1(x+1);           // (f2_2)
10:   return (z+ tmp) / 2;          // (f2_3)
11: }
12: 
13: double f3(double x, double y){
14:   double z = f1(1);             // (f3_1)
15:   double tmp1 = x*z;            // (f3_2)
16:   double tmp2 = f2(y);          // (f3_3)
17:   return tmp1+tmp2;             // (f3_4)
18: }
19: 
20: int main(){
21:   double x = 2;                 // (main_1)
22:   double y = f3(x, x+3);        // (main_2)
23:   printf("%.3lf\n",y);          // (main_3)
24:   return 0;                     // (main_4)
25: }

Compile and run

lila [stack-demo-code]% gcc callstack.c
lila [stack-demo-code]% a.out
10.500

Call stack behavior

This is a somewhat more involved example. The computation doesn't do anything particularly interesting but is a good demonstration of how the stack can grow and shrink as one function calls another function. Little explanation is given in each step so pay careful attention to which line is being executed and remember that after a return is executed, control returns to the previous function and a stack frame is popped off.

Two minor notes

  • All of the variables in this example are double so are assumed to be 8 bytes which means the size of memory cells will differ by 8
  • The stack starts at memory address 2048 this time. In examples it is fairly arbitrary where the frame for main starts in the stack but after specifying its location, the behavior of the program follows a well-defined patter.

main() called

Method Line Var Value Addr Notes
main() 21 x ? 2048  
    y ? 2056  

main() line 1 finished

Method Line Var Value Addr Notes
main() 22 x 2.0 2048  
    y ? 2056  

f3 called

Method Line Var Value Addr Notes
main() 22 x 2.0 2048  
    y ? 2056  
f3() 14 x 2.0 2064  
    y 5.0 2072  
    z ? 2080  
    tmp1 ? 2088  
    tmp2 ? 2096  

f3 calls f1 on line 1

Method Line Var Value Addr Notes
main() 22 x 2.0 2048  
    y ? 2056  
f3() 14 x 2.0 2064  
    y 5.0 2072  
    z ? 2080  
    tmp1 ? 2088  
    tmp2 ? 2096  
f1 4 x 1.0 2104  

f1 returns, stack frame popped, f3 advances to next line

Method Line Var Value Addr Notes
main() 22 x 2.0 2048  
    y ? 2056  
f3() 15 x 2.0 2064  
    y 5.0 2072  
    z 2.0 2080  
    tmp1 ? 2088  
    tmp2 ? 2096  

f3 executes second line

Method Line Var Value Addr Notes
main() 22 x 2.0 2048  
    y ? 2056  
f3() 16 x 2.0 2064  
    y 5.0 2072  
    z 2.0 2080  
    tmp1 4.0 2088  
    tmp2 ? 2096  

f3 calls f2 on third line

Method Line Var Value Addr Notes
main() 22 x 2.0 2048  
    y ? 2056  
f3() 16 x 2.0 2064  
    y 5.0 2072  
    z 2.0 2080  
    tmp1 4.0 2088  
    tmp2 ? 2096  
f2() 8 x 5.0 2104  
    tmp ? 2112  
    z ? 2120  

f2 calls f1 on first line

Method Line Var Value Addr Notes
main() 22 x 2.0 2048  
    y ? 2056  
f3() 16 x 2.0 2064  
    y 5.0 2072  
    z 2.0 2080  
    tmp1 4.0 2088  
    tmp2 ? 2096  
f2() 8 x 5.0 2104  
    tmp ? 2112  
    z ? 2120  
f1() 4 x 5.0 2128  

f1 returns to f2, f2 advances to next line

Method Line Var Value Addr Notes
main() 22 x 2.0 2048  
    y ? 2056  
f3() 16 x 2.0 2064  
    y 5.0 2072  
    z 2.0 2080  
    tmp1 4.0 2088  
    tmp2 ? 2096  
f2() 9 x 5.0 2104  
    tmp 6.0 2112  
    z ? 2120  

f2 calls f1 on second line

Method Line Var Value Addr Notes
main() 22 x 2.0 2048  
    y ? 2056  
f3() 16 x 2.0 2064  
    y 5.0 2072  
    z 2.0 2080  
    tmp1 4.0 2088  
    tmp2 ? 2096  
f2() 9 x 5.0 2104  
    tmp 6.0 2112  
    z ? 2120  
f1() 4 x 6.0 2128  

f1 returns to f2, f2 advances to next line

Method Line Var Value Addr Notes
main() 22 x 2.0 2048  
    y ? 2056  
f3() 16 x 2.0 2064  
    y 5.0 2072  
    z 2.0 2080  
    tmp1 4.0 2088  
    tmp2 ? 2096  
f2() 10 x 5.0 2104  
    tmp 6.0 2112  
    z 7.0 2120  

f2 returns control to f3, f3 advances to next line

Method Line Var Value Addr Notes
main() 22 x 2.0 2048  
    y ? 2056  
f3() 17 x 2.0 2064  
    y 5.0 2072  
    z 2.0 2080  
    tmp1 4.0 2088  
    tmp2 6.5 2096  

f3 returns control to main, main advances to next

Method Line Var Value Addr Notes
main() 23 x 2.0 2048  
    y 10.5 2056  

main calls printf on line 3

Method Line Var Value Addr Notes
main() 23 x 2.0 2048  
    y 10.5 2056  
printf() library call fmt ? 2064 4-byte pointer
    ? 10.5 2068  

Output

10.500

printf returns, main advances to next line

Method Line Var Value Addr Notes
main() 24 x 2.0 2048  
    y 10.5 2056  

main returns 0, program ends

4 Example 3: Swapping variables in try_swap.c

Source Code

 1: /* Demonstration of call-by value and call stack  */
 2: #include <stdio.h>
 3: void swap_ints(int a, int b){
 4:   int tmp = a;                  // (swap_ints_1)
 5:   a = b;                        // (swap_ints_2)
 6:   b = tmp;                      // (swap_ints_3)
 7:   return;                       // (swap_ints_4)
 8: }
 9: int main(){
10:   int x=20, y=50;               // (main_1)
11:   printf("x=%d  y=%d\n",x,y);   // (main_2)
12: 
13:   swap_ints(x,y);               // (main_3)
14:   /* What gets printed here? */ 
15:   printf("x=%d  y=%d\n",x,y);   // (main_4)
16:   return 0;                     // (main_5)
17: }

Compile and run

lila [stack-demo-code]% gcc try_swap.c 
lila [stack-demo-code]% a.out
x=20  y=50
x=20  y=50

Call stack behavior

It is common to want to swap the values of two variables. Unfortunately, writing a function to carry out this task in the most obvious way proves fruitless. One can understand why the swap_ints() function above does not actually swap values using the call stack. C allocates new memory for the arguments to functions and the actual argument values are copied into this area. Thus arguments within the function are distinct from any variables that were used to call the function. Changes to function parameters do not affect any other variables.

main() called

As usual, execution starts in the main function.

Method Line Var Value Addr Notes
main() 21 x ? 2048  
    y ? 2052  

main() executes first line

Method Line Var Value Addr Notes
main() 22 x 20 2048  
    y 50 2052  

main calls printf

Method Line Var Value Addr Notes
main() 22 x 20 2048  
    y 50 2052  
printf() library call fmt ? 2056 4-byte pointer
    ? 20 2060  
    ? 50 2064  

Output

x=20  y=50

printf returns, main advances one line

Method Line Var Value Addr Notes
main() 23 x 20 2048  
    y 50 2052  

main calls swap_ints

Method Line Var Value Addr Notes
main() 23 x 20 2048  
    y 50 2052  
swapints() 4 a 20 2056  
    b 50 2060  
    tmp ? 2064  

swap_ints executes first line

Method Line Var Value Addr Notes
main() 23 x 20 2048  
    y 50 2052  
swapints() 5 a 20 2056  
    b 50 2060  
    tmp 20 2064  

swap_ints executes second line

Method Line Var Value Addr Notes
main() 23 x 20 2048  
    y 50 2052  
swapints() 6 a 50 2056  
    b 50 2060  
    tmp 20 2064  

swap_ints executes third line

Method Line Var Value Addr Notes
main() 23 x 20 2048  
    y 50 2052  
swapints() 7 a 50 2056  
    b 20 2060  
    tmp 20 2064  

swap_ints returns

Method Line Var Value Addr Notes
main() 23 x 20 2048  
    y 50 2052  

Note that the return type of swap_ints is void: it is not meant to return anything, only have side-effects by altering something about the program. This makes line 7

return;

perfectly valid. Unfortunately, the desired side-effect of swapping the variables x,y in main has not been achieved. On advancing to the next line and printing, the output clearly shows that x,y have retained their original values.

x=20  y=50

This is because during the execution of swap_ints, the function had its own copies of the values 20,50 which referred to with names a,b. These are distinct from the x,y in main and change independently. This the swapping done in swap_ints does not affect any values in main.

Real Swap Functions

It is not possible to use a simple function definition to get variable swapping to work. This is due to the call-by-value semantics C uses for function invocation. Instead, one must resort to more complex means to get swapping to work. Two common options are employed: (1) use a preprocessor macro, or (2) use pointers and addresses.

(1) Use a Preprocessor macro

The C preprocessor can be used to perform textural transformations on a program which is useful for swap-like operations. This is discussed in detail on stack overflow. For a simple integer swapping, the following SWAP macro will work.

 1: /* Demonstration of call-by value and call stack  */
 2: #include <stdio.h>
 3: 
 4: #define SWAP(a,b) {int tmp=a; a=b; b=a;}
 5: 
 6: int main(){
 7:   int x=20, y=50;
 8:   printf("x=%d  y=%d\n",x,y);
 9:   
10:   SWAP(x,y);
11: 
12:   printf("x=%d  y=%d\n",x,y);
13:   return 0;
14: }

Demonstration

lila [stack-demo-code]% gcc swap_macro.c 
lila [stack-demo-code]% a.out
x=20  y=50
x=50  y=50

Note that the #define create what appears to be a function but it is really a textural transformation. After running the preprocessor, the program will be transformed to the following.

1: int main(){
2:   int x=20, y=50;
3:   printf("x=%d  y=%d\n",x,y);
4: 
5:   {int tmp=x; x=y; y=x;};  // (macro_expansion)
6: 
7:   printf("x=%d  y=%d\n",x,y);
8:   return 0;
9: }

The preprocessor has substituted the definition of SWAP(a,b) with the macro expansion at line 5.

(2) Use Pointers and Addresses

Pointer manipulations allow a swap function to be defined. We will revisit swapping when we discuss pointers.

5 Terminology

function call stack
An area of program memory that is used to manage function calls. Program memory can be roughly divided into 4 parts:
  • the function call stack which supports functions
  • the heap (or store) which supports dynamic memory allocation,
  • the global variable area which stores values for global variables
  • the code area which stores the actual instructions for the running program which are usually not changed during execution
stack frame
A portion of memory in the function call stack associated with a single running function. Functions that have many parameters and local variables will have larger stack frames than those with few parameters and local variables. The compiler is able to determine during compilation the stack frame size for a function. There are usually as many stack frames on the stack as there are functions that have yet to return.
pushing a frame
When a function is called, a new frame is pushed onto the "top" of the function call stack. The frame will be big enough to hold all of the parameters to the function being called plus the space required for all local variables in a function.
popping a frame
When a function finishes executing, it returns control to the function it which called/invoked it. The frame associated with the returning function is removed from the top of the stack.
active function / frame
In serial programs (non-parallel programs), there is only one active function at any given moment in the program. This function is usually the one at the "top" of the function call stack and the active frame refers to the frame at the top of the call stack. Pushing and popping happen only at the top of the stack and change the active frame.
stack overflow
Memory is a finite resource and if too many functions are called before any returns, a program can run out of space on the stack. This usually happens only in the case of recursive functions that are misbehaving but for programs with very tight memory constraints it may happen in some kinds of programs.

Author: Chris Kauffman (kauffman@cs.gmu.edu)
Date: 2015-06-17 Wed 16:26