4.4 Linux syscall-konvenció AArch64-en

4.4.1 Motiváció: a rendszerhívás nem közönséges függvényhívás

Egy fájl olvasásához a programnak olyan szolgáltatást kell kérnie, amelyhez önmagában néhány gépi utasítás nem elegendő: a Linux ellenőrzi az elérési utat, a jogosultságokat és a célpuffer hozzáférhetőségét. C-ben ezt többnyire könyvtári függvények mögé rejtjük. Assemblyben nekünk kell összeállítanunk a kérést, és értelmeznünk a választ.

A rendszerhívási konvenció (system call convention) meghatározza, hogyan adja át az alkalmazás a szolgáltatás azonosítóját és argumentumait, illetve hogyan kapja vissza az eredményt. Ez az operációs rendszer alkalmazásbináris interfészének (application binary interface, ABI) része, nem pusztán az utasításkészlet tulajdonsága.

Az első assembly program rendszerhívási mintáját most fájlmegnyitással, olvasással és hibakezeléssel egészítjük ki.

Hatókör: Linux/AArch64, A64 ISA, ARMv8-A alapprofil, little-endian környezet. Az alfejezet valamennyi assembly utasítása és regisztere ebben a kontextusban értendő. A direktívák GNU as-direktívák, nem processzorutasítások. A labor egyszálú, libc nélküli programot használ.

4.4.2 A Linux/AArch64 regiszterszerződés

Szerep Linux/AArch64, A64 regiszter vagy utasítás
Rendszerhívásszám X8
Első argumentum X0
Második argumentum X1
Harmadik argumentum X2
Negyedik argumentum X3
Ötödik argumentum X4
Hatodik argumentum X5
A kérés indítása SVC #0
Nyers visszatérési érték X0

Az argumentumok sorrendjét az adott rendszerhívás kernelinterfésze határozza meg. Nem kell minden alkalommal mind a hat argumentumregisztert feltölteni: például a close csak az elsőt használja.

Az A64 SVC utasítás 16 bites, előjel nélküli közvetlen operandust (immediate) kódol. A Linux/AArch64 szokásos hívássorozatában ez 0; nem ide kerül a rendszerhívásszám, hanem az A64 X8 regiszterbe. Az A64 SVC szinkron kivételt vált ki; natív Linuxon ennek nyomán a kernel kezeli a kérést. A qemu-aarch64 user-mode emuláció ezt a felületet emulálja, nem teljes vendégkernelt futtat.

A mutatókat és a 64 bites méreteket az A64 Xn regiszterekben adjuk át. Egy cím A64 Wn nézetbe írása elveszítené a felső 32 bitet. Az egész argumentumok értelmezési szélességét ugyanakkor a rendszerhívás típusszerződése szabja meg: a fájlleíró például int, az olvasási méret size_t.

Fontos különbség a C-hez képest: az A64 X0 először argumentumot, visszatéréskor már eredményt tartalmaz. Ha az eredeti argumentumra később is szükségünk lesz, előbb meg kell őriznünk.

4.4.3 Siker, negatív hibakód és az errno

A közvetlen Linux-rendszerhívás nem állítja be a C-könyvtár errno változóját. Hiba esetén az A64 X0 a hibaszám negáltját, azaz -errno értéket tartalmazza. A szokásos libc-burkolófüggvény ebből állít elő -1 visszatérési értéket és pozitív errno hibaszámot.

Művelet és állapot A64 X0, előjeles olvasat Jelentés
read sikeres 12 12 bájt beolvasva
read fájlvégen, pozitív kért mérettel 0 Nincs több adat
openat hibás, hiányzó útvonal −2 ENOENT
read hibás fájlleíróval −9 EBADF
close sikeres 0 A hívás sikerült

A fájlleíró (file descriptor) kis, nemnegatív egész azonosító. Sikeres megnyitáskor akár 0 is lehet; nem szabad feltételezni, hogy mindig 3.

A nyers Linux-hibatartomány előjelesen −4095 és −1 közötti. Ugyanezek az értékek előjel nélkül a 64 bites tartomány legfelső 4095 értékét jelentik.

