Rust на embedded: cross-compile, musl и грабли с glibc

Хочется собрать один файл, закинуть его на плату и забыть. На практике между «хочется» и «работает» лежит чужой libc.

Исходная боль понятна каждому, кто возил софт на встраиваемое железо. На плате — Debian с ядром 5.10-bsp от вендора и glibc 2.31. На ноутбуке — Ubuntu 24.04 и glibc 2.39. Собираешь у себя, копируешь на плату, получаешь:

./agent: /lib/aarch64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found

Классика. glibc обратно совместим вперёд, но не назад: бинарь, собранный против новой версии, на старой не пойдёт. А обновить libc на вендорском образе — это отдельный вид развлечения, обычно заканчивающийся кирпичом.

Три пути

Из этого есть ровно три выхода, и я прошёл все:

Выбрал третье, о чём в целом не жалею, но нюансов там больше, чем обещают туториалы.

Тулчейн

Начал со стандартного:

rustup target add aarch64-unknown-linux-musl
cargo build --release --target aarch64-unknown-linux-musl

Пока в проекте чистый Rust — это просто работает. Как только появляется хоть один crate с C-кодом (а он появляется: ring, zstd, любой -sys), нужен ещё и кросс-компилятор C. Возиться с musl-cross-make я не захотел и взял cargo-zigbuild:

cargo install cargo-zigbuild
cargo zigbuild --release --target aarch64-unknown-linux-musl

Zig таскает с собой полный кросс-тулчейн и умеет прикидываться C-компилятором под любую тройку. Один инструмент вместо зоопарка. Единственная плата — Zig в зависимостях сборки, но он ставится одним бинарём и в CI ведёт себя прилично.

Если нужен вариант совсем без сторонних утилит — есть cross, который прячет всё в докер. Мне не подошло: на сборочной машине докера нет принципиально.

Где musl ломает: getaddrinfo

Вот главная грабля, ради которой и писалась заметка.

Резолвер у musl свой, минималистичный, и он не поддерживает NSS. Всё, что описано в /etc/nsswitch.conf, для musl-бинаря не существует. Практические последствия:

У меня это выстрелило ровно так: агент ходил на nvr.local, всё работало на ноуте и намертво не работало на плате. Полдня на «да сеть же есть, ping идёт» — ping шёл, потому что ping системный, glibc'шный.

Лечение — не полагаться на системный резолвер вообще. В Rust это делается легко: берём hickory-resolver (бывший trust-dns) и резолвим сами, читая /etc/resolv.conf. Заодно получаем контроль над таймаутами, чего с getaddrinfo не добиться никак.

Либо — просто выкинуть имена и ходить по IP. На стенде это, честно говоря, чаще правильный ответ.

Где musl ломает: malloc

Вторая засада тише и потому противнее. Аллокатор в musl оптимизирован под размер и простоту, а не под скорость и многопоточность. На однопоточной утилите разницы нет. На чём-то, что активно аллоцирует из нескольких потоков, можно словить замедление в разы — у меня парсер метрик просел примерно втрое.

Лечится одной строчкой:

[dependencies]
mimalloc = { version = "0.1", default-features = false }
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

После этого разрыв с glibc-сборкой ушёл в пределы погрешности. jemalloc тоже подходит, но он потяжелее и с musl исторически капризнее.

TLS: не тащите OpenSSL

Отдельный совет, который экономит часы. Если в проекте есть HTTPS — не связывайтесь с openssl-sys при кросс-сборке. Либо vendored-сборка (медленно, но работает), либо, что гораздо лучше, rustls:

reqwest = { version = "0.12", default-features = false,
            features = ["rustls-tls", "json"] }

Чистый Rust, никакого C, кросс-компиляция становится скучной — то есть именно такой, какой должна быть. Единственное: корневые сертификаты придётся принести самому — webpki-roots зашьёт их в бинарь, либо rustls-native-certs прочитает системные.

Итог

Что получилось на выходе:

$ file agent
agent: ELF 64-bit LSB executable, ARM aarch64, statically linked, stripped
$ ls -lh agent
-rwxr-xr-x 1 ilya ilya 4.1M agent
$ ldd agent
        statically linked

4 мегабайта, ноль зависимостей, копируется scp и запускается на любой aarch64-плате независимо от того, что у неё за дистрибутив и какого года. Деплой превратился в одну команду.

Стоило ли терпеть ради этого musl со всеми его особенностями — да, безусловно. Но с оговоркой: если ваш софт полагается на системный резолвер и NSS, musl вас укусит, и укусит не там, где вы будете искать. Резолвите сами.

← все заметки