Static in embedded
February 29, 2024

Sometimes in a meeting it can happen that a question arises that on the surface seems easy, however, it raises a series of further inquiries. Such was the case in the situation at hand, when a whole series of questions emerged from a seemingly simple question about the C language keyword, "Static."

And here is our list:

  1. Where is an uninitialized static variable stored?
  2. Where is the initialized static variable stored?
  3. When is a static variable initialized and with what value?
  4. Where is the static constant stored?
  5. When is a static constant initialized and with what value?
  6. Where is the static variable of the function stored?
  7. When is a static function variable initialized and with what value?
  8. Where is the static constant of the function stored?
  9. When is a static function constant initialized and with what value?
  10. Can a static function variable be initialized with a function argument?
    BONUS -> What about a character array and a pointer to a character array?

Environment

I used the following environment to demonstrate the results. The runtime platform itself should not make much difference in understanding the specifics of using static data in an embedded environment.

  • Target: STM32WB55
  • IDE: STM32CubeIDE
  • Toolchain (compiler, linker, etc) : arm-none-eabi version 10.3-2021.10 provided with the IDE

The code was compiled with flags:

  • ffunction-section - allocates each function a separate section in memory
  • fdata-section - allocates all data to separate sections in memory
  • g3 - enable top level debugging
  • O0 - disable optimization

The above flags will allow us to more easily locate our static data in the memory map file and make it easier to navigate through the source code and analysis during debugging.

Among the linker flags I used:

  • Wl,-Map=path_to_map_file this flag turns on the generation of memory map files necessary for us to find particular data;

‍Base code.

The original form of the main.c file is as follows and will be used in each example as a starting point.

#include "platform.h" int main(void) { platform_init(); while (1) {} }

As you can see, except for the initialization of the platform using the platform_init() function, we only have an infinite loop. The platform_init() function is responsible for initializing our hardware platform, the stm32wb55 microcontroller.

For the code prepared in this way, I saved in a separate directory the memory map file (so-called *.map file) and the sizes of each memory section. These will be useful at a later stage as a reference point to represent the differences that the use of static data introduces.

section size addr .isr_vector 0x13c 0x8000000 .text 0x1e58 0x800013c .rodata 0x178 0x8001f94 .ARM 0x8 0x800210c .init_array 0x4 0x8002114 .fini_array 0x4 0x8002118 .data 0x70 0x20000004 .bss 0x24 0x20000074 ._user_heap_stack 0x600 0x20000098 .ARM.attributes 0x30 0x0 .debug_info 0x411c 0x0 .debug_abbrev 0xd52 0x0 .debug_aranges 0x4d8 0x0 .debug_ranges 0x430 0x0 .debug_macro 0x1965c 0x0 .debug_line 0x57a0 0x0 .debug_str 0x8dd43 0x0 .comment 0x50 0x0 .debug_frame 0x1200 0x0

Already by looking at the addresses and knowing our microcontroller, we can tell what memory each section is in:

Implementation

1 . Where is the uninitialized static variable stored?

First, I added an uninitialized static variable to our main.c file and compared the section sizes and *.map files (on the left the base form, on the right the result for our new code).

Comparison of the size of each section:

Comparison of section sizes

Comparison of *.map files:

Comparison of *.map files

From the comparison of section sizes, we see that adding one static variable in the main.c file increased the .bss section by 4 bytes and the ._user_heap_stack section by 4 bytes.

Thanks to the * .map files, we can tell that our new uninitialized variable landed in the .bss section. and the increase in the ._user_heap_stack section by 4 bytes is due to aligning the data to 8 bytes (called alignment). If I defined two static variables then both would land in the .bss section and the ._user_heap_stack section would return to its original size.

What is the .bss section and the ._user_heap_stack section?

Both of these sections are located in RAM so in order to answer this question well we can look at the linker script (below is an excerpt showing the linker script).The following notation means no more and no less than that a space in RAM1 is reserved for the data in these sections.

/* Uninitialized data section */ . = ALIGN(4); .bss : { /* This is used by the startup in order to initialize the .bss section */ _sbss = .; /* define a global symbol at bss start */ __bss_start__ = _sbss; *(.bss) *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; /* define a global symbol at bss end */ __bss_end__ = _ebss; } >RAM1 /* User_heap_stack section, used to check that there is enough RAM left */ ._user_heap_stack : { . = ALIGN(8); PROVIDE ( end = . ); PROVIDE ( _end = . ); . = . + _Min_Heap_Size; . = . + _Min_Stack_Size; . = ALIGN(8); } >RAM1

Let's pay attention to line 1 and 17

  • .bss - is a section that stores uninitialized variables (both global and static)
  • ._user_heap_stack is a section created for the purpose of keeping an eye on enough RAM for the stack (stack) and heap (heap).

2 . Where is the initialized static variable stored?

We approach this point as we did the previous one. We just add an initialization value to a static variable and watch what changes in the section size and *.map file.

#include "platform.h" static int inintializedStaticVariable = 42; int main(void) { platform_init(); while (1) {} }

Comparison of the size of each section:

Comparison of section sizes

Comparison of *.map files:

Comparison of *.map

From the above, our new initialized variable landed in the .data memory section occupying 4 bytes there. As before, because of the memory alignment to 8 bytes, an additional area of 4 bytes appeared in the ._user_heap_stack section.

What is the .data section?

I have already described the . _user_heap_stack section earlier so I will focus on the .data section.

Again I look into the linker script and find the section I am interested in. As you can see in the description to this section land variables that have an initialization value.  

/* Initialized data sections goes into RAM, load LMA copy after code */ .data : { . = ALIGN(4); _sdata = .; /* create a global symbol at data start */ *(.data) /* .data sections */ *(.data*) /* .data* sections */ *(.RamFunc) /* .RamFunc sections */ *(.RamFunc*) /* .RamFunc* sections */ . = ALIGN(4); _edata = .; /* define a global symbol at data end */ } >RAM1 AT> FLASH

3 . Where is the uninitialized static variable stored?

In the first instance, code:

#include "platform.h" static int uninintializedStaticVariable; static int inintializedStaticVariable = 42; int main(void) { platform_init(); while (1) {} }

In earlier examples, when I presented fragments of the linker script at the closing brackets of a section, you can find markings >RAM1 or >RAM1 AT> FLASH

  • >RAM1 - space is reserved in RAM for the variables in the section.
  • >RAM1 AT> FLASH - space is reserved in RAM for the variables of the section. In addition, it has its counterpart in FLASH memory

And what does it give us?

During compilation and linking, space will be reserved in RAM for the .bss section (with uninitialized variables) as well as for the .data section (initialized variables). In addition, Flash memory (program memory) will hold the values used to initialize variables from the . data section.

When running our software before the main() function is called, quite a lot happens. It all starts with the start point of the entire firmware defined in the linker script. It defines the place from where the execution of our application will start.

/* Entry Point */. ENTRY(Reset_Handler)

I already know that the application starts execution with the Reset_Handler function . Its implementation can usually be found in the startup file.

Reset_Handler: ldr r0, =_estack mov sp, r0 /* set stack pointer */. /* call the clock system initialization function */. bl SystemInit /* Copy the data segment initializers from flash to SRAM */. INIT_DATA _sdata, _edata, _sidata /* Zero fill the bss segments. */ INIT_BSS _sbss, _ebss INIT_BSS _sMB_MEM2, _eMB_MEM2 /* Call static constructors */ bl __libc_init_array /* Call the application s entry point */. bl main

In addition to setting the stack pointer, the following are performed sequentially:

  • SystemInit - Initialization of our microcontroller (only the most important settings)
  • INIT_DATA - Initialization of .data section in RAM with data from FLASH memory
  • INIT_BSS - Initialization of the .bss section in RAM. The default is to fill the entire section with 0 values.
  • __libc_init_array - Calling static constructors
  • main - Jump to the main function of our program

To better illustrate this I will use a small drawing and some steps from the code debug session.

Data sections during program startup

Before initializing the .data section:

After initializing the .data section, before initializing the .bss section:

After initializing the .data and .bss sections:

To summarize this question, both initialized and uninitialized static variables are initialized according to the startup file. Data to an initialized static variable is copied from Flash memory, and zeros are assigned to uninitialized static variables by default (but this can be changed). Assigning 0x00 to uninitialized static variables is in accordance with ISO 9899 section 6.7.9.

4 . Where is the static constant stored?

It's time to go further into the woods and take a look at what happens if we deal with constants instead of variables. The initial code base is the same as before. To make the whole thing a little shorter I will now deal with constants both initialized and uninitialized at the beginning. I will omit the fact that I personally do not see any reason for anyone to need an uninitialized constant. The compiler allows us to create one so we will check it here.

#include "platform.h" static const int uninintializedStaticConstant; static const int inintializedStaticConstant = 100; int main(void) { platform_init(); while (1) {} }

Comparison of the size of each section:

Comparison of *.map files:

As you can see from the above, we changed the size of the .rodata section by 8 bytes. It's not hard to guess that this is the section where both of our constants went. The .rodata is a section in Flash memory that stores read-only data constants.

