Встраиваемые системы

Rust для embedded

2019 год. Microsoft Security Response Center анализирует 12 лет CVE и находит: 70% уязвимостей - memory safety ошибки на C/C++. В операционной системе патч выходит за ночь. В прошивке кардиостимулятора патч - это хирургическая операция. Rust решает эту проблему не сборщиком мусора, а компилятором - без накладных расходов в runtime.

  • Medtronic insulin pump (2019): CVE позволял удалённо управлять дозой инсулина через радиоканал - buffer overflow в C firmware
  • Tesla Model S (2015): Charlie Miller и Chris Valasek взяли управление через CAN bus - memory corruption в обработчике CAN-фреймов
  • Boeing 787 MCAS: 737 MAX катастрофы частично обусловлены отсутствием формальной верификации safety-critical кода
  • Google Android: переход C/C++ модулей Bluetooth и WiFi стека на Rust сократил memory safety баги на 68% за 2 года

no_std и ownership без GC

2019 год. Microsoft Security Response Center публикует анализ всех CVE за последние 12 лет. Вывод: 70% уязвимостей - ошибки memory safety на C и C++. Use-after-free, buffer overflow, data race. В десктопе патч выходит за сутки. В прошивке кардиостимулятора или автомобильного ABS - патч требует отзыва устройства, перепрошивки у дилера, а иногда хирургической операции пациента. Rust решает эту проблему не через garbage collector, а через систему типов.

Стандартная библиотека Rust предполагает OS: heap allocation, потоки, stdin/stdout. Микроконтроллер Cortex-M0 с 16 KB RAM этого не предоставляет. Атрибут `#![no_std]` отключает стандартную библиотеку и оставляет только `core` - подмножество без alloc и OS-зависимостей. Это не ограничение, а честность: компилятор знает ровно столько, сколько есть в железе.

Ownership - это не про embedded специально. Это общее правило: у каждого значения один владелец, при выходе из scope значение уничтожается. Без GC. Без счётчика ссылок. Компилятор доказывает во время сборки, что память освобождается ровно один раз, и нет никаких dangling pointers. То, на что GC тратит паузы в 5-50 мс - Rust делает в compile time без накладных расходов в runtime.

В embedded `#![no_std]` + `#![no_main]` - стандартный старт. Крейт `panic-halt` останавливает процессор при панике (альтернативы: `panic-semihosting` для отладки, `panic-itm` для ITM trace). Без обработчика паники код не скомпилируется - Rust требует явного решения.

Borrow checker работает во время компиляции и отслеживает лайфтаймы. Обращение к периферии через регистры - типичный кейс: два места кода не могут одновременно владеть `GPIOA`. В C это гарантируется дисциплиной разработчика (и нарушается). В Rust - это compile error. Это именно то свойство, которое нужно в safety-critical firmware.

Для чего нужен атрибут `#![no_std]` в embedded Rust?

unsafe: явный контракт с железом

Парадокс: самый безопасный язык для embedded требует `unsafe` для любого обращения к периферии. Это не признак слабости - это честность. Borrow checker не знает, что регистр `0x4800_0014` - это ODR регистр GPIOA, и что запись в него меняет напряжение на ноге PA5. Такие знания выходят за модель памяти языка. `unsafe` - это явный контракт: "здесь разработчик берёт ответственность на себя, а компилятор - в сторону".

`write_volatile` - не каприз. Оптимизатор LLVM видит переменную, которую "никто не читает", и выбрасывает запись как мёртвый код. `volatile` сообщает: у этой записи side effect в железе, не оптимизировать. В C это `volatile uint32_t*` - и это работает, но легко забыть. В Rust, если написать `core::ptr::write` вместо `write_volatile` - borrow checker не поймает, зато firmware перестанет работать при `-O2`.

Блок `unsafe` не отключает borrow checker и типизацию. Он разрешает только четыре вещи: разыменование raw pointer, вызов unsafe функций, доступ к mutable static, реализацию unsafe trait. Всё остальное - те же правила.

Минимизация unsafe - архитектурный принцип. Хороший embedded Rust: один unsafe блок в HAL, который оборачивает регистры в безопасный API, и весь прикладной код - safe Rust. Именно это и делают крейты `stm32f4xx-hal`, `nrf52840-hal`: unsafe внутри, безопасный API снаружи. Если unsafe расползается по всему коду - это запах архитектурной проблемы.

Почему при работе с hardware-регистрами нужен `core::ptr::write_volatile` вместо обычного присваивания?

PAC, HAL и embedded-hal трейты

Каждый микроконтроллер имеет SVD файл - XML с описанием всех периферийных регистров (адреса, поля, разрядность). Инструмент `svd2rust` читает SVD и генерирует Rust-крейт - PAC (Peripheral Access Crate). PAC для STM32F411: `stm32f4`. Внутри - типобезопасные обёртки над каждым регистром. Не `*(0x4800_0014) = 1 << 5`, а `gpioa.odr.modify(|_, w| w.odr5().set_bit())`. Unsafe изолирован внутри сгенерированного кода.

HAL (Hardware Abstraction Layer) строится поверх PAC и добавляет высокоуровневые абстракции. `stm32f4xx-hal` превращает сырой доступ к GPIOA в объект `PA5<Output<PushPull>>`, который знает свой режим работы на уровне типов. Попытка вызвать `read()` на пине в режиме Output - ошибка компиляции, не runtime panic. Это typestate pattern: состояние железа закодировано в типе.

embedded-hal - это крейт с трейтами: `OutputPin`, `InputPin`, `SpiDevice`, `I2c`, `Serial`. Трейты - контракты. HAL для STM32 реализует `OutputPin` для своих GPIO типов. HAL для nRF52 реализует тот же `OutputPin`. Драйвер дисплея SSD1306 написан поверх `SpiDevice<u8>` - и работает на любом железе, где есть реализация этого трейта. Один драйвер для тысячи плат. В C это `#ifdef BOARD_STM32 / #elif BOARD_NRF52` на 200 строк.

Стек Rust embedded: SVD файл -> `svd2rust` -> PAC (типобезопасные регистры) -> HAL (высокоуровневые абстракции, typestate) -> `embedded-hal` трейты (переносимый API) -> драйверы устройств (работают на любом железе). Каждый слой сужает unsafe-зону и поднимает уровень абстракции.

unsafe в embedded Rust означает, что код такой же небезопасный, как C

unsafe - явно размеченная зона, изолированная в HAL/PAC. Весь прикладной код остаётся safe Rust с compile-time гарантиями

В C нет механизма изолировать небезопасные операции. В Rust unsafe - граница с аудитом: можно найти все 47 строк unsafe в проекте одним grep и проверить каждую

В чём главное преимущество typestate pattern в embedded HAL (например, `PA5<Output<PushPull>>`)?

Ключевые идеи

  • `#![no_std]` убирает стандартную библиотеку, оставляя `core` - работает без OS и heap allocation
  • Ownership + borrow checker гарантируют memory safety в compile time - тот же уровень безопасности что у GC, но без runtime пауз
  • `unsafe` - явная граница, а не выключатель безопасности: изолирован в PAC/HAL, прикладной код остаётся safe
  • PAC генерируется из SVD файла через `svd2rust` - типобезопасные обёртки над каждым регистром
  • HAL строится поверх PAC, кодирует состояние периферии в типах (typestate), неправильное использование - compile error
  • `embedded-hal` трейты делают драйверы устройств переносимыми: один `SpiDevice` трейт для STM32, nRF52, RP2040

Связанные темы

Rust embedded опирается на несколько пересекающихся областей:

  • C для embedded — Предшественник, с которым сравнивается Rust: те же задачи, другие гарантии
  • Safety-Critical Systems — Rust как основной инструмент для IEC 61508, ISO 26262 сертификации прошивок
  • Ownership модель — Теоретическая база borrow checker - основа memory safety без GC
  • Управление памятью OS — Контраст: в embedded нет OS memory manager, Rust берёт эту роль на compile time

Вопросы для размышления

  • Если 70% CVE - это memory safety ошибки, почему индустрия до сих пор пишет embedded firmware преимущественно на C?
  • typestate pattern кодирует состояние железа в типе. Какие ещё состояния периферии можно было бы выразить таким образом - и где это будет избыточно?
  • unsafe блоки в Rust vs volatile в C: оба требуют дисциплины разработчика. В чём принципиальное отличие с точки зрения аудитируемости кода?

Связанные уроки

  • emb-04 — C для embedded - фундамент перед переходом на Rust
  • emb-14 — Safety-critical системы используют Rust как основной инструмент
  • plt-13-ownership — Ownership в embedded - те же правила, цена ошибки выше
  • plt-10-linear-types — Линейные типы - теоретическая основа ownership модели Rust
  • os-07-memory — Управление памятью без OS - то, что решает Rust для embedded
  • os-01-intro
Rust для embedded

0

1

Войти