Call Stack

This page was translated by a robot.

Each function in C and C++ represents a small subprogram in which the corresponding code is executed in a closed environment that is not visible from the outside. In order to ensure a closed environment, a data block is created when a function is called, which stores all the data required for the current program flow. As soon as the function has ended, the data block is cleared again.

Since function calls occur very frequently and are also nested within one another, these data blocks must be stored in a hierarchical order that is easy to expand. In C and C++, functions are thus stored on a stack . Since this stack is used for function calls, it is called the call stack .

Details

There is a stack for each thread of a process , which stores all the data required for the current program flow. A stack is allocated once by the runtime system at the start of a thread (or at the start of the process). In modern systems, the size of a stack is between a few dozen kilobytes and a few megabytes.

Each thread also saves a stack pointer, which designates the address within the stack from which no data has yet been written. In modern processors, the stack pointer occupies its own register and is therefore always available.

Each function has the ability to reserve a block of data on the stack. The data blocks are called stack frames and are reserved or cleared by simply adding or subtracting the stack pointer.



Current Stackpointer



Previous Stackpointer




Bottom of Stack
 |            ...            | 
 |        Unused space       |
 +===========================+
 |                           |
 |        Stack-Frame        |
 |                           |
 +===========================+
 |                           |
 | Data of calling functions |
 |           ...             |
 |                           |
 +===========================+

Depending on the processor, system, compiler and situation, different data is stored in a stack frame. It is not possible to give a general description of which data is stored, how, in which order, when and with which machine instructions. This page tries to show a basic principle of a function call. The data to be saved is roughly divided into the following parts: processor status, signature and local variables.

Storage of the Processor State

When a function is called, the system always jumps to the function's machine code and returns to the calling point at the end of the function. In order not to destroy the processor registers of the calling function during the execution of the function, they are saved (saved) in the stack frame. Saving all the necessary registers is also referred to as saving the processor state.

Depending on the processor, system, compiler or situation, different registers are stored. Some systems basically save all registers, others only save those that could be changed by the function and still others only save those that are intended for storage by the architecture. Nevertheless, two registers in particular are always saved: the stack pointer and the command pointer of the calling function.

The storage of the stack pointer and the command pointer is necessary in order to reset the stack to the previous state after the function has returned and to continue the code at the point at which the function has jumped. Depending on the system and architecture, certain details must be observed, which are not explained further here. Basically, when returning from a function, the processor must be restored to the state it was in before the function was called. This can be guaranteed by storing all necessary registers on the stack and restoring them on return.

A stack frame of a very simple function looks like this:



New Stackpointer






Old Stackpointer
 |            ...            | 
 |        Unused space       |
 +===========================+
 |         Register 0        |
 |         Register 1        |
 |         Register 2        |
 |           ...             |
 |     Old Stack-Pointer     |
 |    Instruction-Pointer    |
 +===========================+
 | Data of calling functions |
 |           ...             |

The names of the registers and the position of the old stack pointer and the instruction pointer have been chosen arbitrarily here on this page.

The following happens when a function is called: instruction pointer, stack pointer and all required registers are written onto the stack, starting with the current stack pointer. After that, the stack pointer is moved to the new location. The command pointer is then set to the address of the machine code of the function to be called and the process continues from there.

When returning from the function, the following happens: Starting from the current stack pointer, all values ​​are copied back to the appropriate register. At the very end, the stack pointer and the instruction pointer are also reset to the old value. It then continues with the next machine instruction after the instruction pointer.

Some modern processors have special commands for saving and restoring the status. However, this page will not discuss it further.

Signature: Arguments, Parameters and Return Value

When calling a function, arguments can be passed as input values. In order for these input values ​​to be available as parameters during the runtime of the function, the arguments are also saved on the stack. Furthermore, a function can return a value after completion. For a detailed description of the terms, see the pages about arguments and parameters and the return value .

The signature of a function specifies which parameters are expected by the function, in which order, and what type of return value is returned by the function. The calling function thus saves all input values ​​on the stack before calling the function and reserves space for the return value if necessary. This reservation happens by simply moving the stack pointer. After all arguments have been written, the processor status is saved as described above and the function is started. The stack frame of a function with arguments and return value looks like this:

New Stackpointer







Old Stackpointer
 +==========================+ 
 |         Registers        |
 +--------------------------+
 |        Returnvalue       |
 |        Parameter 1       |
 |        Parameter 2       |
 |        Parameter 3       |
 |           ...            |
 +==========================+
 | Data of calling function |
 |           ...            |

