Найти в Дзене
File Energy

U-Boot SPL и путь к быстрой загрузке Linux через FIT и Falcon Mode

Каждая секунда холодного старта встраиваемого устройства стоит денег. На промышленном контроллере, который обязан выйти на рабочий режим за три секунды после подачи питания, каждые сто миллисекунд в загрузчике это не абстрактная цифра в отчёте, а жёсткое техническое требование. Именно в таких условиях понимаешь, зачем U-Boot разделён на две части, почему FIT-образы заменили устаревший legacy-формат, и что такое Falcon Mode, позволяющий запустить ядро Linux, вообще минуя основную часть загрузчика. Чтобы разобраться во всём этом, нужно начать с того, как Boot ROM передаёт управление, почему U-Boot не может запуститься целиком из on-chip памяти, и как SPL решает эту задачу минимальными средствами. После подачи питания Boot ROM процессора запускается первым. Он умеет немного: инициализирует минимальную периферию, проверяет источник загрузки и ищет первичный загрузчик. Проблема в том, что на момент запуска Boot ROM доступна только небольшая on-chip SRAM, обычно от 64 до 256 килобайт. Полноц
Оглавление

Каждая секунда холодного старта встраиваемого устройства стоит денег. На промышленном контроллере, который обязан выйти на рабочий режим за три секунды после подачи питания, каждые сто миллисекунд в загрузчике это не абстрактная цифра в отчёте, а жёсткое техническое требование. Именно в таких условиях понимаешь, зачем U-Boot разделён на две части, почему FIT-образы заменили устаревший legacy-формат, и что такое Falcon Mode, позволяющий запустить ядро Linux, вообще минуя основную часть загрузчика.

Чтобы разобраться во всём этом, нужно начать с того, как Boot ROM передаёт управление, почему U-Boot не может запуститься целиком из on-chip памяти, и как SPL решает эту задачу минимальными средствами.

Почему U-Boot разделён на SPL и U-Boot proper

После подачи питания Boot ROM процессора запускается первым. Он умеет немного: инициализирует минимальную периферию, проверяет источник загрузки и ищет первичный загрузчик. Проблема в том, что на момент запуска Boot ROM доступна только небольшая on-chip SRAM, обычно от 64 до 256 килобайт. Полноценный U-Boot proper с командной строкой, поддержкой сетевых протоколов, файловых систем и сотнями команд занимает значительно больше. Положить его туда просто не получится.

Решение элегантное: SPL, Secondary Program Loader, это урезанная копия U-Boot, которая помещается в on-chip SRAM. Его единственная задача, инициализировать контроллер DDR-памяти и загрузить следующий этап. Размер SPL жёстко ограничен конфигурацией и обычно не превышает 100-200 килобайт. После того как DRAM доступна, SPL загружает туда U-Boot proper, ATF (ARM Trusted Firmware), а в режиме Falcon Mode напрямую ядро Linux.

Посмотреть размер SPL после сборки и убедиться, что он влезает в SRAM:

# После сборки U-Boot
ls -lh u-boot-spl.bin
size spl/u-boot-spl
# В конфигурации задаётся максимальный размер
grep CONFIG_SPL_MAX_SIZE .config

Включить поддержку SPL и загрузки FIT-образов в конфигурации U-Boot:

make menuconfig
# Или напрямую через .config / фрагменты конфигурации
cat >> configs/myboard_defconfig << 'EOF'
CONFIG_SPL=y
CONFIG_SPL_LOAD_FIT=y
CONFIG_SPL_FIT=y
CONFIG_FIT=y
CONFIG_FIT_SIGNATURE=y
EOF

Типичная последовательность загрузки на ARM SoC выглядит так: Boot ROM читает SPL из raw-раздела флеш-памяти, SPL инициализирует DDR и загружает U-Boot proper вместе с ATF, ATF переводит систему в нужный Exception Level и передаёт управление U-Boot proper, который уже монтирует файловую систему, читает конфигурацию и загружает ядро. На процессорах i.MX 8/9 от NXP эта цепочка выглядит как Boot ROM -> SPL -> ATF (BL31) -> U-Boot proper (BL33).

Формат FIT и почему он вытеснил legacy uImage

До FIT, Flattened Image Tree, стандартным форматом U-Boot был legacy uImage: монолитный бинарник с 64-байтным заголовком, содержащим контрольную сумму CRC32, тип архитектуры и адреса загрузки. Этот формат работал годами, но имел принципиальные ограничения. Он не поддерживал несколько device tree в одном файле, не имел встроенных механизмов верификации подписей и не позволял описывать зависимости между компонентами.

FIT-образ построен на базе libfdt и по сути является device tree blob с вложенными бинарными данными. Исходный файл с расширением .its (Image Tree Source) описывает структуру образа в синтаксисе DTS, а mkimage компилирует его в бинарный .itb. Такой подход даёт гибкость, которой у legacy-формата никогда не было: один файл может содержать несколько ядер, несколько device tree blob для разных ревизий платы, ramdisk, прошивку FPGA и что угодно ещё.

Минимальный ITS-файл для ядра с device tree:

/dts-v1/;
/ {
description = "Kernel with DTB";
#address-cells = <1>;

images {
kernel-1 {
description = "Linux kernel";
data = /incbin/("Image");
type = "kernel";
arch = "arm64";
os = "linux";
compression = "none";
load = <0x40480000>;
entry = <0x40480000>;
hash-1 {
algo = "sha256";
};
};
fdt-1 {
description = "Device Tree";
data = /incbin/("myboard.dtb");
type = "flat_dt";
arch = "arm64";
compression = "none";
hash-1 {
algo = "sha256";
};
};
};

configurations {
default = "conf-1";
conf-1 {
description = "Boot config";
kernel = "kernel-1";
fdt = "fdt-1";
};
};
};

Собрать образ и проверить его содержимое:

mkimage -f kernel.its kernel.itb
mkimage -l kernel.itb

Вывод mkimage -l покажет все компоненты, их хеши и конфигурации. Это принципиальное преимущество перед legacy-форматом: образ самодокументирован, и его содержимое можно проверить без загрузки на устройство.

Подписанные FIT-образы и verified boot через SPL

Формат FIT изначально проектировался с поддержкой криптографических подписей. Механизм verified boot позволяет подписать конфигурацию целиком, включая хеши всех её компонентов, что защищает от атаки mix-and-match, при которой злоумышленник подменяет ядро или device tree по отдельности.

Подпись конфигурации, а не отдельных компонентов, является рекомендуемым подходом. Когда подписана конфигурация, её подпись охватывает хеши всех компонентов, на которые она ссылается. Подписать конкретную конфигурацию значит закрыть весь комплект.

Создать ключевую пару для подписи FIT:

mkdir -p keys
openssl genpkey -algorithm RSA \
-out keys/dev.key \
-pkeyopt rsa_keygen_bits:2048 \
-pkeyopt rsa_keygen_pubexp:65537
openssl req -batch -new -x509 \
-key keys/dev.key \
-out keys/dev.crt

Собрать и подписать FIT, встроив публичный ключ в device tree U-Boot:

# Сборка с подписью конфигурации
mkimage -f kernel_signed.its \
-k keys \
-K u-boot.dtb \
-r conf \
kernel_signed.itb

Флаг -r conf помечает подпись как обязательную: U-Boot откажется загружать образ без успешной верификации. Флаг -K u-boot.dtb записывает публичный ключ в device tree U-Boot, откуда он будет прочитан при проверке. После этого нужно пересобрать U-Boot с обновлённым device tree, чтобы публичный ключ оказался в финальном бинарнике.

Проверить подпись образа без загрузки на устройство, прямо на хосте:

mkimage -F -k keys kernel_signed.itb
# Или через fit_check_sign если он есть в инструментарии

Как работает Falcon Mode изнутри

Обычный путь загрузки через U-Boot proper занимает время: нужно инициализировать команды, разобрать переменные окружения, смонтировать файловую систему, прочитать конфигурацию, загрузить ядро и device tree в память, применить патчи к DT с динамическими значениями (размер памяти, MAC-адреса), и только после этого передать управление ядру. На платформах с медленной NAND или eMMC это легко занимает три-пять секунд только в загрузчике.

Falcon Mode устраняет всё это. SPL загружает ядро и заранее подготовленный device tree напрямую, не запуская U-Boot proper вообще. Ключевая идея в том, что динамические патчи device tree применяются один раз, результат сохраняется в persistent storage, и при каждой последующей загрузке SPL просто читает готовый снимок и передаёт его ядру. Это снижает время в загрузчике с секунд до сотен миллисекунд.

Включить Falcon Mode в конфигурации U-Boot:

cat >> configs/myboard_defconfig << 'EOF'
CONFIG_SPL_OS_BOOT=y
CONFIG_CMD_SPL=y
CONFIG_SPL_LOAD_FIT=y
EOF

Подготовить снимок device tree для Falcon Mode, загрузившись один раз в полноценный U-Boot:

# Загрузить ядро и device tree в память
load mmc 0:1 ${loadaddr} /boot/Image
load mmc 0:1 ${fdtaddr} /boot/myboard.dtb

# Задать аргументы ядра
setenv bootargs "console=ttymxc0,115200 root=/dev/mmcblk0p2 rootwait"

# Экспортировать подготовленный DT-снимок в RAM
spl export fdt ${loadaddr} - ${fdtaddr}

# Вывод покажет адрес снимка, например: Argument image is now in RAM: 0x8ffd9000
# Записать снимок в raw-раздел на MMC (блок 128 = 64KB offset)
mmc write ${fdtargsaddr} 128 ${fdtargslen_aligned}

После сохранения снимка при следующей загрузке SPL обнаружит корректный DT-снимок и запустит ядро напрямую. Если ядро не нашлось или снимок повреждён, SPL автоматически откатится к загрузке U-Boot proper. Эта страховка принципиальна для продакшена.

Falcon Mode с FIT-образом и поддержка RISC-V

Загрузка через legacy uImage в Falcon Mode имеет ограничение: поддерживается только этот устаревший формат. Современный путь для ARM64 предполагает загрузку FIT-образа напрямую из SPL через CONFIG_SPL_LOAD_FIT. В этом случае SPL разбирает FIT, выбирает нужную конфигурацию, проверяет хеши, загружает ядро и device tree в нужные адреса памяти и передаёт управление.

На платформах i.MX 9 с Falcon Mode цепочка выглядит так: Boot ROM загружает SPL, SPL инициализирует DDR, загружает ATF и FIT-образ с ядром, применяет runtime-патчи к device tree и передаёт управление ATF, который сразу запускает ядро. U-Boot proper в этой цепочке отсутствует полностью.

Для RISC-V платформ Falcon Mode работает через OpenSBI. Последовательность: SPL -> OpenSBI -> Linux kernel. Ядро и OpenSBI пакуются в единый FIT-образ:

# Включить Falcon Mode для RISC-V через OpenSBI
cat >> configs/myrisc_defconfig << 'EOF'
CONFIG_SPL_LOAD_FIT_OPENSBI_OS_BOOT=y
CONFIG_RISCV_SMODE=y
EOF

Проверить, что SPL корректно определил Falcon Mode при загрузке, можно по логу последовательного порта:

# Ожидаемый вывод при успешном Falcon Mode
# U-Boot SPL 2024.01 (Jan 15 2024)
# Trying to boot from MMC1
# ## Booting kernel from FIT Image at ...
# Starting kernel ...
# (U-Boot proper НЕ появляется)

Secure Falcon Mode и ограничения безопасности

Обычный Falcon Mode при неудаче откатывается в U-Boot proper. Для устройств с жёсткими требованиями к безопасности это неприемлемо: откат означает доступ к командной строке загрузчика, а это вектор атаки. Secure Falcon Mode, включаемый через CONFIG_SPL_OS_BOOT_SECURE, меняет поведение: при любой ошибке верификации или загрузки система останавливается, без отката в U-Boot proper.

Включить Secure Falcon Mode:

cat >> configs/myboard_defconfig << 'EOF'
CONFIG_SPL_OS_BOOT_SECURE=y
CONFIG_SPL_FIT_SIGNATURE=y
CONFIG_RSA=y
CONFIG_SPL_RSA=y
CONFIG_SPL_CRYPTO=y
EOF

При таком варианте конфигурации SPL проверяет подпись FIT-образа, и только успешная верификация позволяет продолжить загрузку. Приватный ключ хранится на стороне сборочной системы или HSM, публичный ключ встроен в SPL при компиляции. Физически невозможно загрузить неподписанное или подписанное чужим ключом ядро, что вместе с Secure Boot на уровне Boot ROM образует полную цепочку доверия от первого кода после подачи питания до пространства пользователя.

Время, сэкономленное Falcon Mode, на практике составляет от двух до пяти секунд в зависимости от платформы и конфигурации U-Boot proper. Для промышленного оборудования, медицинских приборов и автомобильной электроники, где регуляторные требования диктуют максимальное время выхода на рабочий режим, это нередко разница между прохождением сертификации и полной переработкой архитектуры загрузки. Инструменты для этого существуют и давно готовы к применению в продакшене.

https://fileenergy.com/linux