Программа для платы на процессоре ARM собирается быстрее всего не на самой плате, а на обычном компьютере разработчика, где ядра процессора быстрее и памяти больше. Кросс компиляция превращает написание в цикл: пишешь на большой машине, получаешь бинарный файл под чужую архитектуру, заливаешь на плату и запускаешь. Разберём устройство тулчейна, подключение систем сборки и главную ловушку, из-за которой готовый бинарник отказывается стартовать на плате: несовместимость версий стандартной библиотеки.

Почему компилировать быстрее на большой машине

Плата с ARM четырёх ядер и двумя гигабайтами памяти превращает сборку ядра в час ожидания, а настольная машина укладывает ту же работу в десять минут. Помимо скорости кросс-сборка контролируется: все зависимости лежат в тулчейне, система сборки не подхватывает случайные библиотеки хоста, и результат одинаков у каждого члена команды. Наконец, производство вообще не может стартовать с платы на собственном столе, только в конвейере, и тулчейн узла сборки позволяет использовать мощные серверы для тяжёлых пакетов.

Оборотная сторона такого разделения это различие окружений: бинарный файл собран на одной системе, а запускается на другой, и любое расхождение в путях, версиях и составе библиотек объявляется при первом старте. Именно эту разницу и приходится держать под контролем с самого начала, иначе вечер превращается в коллекцию ошибок о том, что программа просит несуществующую библиотеку или падает при загрузке без диагностики. Дисциплина сборки решает это заранее, а не в день выдачи устройства. Практическая польза такой станции измеряется не размерами, а повторяемостью: когда сборка оформлена одним сценарием, любой член команды собирает проект теми же командами, а каталог артефактов с ведомостями о версиях позволяет быстро узнать, под какой именно ревизией и с каким тулчейном собран конкретный бинарник из продакшена.

Установка тулчейна и первая проверка бинарника

Тулчейн под целевую архитектуру ставится из репозитория хостовой системы, и его части получают имена с префиксом архитектуры. Для 64-битных плат компилятор называется aarch64-linux-gnu-gcc, для 32-битных с аппаратной плавающей точкой вариант с суффиксом gnueabihf. Установка выглядит просто:

sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
aarch64-linux-gnu-gcc --version

Проверка занимает два действия: компилируем крошечную программу и смотрим, какой тип файла получился и какие библиотеки ему нужны при запуске:

aarch64-linux-gnu-gcc -O2 hello.c -o hello
file hello
readelf -d hello | head

Утилита file объявляет архитектуру и тип бинарника, а readelf перечисляет пути и зависимости. Эти две команды стоят копейки и ловят главный класс ошибок на каждой итерации: если бинарник задуман под ARM64, а file отвечает x86-64, значит работал хостовый компилятор, а не тулчейн. Перед первым запуском на плате полезно привыкнуть к такому ритуалу: он занимает секунды и отсекает целый класс нелепостей ещё на столе разработчика.

Настройка configure и CMake под чужую архитектуру

Для проектов на autotools всё решает ключевой параметр host: система сборки получает имя целевой цепочки через переменные окружения, и дальше работает как обычно:

./configure --host=aarch64-linux-gnu \
  CC=aarch64-linux-gnu-gcc \
  --prefix=/usr
make -j$(nproc)

В CMake принят файл тулчейна: в нём описывается компилятор, пути поиска заголовков и библиотек, и такой файл делается единожды на проект. Вот его костяк:

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Переменные с настройкой поиска самое важное в тулчейн-файле: они запрещают находить хостовые библиотеки, иначе сборка линкуется с x86-64 артефактами и валится на стадии установки. Второй момент касается тестовых программ configure: такие проверки не могут выполняться на целевой архитектуре на хосте, поэтому в файлах прописываются заранее известные ответы, иначе configure зависает или отвечает неправильно. У основной массы публичных проектов такие ответы уже сложены в примерах кросс-сборки.

Ловушка версий glibc и как собирать под старую систему

Классическая ошибка кросс-сборки выглядит одинаково: бинарник собран, старт на плате заканчивается сообщением version GLIBC_2.xx not found. Причина простая: компилятор привязывает вызовы к версиям стандартной библиотеки во время сборки, и бинарник, собранный со свежей glibc, рассчитывает на эти версии. Если целевая система старше, динамический загрузчик прерывает старт до main.

Ответа три, и выбор зависит от ограничений. Первый путь: собирать на самой старой системе под контроль. Подойдёт даже контейнер со старым дистрибутивом: сборка в нём выдаёт бинарники, совместимые с широким спектром целевых систем. Второй путь: собрать тулчейн с той версией glibc, что есть на целевых платах, через отдельный проект для генерации кросс-цепочек. Это требует часа на первый прогон, но приносит предсказуемый состав. Третий путь это статическая линковка, о которой разговор дальше.

Отдельные неприятности приходят не с glibc, а с другими библиотеками: программа может зависеть от libssl или libstdc++, и там действует то же правило по версиям. Поэтому кросс-сборку делают в среде старой целевой системы, а не в окружении пакетов последнего выпуска хостовой системы.

Библиотеки, sysroot и отладка на плате

Тулчейн должен не только транслировать код, но и находить заголовки и библиотеки целевой системы. Для этого существует sysroot: каталог, куда складываются дерево include и lib той самой целевой системы, к которой пойдёт бинарник. Практика простая: на плате ставятся пакеты с заголовками нужных библиотек, а каталог копируется в тулчейн. Или всё проще: пакет инструментария, собранный платформенно, уже содержит sysroot и тулчейн, и вопрос уходит.