Az alábbi ellenőrzés ezt a pontos tartományt ismeri fel:

// A64 ISA, Linux/AArch64: X0 a nyers rendszerhívási eredmény.
cmn X0, #4095
b.cs lab_error

Az A64 CMN itt összeadást végez az állapotjelzők beállításához, de az összeget nem tárolja. Az A64 B.CS akkor ágazik el, ha az átviteljelző (carry flag) be van állítva.

Levezetés. Legyen a pozitív hibaszám EE, ahol 1≤E≤40951 \le E \le 4095. A −E érték 64 bites kettes komplemens bitmintája előjel nélkül 264−E2^{64}-E. Ehhez 4095-öt adva:

(264−E)+4095≥264. (2^{64}-E)+4095 \ge 2^{64}.

Ezért átvitel keletkezik a 64 bites eredményen túl. A változók itt dimenzió nélküli egész értékek. A hibatartományon kívüli bitmintáknál nincs ilyen átvitel.

Az A64 CMN ebben az immediate-változatban 12 bites, előjel nélküli mezőt használ, opcionális 12 bites balra tolással; a 4095 eltolás nélkül éppen kódolható.

A most használt hívásoknál egy előjeles „kisebb mint nulla” teszt is megfelelő lenne, mert sikeres eredményük nemnegatív. A pontos tartományteszt azonban általánosabb. A visszatérés utáni állapotjelzőkből önmagukban ne következtess hibára: a Linux/AArch64 hibajelzése az A64 X0 értéke. Az állapotjelzőket mi képezzük belőle. Részletesebb hátterük az NZCV állapotjelzőkről szóló alfejezetben következik.

4.4.4 Gyakori hívások és a referencia helyes használata

Linux/AArch64 hívás Szám az A64 X8-ban Argumentumok az A64 regiszterekben Sikeres eredmény
openat 56 X0: könyvtárleíró, X1: útvonal, X2: jelzők, X3: mód Fájlleíró
close 57 X0: fájlleíró 0
read 63 X0: fájlleíró, X1: puffer, X2: méret Beolvasott bájtok
write 64 X0: fájlleíró, X1: puffer, X2: méret Kiírt bájtok
exit 93 X0: kilépési állapot Nem tér vissza

A C-ben ismert open() nem jelenti azt, hogy minden architektúrán létezik azonos nevű nyers rendszerhívás. A Linux/AArch64 alapprofilban fájlt az openat hívással nyitunk meg; nincs külön hagyományos open rendszerhívásszám.

Az openat első argumentumaként az AT_FDCWD, vagyis −100 azt jelenti, hogy a relatív útvonalat az aktuális munkakönyvtárhoz képest kell értelmezni. A laborban az O_RDONLY értéke 0. Nem kérünk fájllétrehozást, ezért a módargumentum nem játszik szerepet; nullát adunk át.

A man7.org referenciáit három lépésben használd:

  1. A syscall(2) architektúratáblázataiban keresd az arm64 sort: ebből ellenőrizhető a belépési utasítás és a regiszterkiosztás.
  2. Az adott hívás kézikönyvéből ellenőrizd az argumentumokat, a részleges teljesítés lehetőségét és a hibákat. Figyeld a libc és a kernel eltéréseit is.
  3. A számot a célarchitektúra Linux-fejléceiből ellenőrizd, ne az x86_64 host számaiból.

A cross Linux-fejlécek szokásos Ubuntu-elrendezésén:

# Referenciaellenőrzés a qemu-user/AArch64 célhoz; nem build és nem futtatás.
grep -E '^#define __NR_(openat|close|read|write|exit)[[:space:]]' \
    /usr/aarch64-linux-gnu/include/asm-generic/unistd.h

A várt számok a fenti táblázat értékei. Ha a fájl hiányzik, előbb a célarchitektúra fejléceinek telepítését és helyét ellenőrizd; ne helyettesítsd automatikusan a host fejlécével.

4.4.5 Kidolgozott példa: egyszeri olvasás, ellenőrzött lezárás

