Кросс-компиляция оказывает огромное влияние на разработку кроссплатформенного встроенного программного обеспечения C++. Поэтому у нас есть много инструментов, которые нам помогут, и доккросс — один из них, о котором мы кратко поговорим.

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

Когда мы говорим о «компиляции», мы обычно имеем в виду преобразование нашего исходного кода в представление, понятное машине, в конечном счете, в набор нулей и единиц.

Часто мы выполняем нашу программу на платформе, эквивалентной той, которую мы использовали для ее написания и компиляции. Скажем, мой ноутбук для разработки имеет архитектуру x86-64 под управлением Linux, и моя программа будет выполняться на еще одной архитектуре x86-x64 под управлением Linux, возможно, на том же ноутбуке, который использовался для разработки.

Однако, если мы хотим выполнить нашу программу на платформе, отличной от той, которую мы использовали для разработки, все становится немного иначе. Скажем еще раз, мой ноутбук для разработки с x86–64 под управлением Linux, но моя программа будет выполняться на Raspberry Pi с архитектурой ARMv7 под управлением Linux.

В последнем случае у нас есть несоответствие между платформой разработки ( host) и платформой исполнения ( target). Таким образом, программа, скомпилированная для моего хоста, не будет выполняться на цели. Нам нужно перекрестно скомпилировать наш код с хоста на цель.

Термин платформа является абстракцией, он может означать архитектуру процессора, операционную систему, библиотеку C, их комбинацию и т.д. чтобы он мог выполняться на последнем. В этом обсуждении мы будем рассматривать только архитектуру процессора (x86–64 против ARMv7), поскольку поэтому мы можем рассматривать архитектуру процессора как синоним платформы. За более подробной информацией обращайтесь к GNU Triplet.

Для кросс-компиляции на другую платформу нам понадобится кросс-тулчейн.

Кросс-тулчейны

Наш исходный код C++ проходит серию преобразований, прежде чем получить окончательный исполняемый файл, примерно:

  1. Препроцессор расширяет директивы (строки, начинающиеся с #), представленные в исходном коде, для создания единиц перевода.
  2. Компилятор компилирует единицы трансляции в ассемблерные инструкции.
  3. Ассемблер собирает ассемблерные (довольно забавная фраза!) инструкции в «неполные» (без зависимостей) исполняемые файлы.
  4. Компоновщик связывает наши «неполные» исполняемые файлы с их зависимостями в окончательный («полный») исполняемый файл.

Каждый шаг в последовательности («цепочке») обычно выполняется определенным инструментом. Отсюда набор инструментов, используемых цепочкой, получает название «тулчейн».

Цепочка инструментов — это набор отдельных инструментов разработки программного обеспечения, которые связаны (или объединены в цепочку) вместе определенными этапами, такими как GCC, binutils и glibc [1].

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

Иногда мы называем всю цепочку сборки компиляцией, а кросс-тулчейн — кросс-компилятором. Все должно быть в порядке™.

Нам нужна кросс-инструментальная цепочка для каждой конкретной платформы, на которой мы хотели бы выполнять код, скажем, ARMv7 + Linux.

Есть много способов получить набор инструментов. Как правило, они сводятся к (i) компиляции из исходников, (ii) использованию предварительно созданного набора инструментов.

Компиляция цепочки инструментов из исходников может дать нам больший контроль над всем процессом за счет более крутой кривой обучения. С другой стороны, предварительно созданные цепочки инструментов могут дать нам меньше контроля, но обычно их легче настроить и запустить. Следовательно, здесь нет правильного выбора, который тесно связан с вашими требованиями.

В следующих разделах мы будем использовать предварительно созданную цепочку инструментов от dockcross, чтобы показать, как мы можем быстро получить игрушечный проект C++, кросс-компилированный в ARMv7 и выполненный на Raspberry PI 3 (rpi3). Кроме того, мы выполним нашу кросс-компилированную программу в эмуляторе, работающем на нашей машине разработки, опять же, используя доккросс.

Пример: нативная компиляция

В качестве базового примера у нас есть исполняемый файл C++ с модульным тестом и зависимостью от GoogleTest. Сборка управляется с помощью CMake, создавая все артефакты сборки в каталоге build. Вся процедура выполняется на машине разработки x86-64, работающей под управлением дистрибутива Linux.

Макет:

$ tree -L 1
.
├── build
├── CMakeLists.txt
└── main.cpp

main.cpp:

#include <gtest/gtest.h>

auto add(int const x, int const y) -> int {
        return x + y;
}

TEST(add, OnePlusOneEqualsTwo) {
        ASSERT_EQ(add(1, 1), 2);  
}

CMakeLists.txt:

Чтобы уменьшить масштаб, мы интегрируем GoogleTest в наш проект с помощью CMake FetchContent вместо надлежащего менеджера пакетов (например, Vcpkg или Conan).

cmake_minimum_required(VERSION 3.14)
project(dock-cross-example CXX)

include(CTest)
include(FetchContent)

FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        release-1.10.0
)
FetchContent_MakeAvailable(googletest)

add_executable(app main.cpp)
target_compile_features(app PUBLIC cxx_std_11)
target_link_libraries(app gtest gtest_main)

add_test(app app)

Прежде чем мы приступим к кросс-компиляции, давайте сначала скомпилируем:

$ cmake -Bbuild/ && cmake --build build/

Проверяя исполняемый файл по адресу build/app, мы можем увидеть:

$ file build/app
build/app: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=f35671505ce650eaddd04652011d5c6ed5fa23e2, not stripped

Это говорит о том, что наш исполняемый файл создан для архитектуры x86–64 и ожидает интерпретатор в /lib64/ld-linux-x86-64.so.2.

Наконец, выполнив программу, мы должны увидеть что-то вроде следующих строк:

$ ./build/app
Running main() from /dock-cross-example/build/_deps/googletest-src/googletest/src/gtest_main.cc
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from add
[ RUN      ] add.OnePlusOneEqualsTwo
[       OK ] add.OnePlusOneEqualsTwo (0 ms)
[----------] 1 test from add (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.

Вот и все! Теперь мы хотим кросс-компилировать ту же программу и иметь возможность выполнять ее на нашем rpi3, не так ли?

Пример: кросс-компиляция с помощью Dockcross

Как мы уже говорили, нам нужна кросс-инструментальная цепочка, которая работает на нашей платформе x86–64/Linux и генерирует исполняемый файл, который затем может выполняться на нашей Raspberry PI 3 с ARMv7/Linux. Простой способ запустить кросс-инструментарий — использовать dockcross.

По сути, Dockcross предлагает предварительно созданные и настроенные наборы инструментов кросс-компиляции C и C++ для нескольких разных платформ в виде образов Docker. Используя Docker, мы можем изолировать инструменты сборки и артефакты и поддерживать чистоту рабочего процесса разработки. И с некоторой дисциплиной, воспроизводимым.

Чтобы начать работу с доккроссом, у нас должен быть установлен Docker. Затем мы можем получить кросс-тулчейн для ARMv7/Linux с помощью следующих команд:

$ docker run --rm dockcross/linux-armv7 > ./dockcross-linux-armv7 && chmod +x ./dockcross-linux-armv7

В результате должен получиться скрипт dockcross-linux-armv7, которому мы дали разрешение на выполнение. Это наша точка входа в кросс-компиляцию и многое другое.

dockcross-linux-armv7 получает команды и запускает их внутри контейнера Docker, который поставляется с предварительно созданной и настроенной кросс-инструментальной цепочкой, которую мы затем можем использовать для кросс-компиляции для ARMv7/Linux. Кроме того, контейнер Docker поставляется с другими инструментами, обычно используемыми для разработки на C++, например. CMake.

Выполним кросс-компиляцию нашего проекта и поместим артефакты сборки в каталог build_armv7:

$ ./dockcross-linux-armv7 cmake -Bbuild_armv7/
$ ./dockcross-linux-armv7 cmake --build build_armv7

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

Вот и все!

Проверяя исполняемый файл по адресу build_armv7/app, мы можем увидеть:

$ file build_armv7/app
build_armv7/app: ELF 32-bit LSB executable, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 4.10.8, with debug_info, not stripped

Среди прочего, это говорит о том, что наш исполняемый файл создан для архитектуры ARM и ожидает интерпретатор на уровне /lib/ld-linux-armhf.so.3.

Теперь мы можем передать его в rpi3 и выполнить:

$ (pi) /tmp/app
Running main() from /work/build_armv7/_deps/googletest-src/googletest/src/gtest_main.cc
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from add
[ RUN      ] add.OnePlusOneEqualsTwo
[       OK ] add.OnePlusOneEqualsTwo (0 ms)
[----------] 1 test from add (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.

Бинго!

Кроме того, многие образы, поставляемые Dockcross, поставляются с эмуляторами. Таким образом, мы можем выполнить нашу кросс-компилированную программу на нашей машине разработки:

$ ./dockcross-linux-armv7 bash -c './build_armv7/app'
Running main() from /work/build_armv7/_deps/googletest-src/googletest/src/gtest_main.cc
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from add
[ RUN      ] add.OnePlusOneEqualsTwo
[       OK ] add.OnePlusOneEqualsTwo (1 ms)
[----------] 1 test from add (2 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (5 ms total)
[  PASSED  ] 1 test.

Это здорово! Думайте о модульных тестах как о части процесса CI.

Вывод

Мы обсудили роль кросс-компиляции в разработке встраиваемого программного обеспечения. Мы кратко рассмотрели, что такое набор инструментов. Затем мы взглянули на доккросс, который дал нам предварительно созданную цепочку инструментов для кросс-компиляции, которую мы позже использовали для кросс-компиляции игрушечного примера C++ и даже выполнили ее в эмуляторе, работающем на нашей машине для разработки.

В нашем примере мы выполнили кросс-компиляцию для Raspberry Pi. Тем не менее, доккросс поставляет множество других перекрестных цепочек инструментов, которые доступны в разных образах Docker.

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

Dockcross — это всего лишь один возможный вариант, а не единственный вариант. Следовательно, мы также можем захотеть рассмотреть и другие решения, например, crosstool-NG, raspberrypi/tools, полноценный Yocto, Nix, скатывать собственные образы Docker и т. д.

Кроме того, возможно, стоит изучить, как инструменты для других языков программирования поддерживают кросс-компиляцию, например, Rust's cross или Go’s GOOS/GOARCH:

cross build --target armv7-unknown-linux-gnueabihf # Rust (+ cross).
GOARCH=arm GOOS=linux go build                     # Go.

Возможно, мы могли бы адаптировать некоторые идеи к C++. Или, в зависимости от ваших требований, рассмотрите возможность разработки на одном из этих языков.

Удачной кросс-компиляции!

использованная литература

[2] elinux.org/Toolchains.

[1] доккросс.

Изображение вверху создано с помощью https://sketch.io/sketchpad/.

Первоначально опубликовано на https://rvarago.github.io 2 октября 2020 г.