Bauen eines Embedded Systems

In diesem Artikel geht es darum, ein Linux-System crashsicher zu machen. Wir bereiten eine Installation soweit vor, dass ein Debian Bullseye bootet und eine Beispiel-node.js-App startet.

Das Root-Dateisystem wird dabei in einem SquashFS-Archiv liegen. Bei Start wird dieses als readonly in den Arbeitsspeicher geladen, und ein weiteres virtuelles Dateisystem drüber gelegt, welches die Änderungen beinhaltet.

Sinn des ganzen: alles, was sich während der Laufzeit im Dateisystem ändert, passiert im RAM. Stürzt das Linux ab, verliert es die Verbindung zum Strom, oder was auch immer passiert – wir booten jedes mal wieder mit exakt dem selben Dateisystem.

Dazu brauchen wir erst mal ein Linux, mit welchem wir das andere Linux-System bauen. Ich selber nutze WSL unter Windows (in Kombination mit Hyper-V, um erstellte Images direkt zu testen) – ihr könnt euch aber auch einen eigenen kleinen Rechner aufsetzen. Zu der Konstellation WSL <-> Hyper-V ganz unten mehr.

Wir erstellen in diesem Tutorial eine ISO-Datei (DVD-Abbild), die eine Anwendung in einer kompletten Linux-Umgebung nach unserem Wunsch startet. Es wird nicht auf Updates o.ä. eingegangen, das wird vermutlich Thema eines anderen Posts 😅

Disclaimer vorweg

Der hier vorliegende Guide orientiert sich nicht nur zufällig sehr stark an folgendem:

https://willhaley.com/blog/custom-debian-live-environment/

Hier kommt eine abgeschlackte Version ohne GUI raus, und es wird noch mal darauf eingegangen, wie man nun z.B. eine App dort reingeknotet bekommt; dennoch möchte ich gerne 85% der Credits einmal abgeben. Im Prinzip habe ich ja „nur auf Deutsch übersetzt“ und ein paar Sachen weggestrichen, ein paar Sachen ergänzt, das auf meine Bedürfnisse angepasst und hier gedumpt.

Vorbereitung des Host-Systemes

Zum Bauen benötigt ihr folgende Software

sudo apt install debootstrap squashfs-tools xorriso isolinux syslinux-efi grub-pc-bin grub-efi-amd64-bin mtools

Und folgende Verzeichnisse

mkdir /build/
mkdir /build/{chroot,staging,staging/{EFI,EFI/boot,boot,boot/grub/,boot/grub/x86_64-efi,isolinux,live},tmp}

In /build/chroot/ werden wir später unser embedded Linux haben. /build/staging/ wird das sein, was bei dem Zielrechner auf der Platte landet. /build/tmp nutzen wir für alles, was weg kann.

Erstellen wir uns doch mal ein basis-Linux:

debootstrap \
    --arch=amd64 \
    --variant=minbase \
    bullseye \
    /build/chroot \
    http://ftp.de.debian.org/debian/

Und chrooten dort rein:

chroot /build/chroot

In der chroot-Umgebung

Nur die Befehle in diesem Abschnitt werden im chroot ausgeführt. Alles danach nicht. Ist der Abschnitt vorbei, solltest du wieder auf deinem tatsächlichen System sein.

Setzen wir als erstes einen Hostname für das Gerät:

echo "embedded-system" > /etc/hostname

Updaten unsere Paketlisten

apt-get update

Und installieren ein paar grundlegene Sachen:

apt-get install --no-install-recommends linux-image-amd64 live-boot systemd-sysv

Sowie etwas Netzwerk-Shizzle:

apt-get install --no-install-recommends network-manager net-tools iputils-ping

Dann vergeben wir ein Root-Kennwort:

passwd root

Die nächsten zwei Schritte sind optional: ich möchte nen SSH-Server auf der Box haben, um das aus der Ferne zu warten – sowie ein paar andere Kleinigkeiten, wie curl und nano

apt-get install --no-install-recommends curl openssh-server nano

Erlauben von Root-SSH-Verbindungen auf die Box:

nano /etc/ssh/sshd_config

Und ersetzen von #PermitRootLogin prohibit_password zu PermitRootLogin yes

Vor dem Bauen bietet es sich an, den apt-Cache zu leeren, und verwaiste Pakete zu löschen – dann ist unser Image später auch schmaller.

apt autoremove
apt clean

