gccarmglobaldestructornewlib

ARM + gcc: global destructors not called after main() returns, but constructors are


I am trying to write a simple "Hello, World!" firmware for Cortex-M0 CPU. The goal is to correctly initialize and shutdown C++ runtime so that global constructors are called before main() and global destructors are called after main(). So far I managed to get exactly half of it working - global ctors run correctly, but global destructors don't. I use gcc on Windows with the Newlib, which is supposed to initialize the runtime.

struct Test {
    Test() // This is invoked correctly.
    {}

    ~Test() // This is not invoked at all.
    {}
};

Test test;

int main()
{
    return 0;
}

extern void __libc_init_array();
extern void __libc_fini_array();

void reset_handler() // This is the entry point.
{
    __libc_init_array(); // Test::Test().

    asm volatile( "bl main" ); // Invoke 'main'.

    __libc_fini_array(); // Expect Test::~Test() but nothing happens.
}

I did quite a bit of research already, and it seems that for ARM compiler should generate a section called .init_array, for global constructors, and .fini_array, for global destructors, where it put pointer to the function to be invoked. Then, the linker will merge the sections from all units together, and __libc_init_array will "walk" through that section and call corresponding functions.

This is the relevant part of the linker script:

/* Initialization functions which run before main(),
       such as global constructors. */
.init_array : ALIGN( 4 ) {
    /* preinit data */
    PROVIDE_HIDDEN ( __preinit_array_start = . );
    KEEP( *( .preinit_array ) )
    PROVIDE_HIDDEN( __preinit_array_end = . );

    . = ALIGN(4);
    /* init data */
    PROVIDE_HIDDEN ( __init_array_start = . );
    KEEP( *( SORT( .init_array.* ) ) )
    KEEP(*(.init_array))
    PROVIDE_HIDDEN ( __init_array_end = . );
} > flash

/* Finalization functions which run after main(),
   such as global destructors. */
.fini_array : ALIGN( 4 ) {
    /* finit data */
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP ( *( .fini_array.* ) )
    KEEP ( *( .fini_array ) )
    PROVIDE_HIDDEN (__fini_array_end = .);
} > flash

What worries me, however, is that when I dump the object file, main.o, not even an executable, I can only see the .init_array section, but no .fini_array:

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .group        00000008  00000000  00000000  00000034  2**2
                  CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
  1 .group        00000008  00000000  00000000  0000003c  2**2
                  CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
  2 .text         0000022c  00000000  00000000  00000044  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  3 .data         00000000  00000000  00000000  00000270  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  4 .bss          00000001  00000000  00000000  00000270  2**2
                  ALLOC
  5 .text._ZN4TestC2Ev 00000012  00000000  00000000  00000270  2**1  << Test::Test()
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  6 .text._ZN4TestD2Ev 00000012  00000000  00000000  00000282  2**1  << Test::~Test()
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  7 .init_array   00000004  00000000  00000000  00000294  2**2    << WHERE IS .fini_array???
                  CONTENTS, ALLOC, LOAD, RELOC, DATA
  8 .debug_info   000012d9  00000000  00000000  00000298  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
  9 .debug_abbrev 000003a1  00000000  00000000  00001571  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 10 .debug_aranges 00000030  00000000  00000000  00001912  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
 11 .debug_ranges 00000020  00000000  00000000  00001942  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
 12 .debug_line   000002d2  00000000  00000000  00001962  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
 13 .debug_str    000009e4  00000000  00000000  00001c34  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 14 .comment      0000004d  00000000  00000000  00002618  2**0
                  CONTENTS, READONLY
 15 .debug_frame  0000054c  00000000  00000000  00002668  2**2
                  CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
 16 .ARM.attributes 0000002c  00000000  00000000  00002bb4  2**0
                  CONTENTS, READONLY

This is how I invoke the compiler:

arm-none-eabi-c++.exe -o main.elf --verbose  -mcpu=cortex-m0 -mthumb --specs=nano.specs --entry reset_handler -T./../src/firmware.ld -ggdb -O0 -Wall -Wextra -Wpedantic -Werror ../src/main.cpp

And this is the log:

GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 7a1ab17ae8404f635d46188cccacd8be
COLLECT_GCC_OPTIONS='-o' 'main.elf' '-v' '-mcpu=cortex-m0' '-mthumb' '-specs=nano.specs' '-e' 'reset_handler' '-T' './../src/firmware.ld' '-ggdb' '-O0' '-Wall' '-Wextra' '-Wpedantic' '-Werror' '-mfloat-abi=soft' '-march=armv6s-m'
 c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/../../../../arm-none-eabi/bin/as.exe -v -march=armv6s-m -mfloat-abi=soft -meabi=5 
GNU assembler version 2.34.0 (arm-none-eabi) using BFD version (GNU Arm Embedded Toolchain 9-2020-q2-update) 2.34.0.20200428
COMPILER_PATH=c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/;c:/id/gcc/bin/../lib/gcc/;c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/../../../../arm-none-eabi/bin/
LIBRARY_PATH=c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/thumb/v6-m/nofp/;c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/../../../../arm-none-eabi/lib/thumb/v6-m/nofp/;c:/id/gcc/bin/../arm-none-eabi/lib/thumb/v6-m/nofp/;c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/;c:/id/gcc/bin/../lib/gcc/;c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/../../../../arm-none-eabi/lib/;c:/id/gcc/bin/../arm-none-eabi/lib/
COLLECT_GCC_OPTIONS='-o' 'main.elf' '-v' '-mcpu=cortex-m0' '-mthumb' '-specs=nano.specs' '-e' 'reset_handler' '-T' './../src/firmware.ld' '-ggdb' '-O0' '-Wall' '-Wextra' '-Wpedantic' '-Werror' '-mfloat-abi=soft' '-march=armv6s-m'
 c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/collect2.exe -plugin c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/liblto_plugin-0.dll -plugin-opt=c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/lto-wrapper.exe -plugin-opt=-fresolution=C:\Users\MAXID~1\AppData\Local\Temp\ccpOTkFN.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lg_nano -plugin-opt=-pass-through=-lc_nano -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc_nano --sysroot=c:\id\gcc\bin\../arm-none-eabi -X -o main.elf -e reset_handler c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/thumb/v6-m/nofp/crti.o c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/thumb/v6-m/nofp/crtbegin.o c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/../../../../arm-none-eabi/lib/thumb/v6-m/nofp/crt0.o -Lc:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/thumb/v6-m/nofp -Lc:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/../../../../arm-none-eabi/lib/thumb/v6-m/nofp -Lc:/id/gcc/bin/../arm-none-eabi/lib/thumb/v6-m/nofp -Lc:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1 -Lc:/id/gcc/bin/../lib/gcc -Lc:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/../../../../arm-none-eabi/lib -Lc:/id/gcc/bin/../arm-none-eabi/lib C:\Users\MAXID~1\AppData\Local\Temp\cc1lU5Gg.o -lstdc++_nano -lm --start-group -lgcc -lg_nano -lc_nano --end-group --start-group -lgcc -lc_nano --end-group c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/thumb/v6-m/nofp/crtend.o c:/id/gcc/bin/../lib/gcc/arm-none-eabi/9.3.1/thumb/v6-m/nofp/crtn.o -T ./../src/firmware.ld
COLLECT_GCC_OPTIONS='-o' 'main.elf' '-v' '-mcpu=cortex-m0' '-mthumb' '-specs=nano.specs' '-e' 'reset_handler' '-T' './../src/firmware.ld' '-ggdb' '-O0' '-Wall' '-Wextra' '-Wpedantic' '-Werror' '-mfloat-abi=soft' '-march=armv6s-m'

To be fair, I have zero idea of what could be wrong - I tried the default linker script, .fini_array did not appear. The only idea which comes to mind is that my compiler was built with some kind of a flag which disabled the .fini_array section???

Any ideas will be much appreciated!

UPDATE: after further investigation, it seems that the destructors have more to do with __cxa_atexit... Will investigate further.

UPDATE2: Thanks to this wonderful discussion here https://forum.osdev.org/viewtopic.php?f=13&t=36728 I added -fno-use-cxa-atexit, and the .fini_array section appeared!


Solution

  • Ok, so for those who are interested, there are two ways for gcc to generate calls to global destructors. One way is via __cxa_atexit. The compiler will inject a piece of code into the global constructor, which uses __cxa_atexit to register the destructor of that object. This way, when exit is called, the runtime "knows" which destructors to invoke. This complexity is needed to deal with loading/unloading of shared libraries.

    However, it is also possible to pass a -fno-cxa-atexit flag, and the compiler will put the calls to global destructors into .fini_array. The Newlib then uses this section to invoke the destructors. More can be found here: https://forum.osdev.org/viewtopic.php?f=13&t=36728