Mastering stack and heap for system reliability: Part 1 – Calculating stack size
Editor's note: In this three part series, Anders Lundgren and Lotta Frimanson take you step by step through the techniques for allocating stack and heap in embedded systems for a robust design that optimizes use of memory resources. In part 1, they describe methods for reliable calculation of the required stack size and detection of stack-related problems.
The stack and the heap are random access memory (RAM) allocations that are fundamental to an embedded system. Setting them up properly is essential to system stability and reliability. Incorrectly used, they may cause your system to wreak havoc in strange ways. Stack and heap memory must be allocated statically by the programmer, but calculating the space required for the stack space is notoriously difficult for all but the smallest embedded systems.
Underestimating stack usage can lead to serious runtime errors that can be difficult to find, while over-estimating stack usage wastes memory resources. Heap memory overflows also can have significant an impact on system behavior, and can be notoriously difficult to debug. In this article, we’ll take a closer look at stacks and heaps for embedded systems, discussing principles and methods for reliable stack and heap design and some special issues that arise in small embedded systems.
Desktop systems and embedded systems share some common stack and heap design errors and considerations, but differ completely in many other aspects. One example of a difference between these environments is the available memory. Windows and Linux default to 1 and 8 Mbytes of stack space; a number that can be increased even more. Heap space is only limited by the available physical memory and/or page file size.
Embedded systems, on the other hand, have very limited memory resources, especially when it comes to RAM space. There is clearly a need to minimize stack and heap in this restricted memory environment. A common issue for small embedded systems is that there is no virtual memory mechanism; allocation of stack, heap and global data (i.e. variables, TCP/IP, USB buffers, etc.) is static and performed at the time the application is built.
The stack is an area of RAM where a program stores temporary data during the execution of code blocks. The stack is statically allocated and operates on a “last in first out” basis. The life span of variables on the stack is limited to the duration of the function. As soon as the function returns, the used stack memory will be free for use by subsequent function calls.
The types of data stored in the stack include:
- local variables
- return addresses
- function arguments
- compiler temporaries
- interrupt contexts
Stack memory has to be allocated statically by the developer. The stack usually grows downward in memory. If the memory area allocated for the stack isn’t large enough, the executing code writes to the area allocated below the stack and an overflow situation occurs. The area written to is usually the area where global and static variables are stored (see Figure 1). As a result, underestimating stack usage can lead to serious runtime errors such as overwritten variables, wild pointers, and corrupted return addresses. All of these errors can be difficult to find. At the same time, overestimating stack usage wastes memory resources, which increases cost.
Figure 1: When the data to be stored is larger than stack capacity, it typically gets written to the memory area allocated for global and static variables.
Determining worst-case maximum stack depth is useful in most embedded projects, as it allows you to allocate only as much RAM as needed for the stack while leaving the rest free for the developer to use in their application. We will highlight some methods that can be used to reliably calculate the required stack size and detect stack related problems.
The heap is an area of RAM that represents the dynamic memory of the system. When one module does not need its allocated memory anymore, the developer should return it to the memory allocator to be reused by some other module. Dynamic memory makes memory sharing possible between different pieces of a program. As with the stack allocation, it is important to minimize heap usage in small embedded systems; indeed, dynamic memory and the heap can in many cases be considered optional in small embedded systems.
Some examples of data that is placed on the heap include:
- Transient data objects
- C++ new/delete
- C++ STL containers
- C++ exceptions
Because of the dynamic behavior of the application, calculating heap space in larger systems ranges from difficult to impossible. Moreover, there is not much tool support in the embedded world for measuring heap utilization, but we will discuss some methods to approximate and measure the heap usage of an application.
It is important to maintain heap integrity. Allocated data space is typically interspersed with critical memory allocator housekeeping data. Inefficient use of the allocated data space will corrupt the entire heap memory area and most likely result in an application crash with few traces of how the crash happened. We will discuss some methods to aid in checking for heap integrity.
Another aspect to consider is that the real-time performance of the heap is not deterministic. Memory allocation depends on such factors as previous use and the requested data space size. This ad hoc behavior makes testing and debugging heap-related problems exceedingly difficult.
Managing stacks and heaps
Stretching the limits in everyday life can be rewarding but can sometimes cause you trouble. Stretching the limits in programming when it comes to allocated data will definitely cause you trouble. If you’re lucky, that trouble will arise during system testing, but it might also manifest itself only when the product has already been delivered to thousands or millions of customers, thus necessitating an expensive recall or an embarrassing need for a software patch.
Overflowing allocated data can occur in all three storage areas: global memory, the stack, and the heap. Writing to arrays or pointer references can cause accesses outside of the memory allocated to the object. Some array accesses can be validated by static analysis, for example by the compiler issuing a warning message, a MISRA C checker, or by standalone static analysis tools) as follows:
array = 0x1234;
When the array index is a variable expression, it is difficult for static analysis to find all problems. Pointer references are also hard to trace by static analysis, as this example shows:
int* p = malloc(32 * sizeof(int));
p += 35;
*p = 0x1234;
Runtime methods to catch object overflow errors have been available for desktop systems for a long time, e.g. Purify, Insure++, and Valgrind, to name a few. These tools work by instrumenting the application code to validate memory references at runtime. This comes at the price of slowing down application execution speed, dramatically increasing code size, and has thus not become a viable method for small embedded systems.
Small embedded systems require a more streamlined approach to detecting overflow errors, but a better strategy is to avoid overflow entirely by properly allocating the stack and the heap. In Part 2 of this article, we take a closer look at stack challenges and tools for addressing them. In Part 3, we focus on heaps.
References1. Nigel Jones, "Computing Your Stack Size: Stack Overflow"
2. John Regehr, “Say no to stack overflow,” EE Times Design, 2004.
3. Carnegie Mellon University, “Secure Coding in C and C++, Module 4, Dynamic Memory Management,” 2010.
Anders Lundgren has been with IAR Systems since 1997. He currently works as product manager for the IAR Embedded Workbench for ARM. During the first years with IAR Systems he worked with compiler development and as project manager for compiler and debugger projects. Prior to joining IAR Systems, Lundgren worked with space science instruments at the European Space Agency and spent one year at the space science laboratory at the University of California, Berkeley. He received a M.Sc. in Computer Science from the University of Uppsala, Sweden in 1986.
Lotta Frimanson has been with IAR Systems since 1999. She currently works as product manager for IAR Embedded Workbench for ARM and MSP430, and is also responsible for the IAR RTOS partner program. Prior to joining IAR Systems, Frimanson worked with embedded systems development both at the bioscience company Biacore and at the consultant company Styrex. She received a M.Sc. in Engineering Physics from the University of Uppsala, Sweden in 1989.