Munkakönyvtár: ~/arm-labor/a64/a013/ — az alfejezet minden laborfájlja ide kerül, és minden parancs innen futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le).

mkdir -p ~/arm-labor/a64/a013
cd ~/arm-labor/a64/a013

A program megnyitja a lab-input.txt fájlt, legfeljebb 64 bájtot olvas, majd a kapott adatot a szabványos kimenetre írja. Ez egyszeri olvasási demonstráció, nem teljes fájlmásoló: sem a fájlvégig olvasást, sem a megszakított vagy részleges műveletek újrapróbálását nem valósítja meg.

A fájlleírót az A64 X19, a kilépési állapotot az A64 W20, az olvasott hosszt az A64 X21 őrzi. Ezeket a Linux/AArch64 rendszerhívások megőrzik; itt nem közönséges függvényhívás történik.

Mentsd a következőt lab-syscall.s néven:

// A64 ISA, ARMv8-A, Linux/AArch64, little-endian.
// GNU as direktívák; libc nélküli, egyszálú program.

.equ LAB_CAPACITY, 64

.section .text
.global _start
.type _start, %function
_start:
    // openat(AT_FDCWD, "lab-input.txt", O_RDONLY, 0)
    movn X0, #99                  // ~99 = -100, 64 biten
    ldr  X1, =lab_path
    movz X2, #0
    movz X3, #0
    movz X8, #56
    svc  #0

lab_after_open:
    cmn  X0, #4095
    b.cs .Lopen_error
    mov  X19, X0
    movz W20, #0

    // read(fd, buffer, 64)
    mov  X0, X19
    ldr  X1, =lab_buffer
    movz X2, #LAB_CAPACITY
    movz X8, #63
    svc  #0

lab_after_read:
    cmn  X0, #4095
    b.cs .Lread_error
    cbz  X0, .Lclose
    mov  X21, X0

    // write(1, buffer, ténylegesen_beolvasott_hossz)
    movz X0, #1
    ldr  X1, =lab_buffer
    mov  X2, X21
    movz X8, #64
    svc  #0

lab_after_write:
    cmn  X0, #4095
    b.cs .Lwrite_error
    cmp  X0, X21
    b.ne .Lwrite_error            // Részleges írás is sikertelen laborfutás.
    b    .Lclose

.Lread_error:
    movz W20, #2
    b    .Lclose

.Lwrite_error:
    movz W20, #3

.Lclose:
    mov  X0, X19
    movz X8, #57
    svc  #0
    cmn  X0, #4095
    b.cc .Lexit
    cbnz W20, .Lexit              // Az első hiba maradjon meg.
    movz W20, #4
    b    .Lexit

.Lopen_error:
    movz W20, #1                  // Nincs sikeresen megnyitott fd.

.Lexit:
    mov  W0, W20
    movz X8, #93
    svc  #0

.Lstop:
    b .Lstop                     // Az exit rendesen nem tér vissza.

.size _start, .-_start
.ltorg

.section .rodata
lab_path:
    .asciz "lab-input.txt"

.section .bss
.balign 8
lab_buffer:
    .skip LAB_CAPACITY

.section .note.GNU-stack,"",%progbits

Az A64 MOVZ és MOVN 16 bites immediate-mezőt használ. A64 Xn célregiszternél az engedélyezett eltolás 0, 16, 32 vagy 48, A64 Wn esetén 0 vagy 16; itt mindenütt nulla. Az A64 MOVN a kialakított értéket a célregiszter teljes szélességén invertálja, ezért kapunk −100 értéket.

Az A64 CBZ és CBNZ közvetlenül nullára, illetve nemnullára vizsgálja a regisztert. Az A64 CMP után a B.NE eltérő értékeknél ágazik el. Az A64 B.CC az átvitel hiányát vizsgálja, tehát a használt hibateszt ellenkező ágát választja.

A címkés A64 B 26 bites, a használt feltételes elágazások 19 bites előjeles, néggyel skálázott relatív eltolást kódolnak. Ebben a rövid programban minden cél elérhető. Az A64 LDR Xn, =cím GNU assembler-pszeudoutasítás; a közeli, .ltorg által elhelyezett literálkészletből tölti be a címet.

