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.
| 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.
errnoA 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_errorAz 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
,
ahol
.
A −E érték 64 bites kettes komplemens bitmintája előjel
nélkül
.
Ehhez 4095-öt adva:
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.
| 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:
arm64 sort: ebből
ellenőrizhető a belépési utasítás és a regiszterkiosztás.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.hA 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.
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/a013A 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,"",%progbitsAz 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.
# 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.
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-syscallA 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.
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?
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?
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.
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.
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.
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ó.