cancel
Showing results for 
Search instead for 
Did you mean: 

Object initialization silently skipped with picolibc due to missing linker symbols

FluxPower42
Associate III
Disclaimer:

Probably not quite the right place for this bug report. However, since the issue appeared with the update to CubeIDE VSCode v3.8.0 and STM32CubeMX v6.17.0, and the project in the attached ZIP was created with these versions to reproduce the behavior, I am placing it here.

Introduction

Link to Project on GitHub: init_array_issue (or use the attached ZIP file)

The code in file init_array_issue.cpp demonstrates a misbehavior where, under certain conditions, the initialization of objects is silently skipped without any warning or error message when picolibc is used together with the linker description file STM32H753XX_FLASH.ld generated by STM32CubeMX and the default optimization -Og.

  • picolibc / STARM_PICOLIBC
    The use of picolibc has been the default since the STM32CubeMX update to 6.17.0, configured via the option STARM_PICOLIBC in the file .\cmake\starm-clang.cmake.
    Previously, STARM_HYBRID was set here by default.

    # STARM_TOOLCHAIN_CONFIG allows you to choose the toolchain configuration.
    # Possible values are:
    # "STARM_HYBRID" : Hybrid configuration using starm-clang Assembler and Compiler and GNU Linker
    # "STARM_NEWLIB" : starm-clang toolchain with NEWLIB C library
    # "STARM_PICOLIBC" : starm-clang toolchain with PICOLIBC C library
    set(STARM_TOOLCHAIN_CONFIG "STARM_PICOLIBC")
    (Source: .\cmake\starm-clang.cmake)

 

  • Optimization level -Og
    The use of the `-Og` optimization has been the default since the STM32CubeMX update to 6.17.0, set in the file `.\cmake\starm-clang.cmake`. Previously, `-O0` was set here.

    set(CMAKE_CXX_FLAGS_DEBUG "-Og -g3")
    (Source: .\cmake\starm-clang.cmake)


Code Description

#include <cstdio>
#include <cstdint>

class Data {
  public:
    Data(uint16_t firstValue) : value(firstValue) {}; // Deleting the constructor results in an assignment in .data and masks the bug.
    const uint8_t ClsVers = 0x42;
    uint16_t value = 0;
};

class Operation {
  public:
    uint8_t id = 0;
    uint8_t cmd = 0;
};

class Foo {
  public:
    // uint8_t buffer[16] = {}; // Activating this line don't change the result.
    Data data{0xBABE};
    Operation op[2] = {}; // op[1] instead op[2] results in an assignment in .data and masks the bug.
    //uint8_t buffer[16] = {}; // Activating this line results in an assignment in .data and masks the bug.
};

Foo foo;

extern "C" void init_array_issue(void) {
    printf("foo after Start:\n");
    printf("================\n");
    printf("foo.data.ClsVers = 0x%02X    (expected: 0x42)\n", foo.data.ClsVers);
    printf("foo.data.value   = 0x%04X  (expected: 0xBABE)\n", foo.data.value);
    printf("foo.op[0].id     = %d       (expected: 0)\n", foo.op[0].id);
    printf("foo.op[0].cmd    = 0x%02X    (expected: 0x00)\n", foo.op[0].cmd);
    if (sizeof(foo.op) > sizeof(Operation)) {
        printf("foo.op[1].id     = %d       (expected: 0)\n", foo.op[1].id);
        printf("foo.op[1].cmd    = 0x%02X    (expected: 0x00)\n\n", foo.op[1].cmd);
    }
 
    foo.data.value = 0xCAFE;
    foo.op[0].id = 1;
    foo.op[0].cmd = 0xAA;
    if (sizeof(foo.op) > sizeof(Operation)) {
        foo.op[1].id = 2;
        foo.op[1].cmd = 0xBB;
    }
    printf("foo after writing values:\n");
    printf("=========================\n");
    printf("foo.data.ClsVers = 0x%02X    (expected: 0x42)\n", foo.data.ClsVers);
    printf("foo.data.value   = 0x%04X  (expected: 0xCAFE)\n", foo.data.value);
    printf("foo.op[0].id     = %d       (expected: 1)\n", foo.op[0].id);
    printf("foo.op[0].cmd    = 0x%02X    (expected: 0xAA)\n", foo.op[0].cmd);
    if (sizeof(foo.op) > sizeof(Operation)) {
        printf("foo.op[1].id     = %d       (expected: 2)\n", foo.op[1].id);
        printf("foo.op[1].cmd    = 0x%02X    (expected: 0xBB)\n\n", foo.op[1].cmd);
    }
}

The project illustrate the misbehavior as simply as possible using common patterns.

