Stack and Heap Layout of Embedded Projects
Embedded projects developed with the C/C++ languages use 3 different memory regions to store the variables:
- Global Variables declared outside functions/methods are placed directly after each other starting at the beginning of RAM.
- Local (automatic) variables declared in functions and methods are stored in the stack.
- Variables allocated dynamically (vie the malloc() function or the new() operator) are stored in the heap.
The default linker script layout places the heap directly after the last global variable, and the stack directly before the end of RAM:
Both heap and stack grow dynamically. When the program’s entry point is called, the heap does not yet exist and the stack points to the address of the _estack symbol (normally at the end of RAM). Once the entry point calls other functions and they use stack to store variables and return addresses, the stack pointer grows towards the beginning of the RAM. The $sp register of the CPU normally points to the current end of the stack.
If the application calls malloc() or new() to allocate memory dynamically, the standard C/C++ library calls the _srbk() function to create a heap right after the last global variable (the _end symbol) and increase its size as needed. The default _sbrk() implementation stores the end of the heap address in the static heap_end variable.
Note that both heap and stack grow towards each other and may collide, leading to memory corruption.
Fixed-size Stack and Heap
If you are using VisualGDB-generated linker scripts, you can reserve fixed amounts of memory for the stack and heap: This will create 2 additional sections in the ELF file:
- .heap will be used to store the heap. The fixed-size heap will never exceed the size reserved for it. Instead, malloc() will return NULL.
- .reserved_for_stack will reserve the specified amount of memory for the stack, triggering a linker error if there was not enough free RAM left. Note that the actual stack will be still placed at the end of RAM.
FreeRTOS
If you are using a real-time operating system (e.g. FreeRTOS), each thread will have its own stack. The size of the stack for each thread is specified when calling the xTaskCreate() function. Many examples use the configMINIMAL_STACK_SIZE as the default stack size (note that the stack size passed to xTaskCreate() is specified in machine words, not bytes).
FreeRTOS allocates stacks for each thread from the FreeRTOS heap that is different from the stdlib heap. Use the configTOTAL_HEAP_SIZE variable to control the FreeRTOS heap size.
The stack/heap layout for FreeRTOS applications is shown here:
Editing Stack/Heap Layout
You can arbitrarily change the stack and heap layout of your application by editing the linker script and providing a custom implementation of _sbrk():
- Set the _estack symbol in linker script (see this tutorial) to the desired initial location of the stack. The startup code will automatically initialize the stack pointer accordingly.
- Provide your own implementation of the _sbrk() function. The standard C library will call it each time it needs to increase the size of the heap. On the first call, it should return the starting address of the heap. On subsequent calls, it should return the previous end of the heap, or -1 in case it cannot be extended by the requested number of bytes.
You can use the following _sbrk() implementation from newlib as a starting point:
register char *stack_ptr asm("sp"); void *_sbrk(ptrdiff_t incr) { extern char end asm("end"); /* Defined by the linker. */ static char *heap_end; char *prev_heap_end; if (heap_end == NULL) heap_end = &end; prev_heap_end = heap_end; if (heap_end + incr > stack_ptr) { errno = ENOMEM; return (void *)-1; } heap_end += incr; return (void *)prev_heap_end; } |