4. The Primary and Secondary Stacks

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

4.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.

4.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 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 size by examining the linker scripts used to control and lay out memory in the 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.

4.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);

4.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.

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.

4.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 resolve this problem, GNAT provides each task with a secondary stack that objects of unconstrained types are returned on. On native and cross targets using the full run-time, the secondary stack by default is allocated dynamically on the heap. For cross and bareboard platforms, where stack sizes are fixed, secondary stacks are statically allocated and cannot grow at run time. In both cases the secondary stack is allocated independently from the primary stack.

4.2.1. Fixed Secondary Stack Allocation

Secondary stack allocation on Ravenscar, ZFP, and Cert run-times is fixed, as they do not permit dynamic allocation of memory by the run-time. For these targets, the secondary stack is allocated statically at bind time using the default size given by the System.Parameters.Runtime_Default_Sec_Stack_Size parameter. This default value can be changed using the gnatbind -D switch if a more suitable value is required. For example, to specify a default secondary stack size of 20KB for all tasks:

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;

4.2.2. Dynamic Secondary Stack Allocation

Dynamic secondary stack allocation allows GNAT to size the secondary stack 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 many cross targets. The maximum size a secondary stack for a task can grow to is the smaller of 2GB or the amount of free memory 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;

4.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.