Anschließend verlassen wir das chroot mit:

exit

Und schon haben wir ein sehr minimales Linux, mit einem SSH-Server, ohne eine Anwendung. Die bauen wir in den nächsten zwei Schritten zu einer ISO.

Vorbereiten des Boot-Loaders

Diese Befehle werden wieder nicht im chroot ausgeführt.

Das hier wird nur einmalig nötig sein:

cat <<'EOF' >/build/staging/isolinux/isolinux.cfg
UI vesamenu.c32

MENU TITLE Boot Menu
DEFAULT linux
TIMEOUT 50
MENU RESOLUTION 640 480
MENU COLOR border       30;44   #40ffffff #a0000000 std
MENU COLOR title        1;36;44 #9033ccff #a0000000 std
MENU COLOR sel          7;37;40 #e0ffffff #20ffffff all
MENU COLOR unsel        37;44   #50ffffff #a0000000 std
MENU COLOR help         37;40   #c0ffffff #a0000000 std
MENU COLOR timeout_msg  37;40   #80ffffff #00000000 std
MENU COLOR timeout      1;37;40 #c0ffffff #00000000 std
MENU COLOR msg07        37;40   #90ffffff #a0000000 std
MENU COLOR tabmsg       31;40   #30ffffff #00000000 std

LABEL linux
  MENU LABEL Debian Live [BIOS/ISOLINUX]
  MENU DEFAULT
  KERNEL /live/vmlinuz
  APPEND initrd=/live/initrd boot=live

LABEL linux
  MENU LABEL Debian Live [BIOS/ISOLINUX] (nomodeset)
  MENU DEFAULT
  KERNEL /live/vmlinuz
  APPEND initrd=/live/initrd boot=live nomodeset
EOF

Und selbes für EFI:

cat <<'EOF' >/build/staging/boot/grub/grub.cfg
search --set=root --file /DEBIAN_CUSTOM

set default="0"
set timeout=5

# If X has issues finding screens, experiment with/without nomodeset.

menuentry "Debian Live [EFI/GRUB]" {
    linux ($root)/live/vmlinuz boot=live
    initrd ($root)/live/initrd
}

menuentry "Debian Live [EFI/GRUB] (nomodeset)" {
    linux ($root)/live/vmlinuz boot=live nomodeset
    initrd ($root)/live/initrd
}
EOF

Damit Grub die richtige Partition, von der er starten soll, findet, sagen wir ihm, er soll die nehmen, die eine Datei DEBIAN_CUSTOM direkt im Root-Verzeichnis hat

cat <<'EOF' >/build/tmp/grub-standalone.cfg
search --set=root --file /DEBIAN_CUSTOM
set prefix=($root)/boot/grub/
configfile /boot/grub/grub.cfg
EOF

Und erstellen diese Datei:

touch /build/staging/DEBIAN_CUSTOM

Nun bereiten wir unseren Bootloader vor:

