Static Objects

Let's analyse the code that initialises static objects. test_cpp_statics is a simple application that has two static objects, one is in the global scope, the other is in the function scope.

class SomeObj 
{ 
public: 
   static SomeObj& instanceGlobal(); 
   static SomeObj& instanceLocal(); 

private: 
    SomeObj(int v1, int v2); 
    int m_v1; 
    int m_v2; 

    static SomeObj globalObj; 
}; 

SomeObj SomeObj::globalObj(1, 2); 

SomeObj& SomeObj::instanceGlobal() 
{ 
    return globalObj; 
} 

SomeObj& SomeObj::instanceLocal() 
{ 
    static SomeObj localObj(3, 4); 
    return localObj; 
} 

int main(int argc, const char** argv) 
{ 
    static_cast<void>(argc); 
    static_cast<void>(argv); 

    auto& glob = SomeObj::instanceGlobal(); 
    auto& local = SomeObj::instanceLocal(); 
    static_cast<void>(glob); 
    static_cast<void>(local); 

    while (true) {}; 
    return 0; 
}

Note, that compiler will try to inline the code above if implemented in the same file. To properly analyse the code that initialises global variables, you should put implementation of constructor and instanceGlobal()/instanceLocal() functions into separate files. If -nostdlib option is passed to the compiler to exclude linking with standard library, the compilation of the code above will fail with following error:

main.cpp:(.text.startup+0x1c): undefined reference to `__cxa_guard_acquire' 
main.cpp:(.text.startup+0x3c): undefined reference to `__cxa_guard_release'

It means that compiler attempts to make static variables initialisation thread-safe. The get it compiled you have to either implement the locking functionality yourself or allow compiler to do it in an unsafe way by adding -fno-threadsafe-statics compilation option. I think it is quite safe to use this option in the bare-metal development if you make sure the statics are not accessed in the interrupt context or have been initialised at the beginning of main() function before any interrupts are enabled. To grab a reference to such object without any use is enough:

    auto& local = SomeObj::instanceLocal(); 
    static_cast<void>(local);

Now, let's analyse the initialisation of globalObj. The .init.array section contains pointer to initialisation function _GLOBAL__sub_I__ZN7SomeObj9globalObjE.

Disassembly of section .init.array:

00008180 <__init_array_start>: 
    8180:    00008154     andeq    r8, r0, r4, asr r1

The initialisation function loads the address of the object and passes it to the constructor of SomeObj together with the initialisation parameters (“1” and “2” integer values).

