Search code examples
linkerarmvtable

GCC ARM : vtable not initialized


I'm using arm-none-eabi-g++ to compile for an ARM Cortex M microcontroller. My code statically instantiates some modules, initialize them, and execute them sequentially in a loop. Each module (ModuleFoo, ModuleBar...) is a class which herits from a common Module class. The Module class defines two virtual functions, init() and exec(), which are implemented in every derived module. There are no explicit constructors, neither in the base nor the derived classes. Finally, I have a Context struct which is passed around and contains a list of pointers to the modules (Module* modules[]).

I had the following code which worked :

int main() {
    ModuleFoo foo;
    ModuleBar bar;
    Context context;
    const int N_MODULES = 2;
    context.modules[0] = &foo; // Indexes are actually an enum but I stripped it to make it shorter
    context.modules[1] = &bar;

    for (int i = 0; i < N_MODULES; i++) {
        context.modules[i]->init(context);
    }

    while (1) {
        for (int i = 0; i < N_MODULES; i++) {
            context.modules[i]->exec(context);
        }
    }
}

So far, so good (at least I think so, in any case it worked).

Now, I want to make the system more maintainable by moving all the code related to "which modules are used in a particular configuration" to a separate config.cpp/config.h file :

config.cpp :

ModuleFoo foo;
ModuleBar bar;

void initContext(Context& context) {
    context.nModules = 2;
    context.modules[0] = &foo;
    context.modules[1] = &bar;
}

main.cpp :

#include "config.h"

int main() {
    Context context;
    initContext(context);

    for (int i = 0; i < context.nModules; i++) {
        context.modules[i]->init(context);
    }

    while (1) {
        for (int i = 0; i < context.nModules; i++) {
            context.modules[i]->exec(context);
        }
    }
}

The problem appears when init() is called on the first module (the MCU HardFaults). This is because, according to GDB, the vtable pointer is not initialized :

(gdb) p foo
$1 = {
  <Module> = {
    _vptr.Module = 0x0 <__isr_vector>, 
    _enabled = false
  },

I rolled back with Git to check, with the previous code structure the vtable pointer was correctly initialized. And according to the linker's map file and GDB, the vtable exists (at around the same address as before):

.rodata        0x0000000000008e14       0x2c ModuleFoo.o
               0x0000000000008e14                typeinfo name for ModuleFoo
               0x0000000000008e1c                typeinfo for ModuleFoo
               0x0000000000008e28                vtable for ModuleFoo

The pointer is simply not set. The only difference I see between the two versions is that in the first one the modules are instanciated on the stack, whereas on the second they are instanciated globally in the bss :

.bss           0x00000000200015fc      0x22c config.o
               0x00000000200015fc                foo
               0x000000002000164c                bar

Could this be the problem?

In any case, thanks for taking the time to read this far!

**EDIT : ** The problem was coming from the startup code and the linker script. I used the sample files provided with Atmel's ARM GCC toolchain, which seem to be poorely written and, most importantly, didn't call __libc_init_array() (which is used to call global constructors). I switched to using the startup/linker script from ASF, and it works way better. Thanks @FreddieChopin !


Solution

  • Show us the startup code you are using. Most likely you did not enable global constructors, which can be done by calling __libc_init_array() function. You can test this theory, by manually calling this function at the beginning of main() - it should work fine then. If it does, then you should add that function to your startup code (Reset_Handler).

    Quick test:

    int main() {
        extern "C" void __libc_init_array();
        __libc_init_array();
        // rest of your code...
    

    To do it properly, find the place where your startup code calls main() (usually sth like ldr rX, =main and blx rX or maybe directly as bl main) and right before that do exactly the same but with __libc_init_array instead of main.