Jak emulować różne wersje Raspberry Pi za pomocą QEMU?
Jak korzystać z QEMU w embedded, testować kernel i inne komponenty bez sprzętu?
Sytuacja jest trudna: zamówiłem sobie Raspberry Pi ale ze względu na kontrolę celną sprzęt przyjdzie dopiero za 2 tygodnie. Chciałbym już teraz zbudować system i przetestować czy działa. Może nawet wprowadzić pierwsze własne zmiany.
Brzmi nierealistycznie? Trochę tak, ale uwierz – tak sytuacja w projektach komercyjnych zdarza się całkiem często. W prywatnych również, paczki się gubią, nastają globalne pandemie, kurier zanosi towar dwa kody pocztowe dalej itp. itd.
Czym jest QEMU?
QEMU to program służący do emulacji sprzętu. Działa przez zastosowanie specjalnego “rekompilatora”, który przetwarza instrukcje jednej architektury na inną (np. x86 → ARM). Dzięki temu, mając tylko laptopa z Ubuntu możemy symulować wiele innych architektur i systemów.
Razem z QEMU często spotyka się pojęcie KVM. O co chodzi?
KVM jest modułem kernela (Kernel-based Virtual Machine) która pozwala naszemu systemowi objąć rolę hypervisora. To pozwala hostowi na odpalanie odizolowanych maszyn wirtualnych.
Jako moduł, KVM jest bardzo blisko sprzętu – dzięki temu emulacja oparta na nim jest bardzo szybka.
Gdy KVM pracuje razem z QEmu (jesteśmy na systemie z KVM), KVM zarządza dostępem do CPU oraz pamięci. QEMU emuluje zasoby sprzętowe (twardy dysk, video, USB itd.)
Gdy działa samo, QEMU emuluje CPU i sprzęt jednocześnie.
QEmu jest darmowe, działa na większości systemów i ma rozbudowaną dokumentację – w zasadzie prawie idealny projekt open source. Może gdyby był prostszy w obsłudze…
W ciągu tego wpisu tyle razy powtórzyłem jedno konkretne słowo, że wpadłem w satiację semantyczną – w dalszej części będzie lepiej 🙂
Instalacja
Instalacja jest prosta – najłatwiej przeprowadzić ją przez użycie packagae managera. Dla Ubuntu:
$ apt-get install qemu-system
Jeżeli masz inny system – na stronie projektu są aktualne instrukcje do ściągania:
https://www.qemu.org/download/#linux
Można oczywiście skompilować aplikację samemu – to będzie jednym z tematów kolejnego wpisu.
“Zwykły” kernel w QEMU
Robimy pierwszy krok – na początku będziemy emulować system o tej samej architekturze co komputer, na którym pracujemy. Jeśli masz laptopa – najprawdopodobniej będzie miał procesor o architekturze x86_64 (64bitowy) i będzie na nim hulać jakaś dystrybucja Linuxa np. Ubuntu albo Fedora.
Uwaga: architektura x86_64 jest również często oznaczana jako arm64 ale nie daj się zwieść – to dokładnie to samo.
QEMU jest już zainstalowane – teraz potrzebujemy kernela do emulacji systemu.
Mamy dwie drogi do wyboru.
Kernel z naszego systemu
Na Ubuntu i wielu innych systemach operacyjnych znajdziemy obraz kernela w katalogu /boot/.
Na moim systemie (Ubuntu 22.04.4) wygląda to tak:
$ ls /boot/vmlinuz*
/boot/vmlinuz /boot/vmlinuz-6.5.0-26-generic
Interesują nas pliki zaczynające się od vmlinuz.
Dlaczego?
Jest to nazwa wynikająca z konwencji – oznaczane są w ten sposób skompresowane obrazy kernela. Po starcie systemu są dekompresowane, ładowane do pamięci i odpalane przez bootloader (np. GRUB albo u-boot).
Kopiujemy kernel do ścieżki przeznaczonej na eksperymenty:
mkdir ~/experiments
cp /boot/vmlinuz-6.5.0-26-generic ~/experiments
Druga opcja – ściągamy kernel
Możemy również ściągnąć jądro z zewnętrznego serwera, na przykład z serwera ubuntu.
Jest to plik .deb – możemy go rozłożyć na czynniki pierwsze komendą:
cd ~/experiments
wget http://security.ubuntu.com/ubuntu/pool/main/l/linux-signed-hwe-5.11/linux-image-5.11.0-27-generic_5.11.0-27.29~20.04.1_amd64.deb
dpkg-deb -xv linux-image-5.11.0-27-generic_5.11.0-27.29~20.04.1_amd64.deb ~/experiments/tmp
I elegancko mamy dostęp do vmlinuz, jak w pierwszej opcji.
Initramfs (ramdisk)
W wypadku emulacji systemu Ubuntu sam kernel nie wystarczy – potrzebujemy też przestrzeni, na której kernel będzie mógł zamontować pliki, które są potrzebne do startu systemu. Do tego właśnie posłuży initramfs.
Tworzymy roboczy initramfs:
mkinitramfs -o ramdisk.img
Co znajduje się w środku?
Możemy to sprawdzić za pomocą polecenia:
lsinitramfs ramdisk.img | less
W skrócie, mamy tutaj hierarchię bardzo podobną do naszego standardowego rootfs’a.
Initramfs zasługuje na swój własny wpis. Teraz możemy zwrócić uwagę na jeden plik – /init. Jest to program który po zamontowaniu ramdysku przejmuje kontrolę i odpowiada za kolejne kroki inicjalizacji.
Pierwsza emulacja
Mamy już wszystko co potrzebne. Teraz trzeba rozszyfrować baaaardzo rozległą dokumentację QEMU i dojść do tego jakich przełączników trzeba użyć. Serio, binarka QEMU to chyba najbardziej skomplikowany samodzielny program jaki widziałem na Linuxie. Jeśli znasz coś potężniejszego – daj znać, chętnie się dowiem co to 🙂
Ostateczna komenda wygląda tak:
qemu-system-x86_64 \
-kernel vmlinuz-5.11.0-27-generic \
-nographic \
-append "console=ttyS0" \
-initrd ramdisk.img \
-m 2048 \
--enable-kvm \
-cpu host
Co tutaj się dzieje?
-kernel → wskazujemy którego kernela chcemy użyć
-nographic → chcemy użyć tylko dostępu przez linię komend, bez GUI
-append → co przekazujemy cmdline kernela (parametry początkowe)
-initrd → wskazujemy obraz initramfs
-m → przydzielamy pamięć dla systemu
—enable-kvm → podajemy z jakiego wsparcia chcemy skorzystać
-cpu → na jakim procesorze chcemy emulować, w tym wypadku na tym samym na którym chodzi nasz komputer
Po kilku chwilach powinniśmy zobaczyć jak system wstaje i pojawia się komunikat podobny do tego:
BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) built-in shell (ash)
Enter 'help' for a list of built-in commands.
(initramfs) cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
I mamy już gotowy system, można się bawić 🙂
Jak wyjść z trybu emulacji?
Podobnie jak wyjście z VIM’a – nie jest to oczywiste. Żeby wyjść z QEMU używamy sekwencji klawiszy:
Ctrl+a, c, q <enter>
Raspberry Pi 3b+
Mamy już opanowane podstawy – działającą emulację na x86_64. Teraz czas na emulację targetu embedded na naszym hoście.
Chciałbym sprawdzić czy mój image kernela zadziała dla Raspberry Pi 3b plus. Dobrze się składa, akurat nie mam takiego HW. Idealna okazja żeby zaprząc do pracy QEMU.
Ściągamy odpowiedni obraz dla Raspberry Pi i rozpakowujemy:
cd ~/experiments
wget https://downloads.raspberrypi.org/raspios_arm64/images/raspios_arm64-2023-05-03/2023-05-03-raspios-bullseye-arm64.img.xz
xz -d 2023-05-03-raspios-bullseye-arm64.img.xz
Musimy wydobyć z obrazu kilka plików. Ponieważ jest to obraz karty SD, żeby się do niego dobrać musimy go zamontować.
Tutaj napotykamy małą przeszkodę – obraz zawiera w sobie dwie partycje, bootfs oraz rootfs. Nas interesuje bootfs – znajdują się tam pliki, których potrzebujemy żeby nasz wirtualny sprzęt było gotów do działania.
Za pomocą polecenia $ fdisk:
fdisk -l 2023-05-03-raspios-bullseye-arm64.img
Disk 2023-05-03-raspios-bullseye-arm64.img: 8 GiB, 8589934592 bytes, 16777216 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x3e247b30
Device Boot Start End Sectors Size Id Type
2023-05-03-raspios-bullseye-arm64.img1 8192 532479 524288 256M c W95 FAT32 (LBA)
2023-05-03-raspios-bullseye-arm64.img2 532480 8617983 8085504 3,9G 83 Linux
Dowiadujemy się, że interesująca nas partycja zaczyna się w pozycji 8192 a pojedynczy sektor ma wielkość 512 bajtów. Dzięki temu możemy policzyć offset= 8192 * 512 = 4194304
sudo mkdir -p /mnt/image
sudo mount -o loop,offset=4194304 "$RPI_DIR"/2023-05-03-raspios-bullseye-arm64.img /mnt/image/
Uwaga: nie trzeba robić tego manualnie. W tym wypadku używam metody “krok po kroku” żeby łatwiej było ją oskryptować. Jeśli chcemy żeby wszystko działo się automagicznie możemy skorzystać z polecenia $ kpartx:
kpartx -av 2023-05-03-raspios-bullseye-arm64.img
Obie partycje zostaną zamontowane w tym samym miejscu co nośniki usb (np. pendrive).
Kopiujemy potrzebne pliki z obrazu:
cp /mnt/image/bcm2710-rpi-3-b-plus.dtb "$RPI_DIR"
cp /mnt/image/kernel8.img "$RPI_DIR"
Co skopiowaliśmy oprócz kernela?
Plik .dtb to devicetree – jest to struktura opisująca sprzęt podłączony do procesora, na którym startuje kernel. Dzięki temu jądro wie jakie sterowniki załadować i co zrobić, żeby system mógł w pełni wstać.
Właściwie mamy już wszystko ale… nie zalogujemy się po domyślnej kombinacji login/hasło. Te rzeczy są ustalane przy wypalaniu obrazu na karcie SD a my przecież nie chcemy tego robić 🙂 W związku z tym musimy dodać je osobiście.
Robi się to przez modyfikację pliku userconf:
echo malina | openssl passwd -6 -stdin
<hash>
echo pi:<hash> | tee /mnt/image/userconf
Plik userconf wymaga podania hasła w formacie nazwa_użytkownika:hash_hasła. Hash generujemy przez użycie $ openssl.
Wszystko składa się w całość – jesteśmy gotowi na magię QEMU.
Polecenie:
qemu-system-aarch64 \
-machine raspi3b \
-cpu cortex-a72 \
-nographic \
-dtb bcm2710-rpi-3-b-plus.dtb \
-m 1G \
-smp 4 \
-kernel kernel8.img \
-sd 2023-05-03-raspios-bullseye-arm64.img \
-append "rw earlyprintk loglevel=8 console=ttyAMA0,115200 dwc_otg.lpm_enable=0 root=/dev/mmcblk0p2 rootdelay=1"
Względem poprzedniego przykładu, z Ubuntu, widzimy kilka nowych parametrów:
-machine → definiujemy jaką maszynę chcemy emulować
-smp → ile CPU chcemy mieć do dyspozycji
-sd → plik obrazu przeznaczony na kartę SD
-append → tutaj kernel cmdline zapożyczamy z oryginalnej wersji, z pliku cmdline z partycji bootfs/
Po krótkiej chwili powinniśmy zobaczyć log z bootowania a następnie prompt logowania:
[ OK ] Started Modem Manager.
Debian GNU/Linux 11 raspberrypi ttyAMA0
raspberrypi login:
Wpisujemy wcześniej ustaloną kombinację – pi/malina i możemy się cieszyć w pełni wirtualnym systemem embedded!
Pełny kod użyty w poradniku dla Raspberry Pi 3b znajdziesz TUTAJ.
QEMU dla najnowszych Raspberry Pi
Jak emulować w QEMU najnowsze wersje Raspberry Pi?
W powyższych rozdziałach poznaliśmy praktyczne podstawy emulacji i udało nam się stworzyć pełną emulację sprzętu dla Raspberry Pi 3b+. Pozostaje pytanie:
Co z nowszymi wersjami Raspberry Pi, np 4 lub 5?
Problem
Sprawa jest skomplikowana.
QEMU wprowadziło wsparcie dla Raspberry Pi 4 dopiero w tym roku. Niestety, najnowsza wersja paczki nie jest dostępna dla Ubuntu 22.04, którego używam. W takiej sytuacji, jeśli chcemy z niej skorzystać, musimy skompilować aplikację sami.
Jak sprawdzić jaką wersję QEMU mamy zainstalowaną?
$ qemu-system-aarch64 --version
QEMU emulator version 6.2.0 (Debian 1:6.2+dfsg-2ubuntu6.19)
Wsparcie dla Raspberry Pi jest dostępne w wersji 9.0.0
Poszedłem dokładnie tą drogą – skompilowałem QEMU i odpaliłem dla plików z Raspberry Pi 4. Niestety, złe wieści – emulowany sprzęt nie posiada wsparcia dla klawiatury i myszki. Mi nawet nie udało się dojść do promptu logowania.
Mam nadzieję, że pełny support zostanie dodany w niedalekiej przyszłości. Jeśli chcesz wiedzieć więcej – szczegóły znajdziesz w tutaj (issue na gitlabie).
Ale to nie koniec przygody – możemy obejść się bez pełnego wsparcia emulacji sprzętu. Tą opcją jest odpalenie naszego kernela i obrazu karty SD na wirtualnym procesorze. Jest tylko jeden kruczek:
Potrzebujemy kernela wspierającego wirtualny system plików (virtual filesystem).
Większość ‘desktopowych’ kerneli ma ten support już wkompilowany. Dzięki temu możemy odpalać na naszych laptopach np. maszyny wirtualne. Raspberry Pi domyślnie tego supportu nie ma, musimy go dołożyć sami.
Jak? Kompilując kernel własnymi siłami 🙂
Nie jest to bardzo trudne – pokażę Ci jak to zrobić, krok po kroku.
Kernel wspierający emulację
Poniższa instrukcja jest krótka i zawiera tylko samo mięso. Jeśli jesteś zainteresowany bardziej dogłębnym poradnikiem o kompilowaniu swojego kernela, zostaw komentarz albo napisz na karol@linuxdev.pl
Na początku – potrzebujemy kodu źródłowego. Ściągamy go z oficjalnego źródła:
mkdir ~/experiments
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.34.tar.xz
tar xvJf linux-6.1.34.tar.xz
cd linux-6.1.34
Będziemy również potrzebowali narzędzia do kompilacji. Nie możemy użyć kompilatora, który mamy dostępny na naszym komputerze. Powód jest prosty:
Nie zgadza się architektura docelowego procesora.
Na komputerze na którym najprawdopodobniej pracujesz, procesor ma architekturę x86. Raspberry Pi jest oparta na architekturze ARM, czyli musimy skompilować kernel na inny rodzaj procesora od tego, który jest na maszynie kompilującej. Ten proces nazywamy cross-compilation lub bardziej po polsku, kompilacją skrośną (choć w projektach komercyjnych absolutnie nikt tak nie mówi). Najczęściej spotkasz się ze sformułowanie “cross kompilacja”, taki trochę ponglish.
Cross compilation potrafi być trudnym zadaniem, na szczęście w naszym przypadku sprowadzi się do instalacji binutils (kompilator, assembler, linker itd.) dla ARM i zdefiniowania kilku zmiennych środowiskowych:
sudo apt-get install gcc-aarch64-linux-gnu
Następnie zaciągamy dla naszego kernela domyślną konfigurację oraz konfigurację wspierającą KVM. Dzięki temu będziemy mogli stworzyć wirtualny filesystem, który umożliwi emulację.
Zwróć uwagę, że musimy tutaj zdefiniować architekturę, dla której kompilujemy i podać nazwę binutils przeznaczonych na tą architekturę.
ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make defconfig
ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make kvm_guest.config
Budujemy:
ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make -j8
Hint: make -j8 oznacza, że kompilacja obciąży maksymalnie 8 rdzeni procesora. Możesz tą wartość zmienić po swoje potrzeby lub użyć opcji make -j$(nproc), która automatycznie obciąży wszystkie rdzenie, a ty możesz wyjść w tym czasie z psem.
Obraz dla Raspberry Pi 4
Mamy już kernela, teraz powinniśmy dodać drugi element naszego przepisu – czyli obraz karty SD. Możemy go ściągnąć z oficjalnej strony Rasppberry Pi Foundation i wypakować:
cd ~/experiments
wget https://downloads.raspberrypi.org/raspios_arm64/images/raspios_arm64-2023-05-03/2023-05-03-raspios-bullseye-arm64.img.xz
xz -d 2023-05-03-raspios-bullseye-arm64.img.xz
QEMU będzie marudziło że rozmiar pobranego obrazu nie jest taki jaki powinien być. Zazwyczaj resize jest jednym z kroków w RPi Imager, przy wrzucaniu obrazu na prawdziwą kartę SD. QEMU oferuje sposób na dostosowanie rozmiaru:
qemu-img resize 2023-05-03-raspios-bullseye-arm64.img 8G
Podobnie jak wyżej, musimy zamontować obraz żeby zapisać w nim hasło. Tylko w ten sposób będziemy mogli zalogować się do systemu gdy już wstanie.
mkdir -p /mnt/image
mount -o loop,offset=4194304 2023-05-03-raspios-bullseye-arm64.img /mnt/image/
Generujemy hasło:
PASSWORD="malina"
PASSWORD_HASH=$(echo "$PASSWORD" | openssl passwd -6 -stdin)
echo pi:"$PASSWORD_HASH" | tee /mnt/image/userconf
Finał – odpalamy
Ostateczna komenda będzie wyglądać tak:
qemu-system-aarch64 -machine virt -cpu cortex-a72 -smp 6 -m 4G \
-kernel Image -append "root=/dev/vda2 rootfstype=ext4 rw panic=0 console=ttyAMA0" \
-drive format=raw,file=2023-05-03-raspios-bullseye-arm64.img,if=none,id=hd0,cache=writeback \
-device virtio-blk,drive=hd0,bootindex=0 \
Jak to w QEMU, ilość argumentów może trochę przytłoczyć. Względem komendy z poprzedniego wpisu, pojawiło się kilka nowych opcji:
Do cmdline kernela dodaliśmy opcję ‘root=/dev/vda2’. To jest ścieżka do wirtualnego filesystemu i będzie dostępna tylko jeśli mamy wkompilowany odpowiedni support dla KVM w kernelu (co zrobiliśmy na początku).
-device virtio-blk,drive=hd0,bootindex=0 → tutaj dodajemy wirtualne urządzenie blokowe. Używamy też zdefiniowanego wcześniej id i definiujemy że to z tego urządzenia powinien zbootować się nasz system.
Po krótkiej chwili powinien pojawić się prompt logowania. Logujemy się kombinacją pi/mailna.
Uwaga: widok promptu logowania jest widoczny w menu View → serial0
I to wszystko! Możemy cieszyć się platformą wbudowaną BEZ sprzętu.
Podsumowanie + kod
Bardzo zdziwił mnie brak dobrego supportu dla Raspberry Pi 4 w QEMU – przecież nie jest to najnowocześniejszy HW. Już zdążyła wyjść “latest, greatest” rewizja sprzętu, czyli Raspberry Pi 5.
Jest spora szansa, że już za niedługo taki support będzie stabilny. Wtedy będzie można odpalać emulację dokładnie tak samo jak robiliśmy to dla Raspberry Pi 3b+. Do tego czasu, zostaje nam emulowanie sprzętu wirtualnego.
Cały kod w formie skryptu powłoki jest dostępny tutaj. Dzięki niemu cała sekwencja powinna zadziałać “automagicznie”
Dodaj komentarz