3. The Primary and Secondary Stacks

This chapter covers the primary and secondary stacks, explaining their purposes and how to control their sizes.

3.1. The Primary Stack

In GNAT, each user-defined task (if any) has a dedicated primary stack, as does the language-defined “environment task” that ultimately calls the main subprogram.

At run time, this primary stack is used by the compiler-generated code for several purposes, including representing subprogram calls. Each subprogram call typically represents a new entry on the stack, popped when the call returns. In addition, the stack is used for subprogram calls to pass data for the parameters, including the results of calls to functions that return values of constrained types. The stack is used for other purposes as well.

3.1.1. The Default Primary Stack Size

Applications vary widely, including, for example, the depth to which subprograms call each other, so the required size of each primary stack is application dependent. Therefore, the default stack sizes provided by the implementation for the environment task and for application-defined tasks may or may not suffice.

In GNAT the same default value is applied to both the environment task and to application-defined tasks. The default primary stack size is determined by the underlying operating system, or in the case of bareboard targets by the specific run-time library.

For bareboard targets, the default stack size varies across specific platforms because different classes of target boards will have different ranges of RAM available. For example, the Stellaris LM3S board, with a Cortex-M3 MCU, has a default stack size of 2K bytes in GNAT, whereas the STM32F4 targets have Cortex-M4 MCUs and a 4K default stack size.

You can determine the default primary stack size for a given bareboard run-time library by examining the linker scripts used to lay out memory. These files are located in the run-time library’s board support package (BSP) files. On the ARM STM32 targets, for example, in common-RAM.ld the following declaration appears near the top

_DEFAULT_STACK_SIZE = 4*1024;

whereas for the LM3S (in lm3s-ram.ld) a different value is specified

_DEFAULT_STACK_SIZE = 2*1024;

In all cases the symbol name is the same, only the name of the linker script file varies.

3.1.2. Per-Task Primary Stack Size

The primary stack size for a task object can be specified either on the type declaration, if explicitly defined, or directly on the task object declaration. To do so, use the Storage_Size aspect (or pragma). For example, the following code defines a task type with a primary stack size of 20KB:

task type My_Task_Type with
  Storage_Size => 20 * 1024;

Every task object of that type will have that primary stack size.

For an anonymously-typed task object:

task My_Task with
  Storage_Size => 20 * 1024;

That one task object will then have that primary stack size.

If you want to have a task type that does not apply the same stack size to every object of the type, you can use a discriminant:

task type My_Task_Type (Primary_Stack : Positive) with
  Storage_Size => Primary_Stack;

Each task object will specify a value for the Primary_Stack discriminant, thereby defining the primary stack size for that individual object:

T1 : My_Task_Type (Primary_Stack => 20 * 1024);
T2 : My_Task_Type (Primary_Stack =>  4 * 1024);

3.1.3. Environment Task Primary Stack Size

The application environment task is “created” by the target execution environment. Therefore, everything required for the environment task’s execution, including the primary stack size, must be known before the application starts. For non-bareboard targets, refer to your operating system’s manual.

For bareboard targets, the mechanism for specifying the environment task’s primary stack size is implementation-defined. In GNAT, there are two ways to do so:

  • Edit the default value in the linker script

  • Specify the default using a linker switch when building

You can change the linker script to specify a different value for the _DEFAULT_STACK_SIZE symbol. The new default will be applied whenever the corresponding run-time library is linked with an application. The same default will be used in each build.

If, however, you do not want to use the same default, even if you have changed the default in the run-time library, you can specify the different primary stack size using a linker switch. Thus the difference will be project-specific (or even invocation-specific) rather than specific to the individual run-time library.

The linker switch is of the form

"-Wl,--defsym=__stack_size=x"

where X is is the number of bytes required. The value can be either in decimal or hexadecimal (with a leading “0x”).

For example, to set a primary stack size of 20K bytes the switch would be, in decimal:

"-Wl,--defsym=__stack_size=20480"

or, in hexadecimal:

"-Wl,--defsym=__stack_size=0x5000"

Note how the switch defines the __stack_size symbol, which is used inside the linker scripts (e.g., common-RAM.ld). If that symbol is defined the corresponding value is used instead of that of the default symbol.

Although you can add the switch to an invocation of the builder on the command line (using the -largs switch), we suggest more permanently specifying it in the project file, like so:

package Linker is
   for Default_Switches ("ada") use ("-Wl,--defsym=__stack_size=20480");
end Linker;

Note that there are no spaces anywhere within the string.

However you specify it, if the primary stack size value is too large the builder will not create the executable image.

3.2. The Secondary Stack

GNAT returns objects from functions via registers (if small) or via the primary stack. For the latter, the caller of the function typically allocates space for the return object on its primary stack before the call. However, Ada allows functions to return objects of unconstrained types, for example unbounded array types such as String, and unconstrained discriminated record types. In this case the caller does not know the size of the returned object at the point of the call. To accommodate such returns, GNAT provides each task with a separate secondary stack that objects of unconstrained types are returned on.

Most applications using the full GNAT run-time on native and cross targets will find the operation of the secondary stack to be transparent. GNAT achieves this by dynamically sizing secondary stacks at run time.

Secondary stacks on the Light, Light-Tasking, and Embedded run-times on the other hand cannot grow and shrink at run time. Instead, their size is fixed when the application is built. This is done because dynamic allocation of memory is frequently not allowed by embedded application during their normal operation. Care needs to be taken to ensure each task’s secondary stack is sized appropriately. Failure to do so will result in the run-time raising Storage_Error in System.Secondary_Stack.SS_Allocate at run time.

3.2.1. Dynamically-Sized Secondary Stacks

Dynamically-sized secondary stacks allow GNAT to size secondary stacks to the needs of each task at run time without input from the developer and is used by default on the full GNAT run-time used by native and cross targets. The size the secondary stack can grow to is limited by the amount of free heap space on the target.

The initial memory allocated to the secondary stack is governed by the parameter System.Parameters.Runtime_Default_Sec_Stack_Size and grows in increments of this value unless an object larger than this value needs to be placed on the stack. In this case, the stack will grow by the size of the object.

The value of Runtime_Default_Sec_Stack_Size is suitable for nearly all applications, balancing the number of heap allocations required during the life of the stack against stack fragmentation. However, if the default allocation size is unsuitable, it can be modified via the gnatbind -D switch. Per-task increment sizes can also be specified via the Secondary_Stack_Size aspect:

task A_Task with
  Secondary_Stack_Size => 20 * 1024;

In certain circumstances there can be a performance benefit by increasing the default secondary stack size to reduce the number of memory allocations performed by the secondary stack. These situations include string operations within a loop that cause a string to grow on each iteration. In this case it is advised to set the secondary stack size for the task performing the operation (or the default secondary stack size) to the expected final size of the string in question.

Memory for dynamically-sized secondary stacks is assigned from the heap except for the initial stack allocation for tasks declared at the library level that use the default secondary stack size. In this case the initial stack allocations come from a pool of statically allocated secondary stacks generated by gnatbind.

3.2.2. Fixed Secondary Stacks

Fixed-size secondary stacks on the Light, Light-Tasking and Embedded run-times are generally allocated from a pool of static default-sized secondary stacks generated by gnatbind. The System.Parameters.Runtime_Default_Sec_Stack_Size parameter determines the size of these stacks, which can be changed by rebinding the application with the gnatbind -D switch if required. For example, a default secondary stack size of 20KB can be specified with:

gnatbind -D20k main.ali

Like all gnatbind switches, the -D switch can be included in the binder section of a project file.

The default secondary stack size can be overridden on a per-task basis if individual tasks have different secondary stack requirements. This is achieved through the Secondary_Stack_Size aspect that takes the size of the secondary stack in bytes. For example, to specify a 20KB secondary stack for the task A_Task:

task A_Task with
  Secondary_Stack_Size => 20 * 1024;

Secondary stacks with explicit sizes are statically allocated with the task they are assigned to.

3.2.2.1. Foreign Threads

Foreign threads need to be explicitly registered with the Light run-time using the System.Threads.Thread_Body_Enter procedure. As part of the procedure, a secondary stack is associated to the foreign thread. The secondary stack assigned to the foreign thread needs to be:

  • Created on the stack or heap (by creating a new System.Secondary_Stack.SS_Stack object) and passed explicitly to the Thread_Body_Enter procedure.

  • Assigned from the default-sized secondary stack pool by passing a null secondary stack pointer to Thread_Body_Enter. You will need to increase the size of the pool by the number of foreign threads that will be registered using this approach. This is done by passing gnatbind the -Q switch with the number of additional secondary stacks needed.

3.2.3. Disabling the Secondary Stack

The secondary stack can be disabled by using pragma Restrictions (No_Secondary_Stack); This will cause an error to be raised at compile time for each call to a function that returns an object of unconstrained type. When this restriction is in effect, Ada tasks (excluding the environment task) will not have their secondary stacks allocated.

Note: The GNAT Last Chance Handler on the Embedded run-time uses the secondary stack. If you wish to use the No_Secondary_Stack restriction with this run-time, you will need to provide your own last chance handler implementation, otherwise an exception will be raised during the elaboration of your program. See Exceptions for details on how to provide your own last chance handler.