00008154 <_GLOBAL__sub_I__ZN7SomeObj9globalObjE>: 
    8154:    e59f0008     ldr    r0, [pc, #8]    ; 8164 <_GLOBAL__sub_I__ZN7SomeObj9globalObjE+0x10> 
    8158:    e3a01001     mov    r1, #1 
    815c:    e3a02002     mov    r2, #2 
    8160:    eaffffee     b    8120 <_ZN7SomeObjC1Eii> 
    8164:    00008168     andeq    r8, r0, r8, ror #2 

00008168 <_ZN7SomeObj9globalObjE>: 
    ...

The code above loads the address of the global object (0x00008168) into r0, and initialisation parameters into r1 and r2, then invokes the constructor of SomeObj.

Please remember to call all the initialisation functions from .init.array section in your startup code before calling the main() function.

In the linker file:

    .init.array :
    {
        __init_array_start = .;
        *(.init_array)
        *(.init_array.*)
        __init_array_end = .;
    } > RAM

In the startup code:

    ;@ Call constructors of all global objects
    ldr r0, =__init_array_start
    ldr r1, =__init_array_end

globals_init_loop:
    cmp     r0,r1
    it      lt
    ldrlt   r2, [r0], #4
    blxlt   r2
    blt     globals_init_loop

    ;@ Main function
    bl main
    b reset ;@ restart if main function returns

However, if standard library is NOT excluded explicitly from the compilation, the __libc_init_array provided by the standard library may be used:

    ;@ Call constructors of all global objects
    bl    __libc_init_array

    ;@ Main function
    bl main
    b reset ;@ restart if main function returns

Let's also perform analysis of initialisation of localObj in SomeObj::instanceLocal().

000080e4 <_ZN7SomeObj13instanceLocalEv>: 
    80e4:    e92d4010     push    {r4, lr} 
    80e8:    e59f4028     ldr    r4, [pc, #40]    ; 8118 <_ZN7SomeObj13instanceLocalEv+0x34> 
    80ec:    e5943008     ldr    r3, [r4, #8] 
    80f0:    e3130001     tst    r3, #1 
    80f4:    1a000005     bne    8110 <_ZN7SomeObj13instanceLocalEv+0x2c> 
    80f8:    e284000c     add    r0, r4, #12 
    80fc:    e3a01003     mov    r1, #3 
    8100:    e3a02004     mov    r2, #4 
    8104:    eb000005     bl    8120 <_ZN7SomeObjC1Eii> 
    8108:    e3a03001     mov    r3, #1 
    810c:    e5843008     str    r3, [r4, #8] 
    8110:    e59f0004     ldr    r0, [pc, #4]    ; 811c <_ZN7SomeObj13instanceLocalEv+0x38> 
    8114:    e8bd8010     pop    {r4, pc} 
    8118:    00008168     andeq    r8, r0, r8, ror #2 
    811c:    00008174     andeq    r8, r0, r4, ror r1

The code above loads the address of the flag that indicates that the object was already initialised into r4, then loads the value into r3 and checks it using tst instruction. If the flag indicates that the object wasn't initialised, the constructor of the object is called and the flag value is updated prior to returning address of the object. Note that tst r3, #1 instruction performs binary AND between value r3 and integer value #1, then next bne instruction performs branch if result is not 0, i.e. the object was already initialised.

CONCLUSION: Access to global objects are a bit cheaper than access to local static ones, because access to the latter involves a check whether the object was already initialised.

Custom Destructors

And what about destruction of static objects with non-trivial destructors? Let's add a destructor to the above class and try to compile:

class SomeObj 
{ 
public: 
   ~SomeObj(); 

}

Somewhere in *.cpp file:

SomeObj::~SomeObj() {}

This time the compilation will fail with following errors:

CMakeFiles/03_test_statics.dir/SomeObj.cpp.o: In function `SomeObj::instanceLocal()': 
SomeObj.cpp:(.text+0x44): undefined reference to `__aeabi_atexit' 
SomeObj.cpp:(.text+0x58): undefined reference to `__dso_handle' 
CMakeFiles/03_test_statics.dir/SomeObj.cpp.o: In function `_GLOBAL__sub_I__ZN7SomeObj9globalObjE': 
SomeObj.cpp:(.text.startup+0x28): undefined reference to `__aeabi_atexit' 
SomeObj.cpp:(.text.startup+0x34): undefined reference to `__dso_handle'

According to this document, the __aeabi_atexit function is used to register pointer to the destructor function together with pointer to the relevant static object to be destructed after main function returns. The reason for this behaviour is that these objects must be destructed in the opposite order to which they were constructed. The compiler cannot know the exact construction order for local static objects. There may even be some static objects are not constructed at all. The __dso_handle is a global pointer to the current address where the next {destructor_ptr, object_ptr} pair will be stored. The main function of most bare metal applications is not supposed to return and global/static objects will not be destructed. In this case it will be enough to implement the required function the following way:

extern "C" int __aeabi_atexit( 
    void *object, 
    void (*destructor)(void *), 
    void *dso_handle) 
{ 
    static_cast<void>(object); 
    static_cast<void>(destructor); 
    static_cast<void>(dso_handle); 
    return 0; 
} 

void* __dso_handle = nullptr;

However, if your main function returns and then the code jumps back to the initialisation/reset routine, there is a need to properly perform destruction of global/static objects. You'll have to allocate enough space to store all the necessary {destructor_ptr, object_ptr} pairs, then in __aeabi_atexit function store the pair in the area pointed by __dso_handle, while incrementing value of later. Note, that dso_handle parameter to the __aeabi_atexit function is actually a pointer to the global __dso_handle value. Then, when the main function returns, invoke the stored destructors in the opposite order while passing addresses of the relevant objects as their first arguments.

To verify all the stated above let's take a look again at the generated code of initialisation function (after the destructor was added):

00008170 <_GLOBAL__sub_I__ZN7SomeObj9globalObjE>: 
    8170:    e92d4010     push    {r4, lr} 
    8174:    e59f4020     ldr    r4, [pc, #32]    ; 819c <_GLOBAL__sub_I__ZN7SomeObj9globalObjE+0x2c> 
    8178:    e3a01001     mov    r1, #1 
    817c:    e1a00004     mov    r0, r4 
    8180:    e3a02002     mov    r2, #2 
    8184:    ebffffeb     bl    8138 <_ZN7SomeObjC1Eii> 
    8188:    e1a00004     mov    r0, r4 
    818c:    e59f100c     ldr    r1, [pc, #12]    ; 81a0 <_GLOBAL__sub_I__ZN7SomeObj9globalObjE+0x30> 
    8190:    e59f200c     ldr    r2, [pc, #12]    ; 81a4 <_GLOBAL__sub_I__ZN7SomeObj9globalObjE+0x34> 
    8194:    e8bd4010     pop    {r4, lr} 
    8198:    eaffffe9     b    8144 <__aeabi_atexit> 
    819c:    000081a8     andeq    r8, r0, r8, lsr #3 
    81a0:    00008140     andeq    r8, r0, r0, asr #2 
    81a4:    000081bc             ; <UNDEFINED> instruction: 0x000081bc 

00008140 <_ZN7SomeObjD1Ev>: 
    8140:    e12fff1e     bx    lr 

000081bc <__dso_handle>: 
    81bc:    00000000     andeq    r0, r0, r0

Indeed, the call to the constructor immediately followed by the call to __aeabi_atexit with address of the object in r0 (first parameter), address of the destructor in r1 (second parameter) and address of __dso_handle in r2 (third parameter).

CONCLUSION: It is better to design the “main” function to contain infinite loop and never return to save the implementation of destructing global/static objects functionality.

Last updated