Since the compiler knows how large the register part of the stack frame is when compiling a function, the parameters can simply be represented by the address of the current stack pointer plus an offset. This means that all parameters and the return value can be addressed directly while the function is running.

Since all parameters within the called function are always addressed from the (new) stack pointer, any number of arguments can be passed using the arrangement of the parameters shown above. Such functions, which are called variadic , can read out these parameters using suitable macros .

When jumping back, the stack frame is cleared. First the registers are restored as described above. All arguments and the return value are then discarded by moving the stack pointer back to its original position. During compilation, the compiler knows how many bytes were reserved for the arguments and the return value and can therefore simply shift the stack pointer back by the same number of bytes. In order for the calling function to be able to use the return value despite clearing the stack frame, it must be copied to a local variable before clearing.

If a struct or an object of a class is expected as a return value, the entire object would have to be copied to a local variable using the copy constructor before the data block is broken down. To save this effort, the compiler will reserve enough space for the desired object in the local variables of the calling function. When the function to be called is called, a pointer to this location is passed instead of the return value. The called function can thus use this address to access the local variable of the calling function and store the desired return object there.

In principle, arguments and the return value could also be transferred via processor registers. This means that the data no longer has to be stored on the stack and the return value does not have to be copied back. In fact, this is also done for very simple functions. Nevertheless, the stack is still required in many cases. A simple reason for using the stack is that there are not enough registers.

Another reason is if the pointer of a value is to be addressed within the function. Since a processor register does not have an address, the value must be passed on the stack.

Another reason is when the processor registers are too small for the value to be stored. In today's 64-bit systems, this is usually no longer a problem with the basic types, but structs and objects can also be transferred. In the case of an object, a constructor must be called on the stack to instantiate the object, and when the function returns, a destructor in C++ must destroy this object again. The compiler will automatically build this into the function call. Depending on the frequency of a function call and the complexity of the constructors and destructors, this can have a major impact on runtime.

Local Variables

Local variables are created by a function simply by shifting the stack pointer the required number of bytes. When returning from the function, the stack pointer is reset to its original position. A stack frame with local variables looks like this:

New Stackpointer








Old Stackpointer
 +==========================+ 
 |     local Variable 1     |
 |     local Variable 2     |
 |     local Variable 3     |
 |           ...            |
 +--------------------------+
 |         Registers        |
 +--------------------------+
 |        Parameters        |
 +==========================+
 | Data of calling function |
 |           ...            |

Since a compiler knows exactly how many bytes are required for the local variables and for the registers during the translation of the function code, all parameters can be addressed from the current stack pointer using a simple offset.

Once a function has reserved space for its local variables on the stack, the data can be initialized. Constructors are thus called on the stack for objects. When returning from a function, the stack frame is cleared and the corresponding destructors are called for local objects in C++. Thus, the local variables of the function are considered invalid after the return. For this reason, no local objects (also called temporary objects in this context) should be used as return values.

Final Remarks

The different parts of a stack frame have been explained on this page according to a logical understanding. How a stack frame is actually built in a real system, with a real processor and a real compiler, can change at every turn. Some environments keep registers on top, others reserve spare bytes, and still others automatically optimize function calls so that a stack frame is completely obsolete.

Basically, a programmer does not have to concern himself with all the organization of the stack and other subtleties of function calls. The compiler does all the work, that's what it's there for. However, a programmer can use attributes to tell the compiler how the function should be called and how the parameters and local variables are stored. However, since modern compilers perform very good optimizations, dealing with attributes is almost uncommon today.

A programmer only comes into contact with the call stack when something doesn't work. Debuggers can use the call stack to read all local variables, parameters and processor registers and present them to the programmer. For the programmer, looking through the call stack is often a valuable help in finding out where in the program an error originated.

Because the call stack is such a valuable tool for debugging, exception handling was introduced in C++. A programmer can use this to describe directly in the code up to which point the stack should be automatically cleared if an exception or error occurs. Further information can be found in the try– catchstructure .

If the stack pointer runs out of the stack when a function is called (overflow), the system intervenes and causes the program to crash. This happens extremely rarely. Very often, a stack overflow indicates a faulty infinite recursion. However, even correct functions can call each other so often that the stack overflows. In this case another solution for the program has to be found.

Next Chapter: Structs