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) anddoubles
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.
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 variablemog
. - 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
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
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
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 upmain
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, thatgcc
did not succeed in compiling some code or that the search programgrep
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
f3 calls f1 on line 1
f1 returns, stack frame popped, f3 advances to next line
f3 executes second line
f3 calls f2 on third line
f2 calls f1 on first line
f1 returns to f2, f2 advances to next line
f2 calls f1 on second line
f1 returns to f2, f2 advances to next line
f2 returns control to f3, f3 advances to next line
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
swap_ints
executes first line
swap_ints
executes second line
swap_ints
executes third line
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.