Для сложных связок вроде openssl или библиотек камеры имеет смысл взять sysroot из работающего на плате дерева: синхронизация целевой системы на хост и сборка под новую версию решают зависимости без ручной охоты на пакеты. В сборочных платформах есть готовые команды, генерирующие такой инструментарий прямо из дерева, и там всё подготовлено заранее.

Отдельная догадка многих проектов состоит в том, чтобы не таскать чужие библиотеки в каталог тулчейна, а сослаться на sysroot через параметр. Такой путь сохраняет оригинальное дерево целевой системы нетронутым, а сборка остаётся актуальной по мере изменения пакетов на удалённом узле. Для небольшой группы проектов работает и более лёгкий вариант: держать набор скомпилированных библиотек в репозитории рядом с исходниками и обновлять его вручную при смене версии.

Когда программа не стартует на плате с сообщением о недостающей библиотеке, порядок разбора одинаковый. Сначала на целевой системе запускается команда ldd для бинарника, и она показывает, какие пути ищет загрузчик и какие из них не найдены. На этом этапе половина всех проблем кроется в несовпадении путей: тулчейн надеялся на sysroot, а на плате файл лежит в другом каталоге. Вторая половина это версии: библиотека существует, но слишком старая для содержимого бинарника.

Для деталей работает переменная LD_DEBUG со значением libs, где динамический загрузчик печатает каждый пробуемый путь и ответ на него. Этот вывод выглядит как стена текста, но именно там виден последний успешный шаг до падения. Параллельно полезно помнить про ldconfig и каталог с правилами поиска библиотек: добавление пути в конфигурацию во многих случаях решает задачу без борьбы с самим бинарником.

Отдельный случай это библиотека, найденная, но не та что нужна: на плате может быть устаревший вариант из пакетов производителя, и программа работает, но неправильно. Тогда вариантов два: либо поставить обновлённую библиотеку рядом и настроить loader на неё, либо роскомпилировать программу под ту версию, что есть на плате. Второй вариант по итогам всегда надёжнее, первый используется строго как временный.

Модули ядра и статическая сборка на musl

Модули ядра собираются по-другому: они привязаны к конкретной версии ядра, поэтому тулчейн должен совпадать с тем, которым собрано ядро на плате. Переменная окружения с префиксом инструментов объявляет make целевым, а компиляция идёт прямо по исходникам того ядра, которое будет работать на плате.

Для задач, где плате не нужны никакие внешние библиотеки, идеальный вариант это статически слинкованные бинарники. На glibc статическая привязка требует осторожности с сетевыми функциями, зато musl собирает чисто: бинарный файл запускается на любой системе с musl, и версия библиотеки вообще не играет роли. Для простых утилит и агентов мониторинга сборка через musl-gcc мало того что решает вопрос, так ещё и уменьшает размер и ликвидирует обращения к внешним путям библиотек.

Выбор здесь не политический: чем целостнее требуется состояние на устройстве, тем больше тянет в сторону статического бинарника, потому что такой файл запускается даже на разорванной по зависимостям системе. Плата за независимость это несколько лишних мегабайт на диске и отсутствие вызовов внешних библиотек, что для агентов и утилит обычно выгодный обмен. На практике граница проходит просто: как только программе нужны сетевые библиотеки или библиотеки с собственными плагинами, статика проигрывает, и проект остаётся на динамической линковке. Чистая же агентская утилита, которая собирается заново с каждым выпуском и размещается на хранилище для загрузки, годами живёт статически и не требует оперативного вмешательства при обновлении библиотечной базы системы.

Проверка через эмулятор, замер производительности и привычки станции сборки Перед первым стартом на плате бинарник имеет смысл проверить в эмуляторе: программа qemu в режиме пользователя исполняет чужие бинарники без виртуальной машины, и маленькая программа запускается вызовом с указанием библиотек. Так можно сразу увидеть проблемы с версией системной библиотеки, а не на узле.

Когда программа заработала, итоговая проверка всегда происходит на железе, потому что эмулятор не воспроизводит тонкости таймингов и кэшей. Меряем реальное потребление процессора и памяти через стандартные утилиты на плате, а инструментарий вроде perf позволяет снять профиль производительности на целевой системе и сравнить с хостом. Часто именно такое сравнение объясняет, почему тривиальный алгоритм на хосте занимает микросекунды, а на плате десятки миллисекунд: у платы меньше кэш-памяти, и данные из внешнего хранилища подаются медленнее.

Отдельно отметим мягкий способ сборки через контейнер: официальный образ дистрибутива плюс тулчейн дают переносимую среду, которую проще передать подрядчику, чем длинную инструкцию по установке пакетов, и результат не зависит от особенностей хоста.

Финальный штрих к статье про кросс-сборку: бинарник, собранный раз и навсегда, не ремонтируется. Подобный артефакт бесследно устаревает, если ему самому не заложили управление версией проекта и отметку о тулчейне, поэтому дисциплина сохранения версий должна быть заведена до того, как первая программа уйдёт на первую плату. Приятный итог тактики выглядит просто: когда очередная версия отправляется в сборку, и через полчаса выходит подписанный набор артефактов под все поддерживаемые архитектуры, вопрос про кросс-сборку перестаёт быть историей о боли - он становится частью нормальной производственной процедуры, точно такой же, как выпуск rpm-пакета или контейнерного образа. Тот, кто прошёл этот путь до конца, уже не спрашивает, с чего начинать кросс-сборку: ответ записан в дереве конфигурации проекта и воспроизводится по расписанию. И весь этот механизм, стоящий одну строчку в настройках проекта, сам служит в будущем как документация: состав, версии и ключи сборки видны в тексте, а не в памяти одного инженера.