Lekcja 04- Buildsystemy i rootfs

Wstęp

W tej lekcji zapoznamy się z tym co odróżnia poszczególne dystrybucje od siebie czyli z rootfsem oraz z narzędziami służącymi do jego przygotowania.

Czym jest rootfs?

Włącz konsolę na swoim komputerze i wykonaj następującą komendę:

ls /

Komenda ta wyświetla wszystkie katalogi na twoim komputerze. Ta komenda chyba najlepiej obrazuje czym jest rootfs- jest to system plików o konkretnej strukturze. W najprostszej postaci na rootfs będą się składać programy oraz pliki konfiguracyjne niezbędne do działania systemu.

Teraz pojawia się pytanie jak zbudować taki rootfs? Możemy oczywiście przebudowywać wszystkie programy ręcznie, stworzyć pliki konfiguracyjne, ułożyć to wszystko w odpowiednią strukturę i musimy jeszcze pamiętać aby każdy plik miał odpowiednie prawa dostępu. Generalnie sporo roboty, a my jesteśmy leniwi jak to rasowi programiści. Dlatego też do tworzenia rootfs’ów korzysta się z narzędzi zwanych buildsystemami.

Czym jest buildsystem?

Buildsystem to narzędzie umożliwiające nam skonfigurowanie i przebudowanie całego systemu w przystępny sposób. Dzięki buildsystemowi możemy przebudować cały rootfs, ale nie tylko, wiele buildsystemów umożliwia również zbudowanie:

  1. toolchaina, który my pobieraliśmy
  2. bootloadera, którego kod pobieraliśmy i sami budowaliśmy
  3. kernela, który również sami budowaliśmy

Czyli buildsystem umożliwia nam zbudowanie wszystkiego nie mając właściwie nic.

Na rynku jest dostępnych kilka rozwiązań, w poważnych komercyjnych zastosowaniach Yocto(a właściwie The Yocto Project) wydaję się być najpopularniejsze, ale jest również dosyć złożone i prawdę mówiąc w momencie pisania kursu sam niezbyt je ogarniam. Dlatego też nie będziemy się nim zajmować. Innym popularnym buildsystemem jest Buildroot. Bardzo dobrze nadaje się on do tworzenia prostych systemów. Jego dodatkową zaletą jest prostota jego konfiguracji, jest ona bardzo zbliżona do konfiguracji kernela Linuksowego- jest dostępny menuconfig. To właśnie go będziemy używać do budowania naszego Linuksa.

Budowanie rootfs

Drobna uwaga- proces konfiguracji w tym przypadku jest bardzo zbliżony dla wszystkich trzech platform dlatego też rozróżnienie nastąpi dopiero podczas uruchamiania.

Oczywiście musimy najpierw pobrać nasz buildsystem, możesz pobrać wersję stabilną dostępną na stronie Buildroota lub możesz pobrać wersję używaną przeze mnie:

wget https://buildroot.org/downloads/buildroot-2021.02.4.tar.gz
tar xf buildroot-2021.02.4.tar.gz

Przejdź do otrzymanego katalogu. Na samym początku, podobnie jak w przypadku kernela musimy określić domyślną konfigurację dla naszej platformy:

# dla BBB
make beaglebone_defconfig
# dla RPi4
make raspberrypi4_64_defconfig
# dla QEMU versatilepb:
make qemu_arm_versatile_defconfig

Zwróć uwagę, że tym razem nie ustawiamy ani zmiennej ARCH ani CROSS_COMPILE, Buildroot sam ogarnia co musi być ustawione.

Teraz możemy przejść do konfiguracji. Uruchom konfigurator znanym Ci już poleceniem:

make menuconfig

Powinieneś zobaczyć widok przypominający to co było widoczne w przypadku Linuksa:

Buildroot menuconfig

Może kilka słów o tym co będziemy chcieli zbudować:

  1. Toolchain- moglibyśmy użyć toolchaina, który był używany do przebudowania kernela, ale zbudujemy go w celach instruktażowych
  2. Kernel + device-tree- w zasadzie już je mamy dla każdej platformy, ale dla celów demonstracyjnych również je zbudujemy
  3. rootfs- czyli to czego na brakuje do uruchomienia naszego Linuksa

Odpuścimy sobie budowanie bootloaderów.