4.4.5.1 Összeállítás, linkelés és futtatás

# Target: qemu-user, Linux/AArch64; host: Ubuntu 24.04 x86_64.
printf 'ARM syscall\n' > lab-input.txt

aarch64-linux-gnu-as -march=armv8-a -g \
    -o lab-syscall.o lab-syscall.s
aarch64-linux-gnu-ld -e _start \
    -o lab-syscall lab-syscall.o

qemu-aarch64 ./lab-syscall > lab-output.txt
lab_status=$?

printf 'Kilépési állapot: %d\n' "$lab_status"
cat lab-output.txt
cmp lab-input.txt lab-output.txt
printf 'Összehasonlítás állapota: %d\n' "$?"

Várt, nem ténylegesen mért eredmény: a program kilépési állapota 0, a kimenet ARM syscall és egy soremelés, az összehasonlítás állapota szintén 0.

Ellenőrzési pont Várt állapot, Linux/AArch64, A64
lab_after_open X0 nemnegatív fájlleíró
lab_after_read X0 = 12; a puffer első 12 bájtja a fájl tartalma
lab_after_write X0 = 12
Kilépés előtt W20 = 0

A fájlolvasás nem fűz automatikusan nulla bájtot az adatok végére. A program ezért hosszal dolgozik, nem C-karakterláncként kezeli a puffert.

4.4.6 Hibák és ellenpróbák

A kilépési állapotok jelentése: 1 megnyitási, 2 olvasási, 3 írási vagy részleges írási, 4 lezárási hiba. Ezek a program saját állapotkódjai, nem Linux-hibaszámok.

Kontrollált hiányzófájl-próba:

# Target: qemu-user, Linux/AArch64; a már felépített programmal.
mv lab-input.txt lab-input.saved
qemu-aarch64 ./lab-syscall
lab_status=$?
mv lab-input.saved lab-input.txt
printf 'Várt állapot: 1; kapott: %d\n' "$lab_status"

A várt nyers eredmény a megnyitás után −ENOENT, vagyis −2. A program nem próbálja ezt fájlleíróként lezárni.

Hibakereséshez az egyik terminálban:

# Target: qemu-user, Linux/AArch64; GDB-kapcsolatra vár.
qemu-aarch64 -g 1234 ./lab-syscall

A másikban, helyreállított, 12 bájtos bemenettel:

# Debugger: AArch64 cél a qemu-user GDB-felületén.
gdb-multiarch -q ./lab-syscall \
    -ex 'target remote localhost:1234' \
    -ex 'break lab_after_read' \
    -ex 'continue' \
    -ex 'p/d (long long)$x0' \
    -ex 'x/12bx &lab_buffer'

Várt érték: az A64 X0 előjelesen 12. A vizsgált memóriát szimbólum azonosítja, nem kézzel választott cím. A futtatás a GDB continue parancsával folytatható.

Tünet Hipotézis Adatgyűjtés és egyetlen javítás
Óriási pozitív „bájtszám” Negatív hiba előjel nélküli olvasata A64 X0 vizsgálata előjelesen; hibateszt a felhasználás elé
A kimenet a fájlon túli bájtokat is tartalmaz A kért, nem a tényleges olvasási hossz került az íráshoz A64 X2 vizsgálata a write előtt; hossz átvétele az olvasási eredményből
Sikeres megnyitás után hibás olvasás Elveszett vagy felülírt fájlleíró A64 X19 és az olvasás előtti X0 összevetése
Eltűnik az olvasási hiba A lezárás felülírta az eredményt Hibautak követése; külön programállapot megőrzése

A read rövidebb eredménye önmagában nem hiba. A write is teljesíthet részlegesen; ezt a labor felismeri, de nem folytatja ciklussal. Linuxon a hibás close vak újrapróbálása sem helyes általános recept: a leíró rendszerint már felszabadult, ezért újrapróbálással más erőforrást is érinthetnénk.

4.4.7 Feladatok

  1. Magyarázó (alapszint — cél: megértés-ellenőrzés): miért nem az A64 SVC operandusa a rendszerhívásszám? Mi a különbség a nyers −9 eredmény és a libc −1 eredménye között?

  2. Implementációs (alapszint — cél: rutinépítés): módosítsd a lab-syscall.s pufferméretét és olvasási korlátját 8 bájtra. A fenti parancsokkal építsd újra és futtasd. Mi legyen a kimenet és a kilépési állapot?

  3. Hibakeresési (középszint — cél: rutinépítés): az olvasás előtt az A64 mov X0, X19 utasítást cseréld movn X0, #0 utasításra. Újraépítés után állj meg a lab_after_read címkén. Jósolj nyers visszatérési értéket és programkilépési állapotot, majd ellenőrizd őket.

4.4.8 Megoldások

1. megoldás — Segítség. Gondold végig, melyik regiszter szerepe rögzített a kérés kiválasztására, és hogy a libc köztes rétege mit tesz a nyers kernel-hibakóddal, mielőtt a C programhoz visszaér.

1. megoldás — Teljes megoldás. A Linux/AArch64 szerződés az A64 X8 regisztert rendeli a szolgáltatás azonosításához; az A64 SVC #0 a kérésindítás mintája. A nyers −9 közvetlenül az EBADF hibát jelenti. A libc ugyanebből általában −1 visszatérési értéket és errno = EBADF állapotot készít. Közvetlen assembly hívásnál ez az átalakítás nem történik meg.

2. megoldás — Segítség. Keresd meg, hány helyen szerepel a pufferméret-konstans a forrásban — elég-e egyetlen definíciót módosítani, vagy több helyen van hardkódolva az érték?

2. megoldás — Teljes megoldás. Elég a LAB_CAPACITY értékét 8-ra változtatni, mert a foglalás és az olvasási kérés is ezt használja. A várt kimenet ARM sysc, soremelés nélkül; a kilépési állapot 0. A program csak egy olvasást végez. A teljes eredeti fájllal végzett cmp ezért eltérést jelez, ami most nem programhiba.

3. megoldás — Segítség. Milyen 64 bites értéket ad a MOVN X0, #0, és fájlleíróként értelmezve ez érvényes-e? Ebből már következik, milyen hibát ad vissza a read.

3. megoldás — Teljes megoldás. Az A64 MOVN X0, #0 minden bitet egyre állít, vagyis előjelesen −1 értéket ad. Ez érvénytelen fájlleíró. A várt olvasási eredmény −9, azaz −EBADF; a hibateszt elágazik, és a program 2 állapottal lép ki. Az A64 X19 továbbra is a ténylegesen megnyitott leírót tartalmazza, ezért a lezárási út azt tudja felszabadítani. Ellenőrzés után állítsd vissza az eredeti utasítást.

4.4.9 Rövid összefoglaló

A Linux/AArch64 rendszerhívás helyességét három együtt teljesülő feltétel adja: megfelelő szolgáltatásszám, megfelelő argumentumok és helyesen értelmezett visszatérés. Az A64 X8 választ szolgáltatást, az A64 X0–X5 argumentumot közvetít, az eredményt pedig az A64 X0 adja. A nyers hibát még az eredmény felülírása előtt kell felismerni. A fájlkezelési példa külön kezeli a megnyitást, az olvasott hosszt és az erőforrás lezárását. A sikeres olvasás nem feltétlenül teljes olvasás, a sikeres írás pedig nem feltétlenül teljes írás. A labor kilépési kódjai nem azonosak a kernel hibaszámaival.

4.4.10 Források és ellenőrzési hiányok

Az alábbi hivatkozások dokumentációs kiindulópontok; ebben a munkafolyamatban nem történt webes megnyitás vagy laborfuttatás.

Nyitott ellenőrzési igény: a telepített Ubuntu-csomagok pontos verziója, a cross fejlécek helye, valamint a felsorolt várt eredmények tényleges laborvalidálása. A qemu-aarch64 futtatás a host fájlrendszerét használja; nem elkülönített vendéggép és nem biztonsági homokozó.