Function Call: Stack Frame
Let's look at the following code:
It is a simple program, but what exactly happened when the program is running? You may have two questions:
After main() called sum(), how does the program know which function to return back to?
After sum() returns to main(), how does the program know which line to continue?
Now let's start from assembly language to see what happens in the black box.
Stack Frame
Every time a function is called, an independent stack frame is created on the stack. We use two registers, ebp and esp to determine the scope. ebp points to the bottom of the stack, while esp always points to the top of the stack.
In the program above, we first assign values to integers a and b. Each integer is 4 bytes in size, so we push them into the stack with
Then we meet the next commands. We assign the return value of sum() to variable ret. How does that happen?
Function Call
After a function is called, we first push all the arguments into the stack. In C and C++, the push order is from right to left, so we push b and a in order.
Then we need push the address of the next command into stack, the call command does so. This ensures that we can find our way to continue after we return from the function.
Now we enter sum(). Remember that stack frame is independent for each function, so we need to adjust it. First, we push the address of caller's ebp into the stack, so that we can restore it after. Then, we set ebp to esp, which means that the new stack frame is on the top of the older one. Finally, we need to assign memory space for the new stack frame. In Visual Studio, the compiler will automatically assign an value 0xCCCCCCCC to it, while not in g++ or gcc.
Then inside sum(), we calculate the sum of a and b, and store the result into temp.
Function Return
When we return from a function, we also need to return its stack frame back to the system. If a function has a return value, it first store the value inside register eax. Then it recycle the local variable space by move esp to the bottom. By popping the old address out, we restore ebp to the bottom of the caller function.
Now we want to go back to where we called the function. ret command pop the current element on top of the stack, and put it inside a PC register. PC register always store the address of the next command for the CPU to execute, so we are able to jump to the position we store before.
After that, the function parameters are no longer useful, we can simply pop them out. Then we assign the return value from eax to ret.
Now the stack frame remains the same as initially. All memories related to the called function has been eliminated.
At Last
This is all about the underlying principle of function call and return. Now we are able to answer these two questions at the beginning. You may also figure out what's the problem in the following code, and how to avoid this in your own codes.
Last updated