Несколько пасхальных яиц в ESP32 gcc toolchain

Я отдаю себе отчет в том, что тема может быть интересна всего паре человек, но тем не менее, оставлю это тут для поисковых машин.

Короче говоря.

Первое:

В ESP32, с текущими версями toolchain, так делать нельзя:

#include <stdio.h>

// переменная, указатель, для каждого потока - своя.
// хранится на стеке задачи.
//
static __thread char *example_variable = "42";

// функция вызывается до main()
//
static void __attribute__((constructor)) run_before_main() {
      example_variable = "43";
}

int main() {
   return printf("Value = %s\r\n", example_variable);
}

Все люто повиснет.
Exception while in exception, даже coredump сохранить не может. Красиво грохается, редко такое бывает.

В документации ничего на эту тему нет, и, судя по всему \то фишка именно esp32-gcc и FreeRTOS. Это сильно странно, потому, что можно спокойно создавать и завершать задачи непосредственно в функции-конструкторе. Все кагбэ уже проинициализировано: и память и куча и шедулер. Но тем не менее.

Второе:

Следующий прикол, описание которого, правда, при усердном поиске удалось обнаружить на сайте Espressif

Если у вас определена локальная функция (функция в функции)

void a() {
  void b(int i) {
    printf("%d\r\n",i);
  }
  b(1); b(2); b(3); b(4); b(5); b(6);
}

То указатель на такую (внутреннюю) функцию добыть можно, а вот использовать - нет.

Нельзя передать адрес b() куда-то, чтобы вызвать. Проблема связана с тем, как GCC устраивает точку входа в такую функцию. Проблема, насколько я знаю, нерешаемая, но если сильно интересно, поищите в гугле по словам “esp32 gcc trampoline toolchain”

// Чтение 32-битного числа из массива байт
void read_uint32(const uint8_t *data) {
// Преобразуем указатель на byte в указатель на uint32_t.
uint32_t value = *((uint32_t*)data);
printf("Value: %lu\n", value);
}

void app_main() {
uint8_t byte_array[10] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA};
// Всё работает, потому что массив на стеке обычно выровнен
read_uint32(byte_array); // OK: address 0x3ffb5030 % 4 == 0
// А вот здесь будет КРАХ!
read_uint32(byte_array + 1); // CRASH: address 0x3ffb5031 % 4 == 1 -> Unaligned!
}



//vTaskDelete — это не как return или exit()
void my_task(void *arg) {
printf("Задача запущена!\n");
if (some_condition) {
printf("Завершаю себя...\n");
vTaskDelete(NULL); // Задача уничтожается здесь и сейчас.
printf("Я никогда не напечатаюсь!\n"); // Эта строка мертва.
}
// А этот код может выполниться...
vTaskDelay(1000);
// Но если условие выше выполнилось, то сюда управление не вернется.
// Бесполезный "аварийный" светодиод, который никогда не загорится.
gpio_set_level(GPIO_NUM_2, 1);
}

// должно быть так
void my_task(void *arg) {
printf("Задача запущена!\n");
if (some_condition) {
printf("Завершаю себя...\n");
// ВСЕ действия по очистке  должны быть выполнены ДО вызова vTaskDelete.
release_all_resources();
gpio_set_level(GPIO_NUM_2, 0); // Погасить светодиод ДО удаления
vTaskDelete(NULL);
// Кода после быть не должно.
}

// Основной цикл задачи
while(1) {
// ... полезная работа ...
vTaskDelay(1000);
}
}


кстате вот случай похожий на первый пример, неправильная инициализация)))
При переходе в Light-Sleep отключается питание с RAM, но сама память остаётся под напряжением (данные сохраняются). 
Однако, WiFi и BT стеки полностью выключаются. 
Если попытаться использовать их сразу после выхода из сна, не дождавшись инициализации — получим крах или тихий отказ.

void app_main() {
// 1. Настраиваем и подключаем WiFi
wifi_init_sta(); // Всё работает, подключились к AP
// 2. Уходим сон на 5 секунд
esp_sleep_enable_timer_wakeup(5000000);
printf("Уходим спать...\n");
esp_light_sleep_start(); // Тут MCU уснёт
// 3. Программа продолжится здесь после пробуждения
printf("Проснулись! Пробуем отправить данные...\n");
// СНОСИМ СИСТЕМУ или получаем молчаливый FAIL
esp_http_client_perform(...); // Краш! WiFi стек ещё не готов!
}

попробуйте дважды очисть psram)))
#include <esp_spiram.h>
void do_something() {
// Выделяем буфер в PSRAM (если она подключена)
char *buffer = heap_caps_malloc(1024, MALLOC_CAP_SPIRAM);
if (buffer) {
// ... что-то делаем с буфером ...
process_data(buffer);
// Освобождаем память
free(buffer); // Всё ок
// ... проходит ещё 100 строк кода ...
// ОШИБКА: По ошибке освобождаем УЖЕ ОСВОБОЖДЕННЫЙ указатель!
// Это не упадёт здесь и сейчас. Это отравит весь код.
free(buffer);
}
}

Многие функции в IDF неявно вызывают vTaskYield() или блокируют задачу (например, vTaskDelay, ожидание семафора). 
Если вы находитесь внутри критической секции, защищённой taskENTER_CRITICAL(), и вызываете такую функцию — вы заблокируете всю систему навсегда.
// Глобальный счётчик, доступ к которому нужно защитить
static uint32_t critical_counter = 0;

void increment_counter() {
taskENTER_CRITICAL(); // Запрещаем переключение задач и прерывания
critical_counter++;    // Начинаем критическую операцию...
// ОПАСНОСТЬ: Эта функция внутри себя может вызвать vTaskDelay() или yield!
// Например, если драйвер I2C не смог получить блокировку и "ждёт".
i2c_write_data(0x55, 0xAA);
// ... продолжаем операцию
critical_counter++;
taskEXIT_CRITICAL();  // ЭТА СТРОКА НИКОГДА НЕ ВЫПОЛНИТСЯ, если в i2c_write_data был yield.
}
// Система зависнет, потому что критические секции не сняты, прерывания отключены.


и еще 10ки примеров … мы будем обсуждать как поднасрать потомкам ?)))

нет, не будем.
я думаю, что ты не понимаешь, о чем я написал в первом посте :frowning:

#include <stdio.h>
// Объявляем thread-local переменную БЕЗ инициализации указателем.
// Компилятор проинициализирует её нулём для каждого потока.
static __thread char *example_variable = NULL;

// Функция-геттер, которая обеспечивает инициализацию при первом вызове из потока.
const char* get_example_value(void) {
// Проверяем, инициализирована ли переменная для текущей задачи
if (example_variable == NULL) {
// Инициализируем её. Для строковых констант можно просто присвоить указатель.
example_variable = "43";
// Если бы нужно было копировать строку в thread-local буфер,
// это делалось бы здесь через malloc или статический буфер.
} return example_variable; }

// Функция, вызываемая до app_main, НЕ ИСПОЛЬЗУЕТ TLS!
static void __attribute__((constructor)) run_before_main() {
// В конструкторе можно инициализировать только глобальные структуры,
// не зависящие от ОС.
printf("System initializing...\n");
}

void app_main() {
// Мы внутри задачи "main", планировщик уже запущен, TLS работает.
// Первый вызов функции инициализирует переменную для этой задачи.
printf("Value in main task = %s\n", get_example_value());
// Если мы создадим другую задачу, для неё переменная будет проинициализирована при первом вызове get_example_value.
xTaskCreate(another_task, "another_task", 4096, NULL, 5, NULL);
}

void another_task(void *arg) {
// Для этой новой задачи перемен example_variable изначально равна NULL.
// get_example_value() инициализирует её и вернёт "43".
printf("Value in another task = %s\n", get_example_value());
vTaskDelete(NULL);
}

или