5 . When is a static constant initialized and with what value?

We already know that constants are stored in Flash memory in the .rodata section, and they are read-only. Knowing the addresses of the individual constants, we can read their values (for example, during a debug session using the Memory view).

In our example:  

  • At address 0x08002804 is our uninitialized constant (with value 0)
  • At address 0x08002808 is our initialized constant (with the value 0x64 which is 100)
Constants in the .rodata section

These constants are initialized during compilation based on the assigned values and do not change during the course of the program (although there are ways to do this as well). Uninitialized constants have the value 0, initialized constants have the value that was assigned to them during definition.

6 . Where is the static variable of the function stored?

We have already discussed variables and constants that are defined globally for a specific translation unit, now let's see how data defined locally within a single function will behave.For the purpose of this piece, I have prepared a new base code. As simple as possible.

#include "platform.h" void foo(int const arg) { } int main(void) { platform_init(); foo(0x42); while (1) {} }

I added one function foo(int const) taking one argument and called it from the main() function with a fixed value.

To the base code thus prepared, I added an uninitialized static variable within the foo() function and compared the section sizes and *.map files.

#include "platform.h" void foo(int const arg) { static int uninitializedStaticVar; } int main(void) { platform_init(); foo(0x42); while (1) {} }

Comparison of the size of each section:

Comparison of *.map files:

We quickly notice that this variable landed in the.bss section of RAM. By the way, the ._user_heap_stack section increased again due to the alignment to 8 bytes. This is exactly what happened with variables defined within the file.

What if we initialize a static variable of a function with a specific value?

#include "platform.h" void foo(int const arg) { static int initializedStaticVar = 0x99; } int main(void) { platform_init(); foo(0x42); while (1) {} }

Comparison of the size of each section:

Comparison of *.map files:

Here, identical to the static file variables, our local static function variable landed in the .data section.

7 . When is a static function variable initialized and with what value?

Local variables in a function, whether they are initialized with an initial value or not, go into the same sections of memory as static variables defined globally in the file.

#include "platform.h" void foo(int const arg) { static int uninitializedStaticVar; static int initializedStaticVar = 0x25; } int main(void) { platform_init(); foo(0x42); while (1) {} }

Therefore, we can conclude that they initialize identically during startup.

  • The variable with the initial value is initialized with data from flash memory;
  • A variable with no value is stored with a value of zero;
Initialization in a startup

NOTE: Here it should be mentioned that in C++, static function variables are initialized the first time the function is called.
You can read more here.

8. where is the static constant of the function stored?

The next step is a static function constant. For simplicity's sake, I added two constants to our foo function and analyzed the section sizes and *.map files.
Let me remind you that I don't see why anyone needs an uninitialized constant in their project. The compiler allows you to create one, so we check it.

#include "platform.h" void foo(int const arg) { static const int uninitializedStaticConstant; static const int initializedStaticConstant= 0x25; } int main(void) { platform_init(); foo(0x42); while (1) {} }

Comparison of the size of each section:

Comparison of *.map files:

Here our constants also behave identically to the static constants defined globally for the file and land in Flash memory in the .rodata sector (read-only section).

9. when is the static constant of a function inicialized and with what value?

Constants are not initialized. Values are assigned to them at compile time and do not change while the program is running. By checking the addresses of our constants in memory, we can make sure what values they store.

  • At address 0x0800281c is placed our initialized constant and stores the value 0x25
  • An uninitialized constant is placed at address 0x08002820 and stores the value 0x00
Constants in the .rodata section

10. can a static function variable be initialized with a function argument?

Finally, I left myself a question about which I myself was not sure how I should answer. Since a static variable of a function can have a value of 0 (if it is uninitialized) or a predetermined value (copied from Flash memory), is it possible to initialize such a variable with the value passed to the function via an argument?
The description sounds terrible but in code it doesn't look so bad.

#include "platform.h" void foo(int const arg) { static int initializedStaticVariable = arg; } int main(void) { platform_init(); foo(0x42); while (1) {} }

Well, in this case, the compiler will report an error demanding a constant (known during compilation) initialization value for a static variable.

../Core/Src/main.c: In function 'foo': ../Core/Src/main.c:5:42: error: initializer element is not constant 5 | static int initializedStaticVariable = arg; make[1]: *** [Core/Src/subdir.mk:34: Core/Src/main.o] Error 1 make: *** [makefile:61: all] Error 2

This is also in accordance with ISO 9899 Section 6.7.9 Initialization.

"All the expressions in an initializer for an object that has static or thread storage duration shall be constant expressions or string literals."

Thus, all values used for initialization must be a constant expression, that is, a value known at compile time.

BONUS -> What about a character array and a pointer to a character array?

What about a pointer that points to a string "string" stored in a fixed part of memory?
What about an array that contains a string "string"?

char* string = "string" char string[] = "string"

Also, I recommend you to throw yourself such or similar piece of code and preview the contents of the *.map files analyze what goes where and how it behaves after startup (the startup itself is enough).

#include "stddef.h" char *glob_string_ptr = "glob_string_ptr"; char glob_string_arr[] = "glob_string_arr"; int main(void) { size_t s_gsp = sizeof(glob_string_ptr); size_t s_gsa = sizeof(glob_string_arr); glob_string_ptr[0] = 'z'; glob_string_arr[0] = 'r'; platform_init(); while (1) {} }

The memory map includes:

Memory map
  • 0x20000004 → __dso_handle → object used when executing global destructors (about it another time)
  • 0x20000008 → glob_string_ptr → that is our pointer to the writing (pointer size 4 bytes)
  • 0x2000000c → glob_string_arr → i.e. array of 16 characters (size 16 bytes = 15 characters and terminator '\0')

We remember that:

  • The .data section where our objects are located is just a reserved area of RAM for initialized data/variables
  • The .data section has its counterpart with initialization data in Flash memory (in our case it starts at the address pointed to by _sidata i.e. 0x08002924
  • Initialization data is copied from Flash memory to .data section during startup execution

Since we know that the data from under address 0x08002924 is copied into RAM to the .data section at address 0x20000004. Let's take a look at what's in there:

The addresses on the left are given as an offset to address 0x80000000.
I've marked such a flash memory area for a reason. Let's analyze step by step what we have here:

  • From address 0x08002924 we copy data 0000 0000 0000 b427 0008 676c 6f62 5f73 7472 696e 675f 6172 7200 ...... to address 0x20000004
  • This is how our next variables get their values (remembering the little endian):
    0x20000004 → __dso_handle0x000000
    0x20000008 → glob_string_ptr 0x080027b4 → 676c 6f62 5f73 7472 696e 675f 7074 7200
    0x2000000c → glob_string_arr → 676c 6f62 5f73 7472 696e 675f 6172 7200

As you can see:

  • __dso_handle is zeroed = NULL
  • The glob_string_ptr pointer got the address to the "glob_string_ptr" text located in Flash memory.
  • The glob_string_arr array has been initialized with the inscription "glob_string_arr". This inscription is now located in RAM

To be sure, let's also find the entry with address 0x080027b4 in the memory map

Memory map - string

Here we can see that our .rodata memory area storing the fixed data has increased by 16 bytes and they come from the main.o object file. It's not hard to guess that the string pointed to by the glob_string_ptr pointer landed here, which we proved a moment earlier by reviewing the binary

In the code we have prepared, we also have the retrieval of the sizes of both of these objects.
As it is not hard to guess (already knowing the contents of the *.map file):

  • The size of the glob_string_ptr is 4 bytes (pointer size)
  • The size of the glob_string_arr is 16 bytes (as many characters + 1 for the termiantor 'ƒ0')

In addition:

  • The text contained in the glob_string_arr object can be changed without any obstacles, since the entire object and its contents are in RAM memory
  • The text contained in the glob_string_ptr object can not be modified because it is located in Flash memory. The only thing we can do is modify the address of the pointer  

Copy initialization data from Flash memory to the .data section of RAM in the startup.

Before:

After:

Object sizes and the impact of modifying 1 character in both objects. As you can see, the change occurred in only one object glob_string_arr

Summary

There are some key takeaways from today's article:

  • Uninitialized static variables (global and local) have their place in the .bss section of RAM;
  • Variables located in the .bss section are initialized at startup with the value 0x00 by default, but this can be changed by modifying the startup file accordingly;
  • Initialized static variables (global and local) have their place reserved in the .data section in RAM, and the values with which they are initialized are in Flash memory;
  • During startup, initialization values are copied from Flash memory to the .data section in RAM;
  • Static (initialized and uninitialized) constants (global and local) have their place in the .rodata (read-only) section;
  • Uninitialized static constants and global variables according to the standard are initialized with the value 0x00. We omit the fact that it is unlikely that anyone needs an uninitialized constant;
  • A function argument cannot directly initialize a static variable. A value known at compile time must be used for initialization;

What to keep in mind after reading this article:

  • I analyzed here the code only in C language. As far as C++ is concerned, there are some deviations from the results and conclusions presented there
  • I have optimization completely disabled. With optimization enabled (depending on the level), toolchain can make significant modifications and simplifications;

REFERENCES