The project contains three classes.
Class Foo is the container holding an object of class Data and an array of two elements of class Operation.
The single object foo is instantiated in the global scope of the program shortly after the class declarations.

The class Data has a const uint8_t ClsVers with the value 0x42.
Note: This value should never deviate from 0x42.

Furthermore, the class expects a value to be passed via the constructor, which is written to the variable value.

The class Operation is simple and contains only two variables: id and cmd.
The class Foo holds an array of two objects of this class.


This construction causes the object foo to be linked into the .bss section when optimization -Og is used.

Excerpt from the ./build/Debug/init_array_issue.map file:

200000b8 200000b8 8 2 CMakeFiles/init_array_issue.dir/init_array_issue.cpp.obj:(.bss.foo)
200000b8 200000b8 8 1 foo

 

Expected vs. Actual Behavior

The method init_array_issue() uses the object foo to test the behavior.
The following output is generated:

foo after Start:
================
foo.data.ClsVers = 0x00 (expected: 0x42)
foo.data.value = 0x0000 (expected: 0xBABE)
foo.op[0].id = 0 (expected: 0)
foo.op[0].cmd = 0x00 (expected: 0x00)
foo.op[1].id = 0 (expected: 0)
foo.op[1].cmd = 0x00 (expected: 0x00)

foo after writing values:
=========================
foo.data.ClsVers = 0x00 (expected: 0x42)
foo.data.value = 0xCAFE (expected: 0xCAFE)
foo.op[0].id = 1 (expected: 1)
foo.op[0].cmd = 0xAA (expected: 0xAA)
foo.op[1].id = 2 (expected: 2)
foo.op[1].cmd = 0xBB (expected: 0xBB)

Neither foo.data.ClsVers nor foo.data.value match expectations after program start.
After writing various values, they are stored correctly. However, the const value foo.data.ClsVers remains on 0x00.


Background Behavior

Just before the call to the main function, the startup file startup_stm32h753xx.s calls the function __libc_init_array. This routine is responsible for initializing objects in the .bss segment.

This __libc_init_array function is part of picolibc. A part of this function calls all function pointers stored between __bothinit_array_start and __bothinit_array_end. The called functions were generated by the compiler to initialize objects in the .bss section.

The following code is a part of picolibc\libc\init.c and shows the call of __bothinit_array_start and __bothinit_array_end:

[...]
/*
* The init array immediately follows the preinit array,
* so we can just run both in one loop
*/
fn = __bothinit_array_start;
fn_end = __bothinit_array_end;
while (fn != fn_end)
(*fn++)();
(Source: https://github.com/picolibc/picolibc/blob/main/libc/misc/init.c)

 

The file picolibc\libc\includes\sys\_initfini.h defines these two symbols __bothinit_array_start and __bothinit_array_end as weak.
The following code shows this part:

[...]
/* These magic symbols are provided by the linker. */
extern void (*__preinit_array_start[])(void) __weak;
extern void (*__preinit_array_end[])(void) __weak;
extern void (*__init_array_start[])(void) __weak;
extern void (*__init_array_end[])(void) __weak;
extern void (*__bothinit_array_start[])(void) __weak;
extern void (*__bothinit_array_end[])(void) __weak;
(Source: https://github.com/picolibc/picolibc/blob/main/libc/include/sys/_initfini.h)


Root Cause

For this mechanism to work, it is absolutely essential that the linker writes the above-mentioned list of function pointers to the memory. To do this, it requires the two symbols __bothinit_array_start and __bothinit_array_end in the linker description file, which define the start and end of this memory region.
If these symbols are missing, picolibc cannot initialize anything at this point.


STM32H753XX_FLASH.ld

The correct place to define these symbols is the file STM32H753XX_FLASH.ld.
An analysis of the file shows that the symbols are not present.
Adding the following section to the file causes the foo object to be correctly initialized:

PROVIDE(__bothinit_array_start = __preinit_array_start);
PROVIDE(__bothinit_array_end = __init_array_end);
(see: `.\STM32H753XX_FLASH.ld_fixed`)


The Ouput after the fix is as expected:


foo after Start:
================
foo.data.ClsVers = 0x42 (expected: 0x42)
foo.data.value = 0xBABE (expected: 0xBABE)
foo.op[0].id = 0 (expected: 0)
foo.op[0].cmd = 0x00 (expected: 0x00)
foo.op[1].id = 0 (expected: 0)
foo.op[1].cmd = 0x00 (expected: 0x00)

foo after writing values:
=========================
foo.data.ClsVers = 0x42 (expected: 0x42)
foo.data.value = 0xCAFE (expected: 0xCAFE)
foo.op[0].id = 1 (expected: 1)
foo.op[0].cmd = 0xAA (expected: 0xAA)
foo.op[1].id = 2 (expected: 2)
foo.op[1].cmd = 0xBB (expected: 0xBB)


Influencing Factors

During testing, several factors were identified that influence whether the foo object is placed in the .bss segment (then the bug is visible) or in the .data segment (then the bug is hidden):

  • Number of elements in the Operation op[] array:
    Operation op[1] => .data
    Operation op[2] => .bss`
  • Presence of a constructor in class Data:
    no constructor => .data
    with constructor => .bss
  • Size and structure of `Foo` class:
    Additional line `uint8_t buffer[16];` before `Data data{0xBABE};` => .data
    Additional line `uint8_t buffer[16];` after `Operation op[2] = {};` => .bss

  • Optimization:
    -O0 => .data
    -Og => .bss

  • Toolchain config:
    Using "STARM_NEWLIB" don't need the symbols => normal behavior.
    Using "STARM_PICOLIBC" needs the missing symbols => bug

All of that is entirely correct behavior. The compiler only decides where the object is placed.

The bug is simply the interaction between the weak declaration in picolibc and the missing symbols __bothinit_array_start and __bothinit_array_end in the linker description file.

 

Possible Safety Impact

There is neither a warning nor an error message at build time or at runtime!
Without thorough firmware testing, such a bug can go undetected and — depending on the project — have very unpleasant consequences!

 

Proposal

@ STMicroelectronics:

  • Please insert the missing part when generating linker description files.
  • Think about the concept. In my opinion, it’s not good practice for a system to not give a warning or error message when there’s a drastic change in how it works. Something fatal could happen.

 

Test Setup

STM32CubeMX v6.17.0:

  • configuration see: init_array_issue.ioc
  • Board: NUCLEO-H753ZI
  • Toolchain: CMake
  • Default Compiler/Linker: ST Arm Clang
  • USART3: Tx=PD8, Rx=PD9, Baudrate=115200
  • Clock: HSE 5MHz, CPU Clock=480MHz
  • MCU Package: STM32H7 Series v1.13.0
  • this includes the STM32H753XX_FLASH.ld linker script file

Toolchain:

  • STM32CubeIDE for Visual Studio Code v3.8.0
  • C library: picolibc (default since STM32CubeMX v6.17.0 in .\cmake\starm-clang.cmake)
  • starm-clang 21.1.1+st.6
  • C++ Standard: C++11, C++17, C++23 (additionally defined in CMakeLists.txt)
  • Optimization: -Og (default in cmake/starm-clang.cmake)

Modified Files:

  • main.c (prototype void init_array_issue(void) and call init_array_issue() added)
  • syscall.c (__io_putchar added)
  • CMakeLists.txt (C++ Standard added, init_array_issue.cpp added)


Steps to use the Project

  1. Install the STM32CubeIDE for Visual Studio Code v3.8.0 extension in VSCode
  2. Install the Serial Monitor extension in VSCode (https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-serial-monitor)
    or use another RS232 serial tool
  3. Connect the NUCLEO-H753ZI board via USB
  4. Open init_array_issue.ioc in STM32CubeMX v6.17.0 and click the button GENERATE CODE
  5. Double-click on init_array_issue.code-workspace
  6. Select `debug` as target
  7. Build the project
  8. Connect the Serial Monitor at 115200 baud to the board
  9. Start the debugger and check the output

Hint: if you change something like STARM_TOOLCHAIN_CONFIG or the STM32H753XX_FLASH.ld file, make sure the project is completely rebuilt.

4 REPLIES 4
Nawres GHARBI
ST Employee

Hi @FluxPower42 
a fi is under cooking and will be delivered ASAP

vincent_grenet
ST Employee

@FluxPower42 
Please be aware STM32Cube bundle st-arm-clang@21.1.1+st.7 delivered today is fixing issue.
Please have a try updating and locking proper version to your project:

To give better visibility on the answered topics, please click on Accept as Solution on the reply which solved your issue or answered your question.

image.png

To give better visibility on the answered topics, please click on Accept as Solution on the reply which solved your issue or answered your question.
ThomasBzt
ST Employee

Hello

Some additional information about the fix.

st-arm-clang@21.1.1+st.7 reverts to the previous Picolibc implementation of __libc_init_array(), i.e. the one using the usual symbols:

  • __preinit_array_start
  • __preinit_array_end
  • __init_array_start
  • __init_array_end

This restores compatibility with the linker scripts provided by ST, as well as any custom linker script based on those symbols.

As a consequence, any manual addition of __bothinit_array_start and __bothinit_array_end to the linker script is no longer needed with st-arm-clang@21.1.1+st.7 and can be discarded.

Best regards