#include <stdio.h>
// Объявляем переменную. Инициализацию в NULL оставляем для безопасности.
static __thread char *example_variable = NULL;

// Структура для передачи параметров в задачу
typedef struct {
const char *initial_value;
} task_params_t;

void my_task(void *arg) {
task_params_t *params = (task_params_t *)arg;
// Явно инициализируем TLS-переменную переданным значением
example_variable = (char *)params->initial_value;
// Не забываем освободить переданные параметры, если они были выделены в куче
free(arg);
// Теперь можно работать с переменной
printf("Value in my_task = %s\n", example_variable);
vTaskDelete(NULL);
}

void app_main() {
// Создаём параметры для задачи в куче (или можно на стеке, если задача заберёт их сразу)
task_params_t *params = malloc(sizeof(task_params_t));
if (params == NULL) {
// обработка ошибки
return;
}
params->initial_value = "43"; // Устанавливаем нужное начальное значение
// Создаём задачу и передаём параметры
xTaskCreate(my_task, "my_task", 4096, params, 5, NULL);
// Для главной задачи инициализируем так:
example_variable = "42";
printf("Value in main task = %s\n", example_variable);
}

Итог: Почему это правильно

  1. Уважается порядок инициализации: Не делается попыток использовать механизмы FreeRTOS (TLS) до того, как планировщик запустился (app_main ).
  2. Явность: Код чётко показывает, когда и как происходит инициализация переменных для каждого потока.
  3. Безопасность: Исключается состояние гонки и обращение к неинициализированным структурам данных FreeRTOS.
  4. Портативность: Такой подход будет работать не только на ESP32, но и на других системах с FreeRTOS.

Использование __attribute__((constructor)) в ESP32 оправдано только для самой элементарной инициализации глобальных переменных, которые никак не зависят от операционной системы (например, заполнение статических lookup-таблиц). Всё, что связано с задачами, памятью или периферией, должно инициализироваться в app_main или после неё.

но вот так точно не интересно общаться будет))) по мне нечего не обычного… но может вы там видите что то большее… а вот как сделать себя незаменимым в коде, что бы при увольнении тебя было сложно заменить, или как дополнительно защитить прошивку даже если ее извлекут я бы обсудил))) как по мне так это веселее…

Внезапно, ЧатЖПТ тут лукавит. Дело в том, что шедулер УЖЕ запущен к моменту исполнения конструкторов. Чтобы в этом убедиться, можно попытаться запустить новую задачу (xTaskCreate(…)) непосредственно из функции-конструктора. И оно запустится. Все там можно делать. Я больше скажу, на моем рабочем компутере я написал маленбкую програмку для windows, и нет у нее ниаких проблем с доступом к TLS из функции-конструктора.

Это фишка ЕСП32 и его корявого компилятора :frowning:

Пиши ключевые части проекта сам, оставляй только двусмысленные комментарии :).

Если серьезно, то пару раз я наблюдал эффект, которого ты хочешь добиться. В обоих случаях было так:
Контора писала какой-то код. Операционку для роутера. А чувак занимался тем, что портировал туда всякие гнутые штуки, ну, там OSPF, BGP.

Ну и все.

Никто кроме него и не разбирался как оно спортировано и приклеено к проекту, поэтому Васю держали в штате. Вася знал, где баги искать, хотя бы.

Заткм - если ты в проекте пишешь, например, загрузчик, драйвера, BSP, или, не дай бог инициализацию (кэш, там, прерывания), то тоже будешь в штате сидеть. Даже, если последний раз твой бутлоадeр перекомпиляли год назад.

Если ты написал какой-то железячной конторе кодогенератор для GCC. Тебя будут беречь.

Да много есть способов стать незаменимым, безо всяких нехорошестей. Если ты в проекте отвечаешь за какую-то сложную его часть (или не сложную, но требующую специальных знаний) то ты и так уже незаменим.

Выгонять программиста, это не только весело, а еще и геморой. Найти нового нормального программиста - это непросто.