Skip to content

ESP-IDF startup: initialize Xtensa THREADPTR before C++ global constructors #21

@ltowarek

Description

@ltowarek

Fix lives in: ESP-IDF startup / FreeRTOS-Xtensa port

What the workaround does

We run a constructor very early in the static-init sequence that points the Xtensa THREADPTR register at a static 4 KB TLS area, so subsequent thread_local accesses succeed even before vTaskStartScheduler:

// src/esp_posix_shims.c:20-65
static __attribute__((aligned(16))) uint8_t s_pre_main_tls[4096];
__attribute__((constructor(101)))
static void esp_posix_shims_init_threadptr(void) {
    asm volatile ("wsr.threadptr %0" :: "a"(s_pre_main_tls));
}

We additionally inject -DNDEBUG on Abseil/protobuf to disable Mutex deadlock detection:

# cmake/protobuf_setup.cmake:170-178
target_compile_definitions(absl_synchronization PRIVATE NDEBUG)
target_compile_definitions(libprotobuf PRIVATE NDEBUG)
# ...

Root cause

protobuf's global static constructors build descriptor tables via Abseil hash maps. Two TLS-touching paths fire before vTaskStartScheduler:

  1. Abseil hash tables use thread_local uint16_t seed without an initialization guard. Reading the variable accesses the Xtensa THREADPTR register, which is 0 at static-init time → LoadProhibited exception.
  2. Abseil Mutex deadlock detection (DebugOnlyDeadlockCheck) calls GetOrCreateCurrentThreadIdentitypthread_key_createpvTaskGetThreadLocalStoragePointer. FreeRTOS TLS is not initialized at this point.

The C++ standard guarantees thread_local works during dynamic initialization. The Xtensa port is the odd one out — most architectures initialize their thread pointer in C runtime startup (_init / crt0) before global constructors run.

Proposed fix

ESP-IDF should initialize THREADPTR (and FreeRTOS TLS) before C++ global constructors. Concretely:

  • ESP-IDF's start_app_cpu / start_cpu0 / startup hooks should set THREADPTR to a valid address (a per-app static TLS area, or the bootstrap idle task's TLS) before invoking __libc_init_array.
  • Once THREADPTR is valid pre-main, both paths above stop crashing — and -DNDEBUG becomes unnecessary because Mutex deadlock detection's TLS path works.

What we delete once fixed

  • The THREADPTR pre-init constructor and the s_pre_main_tls buffer in src/esp_posix_shims.c:20-65
  • The -DNDEBUG injection on Abseil/protobuf targets in cmake/protobuf_setup.cmake:170-178

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions