A stack is like a special buffer, or working memory, where processes are tracked. The stack is where processes or tasks “keep notes” on what they need to do the next time the processor becomes available. The stack works “last in, first out,” and tracks local variables as they get pushed onto and popped off the stack. The stack tracks function arguments, stores local variables, compiler temporaries, return addresses, and interrupt contexts. The life span of tracking a function on the stack lasts only as long as the duration of the function, so the stack contents are constantly changing.
Stack overflow is when a function or program uses more memory than is in the stack. As it grows beyond its allocated space, the dynamic stack contents begin to overwrite other things, such as critical application code and data. The stack is a fixed amount of memory that is set statically by the programmer before execution. How do you set a stack up properly? If you set the stack up with too little space, with not enough memory allocated to the stack, then the stack is going to overflow. If you set the stack too large, you waste memory that could be used elsewhere. Although stack overflow is an equally terrible event for either non-embedded (desktop) or embedded, memory is much more critical for embedded applications. Thus precautions for stack overflow are trickier. If there’s a stack overflow, unless there’s a memory protection unit on the core that you are working with, the stack can grow beyond its allocated space.
If the stack grows into the global area, variables get overwritten, pointers get pushed “off” where they are supposed to be and “go wild,” and corrupted return addresses are out of control. In most instances, these errors are hard to catch, as there you can only find some fragments of what has happened after the stack has grown beyond itself and been corrupted. Stack overflow errors are difficult to detect.
Steps to Prevent Stack Overflow
One method to prevent stack overflow is to track the stack pointer with test and measurement methods. Use timer interrupts that periodically check the location of the stack pointer, record the largest value, and watch that it does not grow beyond that value. This method can be tricky, however, in determining how often the timer interrupt checks the stack pointer. If not often enough, it’s possible to miss the stage where it grows beyond the largest value, and if too often, problems can arise that impact performance such that code doesn’t respond as quickly as it could.
Also effective are “stack guard zones,” an area that will show writes as they happen. A stack guard zone is a chunk of allocated memory located immediately below the stack. The stack will leave evidence there if it overflows. A similar method is to set data breakpoints at the top of a stack. Stack overflow will trip a data breakpoint, so it catches the overflow, making it possible to see exactly where the programmer is in his/her source code.
One other way is to fill the entire stack space with a particular repeating bit pattern before execution. Then, when the programmer does test cases, he/she can halt and check the stack area to see how much space is still filled with that repeating bit pattern. Filling the stack space makes it easy to see how deep you have gone into your stack during testing. However, this works best to tell if a stack overflow has already happened and needs to be checked often.
Last, calculation methods are also a possible stack-checking tool. Some software development tools include stack analysis tools to determine how much stack you plan to use based on information you feed into it. The estimated required stack size calculation method can provide a worst-case scenario, which might be “expensive” for embedded applications. Calculation methods can be found in both free and paid tools.
Some best practices for preventing stack overflow
Avoid stack-hogging functions like printf( ) and related functions. Try to pass by reference instead of by copy. When passing by copy, it tends to go on the stack, particularly if it’s an array. With an array, it’s easier to run out of the stack and overflow the stack rapidly.
Another best practice is to limit the number of arguments to a function; for example, the first four arguments go into registers, and all subsequent arguments go onto the stack. The Application Binary Interface (ABI)[i] of an MCU tells a compiler how many arguments can be passed in the registers; all others must be passed on the stack.
Last, avoid recursive functions (a function that calls itself) as they use a significant portion of the stack as they go deeper. If you must use recursion, set variables to stop infinite recursion from happening. However, it’s a good practice to avoid recursion altogether.
Some solutions mentioned above are not portable, but quite fine wh
[i] It is typically part of compiler’s job to adhere to an ABI. An embedded ABI operates at the machine code level and determines how function calls are made, file formatting, use of the register, and the framework of the stack. ABIs vary between architectures in the embedded world.
Dey, Kashi Nath, and Samir Kumar Bandyopadhyay. C Programming Essentials. Delhi: Pearson Education, 20 Apr. 2010.