Yocto für Bare-Metal: Cross-Compiler und CI-Toolchain aus einem Guss
Wer Firmware für Mikrocontroller baut, braucht einen Cross-Compiler. Den bekommt man mit crosstool-ng, als ARM-Download oder aus dem Paketmanager der Distribution. Das funktioniert — und wenn man dann noch Unit-Tests in der CI haben will, wird es richtig spannend.
Denn dann braucht man zwei Toolchains: eine für das Target
(arm-none-eabi-gcc mit newlib) und eine für den Host (x86_64
oder aarch64), mit der man denselben Code gegen ein Test-Framework
kompiliert. Auf dem Host läuft natürlich eine andere libc — aber
GCC-Generation, Google-Test-Version, gcov und die
Analyse-Werkzeuge müssen auf Punkt und Komma zusammenpassen.
Bare-Metal klingt dabei nach wenig Aufwand: Wenn newlib schon das Maß der Dinge ist, ist der Horizont quasi anfassbar. Die Herausforderung steckt nicht in der Menge der Abhängigkeiten, sondern darin, dass die Werkzeuge drumherum für Target und Host aus einem Guss sein müssen. Genau das lässt sich elegant lösen.
Das Problem: Zwei Toolchains, eine Wahrheit
Nehmen wir an, der Cross-Compiler ist ein GCC 15 mit newlib. Die Unit-Tests laufen auf dem CI-Host, also brauchen wir dort ebenfalls einen GCC 15 — nicht den GCC 14, den Fedora 43 mitbringt, und schon gar nicht den GCC 13 aus dem Ubuntu-LTS der CI-Runner.
Dazu kommen ABI-gekoppelte Werkzeuge:
-
Google Test / GMock — kompiliert gegen die jeweilige Toolchain
-
gcov / lcov — müssen zur GCC-Version passen, sonst stimmt die Coverage nicht
-
valgrind — muss die ABI der getesteten Binaries verstehen
Wenn man das manuell pflegt, landen Target- und Host-Toolchain schnell in getrennten Beschaffungsketten. Das muss nicht sein.
Warum ich da heute anders rangehe
Vor einigen Jahren bei einem Telekommunikationsausrüster: Ein SoM mit Netzwerk-Beschleunigern, dazu ein Binary-SDK vom Chip-Hersteller. 32-Bit, eine uralte GCC-Version, gelinkt gegen eine Linux-Distribution, die es nur noch auf dem Schwarzmarkt gegen Kamele gab. Lange bevor LLVM/Clang als ernstzunehmende GCC-Konkurrenz die Stagnation aufmischte.
Die IT betrieb dafür einen Multiprozessor-Knoten mit rund 64 VMs — je 2 VCPUs, 2 GB RAM, komplett überbucht, kaum wartbar. Das SDK ließ sich nicht reproduzieren, nicht aktualisieren und nicht auf eine andere Host-Plattform portieren. Dabei war der Chip sogar Linux-fähig — da wäre deutlich mehr gegangen.
Das Muster habe ich seitdem mehrfach gesehen: Chip-Hersteller wollen Chips verkaufen, keine Entwicklungsumgebungen. Das SDK ist Beiwerk — es erleichtert den Einstieg, soll aber nicht langfristig tragen. Ein Hersteller-BSP auf LTS-Basis, jahrelang gut genug — und wenn der LTS-Zyklus ausläuft, heißt es: Updaten, in sechs Wochen liefern, nichts darf kosten.
Das hat mich motiviert, einen anderen Weg zu gehen.
Die Yocto-Lösung: MACHINE-Abstraktion als SSOT
Das Yocto Project ist primär als Build-System für Embedded-Linux-Distributionen bekannt. Aber sein Rezept-System kann mehr: Es baut beliebige Toolchains aus den Quellen — und zwar für verschiedene Zielarchitekturen aus denselben Rezepten.
Der Schlüssel ist die MACHINE-Variable. Dieselben Rezepte für GCC,
newlib, Google Test und gcov erzeugen je nach Target:
| MACHINE | Ergebnis | Beispiel |
|---|---|---|
|
|
Mess-MCU, ADC-Frontend |
|
|
Kommunikations-MCU, DCP-Stack |
|
|
Kamera-Steuerung |
|
|
RISC-V IoT, Wi-Fi/BLE |
|
|
RISC-V mit DSP-Erweiterungen, atomthreads |
|
Host-nativer GCC derselben Generation |
CI: Unit-Tests, valgrind, Coverage |
Die Liste ließe sich fortsetzen. Der Punkt ist: Ob ARM Cortex-M, RISC-V oder Host-nativ — die Rezepte sind dieselben. Nur MACHINE und die zugehörige
Tune-Konfiguration unterscheiden sich.
Eine Quelle, eine GCC-Version, eine ABI-Kopplung — DRY und SSOT statt n getrennt gepflegte Toolchains.
Yocto baut dabei nicht nur den Compiler: Auch ein QEMU für das jeweilige Target lässt sich aus denselben Rezepten erzeugen. Damit kann man Firmware-Binaries auf dem CI-Host emulieren — ohne echte Hardware, aber mit der richtigen CPU-Emulation. Für Integrationstests auf einem Cortex-M oder RISC-V ist das Gold wert.
Was gehört ins SDK — und was nicht?
Die erste Intuition ist: Nur der Compiler und seine unmittelbaren Abhängigkeiten. In der Praxis zieht man die Grenze weiter.
Aus Yocto — eingefroren und versioniert:
-
GCC + binutils + newlib (der Compiler-Kern)
-
Google Test / GMock (Test-Framework, gegen die Toolchain kompiliert)
-
gcov / lcov (Coverage — muss zur GCC-Version passen)
-
valgrind (Speicheranalyse — muss die ABI verstehen)
-
clang-tidy (statische Analyse — braucht die richtigen Header und Bibliotheken des Targets, nicht die des Host-Systems)
-
clang-format (eine eingefrorene Version verhindert Whitespace-Kriege zwischen Entwicklern)
-
CMake, Ninja (wenn das CMake der Host-Distribution plötzlich ein Major-Update bekommt, spuckt es unter Umständen in den Build)
-
QEMU (Target-Emulation für Integrationstests)
Der gemeinsame Nenner: Alles, was das Build-Ergebnis oder die Analyse des Build-Ergebnisses beeinflusst, muss einfrierbar und reproduzierbar sein. clang-tidy klingt zunächst orthogonal — bis man merkt, dass es die Target-Header parsen muss, nicht die des Host-Systems.
Aus der Distribution — unabhängig aktualisierbar:
-
Doxygen (API-Dokumentation)
-
srecord (ELF → HEX-Konvertierung)
-
doctoolchain, asciidoctor (Projektdokumentation)
Diese Werkzeuge beeinflussen weder das Binary noch die Analyse und können unabhängig aktualisiert werden — ohne Firmware-Requalifizierung.
Bare-Metal-Hardening: Was geht ohne OS?
Ein Nebeneffekt der Yocto-basierten Toolchain: Man hat volle Kontrolle über die Compiler-Flags, und zwar in sauberen Schichten.
Auf Bare-Metal fehlen die üblichen OS-Schutzmechanismen
(_stack_chk_fail ohne libc, kein ASLR ohne Loader, kein PIE ohne
MMU). Aber GCC 13+ bietet Hardening-Flags, die _ohne OS
funktionieren:
-fharden-compares
-fharden-conditional-branches
-fharden-control-flow-redundancy
-ftrivial-auto-var-init=zero
Diese Flags landen nicht in der Toolchain selbst (sonst würde newlib
nicht mehr bauen), sondern in einem SDK-Environment-Hook — einer
dünnen Shell-Datei, die beim Aktivieren der Toolchain die
CFLAGS/CXXFLAGS setzt. Drei Schichten:
-
Distro-Config (Yocto): Flags, die auch newlib braucht (
-ffunction-sections,-fdata-sections) -
SDK-Hook: Hardening und Warnungen für die Firmware-Entwicklung
-
Firmware-Projekt (CMakeLists.txt): Projektspezifisches (
-std=gnu23, Target-Defines)
Langzeitwirkung: 10 Jahre Firmware-Support
In regulierten Branchen liefert man Firmware-Fixes für Produkte,
die vor 10 oder 15 Jahren ausgeliefert wurden. Mit einer
Yocto-basierten Toolchain ist das ein git checkout auf den
Release-Stand und ein bitbake meta-toolchain — fertig.
Yocto liefert dafür zwei Mechanismen, die oft als Implementierungs- detail abgetan werden, aber für das Release-Management essentiell sind:
DL_DIR-
Das Verzeichnis, in das Yocto alle heruntergeladenen Quellen ablegt — Tarballs, Git-Snapshots, Patches. Das ist kein Nebenprodukt, das ist ein Quellenarchiv. Wer
DL_DIRarchiviert, kann die Toolchain auch dann noch bauen, wenn die Upstream-Server längst offline sind. SSTATE_DIR-
Der Shared-State-Cache. Yocto speichert hier die Zwischenergebnisse jedes Build-Schritts. Bei einem Release-Fix Jahre später muss nicht alles von Grund auf neu gebaut werden — der Cache beschleunigt den Rebuild auf die tatsächlich geänderten Komponenten.
Beide Verzeichnisse zusammen bilden das Rückgrat für langfristige Nachlieferbarkeit: Quellen gesichert, Build-Ergebnisse nachvollziehbar, Rebuild-Zeiten beherrschbar.
CRA und SBOM: Vom Nice-to-have zur Pflicht
Mit dem Cyber Resilience Act wird die Frage „Was steckt in meiner Toolchain?" regulatorisch relevant. Ein SBOM (Software Bill of Materials) mit voller Provenance ist keine Kür mehr — es wird Pflicht für Produkte mit digitalen Elementen.
Yocto erzeugt SPDX-SBOMs als Nebenprodukt des Builds — für jede
Komponente, die aus den Quellen gebaut wurde. DL_DIR liefert die
Quellenarchivierung, die Rezepte liefern die Abhängigkeitskette,
und INHERIT += "create-spdx" bindet beides zu einem maschinenlesbaren
SBOM zusammen.
Aus Yocto gebaut heißt: Die Provenance stimmt per Konstruktion, nicht per Vertrauen. Das ist ein echtes Pfund, wenn der Auditor vor der Tür steht.
Fazit
Cross-Compiler für Bare-Metal gibt es an jeder Ecke. Der Mehrwert von Yocto liegt nicht im Cross-Compiler selbst, sondern darin, dass Target-Toolchain, Host-CI-Toolchain und alle Werkzeuge drumherum aus derselben Quelle kommen — reproduzierbar, einfrierbar, CRA-ready.
Die Einstiegshürde ist real: Yocto hat eine steile Lernkurve. Aber wer den Schritt wagt, bekommt eine Toolchain-Infrastruktur, die mit jedem neuen Target und jedem neuen Projekt mitträgt — statt jedes Mal bei null anzufangen.