Understanding and mastering the great unknowns of embedded software
Author: Martin Gisbert, IAR Systems
Contribution – Embedded Software Engineering Congress 2015
Stack and heap are often mentioned together because both are non-static storage devices. Another unfortunate similarity is their limited determinism during access and the risks associated with overflows. This article provides an overview of how the stack works and tips for proper sizing. Since the heap, unlike the obligatory stack, is used relatively infrequently in embedded systems, this discussion of dynamic storage will be briefer.
Stack overview
The runtime stack, also called the stack memory, is a defined area in RAM whose size is specified by the user. The linker reserves this area and typically places the stack in the lower part of RAM, above the global and static variables. Access to the contents is via the stack pointer, which is set to the top of the stack during initialization; at runtime, the occupied portion of the stack grows downwards (see Figure 1)., PDF).
The stack holds local variables of a function unless they are allocated to registers by the compiler; this includes non-scalar variables such as arrays or structures. Once the function has finished executing, this data is removed from the stack. Additionally, the contents of registers and return addresses are placed on the stack when subfunctions are called. Function parameters may also be passed via the stack.
Stack overflow
Specifying the stack size merely tells the linker that it must not use this area for anything else. However, the microprocessor has no knowledge of the lower limit of the stack and will unconditionally exceed it if the stack requirements surpass the developer's estimate (see Figure 2)., PDF).
The result is a stack overflow, which means that global variables are overwritten with random data. Equally dangerous, however, is the reverse: the stack contents are overwritten if variables in the conflict zone are written to, thereby corrupting return addresses from functions, for example. In either case, the consequences are a malfunction or even a crash of the application. The goal must therefore be to choose a stack large enough to prevent overflows, but it shouldn't be too large either, as this would waste RAM.
Methods for correctly sizing the stack
There are several helpful methods for determining the required stack size. First, one can leverage the fact that the compiler knows the stack size of each function and documents it in the list file. Using appropriate tools such as the IAR Embedded Workbench, a stack analysis can be performed and an initial estimate calculated (Figure 1, see...). PDF).
Indirect function calls or recursions can be taken into account, but not the need for saved registers. Therefore, the actual need should be empirically verified. If the stack area is filled with a specific pattern during debugging, it can be determined after a complete test run how much of the stack remains untouched. Modern debuggers offer graphical support for this. Figure 4 (see PDF) shows an example of the C-SPY debugger of the IAR Embedded Workbench, which displays the proportion of „used“ stacks.
Another way to measure the maximum stack depth during runtime is to poll the stack pointer regularly, e.g. in a timer interrupt routine.
In the example in Figure 5 (see PDFAt the start of the program, i.e., with an empty stack, the upper limit of the stack is stored in the variable `highStack`. With each call to the polling routine, the current position of the stack pointer is read, and the value of the current stack depth is recorded. At the end of the test, `highStack` minus `lowStack` represents the maximum stack requirement, provided the polling was performed quickly enough.
To avoid potentially fatal consequences in the event of a stack overflow, a safety zone can be placed between the memory areas of the stack and variables (see Figure 6)., PDFThis security zone is initialized with a specific pattern that is regularly checked during execution. If the pattern has been overwritten, the stack has indeed overflowed, but no data has yet been corrupted, provided the security zone is sufficiently large.
The advantage of this method is that it can also be used outside the laboratory. If, during operation, regular tests detect write access to the safe zone, the device can, for example, be shut down in a controlled manner or at least a stack overflow warning can be displayed. If the microcontroller used has a memory protection unit, this can be used to monitor the safe zone. In the debugging environment, an access attempt can be immediately detected using a data breakpoint.
Dynamic memory management using the heap
The heap, similar to the stack, is a dedicated part of RAM where the application can dynamically allocate memory. Its most important functions are: malloc and free: Fields marked with malloc A block of a specific size is allocated on the heap, and the address of that block is returned. Variations include: calloc (Initializing the allocated memory with 0) and realloc (Increasing or decreasing the size of an already allocated block). If the memory is no longer needed, it is allocated using the free Function reactivated. If the requested memory cannot be provided, it returns. malloc the value NULL (see Figure 7, PDF).
The exact function of malloc The heap handler is not defined in the ANSI C standard. Therefore, the implementation of the heap handler is left to the user, unless they require a universal implementation such as... dlmalloc want to use it.
Unlike the stack, the heap is not mandatory for embedded systems and is very rarely used, not least because of its non-deterministic behavior: If the dynamically allocated objects have different sizes, the heap becomes fragmented over time. Allocating memory thus takes increasingly longer and may eventually become impossible. Furthermore, the heap is very easily corrupted, for example, by repeatedly freeing a memory area or accessing it from outside the boundaries of an allocated object. Since the heap is not linear memory but also contains pointers to other blocks, the dynamic memory can be completely destroyed and can only be restored by a reboot.
Most heap errors can be detected by using a "wrapper". This involves inserting an additional field before and after the actual block when allocating memory, which monitors the consistency of the heap (see Figure 8)., PDF).
Two additional heap functions are used for the wrapper, e.g. MyMalloc and MyFree implemented. MyMalloc calls malloc but allocates 8 more bytes, which are initialized with a specific pattern and the size of the block. MyFree checked before calling free, whether the fill pattern is still intact, and otherwise terminates with an error message.
Summary
The runtime stack often only becomes apparent when it overflows, causing the program to malfunction or even crash. Therefore, it's crucial to ensure that the stack is sufficiently large for all use cases to prevent potentially disastrous surprises later on. Dynamic memory should be avoided altogether in embedded systems whenever possible. However, implementing a wrapper can be helpful in at least detecting a corrupted heap.
Implementation – our training & coaching
Do you want to bring yourself up to date with the latest technology?
Then find out more here MircoConsult offers training courses/seminars/workshops and individual coaching on the topic of implementation/embedded and real-time software development.
Training & coaching on the other topics in our portfolio can be found here. here.
Implementation – Expertise
Valuable expertise in the field of implementation/embedded and real-time software development is available. here Available for you to download free of charge.
You can find expertise on other topics in our portfolio here. here.
