Vor einigen Monaten ließ sich die CIFS-Freigabe meiner Fritzbox nicht mehr ordentlich einbinden bzw. kam es zu merkwürdigen I/O-Fehlern bei einigen Lesezugriffen. Da dies mit einem Wechsel auf die Testversion von Fritz-OS zusammenfiel, hatte ich die Schuld innerlich bei Fritz gesehen. Aber die Tage habe ich mit einem alten Debian-Live-System auf die Freigabe zugegriffen und es funktionierte. Der Fehler scheint nicht im Fritz-OS, sondern im Linux zu liegen.
Für Linux hatte ich nun eine funktionsfähige und eine kaputte Version, was förmlich nach git-bisect schreit. Nur hatte ich keine Lust, das mit meinem Rechner zu probieren und 10 mal oder öfter immer wieder neustarten zu müssen. Aber mit Qemu hat sich eine super Lösung ergeben, die am Ende auch noch vollautomatisch durchlief und sich sehr leicht für ähnliche Fälle anpassen lässt.
Der Fairness halber muss ich sagen, dass ich einen Tag lang an der Lösung gebastelt habe, weil Gcc 10 nicht mit den älteren Kernelversionen funktioniert und mir alle Schritte am Beginn nicht so klar waren, wie sie jetzt hier erscheinen.
VM-Image erstellen
Der erste Schritt ist die Erstellung des Images mit debootstrap, was leider noch Root-Rechte erfordert. Für das Image kann man auch qemu-img verwenden, um zum Beispiel Snapshots nutzen zu können.
% truncate -s32G ~/kein_Backup/disk.img
% mke2fs -L Linux_Root -t ext4 ~/kein_Backup/disk.img
% sudo mount ~/kein_Backup/disk.img /mnt/other
% sudo debootstrap testing /mnt/other
Sollte sich im Nachhinein herausstellen, dass das Image zu klein ist, kann man es mit truncate und resize2fs vergrößern.
Zum Erleichtern der Arbeit habe ich für Root das Passwort entfernt und den Kernel1 installiert, um das System booten zu können.
-
Es gibt auch noch die Variante linux-image-cloud-amd64, allerdings hat diese keine Unterstützung für 9P für den Datenaustausch. ↩
% sudo sed -i '1s/:x:/::/' /mnt/other/etc/passwd
% sudo chroot /mnt/other apt install --no-install-recommends linux-image-amd64
% sudo umount -d /mnt/other
Die folgenden Anpassungen können sowohl in der Chroot-Umgebung oder nach dem ersten Start ausgeführt werden. Die Meldung vor der Anmeldung habe ich angepasst, um bei den regelmäßigen Wechseln der Kernelversion den Überblick zu behalten, und das Paket console-setup ist notwendig, um die deutsche Tastatur einzurichten.
# echo LANG=C.UTF-8 > /etc/default/locale
# echo Europe/Berlin > /etc/default/locale
# echo debian-vm > /etc/hostname
# echo '[\d \t] Debian GNU/Linux \r bullseye/sid \n \l' > /etc/issue
# apt install console-setup
Weiterhin habe ich noch die Netzwerkverwaltung auf Systemd umgestellt, weil dies praktischer ist. Das Paket dbus ist dabei nur notwendig, wenn man networkctl zur Statusabfrage verwenden will.
# systemctl enable --now systemd-networkd
# apt install dbus
# networkctl
IDX LINK TYPE OPERATIONAL SETUP
1 lo loopback carrier unmanaged
2 ens3 ether routable configured
Networkd im Hostsystem für DHCP einrichten
Mit Qemu verwende ich als Verbindung einen TAP/TUN-Netzwerkanschluss, damit ich Qemu (fast) ohne Root-Rechte starten kann. Hierbei ist sehr nützlich, dass Systemd-Networkd auch einen DHCP-Server integriert hat, den man für bestimmte Schnittstellen konfigurieren kann. Dabei lässt sich auch noch sehr einfach eine Paketweiterleitung und die Maskierung der Pakete einrichten, sodass das Gastsystem direkt ins Internet kann.
Diese Regeln sollen für alle TUN-Netzwerkanschlüsse gelten, die auf -vm
enden, damit ich auch mehrere virtuelle Maschinen auf diese Weise betreiben
kann. Abgespeichert habe ich die Konfiguration unter
/etc/systemd/network/19-vm-tun.network, wobei man die Nummer im Dateinamen
variieren muss, damit die Regeln auch wirklich auf den Anschluss zutreffen. Dies
geht am besten mit networkctl reload
und networkctl status server-vm
.
[Match]
Driver=tun
Name=*-vm
[Network]
Address=0.0.0.0/24
Address=::/64
DHCPServer=yes
IPForward=yes
IPMasquerade=yes
[DHCPServer]
#EmitDNS=no
DNS=192.168.178.1
Start der virtuellen Maschine
Für den Start der virtuellen Maschine nutze ich das Zsh-Skript start-vm, um gewisse Konfigurationen für den Aufruf zu treffen, den Kernel und die Initrd aus dem Dateisystem zu extrahieren und Qemu selbst damit aufzurufen.
Wenn der Benutzer des Skripts Mitglied in der Gruppe kvm ist, benötigt man für
den Aufruf von Qemu keine Root-Rechte. Allein für die Erstellung des
Netzwerkanschlusses sind Root-Rechte notwendig, die ich einfach mit
sudo und einem
entsprechenden Eintrag mit sudoedit /etc/sudoers.d/qemu-tap-up
erlange:
# used by qemu-tap-up
%kvm ALL= (root) NOPASSWD: /usr/bin/ip tuntap add dev *-vm mode tap user *
Alternativ lässt sich auch der TAP-Netzwerkanschluss von Systemd-Networkd mit folgender Konfiguration verwalten. Da das Gerät aber nicht ständig bei mir im System präsent seien soll, habe ich die Variante mit sudo gewählt.
[NetDev]
Description=Virtual network for Qemu Debian-VM
Kind=tap
Name=debian-vm
[Tap]
Group=kvm
Als sehr praktisch hat sich erwiesen, dass Qemu in der Oberfläche die die
serielle Schnittstelle integriert hat, die man über Strg+Alt+3
oder den
Menüpunkt Ansicht, serial0 öffnen kann. Zurück zum Konsole kommt man wieder
mit Strg+Alt+1
oder dem Menüpunkt Ansicht, virtio-vga. Für die serielle
Schnittstelle funktioniert nämlich das Kopieren von Text mit der Maus, das
Einfügen mit der mittleren Maustaste (bzw. linke und rechte Maustaste
gleichzeitig oder linke Maustaste auf dem Touchpad mit drei Fingern) und die
Historie über einen Reboot hinweg.
Durch Systemd wird auch automatisch eine Anmeldung für die serielle
Schnittstelle gestartet, wenn es feststellt, dass diese vorhanden ist. Damit
Linux mit der größer als üblichen seriellen Ausgabe zurechtkommt, muss man nach
dem Login setterm --resize
ausführen.
Datenaustausch mit dem Hostsystem
Plan 9 ist eigentlich ein anderes Betriebssystem aus den 1980er Jahren, das immer als Fortentwicklung der Unix-Prinzipien beschrieben wird, aber keine solch große Verbreitung wie Unix oder Linux gefunden hat. Dennoch gibt es im Kernel und in Qemu einen Treiber für das Dateisystem 9p, worüber Host- und Gastsystem sehr leicht Dateien austauschen können.
Für Qemu bedarf es nur der Option -virtfs
local,mount_tag=share,path=…,security_model=none
mit dem entsprechenden
Verzeichnis im Hostsystem und im Gastsystem kann man mit dem Eintrag share /mnt
9p noauto,trans=virtio,version=9p2000.L 0 0
in /etc/fstab die Gegenseite
anbinden.
Irgendwie kann man auch noch für die grafische Oberfläche die Zwischenablage beider Systeme verbinden, aber das ist mir noch gelungen. Hinweise werden gern entgegen genommen. 🙂
Automatische Fehlersuche
Für meine Fehlersuche im Kernel bedarf es natürlich der Kernelquellen und entsprechender Programme und Bibliotheken für den Bau. Außerdem ist das Programm ccache sehr nützlich, wenn man mit git-bisect durch die Versionen springt, um sich Kompilierarbeit zu ersparen1.
-
Am Ende kam
ccache -s
auf eine Trefferquote von 10 %. ↩
Weiterhin war für meinen Fall das Paket cifs-utils notwendig, um die Freigabe einbinden zu können, und nach einigem Probieren hat sich herausgestellt, dass die Version 10 von Gcc alte Kernelversionen nicht bauen kann oder diese Versionen mit der entsprechenden Anpassung (per cherry-pick) mit der Meldung »stack-protector: Kernel stack is corrupted in: start_secondary« abstürzen. Deshalb habe ich noch die Version 9 installiert.
Der Wechsel des Kernels beim Neustart lässt sich sicher auch irgendwie mit dem Aufruf von Qemu lösen1, aber es gibt kexec, womit der Kernel selbst einen neuen Kernel laden kann. Der Anfang der Suche nach dem fehlerhaften Commit war recht leicht:
-
Eine bestimmte Version lässt sich mit dem Aufruf
version=-5.4.0 ./start-vm
starten. ↩
# apt install --no-install-recommends build-essential bc bison flex lz4 libssl-dev libelf-dev \
git ca-certificates ccache cifs-utils gcc-9 kexec-tools
# git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
# cd linux
# git bisect start
# git reset --hard v5.7
# ~/run compile && ~/run restart
# ~/run test; echo $?
32
# cd linux
# git bisect bad
# git reset --hard v4.19
# ~/run compile && ~/run restart
Zentral für die ganze Arbeit ist das folgende Skript run, bei dem die Teile auch einzeln aufgerufen werden können, um sie zum Beispiel am Anfang zu verwenden:
- test führt die Befehle aus, um zu entscheiden, ob die laufende Kernelversion gut oder schlecht ist. Während der Arbeit hat sich gezeigt, dass bei einigen Kernelversionen der Aufruf von md5sum blockiert ist, weshalb der Einzeiler zuvor nach 4 Sekunden Pause den Prozess beendet.
- config: beim Hin- und Herspringen zwischen den Versionen habe ich in der Vergangenheit erlebt, dass die Konfiguration kaputt geht. Deshalb soll sie immer wieder von der Debian-Konfiguration abgeleitet werden. Dabei muss aber die Einstellung für die Trusted-Keys gelöscht und die Erstellung der indivudellen Verion aktiviert werden, damit die Zwischenversionen unterscheidbar sind.
- compile übersetzt und installiert den Kernel.
- restart bootet mit dem neuen Kernel oder die Version, die als Paramter angegeben wurde.
#!/bin/sh
set -e
do_test() {
mount -t cifs -o user=public,password=… //fritz.box/fritzi /mnt
(sleep 4 && pid=$(pidof -sz md5sum) && kill $pid) &
md5sum /mnt/WD_blau/Public/Jörg-Backup/Archiv/keys/63b0d55eb795822facba850363d5148ce657db559552ef3a5cdf6623d110bb98
}
config() {
cp /boot/config-5.7.0-2-amd64 .config
sed -i '/CONFIG_SYSTEM_TRUSTED_KEYS=/s/=.*/=""/;
/CONFIG_LOCALVERSION_AUTO/cCONFIG_LOCALVERSION_AUTO=y' .config
make oldconfig </dev/null
}
compile() {
make -j$(nproc) "$@" all
make modules_install install
}
restart() {
ver=${1:-$(make -s kernelrelease)}
kexec -l /boot/vmlinuz-$ver --initrd=/boot/initrd.img-$ver --reuse-cmdline
systemctl kexec
}
cd ~/linux
PATH=/usr/lib/ccache:"$PATH"
export CCACHE_CC=/usr/bin/gcc-9
case "$1" in
test)
do_test
;;
compile|restart|config)
"$@"
;;
all)
if [ "$(make -s kernelrelease)" != $(uname -r) ]
then
echo "Kernel version doesn't match:" \
"$(make -s kernelrelease) != $(uname -r)" >&2
exit 1
fi
rev=$(git rev-parse HEAD)
if test "$rev" == "$(git rev-parse refs/bisect/bad)"
then
echo
echo "We are done"
exit
fi
if do_test
then
echo git bisect good
git bisect good
else
echo git bisect bad
git bisect bad
fi
if test "$rev" == "$(git rev-parse HEAD)"
then
exit
fi
config
compile
restart
;;
*)
echo 'usage: run test|config|compile|restart|all' >&2
exit 1
esac
Nach jedem Start müssen die Einzelschritte ausgeführt und entsprechend auf die
Ergebnisse reagiert werden. Der komplette Ablauf vom Test über den Bau bis zum
Neustart lässt sich aber vollständig automatisieren, sodass nach dem Login der
Aufruf run all
genügt.
Automatische Anmeldung
Der letzte händische Schritt des Aufrufs von run lässt sich auch noch mit der
automatischen Anmeldung lösen.
Mit systemctl edit getty@tty1.service
kann man festlegen, dass der Benutzer
root sofort auf der ersten Konsole angemeldet wird.
[Service]
ExecStart=
ExecStart=/usr/sbin/agetty -a root --noclear %I $TERM
Restart=on-success
Mit dem folgenden Eintrag in ~/.profile führt die Bash dann auch run aus und die Suche läuft vollautomatisch durch, wenn es nicht zu unerwarteten Fehlern kommt.
case "$(tty)" in
/dev/tty1)
~/run
;;
/dev/ttyS0)
setterm --resize
;;
esac
Als eine kleine Erweiterung wäre noch möglich, die automatische Anmeldung für
die serielle Schnittstelle einzurichten und beim Aufruf von Qemu die Ausgaben
über diese Schnittstelle in eine Datei schreiben zu lassen. Somit bekäme man ein
Protokoll des Ablaufs. Die einzelnen Schritte bei git-bisect lassen sich aber
jederzeit mit git bisect log
nachvollziehen.