No to zaczynamy, najpierw skonfigurujmy toolchain, jak się zapewne domyślasz wybierz opcję Toolchain w menuconfigu. Pojawiło się kilka opcji, po pierwsze ustaw opcję „C Library” na glibc. Tutaj jeszcze drobna uwaga odnośnie tego jaką bibliotekę wybrać. Mamy do wyboru biblioteki uClibc-ng, musl oraz glibc:

  1. uClibc-ng jest forkiem uClibca, która została stworzona dla systemu uCLinux czyli Linuksa, który był uruchamiany na urządzeniach bez jednostki zarządzającej pamięcią(popularnie zwane MMU, memory management unit) czyli na mikrokontrolerach, z czasem została ona dostosowana do pracy z normalnym Linuksem
  2. musl jest biblioteką przystosowaną do pracy z urządzeniami o niewielkiej pamięci RAM, jako granicę często się podaje 32MB pamięci RAM
  3. glibc jest biblioteką dobrze znaną z komputerów, implementuje ona najwięcej elementów standardu POSIX

Ze względu, że każda z użytych platform ma więcej niż 32MB RAMu użyjemy glibc.

Ostatnią opcją, która nas interesuje jest wsparcie dla języka C++, zaznacz więc opcje „Enable C++ support”. Twoja konfiguracja powinna wyglądać następująco:

Konfiguracja toolchaina

Wróć do głównego menu. Następnie przejdź do „System configuration”. Tutaj możesz zmienić nazwę swojego hosta, baner powitalny czy ustawić hasło dla roota. Możesz te opcje skonfigurować dowolnie. Na samym dole są dwie opcje: „Custom scripts to run after creating filesystem images” oraz „Extra arguments passed to custom scripts”. Ustaw je tak aby były puste. Usuwamy te opcje ponieważ nie jesteśmy zainteresowani generowaniem obrazów kart SD. Na koniec twoja konfiguracja powinna wyglądać mniej więcej tak:

System configuration

Ponownie wróć do głównego menu i wybierz „Filesystem images”. Spośród wszystkich opcji zaznaczona powinna zostać jedynie „tar the root filesystem”. Teoretycznie mogłaby nas interesować opcja „ext2/3/4 root filesystem”, która to generuje obraz dysku, który mógłby zostać podmontowany później do QEMU jednak przez pomieszanie z poplątaniem megabajtów z mibibajtami ciężko jest ustalić prawidłowy rozmiar obrazu dysku dla QEMU dla modeli ARMowych, obejdziemy ten problem “ręcznie”. Na koniec konfiguracja systemów plików powinna wyglądać tak:

Konfiguracja systemu plików

Ponownie wróć do głównego menu i przejdź do sekcji „Bootloaders” i odznacz wszystkie opcje jeśli jakaś jest zaznaczona.

Dla jasności, w przypadku kernela użyjemy domyślnych ustawień dlatego nie wchodziliśmy do tej sekcji.

Na koniec możesz wrócić do głównego menu i przejść do „Target packages”. To tam znajdują się pakiety, które możesz dodać do swojej dystrybucji, możesz nawet dodać gry.

Gdy wszystkie ustawienia są gotowe wyjdź z menuconfiga, oczywiście pamiętaj aby zapisać zmiany w konfiguracji podczas wychodzenia. Teraz możesz zbudować wszystko za pomocą komendy:

make

Proces budowania powinien potrwać jakieś 30-40 minut, no chyba, że masz nieco starszy komputer.

Pliki wynikowe będą dostępne w katalogu output/images. Będą się tam znajdować pliki dtb(czyli nasze device-tree), obrazy kernela(zImage lub Image) oraz paczki z systemami plików o nazwie rootfs.tar.

Zwróć uwagę na brak parametru -j w wywołaniu komendy make. Domyślnie Buildroot sam ogarnia ile wątków budowania uruchomić w danym momencie. Jeśli jednak chciałbyś z jakiś względów ustawić jakąś konkretną liczbę wątków budujących to zainteresuj się opcją BR2_JLEVEL.

BeagleBone Black

Bootloader jest już skonfigurowany dlatego też jedyne co musimy zrobić to dodanie naszego rootfsa na kartę pamięci. Aby to zrobić podłącz swoją kartę uSD do komputera, a następnie przejdź do katalogu z obrazami wynikowymi i rozpakuj wygenerowaną paczkę rootfs.tar na odpowiednią partycję:

cd output/images
sudo tar xf rootfs.tar -C /media/user/rootfs
sync # tę komendę wykonaj aby się upewnić, że wszystkie pliki zostały zapisane na karcie
umount /media/user/* # odmontuj kartę

Teraz umieść kartę w BBB, podłącz BBB do komputera za pomocą konwertera UART-USB, uruchom screena i uruchom BBB. Ponownie ujrzysz dużo logów, ale tym razem czeka Cię miła niespodzianka- system pyta się o użytkownika. Wpisz root. Brawo! Właśnie zalogowałeś się do swojego pierwszego własnoręcznie zbudowanego Linuksa! Jako zadanie dodatkowe możesz przetestować device-tree i kernel wygenerowane przez Buildroota.

Raspberry Pi 4

W przypadku RPi4 mamy sytuację identyczną jak z BBB, bootloader jest już skonfigurowany i jedyne co musimy zrobić to zapisać rootfs na karcie pamięci:

cd output/images
sudo tar xf rootfs.tar -C /media/user/rootfs
sync # tę komendę wykonaj aby się upewnić, że wszystkie pliki zostały zapisane na karcie
umount /media/user/* # odmontuj kartę

Umieść kartę w RPi4, podłącz RPi4 do komputera po UARTcie, uruchom screen i włącz zasilanie. Tak! Linux się uruchomił! Pyta się o użytkownika więc wpisz root. Właśnie się zalogowałeś! Brawo Ty użytkowniku RPi4! Też możesz jako dodatkowe zadanie przetestować device-tree i kernel wygenerowane przez Buildroota.

QEMU

Aby uruchomić QEMU z naszym rootfsem musimy przygotować obraz dysku, na którym będzie rootfs i który to obraz zostanie użyty przez QEMU. Przygotuj więc taki obraz za pomocą poniższych komend:

cd output/images
# utwórz obraz dysku
dd if=/dev/zero of=rootfs.img bs=1024 count=65536
# sformatuj dysk na EXT4 i nadaj etykietę rootfs
mkfs.ext4 rootfs.img -L rootfs
# zamontuj dysk pod /mnt
sudo mount rootfs.img /mnt
# wypakuj plik na dysk
sudo tar xf rootfs.tar -C /mnt
# odmontuj dysk
sudo umount /mnt

Teraz mamy już przygotowany dysk, możemy więc uruchomić system:

./qemu-system-arm -m 256 -M versatilepb \
    -kernel /ścieżka/do/linux-x.y.z_arm/arch/arm/boot/zImage \
    -dtb /ścieżka/do/linux-x.y.z_arm/arch/arm/boot/dts/versatile-pb.dtb \
    -append "console=ttyAMA0,115200 root=/dev/sda rw rootwait" \
    -drive file=/ścieżka/do/br/output/images/rootfs.img,if=scsi,format=raw \
    -nographic

Wciskasz enter i… nic, system się zawiesił na logu:

Waiting for root device /dev/sda…

Czyli wygląda jakby nie wykrywał dysku. Spróbuj uruchomić teraz QEMU z kernelem i device-tree wygenerowanym przez Buildroota.

I co tym razem się okazało? Tak, system się uruchomił! Możesz się zalogować i używać swojego systemu. Brawo!

Pojawia się pytanie czemu kernel zbudowany na poprzedniej lekcji nie działa z rootfsem. Otóż domyślna konfiguracja dla versatile’a nie ma włączonego wsparcia dla obsługi urządzeń blokowych. Jeśli chciałbyś użyć swojego kernela musisz przejść do katalogu z jego kodem źródłowym kernela, włączyć menuconfig i ustawić następujące opcje jako wkompilowane w kernel(dla przypomnienia tryb wyszukiwania włączasz klawiszem /):

  1. CONFIG_PCI
  2. CONFIG_PCI_VERSATILE
  3. CONFIG_SCSI
  4. CONFIG_SYM53C8XX_2
  5. CONFIG_BLK_DEV_SD
  6. CONFIG_DEVTMPFS
  7. CONFIG_DEVTMPFS_MOUNT

Przebuduj kernel. Teraz system powinien już wystartować bez żadnych problemów.

Jeśli chcesz zbudować rootfs na architektury ARM64 lub x86_64 to użyj odpowiednio konfiguracji qemu_aarch64_virt_defconfig lub qemu_x86_64_defconfig.

Initramfs

Initramfs to mały system plików, który może zostać załadowany do pamięci RAM podczas startowania systemu. Można taki system plików można wykorzystać do szybkiego uruchomienia systemu i wstępnej inicjalizacji peryferiów, a w momencie gdy dysk z docelowym rootfsem będzie dostępny to ten dysk zostanie zamontowany i użyty jako rootfs. Użycie initramfsa można traktować jako dodatkowy krok w sekwencji startowania systemu. Standardowo Initramfs ma postać archiwum cpio.gz. W przypadku użycia U-Boota, jak ma to miejsce np. w przypadku BBB, takie archiwum musi dostać jeszcze odpowiedni nagłówek.

Nasz zbudowany rootfs nie jest zbyt duży dlatego też możemy go użyć jako initramfs. Przejdź do katalogu Buildroota i włącz menuconfig. Przejdź do sekcji „Filesystem images”, włącz opcję „cpio the root filesystem (for use as an initial RAM filesystem)”, jako metodę kompersji wybierz gzip. Konfiguracja powinna wyglądać tak:

Konfiguracja systemów plików w celu zbudowania initramfs

Zapisz zmiany w konfiguracji i przebuduj ponownie system. Tym razem będzie to trwało bardzo krótko, może z jedną minutę. W katalogu output/images zostanie wygenerowany plik rootfs.cpio.gz.

W naszym przypadku różnica pomiędzy użyciem rootfsa na karcie pamięci a użyciem initramfs będzie bardzo nieznaczna. Wy możecie zauważyć dwie rzeczy:

  1. Brak persystentności plików w przypadku initramfsu, oznacza to, że plik utworzony podczas pracy z urządzeniem zniknie wraz z jego wyłączeniem
  2. Możesz sprawdzić punkty montowania za pomocą programu mount. Zobaczysz, że są one różne w przypadku użycia rootfsa z karty pamięci i initramfsa

BeagleBone Black

Przejdź do katalogu z obrazami. Musisz utworzyć plik z initramfsem, który będzie mógł być użyty przez U-Boota. Taki plik można uzyskać używając programu mkimage. Komenda wygląda następująco:

mkimage -A arm -O linux -T ramdisk -d rootfs.cpio.gz initramfs

Zostanie wygenerowany plik initramfs. Umieść ten plik na swojej karcie uSD na partycji kernel(czyli tam gdzie jest kernel :)), umieść kartę z powrotem w BBB, podłącz go po UARTcie do komputera i uruchom go. Przerwij proces bootowania tak aby mieć dostęp do konsoli U-Boota. Po pierwsze aby mieć pewność, że nie zostanie użyty rootfs z karty uSD musimy zmienić zmienną bootargs:

setenv bootargs ”console=ttyS0,115200 earlyprintk”

Następnie musimy załadować do pamięci kernel, initramfs, device-tree oraz uruchomić system:

ext4load mmc 0:2 0x82000000 zImage
ext4load mmc 0:2 0x88080000 initramfs
ext4load mmc 0:2 0x88000000 am335x-boneblack.dtb
bootz 0x82000000 0x88080000 0x88000000

Tym razem podajemy wszystkie parametry bootz. Powinien uruchomić się system. Jak wspomniano wcześniej nie widać żadnej znaczącej różnicy na pierwszy rzut oka, ale Ty wiesz jak już sprawdzić czy używasz initramfsu.

Raspberry Pi 4

Przejdź do katalogu z obrazami, a następnie skopiuj plik rootfs.cpio.gz na kartę uSD na partycję boot. Następnie zmodyfikuj plik config.txt poprzez dodanie linii:

initramfs rootfs.cpio.gz

Brak znaku równości nie jest błędem.

Żeby mieć pewność, że nie jest używany rootfs z karty pamięci zmodyfikuj plik cmdline.txt do postaci:

console=ttyS0,115200 earlyprintk

Umieść kartę pamięci w RPi4, podłącz je do komputera i uruchom. Powinien uruchomić się system i tak samo jak w przypadku BBB na pierwszy rzut oka nie widać żadnej istotnej zmiany.

Gdy już sprawdzisz wszystko co chciałeś sprawdzić przywróć ustawienia RPi4 do poprzedniego stanu.

QEMU

Tutaj sprawa jest prosta, po prostu użyjemy odpowiedniej komendy do uruchomienia QEMU:

./qemu-system-arm -m 256 -M versatilepb \
    -kernel /ścieżka/do/br/output/images/zImage \
    -dtb /ścieżka/do/br/output/images/versatile-pb.dtb \
    -initrd /ścieżka/do/br/output/images/rootfs.cpio.gz \
    -append "console=ttyAMA0,115200" \
    -nographic

Tutaj korzystamy z nowego parametru -initrd, który to wskazuje na initramfs do użycia. Jak widać tym razem nie korzystamy z żadnego obrazu dysku mamy więc 100% pewności, że nasz system plików to initramfs.

Dodawanie własnych plików do rootfs

Czasami może zajść potrzeba aby dodać jakiś swój plik konfiguracyjny czy program do rootfsa. Oczywiście można by to zrobić ręcznie i samodzielnie wkleić odpowiednie pliki do rootfs na kartę pamięci, ale to jest mało poręczne, łatwo o tym zapomnieć itd.

Przypuśćmy, że chcesz dodać plik o nazwie file.conf do katalogu /etc. Aby to zrobić musisz zrobić nakładkę(po angielsku overlay) na system plików czyli przygotować interesującą Cię strukturę katalogów. Aby to zrobić utwórz katalog gdzieś poza Buildrootem i przejdź do niego:

mkdir overlay
cd overlay

Następnie utwórz to co chcesz nadpisać/dodać w oryginalnym rootfs:

mkdir etc
touch etc/file.conf
echo konfiguracja > etc/file.conf

I mamy już przygotowane wszystko co chcemy dodać. Przebuduj teraz Buildroota za pomocą komendy:

make BR2_ROOTFS_OVERLAY=/ścieżka/do/overlay/

Teraz po rozpakowaniu rootfsa na kartę pamięci będzie tam plik /etc/file.conf. Ta metoda zadziała dla każdego rodzaju plików, także dla skryptów czy plików binarnych.

Dodawanie własnego pakietu do Buildroota

Dodawanie plików nie zawsze też jest idealnym rozwiązaniem, czasami mamy kod, który byśmy chcieli budować razem z innymi pakietami, a na koniec umieścić zbudowany program do systemu plików.

Zacznijmy od zrobienia prostego programu, tzw. Hello World napisanego w C++. Kod źródłowy programu jest następujący:

#include <iostream>

using namespace std;

int main()
{
    cout << "Hello!!!" << endl;
    return 0;
}

Aby móc zrobić pakiet buildrootowy potrzebujemy mieć prostą możliwość przebudowania tego programu dlatego przygotujemy Makefie’a:

all: hello.cpp
	$(CXX) hello.cpp -o hello

clean:
	rm hello

Umieść te dwa pliki w jakimś katalogu poza Buildrootem. Następnie przejdź do katalogu Buildroota. Utwórz katalog dla naszego pakietu:

mkdir package/hello

Następnie utwórz plik hello.mk wewnątrz package/helo o zawartości:

HELLO_VERSION = 1.0.0            # wersja programu
HELLO_SITE = /ścieżka/do/hello  # lokalizacja kodu, może być to również lokalizacja w sieci
HELLO_SITE_METHOD = local   # sposób pozyskania kodu

# komendy do budowania pakietu
define HELLO_BUILD_CMDS
    $(MAKE) CXX="$(TARGET_CXX)" LD="$(TARGET_LD)" -C $(@D) all
endef

# komendy do instalacji pakietu
define HELLO_INSTALL_TARGET_CMDS
    $(INSTALL) -D -m 0755 $(@D)/hello $(TARGET_DIR)/bin/hello
endef

$(eval $(generic-package))

Mamy wszystko co potrzebne do zbudowania pakietu, teraz musimy umożliwić jakoś jego wybór w menuconfigu. Aby to zrobić utwórz plik o nazwie Config.in w package/hello o następującej zawartości:

config BR2_PACKAGE_HELLO
    bool "hello"
    help
        Application that says hello to a user

Następnie otwórz plik package/Config.in i niemalże na samym końcu przed ostatnią linijką endmenu dodaj taki wpis:

menu "My programs"
        source "package/hello/Config.in"
endmenu

Teraz możesz włączyć menuconfiga i przejść do sekcji „Target packages”. Na samym dole powinna być opcja „My programs”, a po przejściu do niej będzie widoczny pakiet hello. Zaznacz go i przebuduj rootfs. Podmień rootfs na swojej karcie pamięci(w swoim obrazie dysku jeśli używasz QEMU). Po uruchomieniu urządzenia będziesz mógł wywołać komendę hello. Plik będzie się znajdować w katalogu /bin.

Wygenerowany toolchain

Toolchain zbudowany przez Buildroota znajduje się w katalogu output/host/bin. Jeśli chcesz zbudować „ręcznie” program na swoją platformę powinieneś użyć kompilatorów z tego miejsca. Ich użycie jest identyczne jak standardowego gcc/g++ na twoim komputerze z wyjątkiem tego, że mają nieco przydługie nazwy.

Ten wpis został opublikowany w kategorii Kurs budowania Linuksa. Dodaj zakładkę do bezpośredniego odnośnika.

1 odpowiedź na Lekcja 04- Buildsystemy i rootfs

  1. Anonim pisze:

    To mi się podoba. Człowiek zawsze się czegoś uczy, a tym przypadku jest to świetnie podane na tacy. Dziękuję,

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *