ABI jądra Linuxa — narzędzia i diagnostyka
Poprzedni artykuł wyjaśnił, czym są trzy kontrakty ABI jądra Linuxa.
Ten wyjaśnia, jak je zobaczyć, zbadać i zdiagnozować — w praktyce, na działającym systemie.
Zanim zaczniesz
Każde z trzech ABI ma swój własny zestaw narzędzi. Artykuł jest podzielony według tej samej struktury co poprzedni:
ABI #1 — Userspace ABI → strace, ausyscall, /proc
ABI #2 — kABI → modinfo, nm, pahole
ABI #3 — Dystrybucyjne → dpkg, dkms, diff Modules.symvers
Dla każdego narzędzia: co robi, kluczowe flagi, przykład wyjścia, praktyczny scenariusz.
ABI #1 — Userspace ABI: co wywołuje twój program?
strace — podgląd syscalli w czasie rzeczywistym
strace przechwytuje wywołania systemowe procesu przez mechanizm jądra ptrace. Jest bezpośrednim oknem na Userspace ABI — widać dokładnie, jakich numerów syscalli używa program, z jakimi argumentami i co dostaje w odpowiedzi.
Instalacja:
# Debian/Ubuntu
apt install strace
# Arch/CachyOS
pacman -S strace
Kluczowe flagi:
| Flaga | Opis |
|---|---|
-e trace=syscall1,syscall2 |
Filtruj tylko wybrane syscalle |
-e trace=%file |
Wszystkie syscalle operujące na plikach |
-e trace=%network |
Wszystkie syscalle sieciowe |
-c |
Statystyki zbiorcze zamiast live output |
-T |
Czas wykonania każdego syscalla |
-f |
Śledź procesy potomne (fork/exec) |
-p PID |
Dołącz do działającego procesu |
-o plik |
Zapisz output do pliku |
Przykład 1 — diagnoza Permission denied:
$ strace -e trace=%file ls /root 2>&1 | head -20
execve("/usr/bin/ls", ["ls", "/root"], 0x... /* 23 vars */) = 0
openat(AT_FDCWD, "/root", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = -1 EACCES (Permission denied)
write(2, "ls: cannot open directory '/root'", 33) = 33
Widoczna sekwencja: openat → EACCES. Nie ma żadnych wątpliwości, który syscall zwrócił błąd i z jakim kodem. Bez strace wiadomo tylko, że coś się nie powiodło.
Przykład 2 — statystyki syscalli procesu:
$ strace -c -f curl -s https://example.com > /dev/null
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
38.21 0.021445 14 1500 read
22.14 0.012431 10 1200 write
18.05 0.010132 42 240 12 openat
9.11 0.005118 8 620 mmap
...
------ ----------- ----------- --------- --------- ----------------
100.00 0.056126 5120 18 total
Przydatne przy optymalizacji — widać, które syscalle dominują i ile czasu zajmują.
Przykład 3 — weryfikacja ABI przy migracji:
# Sprawdź, czy program używa przestarzałego syscalla 'open' (nr 2)
# zamiast nowszego 'openat' (nr 257) — istotne przy seccomp whitelisting
$ strace -e trace=open,openat ./moj_program 2>&1 | grep -c '^open('
0
$ strace -e trace=open,openat ./moj_program 2>&1 | grep -c '^openat('
47
ausyscall — mapa syscalli dla danej architektury
ausyscall tłumaczy nazwy syscalli na numery i odwrotnie, z pełną świadomością architektury procesora. To narzędzie z pakietu audit, niezastąpione przy pisaniu reguł seccomp, auditd oraz przy analizie logów z systemów heterogenicznych.
Instalacja:
# Debian/Ubuntu
apt install auditd
# Arch/CachyOS
pacman -S audit
Kluczowe użycia:
# Numer syscalla po nazwie (bieżąca architektura)
$ ausyscall openat
257
# Nazwa po numerze
$ ausyscall 1
write
# Wyraźna architektura — kluczowe na systemach biarch (x86_64 z 32-bit compat)
$ ausyscall x86_64 openat
257
$ ausyscall i386 openat
295
# Zrzut całej tabeli syscalli dla architektury
$ ausyscall x86_64 --dump | head -10
0 read
1 write
2 open
3 close
4 stat
5 fstat
6 lstat
7 poll
8 lseek
9 mmap
# Wyszukiwanie substring (domyślne)
$ ausyscall chown
chown 92
lchown 94
fchown 93
fchownat 260
# Szukanie dokładne
$ ausyscall --exact chown
chown 92
Praktyczny scenariusz — reguły auditd na systemie biarch:
Na x86_64 z obsługą 32-bitowych binariów ten sam syscall ma dwa różne numery. Bez weryfikacji reguła auditd może pominąć połowę zdarzeń:
$ ausyscall i386 open
5
$ ausyscall x86_64 open
2
# Prawidłowe reguły auditd — muszą pokryć obie architektury
-a always,exit -F arch=b32 -S open -F exit=-EPERM -k fail-open
-a always,exit -F arch=b64 -S open -F exit=-EPERM -k fail-open
/proc/PID/syscall — co robi proces w tej chwili?
Plik /proc/PID/syscall zawiera numer aktualnie wykonanego syscalla oraz wartości rejestrów. Dostępny bez żadnych dodatkowych narzędzi.
# Który syscall blokuje proces sshd?
$ cat /proc/$(pidof sshd)/syscall
202 0x7f3a1c002840 0x0 0x0 0x0 0x0 0x0 0x7ffd2b3e4290 0x7f3a1c44c270
# 202 to epoll_wait — sshd czeka na zdarzenia sieciowe
$ ausyscall 202
epoll_wait
Kolumny: numer_syscalla arg0 arg1 arg2 arg3 arg4 arg5 stack_pointer program_counter
# Szybki przegląd wszystkich procesów i ich aktualnych syscalli
$ for pid in /proc/[0-9]*/syscall; do
p=${pid%/syscall}; p=${p#/proc/}
sc=$(cat "$pid" 2>/dev/null | awk '{print $1}')
[ -n "$sc" ] && [ "$sc" != "running" ] && \
printf "PID %-6s %-20s %s\n" "$p" "$(ps -p $p -o comm= 2>/dev/null)" \
"$(ausyscall $sc 2>/dev/null || echo $sc)"
done
ABI #2 — kABI: czy moduł jest zgodny z jądrem?
modinfo — paszport modułu jądra
modinfo odczytuje metadane z pliku .ko bez jego ładowania. Kluczowe pole dla kABI to vermagic — ciąg znaków, który musi dokładnie zgadzać się z wartością w działającym jądrze.
# Pełne informacje o module
$ modinfo nvidia
filename: /lib/modules/6.12.0-1-cachyos/kernel/drivers/video/nvidia.ko
version: 560.35.03
license: NVIDIA
description: NVIDIA Linux x64 Display Driver Kernel Module
author: NVIDIA Corporation
srcversion: A1B2C3D4E5F6A7B8C9D0E1F
depends: drm
vermagic: 6.12.0-1-cachyos SMP preempt mod_unload
Pole vermagic — anatomia:
6.12.0-1-cachyos SMP preempt mod_unload
^ ^ ^ ^
wersja tryb scheduler możliwość
jądra SMP (opcja) odładowania
Jeśli którakolwiek część nie pasuje — insmod odmówi załadowania:
$ insmod nvidia.ko
insmod: ERROR: could not insert 'nvidia': Invalid module format
$ dmesg | tail -3
[12345.678] nvidia: disagrees about version of symbol module_layout
[12345.679] nvidia: version magic '6.11.0-1-cachyos SMP preempt mod_unload'
[12345.680] nvidia: should be '6.12.0-1-cachyos SMP preempt mod_unload'
Inne przydatne pola modinfo:
# Tylko vermagic
$ modinfo -F vermagic nvidia
6.12.0-1-cachyos SMP preempt mod_unload
# Tylko zależności
$ modinfo -F depends zfs
spl
# Informacje dla konkretnego jądra (nie bieżącego)
$ modinfo -k 6.11.0-1-cachyos -F vermagic nvidia
6.11.0-1-cachyos SMP preempt mod_unload
# Porównanie vermagic modułu z bieżącym jądrem
$ diff \
<(modinfo -F vermagic nvidia) \
<(uname -r | sed 's/$/ SMP preempt mod_unload/')
Skrypt diagnostyczny — sprawdź wszystkie załadowane moduły:
#!/bin/bash
# Wykryj moduły, których vermagic nie pasuje do bieżącego jądra
KERNEL=$(uname -r)
echo "Bieżące jądro: $KERNEL"
echo "---"
lsmod | awk 'NR>1 {print $1}' | while read mod; do
vm=$(modinfo -F vermagic "$mod" 2>/dev/null | awk '{print $1}')
if [ -n "$vm" ] && [ "$vm" != "$KERNEL" ]; then
echo "NIEZGODNY: $mod (vermagic: $vm)"
fi
done
nm -u i modprobe --dump-modversions — symbole i ich CRC
Każdy eksportowany symbol jądra ma CRC — hash opisujący jego sygnaturę. Moduł przechowuje listę oczekiwanych CRC. Jeśli jądro ma inny CRC dla danego symbolu, ładowanie jest blokowane (gdy CONFIG_MODVERSIONS=y).
nm -u — symbole wymagane od jądra:
# Jakich symboli jądra oczekuje moduł?
$ nm -u zfs.ko | head -15
U __alloc_pages
U alloc_workqueue
U blkdev_get_by_dev
U dentry_open
U filp_close
U kmalloc_caches
U printk
U schedule
U oznacza symbol niezdefiniowany w module — musi być dostarczony przez jądro. Jeśli jądro nie eksportuje któregoś z tych symboli (lub eksportuje z innym CRC), moduł się nie załaduje.
modprobe --dump-modversions — CRC symboli w module:
$ modprobe --dump-modversions /lib/modules/$(uname -r)/kernel/net/core/netfilter.ko | head -5
0x3b4e6f2a nf_register_net_hooks
0x1c8d4a91 nf_unregister_net_hooks
0x7f2e9b3c nf_hook_slow
0x4a1d8e05 netfilter_net_init
Porównanie CRC między dwoma wersjami jądra — praktyczny sposób oceny, czy kABI zostało złamane:
# Wyodrębnij symbole z Module.symvers obu wersji
$ diff \
<(awk '{print $2, $1}' /usr/src/linux-headers-6.11.0-1-cachyos/Module.symvers | sort) \
<(awk '{print $2, $1}' /usr/src/linux-headers-6.12.0-1-cachyos/Module.symvers | sort) \
| grep '^[<>]' | head -20
< kmalloc 0x1a2b3c4d
> kmalloc 0x9f8e7d6c ← CRC zmienione — kABI złamane dla tego symbolu
< sk_buff_head_init 0x5e6f7a8b
> sk_buff_head_init 0x5e6f7a8b (brak w diff = CRC bez zmian)
Jeśli diff zwraca wiersze z różnymi CRC dla tego samego symbolu — moduły skompilowane pod starszą wersję nie załadują się na nową.
pahole — układ struktur danych w jądrze
pahole (część pakietu dwarves) odczytuje informacje debugowe DWARF i BTF, pokazując rzeczywisty układ struktur danych w pamięci. To najgłębsze narzędzie diagnostyczne kABI — pozwala zobaczyć, dlaczego CRC symbolu się zmieniło.
Instalacja:
# Debian/Ubuntu
apt install dwarves
# Arch/CachyOS
pacman -S dwarves
Wymaganie: jądro skompilowane z CONFIG_DEBUG_INFO_BTF=y. Sprawdzenie:
$ ls /sys/kernel/btf/vmlinux && echo "BTF dostępne" || echo "BTF niedostępne"
BTF dostępne
Przykład 1 — układ struktury sk_buff (bufor sieciowy):
$ pahole -C sk_buff /sys/kernel/btf/vmlinux | head -40
struct sk_buff {
union {
struct {
struct sk_buff * next; /* 0 8 */
struct sk_buff * prev; /* 8 8 */
union {
struct net_device * dev; /* 16 8 */
long unsigned int dev_scratch; /* 16 8 */
}; /* 16 8 */
}; /* 0 24 */
...
};
/* --- cacheline 1 boundary (64 bytes) --- */
__u32 len; /* 64 4 */
__u32 data_len; /* 68 4 */
__u16 mac_len; /* 72 2 */
Widoczne: każde pole, jego offset w bajtach i rozmiar. Jeśli między wersjami jądra zmieni się offset len — moduł operujący na sk_buff z hardkodowanym offsetem przestanie działać poprawnie.
Przykład 2 — wykrycie zmiany ABI między wersjami:
# Na jądrze 6.11: pole 'mark' w sk_buff jest na offsecie 224
$ pahole -C sk_buff /sys/kernel/btf/vmlinux | grep ' mark'
__u32 mark; /* 224 4 */
# Na jądrze 6.12: nowe pole zostało wstawione wcześniej, 'mark' przesunął się
$ pahole -C sk_buff /path/to/vmlinux-6.12 | grep ' mark'
__u32 mark; /* 228 4 */
Przesunięcie o 4 bajty = zmiana kABI dla wszystkich modułów czytających to pole bezpośrednio. To właśnie wykrywa zmiana CRC w Module.symvers.
Przykład 3 — lista wszystkich struktur zawierających dane pole:
# Które struktury mają pole 'pid_t pid'?
$ pahole -C '' /sys/kernel/btf/vmlinux | grep -B5 'pid_t.*pid;' | grep '^struct'
struct task_struct {
struct signal_struct {
struct perf_event {
Przykład 4 — padding i wyrównanie (optymalizacja):
# Ile miejsca marnuje dana struktura na padding?
$ pahole --show_holes /sys/kernel/btf/vmlinux -C inode | tail -5
/* XXX 4 bytes hole, try to pack */
...
/* size: 608, cachelines: 10, members: 47 */
/* sum members: 584, holes: 1, sum holes: 4 */
/* padding: 20 */
ABI #3 — Dystrybucyjne ABI: pakiety, numery, DKMS
dpkg i apt-cache — odczyt numeru ABI z nazwy pakietu
Numer ABI dystrybucji jest zakodowany w nazwie pakietu jądra. Nie ma osobnego polecenia do jego odczytu — widać go bezpośrednio.
# Lista zainstalowanych pakietów jądra
$ dpkg -l 'linux-image-*' | grep '^ii'
ii linux-image-6.1.0-21-amd64 6.1.99-1 amd64 Linux 6.1 for 64-bit PCs
ii linux-image-6.1.0-22-amd64 6.1.106-3 amd64 Linux 6.1 for 64-bit PCs
# ^^
# numer ABI dystrybucji
Dwa pakiety obok siebie to nie błąd — to wynik inkrementacji ABI. Pakiet linux-image-6.1.0-21-amd64 i linux-image-6.1.0-22-amd64 to dla apt dwa różne pakiety, nie wersje tego samego.
# Sprawdź dostępne wersje w repozytorium
$ apt-cache policy linux-image-amd64
linux-image-amd64:
Installed: 6.1.99-1
Candidate: 6.1.106-3
Version table:
6.1.106-3 500
500 http://deb.debian.org/debian bookworm/main amd64 Packages
*** 6.1.99-1 100
100 /var/lib/dpkg/status
# Która wersja pakietu odpowiada któremu numerowi ABI?
$ apt-cache show linux-image-6.1.0-22-amd64 | grep -E 'Version|Provides'
Version: 6.1.106-3
Provides: linux-image, linux-image-6.1.0-22-amd64
dkms status — stan modułów dynamicznych
DKMS (Dynamic Kernel Module Support) automatycznie przebudowuje moduły zewnętrzne po każdej zmianie ABI. dkms status pokazuje, które moduły są gotowe dla którego jądra.
# Stan wszystkich modułów DKMS
$ dkms status
nvidia/560.35.03, 6.12.0-1-cachyos, x86_64: installed
zfs/2.2.6, 6.12.0-1-cachyos, x86_64: installed
nvidia/560.35.03, 6.11.0-1-cachyos, x86_64: installed
# Stan po aktualizacji jądra — nowe jądro, moduł jeszcze nieprzebudowany
$ dkms status
nvidia/560.35.03, 6.12.1-2-cachyos, x86_64: added ← dodany, ale nie zbudowany
nvidia/560.35.03, 6.12.0-1-cachyos, x86_64: installed
Ręczna przebudowa dla konkretnego jądra:
# Przebuduj wszystkie moduły DKMS dla bieżącego jądra
$ dkms autoinstall -k $(uname -r)
# Przebuduj konkretny moduł dla konkretnego jądra
$ dkms build nvidia/560.35.03 -k 6.12.1-2-cachyos
$ dkms install nvidia/560.35.03 -k 6.12.1-2-cachyos
# Diagnoza gdy build się nie udał
$ dkms build nvidia/560.35.03 -k 6.12.1-2-cachyos 2>&1 | tail -20
# Szczegółowy log budowania
$ cat /var/lib/dkms/nvidia/560.35.03/build/make.log | tail -30
Skrypt weryfikacyjny — które moduły DKMS nie są zbudowane dla bieżącego jądra:
#!/bin/bash
KERNEL=$(uname -r)
echo "Jądro: $KERNEL"
echo "Moduły DKMS bez instalacji dla tego jądra:"
dkms status | grep -v ", ${KERNEL}," | grep -v "^$" | while read line; do
echo " → $line"
done
diff Modules.symvers — które symbole zmieniły CRC?
Module.symvers to plik generowany podczas każdego buildu jądra. Zawiera listę wszystkich eksportowanych symboli z ich CRC. Dystrybucje porównują ten plik między buildami — zmiana CRC oznacza złamanie kABI i inkrementację numeru ABI.
Lokalizacja pliku:
# Dla zainstalowanych nagłówków jądra
ls /usr/src/linux-headers-$(uname -r)/Module.symvers
# Format pliku
$ head -5 /usr/src/linux-headers-$(uname -r)/Module.symvers
0x1a2b3c4d kmalloc vmlinux EXPORT_SYMBOL GPL and additional rights
0x5e6f7a8b schedule vmlinux EXPORT_SYMBOL_GPL GPL
0x9f1e2d3c printk vmlinux EXPORT_SYMBOL GPL and additional rights
Kolumny: CRC nazwa_symbolu skąd_pochodzi typ_eksportu licencja
Porównanie dwóch wersji jądra:
# Które symbole zmieniły CRC? (złamanie kABI)
diff \
<(awk '{print $2, $1}' /usr/src/linux-headers-6.11.0-1-cachyos/Module.symvers | sort) \
<(awk '{print $2, $1}' /usr/src/linux-headers-6.12.0-1-cachyos/Module.symvers | sort) \
| grep '^[<>]' \
| awk '{print $2}' \
| sort | uniq -d
# Wyświetla nazwy symboli, których CRC zmieniło się między wersjami
Które symbole zostały dodane lub usunięte:
# Symbole obecne w 6.11, nieobecne w 6.12 (usunięte lub przeniesione)
comm -23 \
<(awk '{print $2}' /usr/src/linux-headers-6.11.0-1-cachyos/Module.symvers | sort) \
<(awk '{print $2}' /usr/src/linux-headers-6.12.0-1-cachyos/Module.symvers | sort)
Bonus — bpftool btf dump
Nowsze narzędzie, coraz częściej używane przy diagnostyce kABI i modułów BPF. Wymaga jądra z CONFIG_DEBUG_INFO_BTF=y.
# Instalacja
apt install linux-tools-$(uname -r) # Debian/Ubuntu
pacman -S bpf # Arch/CachyOS
# Wyeksportuj pełny nagłówek C z typami jądra
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# Wynikowy plik ma ~300 000 linii — zawiera WSZYSTKIE typy danych jądra
# Pokaż konkretną strukturę
$ bpftool btf dump file /sys/kernel/btf/vmlinux format raw | grep -A5 'sk_buff'
# Lista wszystkich modułów z BTF
$ ls /sys/kernel/btf/
vmlinux nvidia zfs drm ...
bpftool btf dump i pahole operują na tych samych danych BTF, ale z różnych perspektyw — pahole skupia się na układzie struktury (offsety, padding), bpftool na całości systemu typów.
Podsumowanie — tabela narzędzi
| Narzędzie | ABI | Co bada | Pakiet |
|---|---|---|---|
strace |
Userspace | Syscalle live, argumenty, kody błędów | strace |
ausyscall |
Userspace | Mapowanie nazw/numerów syscalli per architektura | audit |
/proc/PID/syscall |
Userspace | Aktywny syscall procesu w tej chwili | wbudowane |
modinfo |
kABI | vermagic, zależności, srcversion |
kmod |
nm -u |
kABI | Symbole wymagane od jądra przez moduł | binutils |
modprobe --dump-modversions |
kABI | CRC symboli zakodowane w pliku .ko |
kmod |
pahole |
kABI | Układ struktur DWARF/BTF, offsety pól | dwarves |
dpkg -l / apt-cache |
Dystrybucyjne | Numer ABI w nazwie pakietu | dpkg / apt |
dkms status |
Dystrybucyjne | Stan modułów DKMS per wersja jądra | dkms |
diff Module.symvers |
Dystrybucyjne | Zmiany CRC symboli między buildami | ręczne |
bpftool btf dump |
kABI / BTF | Pełny system typów jądra, nagłówki C | bpf-tools |
Gdzie szukać więcej
- strace.io — oficjalna dokumentacja, lista obsługiwanych syscalli
- linux-audit/audit-userspace — źródła
ausyscall - aquasecurity/btfhub — how-to-use-pahole.md — praktyczny przewodnik po
paholei BTF - LWN.net — Specifying the kernel ABI — kontekst historyczny i motywacja
- LWN.net — BTF, Rust, and the kernel toolchain — aktualny stan BTF w jądrze (2024)
- Gateworks — kernel modules vermagic — szczegółowy opis pola
vermagic - Debian Wiki — DebianKernelABIChanges — mechanizm dystrybucyjnego ABI od środka
- Ubuntu Wiki — KernelTeam/BuildSystem/ABI — jak Ubuntu zarządza numerami ABI
// brak komentarzy