cp /usr/lib/ISOLINUX/isolinux.bin /build/staging/isolinux/
cp /usr/lib/syslinux/modules/bios/* /build/staging/isolinux/
cp -r /usr/lib/grub/x86_64-efi/* /build/staging/boot/grub/x86_64-efi/

grub-mkstandalone \
    --format=x86_64-efi \
    --output=/build/tmp/bootx64.efi \
    --locales="" \
    --fonts="" \
    "boot/grub/grub.cfg=/build/tmp/grub-standalone.cfg"

cd /build/staging/EFI/boot
dd if=/dev/zero of=efiboot.img bs=1M count=20
mkfs.vfat efiboot.img
mmd -i efiboot.img efi efi/boot
mcopy -vi efiboot.img /build/tmp/bootx64.efi ::efi/boot/

Bauen eines Releases

Nun juckeln wir aus unserem chroot alles, außer /boot, in ein SquashFS und kopieren vmlinuz / initrd noch mit in unser /build/staging:

mksquashfs /build/chroot /build/staging/live/filesystem.squashfs -noappend -e boot
cp /build/chroot/boot/vmlinuz-* /build/staging/live/vmlinuz
cp /build/chroot/boot/initrd.img-* /build/staging/live/initrd

Und bauen daraus eine ISO:

xorriso \
    -as mkisofs \
    -iso-level 3 \
    -o "/build/release.iso" \
    -full-iso9660-filenames \
    -volid "DEBIAN_CUSTOM" \
    -isohybrid-mbr /usr/lib/ISOLINUX/isohdpfx.bin \
    -eltorito-boot \
        isolinux/isolinux.bin \
        -no-emul-boot \
        -boot-load-size 4 \
        -boot-info-table \
        --eltorito-catalog isolinux/isolinux.cat \
    -eltorito-alt-boot \
        -e /EFI/boot/efiboot.img \
        -no-emul-boot \
        -isohybrid-gpt-basdat \
    -append_partition 2 0xef /build/staging/EFI/boot/efiboot.img \
    "/build/staging"

Die eigene Node-App hinzufügen

Auch hier wieder eine Info. Hier wird ein Dienst als „root“ ausgeführt. Ich könnte nun groß und breit darauf eingehen, wie man einen extra Linux-Benutzer für den Dienst einrichtet… aber habe ich keine Lust zu. Macht es vernünftig. Macht es nicht wie ich.

Wir wollen eine dumme node-app programmieren, die bei Start direkt auf Port 8080 läuft und einfach nur „hello world“ anzeigt.

Dazu gehen wir als erstes wieder in das chroot:

chroot /build/chroot

Und installieren uns nodejs. Dazu laden wir die bevorzugte Version von nodejs.org herunter, entpacken sie und verteilen etwas Kram in die entsprechenden Ordner:

cd /tmp
curl https://nodejs.org/dist/v16.15.0/node-v16.15.0-linux-x64.tar.xz --output node.tar.xz
tar -xf node.tar.xz
cd node-*
mv bin/* /usr/local/bin
mv lib/node_modules/ /usr/local/lib/
node -v
npm -v

Die letzten beiden Befehle checken nur, ob alles lief wie geplant.

Erstellen wir uns ein neues Verzeichnis und initialisieren unser Projekt:

mkdir /app
cd /app
npm init

… installieren express…

npm install --save express

… und erstellen eine /app/index.js mit folgendem Inhalt:

const express = require('express');
const app = express();
app.get('/', function(req, res) { res.send('Hello World!'); });
app.listen(8080, function() { console.log("Express running"); });

Testen, ob die App lokal läuft, indem wir (1) node /app/index.js eingeben und (2.) mit einem Browser unserer Wahl http://localhost:8080 aufrufen – läuft. STRG+C zum Beenden der App. (alles hier geht davon aus, dass euer Build-System einen Browser integriert hat. Wie bereits angedeutet, ich nutze WSL. Für Windows gibt es ein paar Browser.)

Nun erstellen wir eine Datei /lib/systemd/system/my_app.service mit folgendem Inhalt:

[Unit]
Description=Meine erste App
Documentation=https://www.tino-ruge.de/
After=network.target

[Service]
Environment
Type=simple
User=root
ExecStart=/usr/local/bin/node /app/index.js
Restart=on-failure

[Install]
WantedBy=multi-user.target

Und legen die in den Autostart:

systemctl enable my_app

Anschließend bauen wir ein neues Image, wie im Abschnitt „Bauen eines Releases“ beschrieben.

Windows <-> Hyper-V: schneller testen

Hatte ich oben versprochen, löse ich hier unten ein. Ich habe das WSL installiert – quasi ein Linux, innerhalb von Windows. Da habe ich leider kein Guide für, aber das haben die Jungs von Microsoft schon ganz gut hinbekommen.

Ebenfalls installiert ist bei mir Hyper-V. Dazu habe ich hier einen Guide.

In Hyper-V habe ich eine VM der Generation 1 erstellt und die ISO, die wir in diesem Artikel bauen, direkt in dem Pfad eingehängt, in dem diese erzeugt wird; bei mir ist es folgender:

C:\Users\tino.ruge.TINO-RUGE\AppData\Local\Packages\CanonicalGroupLimited.UbuntuonWindows_79rhkp1fndgsc\LocalState\rootfs\build\release.iso

Wenn ich einen Build in meinem Subsystem starte, schalte ich die VM aus (kann sie ja ab), lasse den Build durchlaufen, und schalte die VM wieder ein –> direkt testen, ob es passt, ohne jedes mal die ISO kopieren zu müssen o.ä.

Kategorien: Javascript, Live-Systeme

tino-ruge.de wird tino-kuptz.de

Im Laufe des Jahres 2024 wird dieser Blog umziehen. Alle Inhalte werden 1:1 weitergeleitet, nix geht verloren. Neue Domain, alter Autor, alter Inhalt.