Egy assembly függvény önmagában helyesen számolhat, mégis hibássá teheti az egész C programot. Elég, ha felülír egy olyan regisztert, amelyben a hívó egy később felhasználandó mutatót tartott. A fordító nem vizsgálja meg minden külső függvény belsejét: előre rögzített szabályokra támaszkodik.
Az eljáráshívási szabvány (Procedure Call Standard, PCS) meghatározza az argumentumok és eredmények átadását, valamint a hívásokon át megőrzendő gépállapotot. A könyv AArch64-részében ezt az AAPCS64 rögzíti. A tágabb bináris alkalmazásinterfész (Application Binary Interface, ABI) ezen túl például objektumformátumokat és adatábrázolást is szabályoz.
Egy függvény helyességéhez tehát két eredmény szükséges: a megfelelő visszatérési érték és a hívási szerződés betartása.
Az alfejezet az A64 ISA, AArch64 Linux, little-endian, LP64
környezetére vonatkozik: a mutató és a long 64 bites. Az
alap AAPCS64-et használjuk, SVE/SME-kiterjesztések nélkül. Előfeltétel
a veremkeret működésének ismerete. Az összetett
típusok részletes átadása későbbi téma.
A hívó által megőrzendő regiszter (caller-saved register) tartalmát a meghívott függvény szabadon felülírhatja. Ha a hívónak a régi érték a hívás után is kell, még a hívás előtt biztonságba kell helyeznie.
Ez nem jelenti minden ilyen regiszter feltétlen mentését. Csak a hívás után még szükséges, vagyis élő értékeket (live values) kell megóvni.
A hívott által megőrzendő regiszter (callee-saved register) belépéskori tartalmát a meghívott függvénynek visszatéréskor helyre kell állítania, ha módosította. A függvény közben használhatja munkaregiszterként; a szerződés a hívó számára látható végeredményt szabályozza.
Az alábbi táblázat minden regisztere A64/AArch64, I. rész kontextusú.
| Regiszter | Szerep az alap AAPCS64-ben | Mire számíthat a hívó? |
|---|---|---|
X0–X7, illetve
W0–W7 |
Egész és mutató típusú argumentumok; eredményátadás | A régi tartalom elveszhet |
X8 |
Közvetett eredményátadás helyének címe, amikor ez szükséges | Nem általános kilencedik argumentumregiszter; nem megőrzött |
X9–X15 |
Általános ideiglenes regiszterek | Caller-saved |
X16, X17 |
IP0, IP1: hívásközvetítő kód és linker
által is használható ideiglenes regiszterek |
Caller-saved; híváson át nem őriznek értéket |
X18 |
Platformregiszter, ha a platform igényli; egyébként ideiglenes regiszter lehet | Hordozható kézi assemblyben kerüljük |
X19–X28 |
Megőrzött általános célú regiszterek | Callee-saved |
X29 |
Keretmutató, FP |
Callee-saved |
X30 |
Visszatérési cím, LR |
Nem általános megőrzött munkaregiszter |
SP |
Veremmutató | Visszatéréskor a belépéskori értéket kell visszaállítani |
Az A64 X19–X29 regiszterek teljes 64 bites
értékét kell megőrizni. Az A64 W19 írása ezért nem „csak az
alsó felét érinti” a szerződés szempontjából: az írás az
X19 felső 32 bitjét nullázza, tehát a teljes megőrzendő
értéket megváltoztatja.
Az A64 NZCV állapotjelzők megőrzésére sem építhetünk
híváson át. Egy hívás előtti összehasonlítás eredményét nem biztonságos
a visszatérés után feltételes elágazással felhasználni.
Az A64 BL utasítás beírja a következő utasítás címét az
X30 regiszterbe, majd átadja a vezérlést. Nem menti
automatikusan a munkaregisztereket.
A caller-saved besorolás nem azt jelenti, hogy maga az A64
BL minden ilyen regisztert felülír. Azt jelenti, hogy a
meghívott kódnak nem kötelessége megőriznie őket. Egy adott függvény
véletlenül érintetlenül hagyhatja például az A64 X9-et, de
erre a hívó nem alapozhat.
Az újabb hívást végző függvénynek az eredeti visszatérési címét is
meg kell óvnia: a következő A64 BL felülírja az
X30-at. Az alapértelmezett A64 RET az
X30 címére tér vissza. A rendszeres mentési mintákat a prológus és epilógus tárgyalja.
Az A64 X29 keretmutatóként való használatát a
keretláncra vonatkozó platformszabályok is befolyásolják. Ha a függvény
módosítja, akkor is meg kell őriznie a belépéskori értékét. Az A64
SP-re a visszaállításon túl a korábban tanult 16 bájtos
igazítási követelmények is érvényesek.
Egyszerű egész és mutató típusú paramétereknél az első nyolc
argumentum sorrendben az A64 X0–X7
regisztercsaládba kerül. Az operandusszélességet a típus határozza
meg.
| C-függvényszignatúra | Argumentumok, A64/AArch64 | Eredmény, A64/AArch64 |
|---|---|---|
uint64_t f(uint64_t a, uint64_t b) |
X0 = a, X1 = b |
X0 |
uint32_t g(uint32_t a, uint32_t b) |
W0 = a, W1 = b |
W0 |
uint64_t sum(const uint32_t *p, uint64_t n) |
X0 = p, X1 = n |
X0 |
void *advance(void *p, uint32_t n) |
X0 = p, W1 = n |
X0 |
Az A64 W0 és X0 nem két külön regiszter:
ugyanannak a regiszternek 32, illetve 64 bites nézete. Egy
uint32_t argumentumhoz a releváns 32 bitet használjuk. A
szerződés által nem meghatározott felső bitekből nem következtethetünk
az értékre.
Hasonlóképpen, egy int32_t argumentum 64 bites előjeles
feldolgozásához tudatos előjelkiterjesztés szükséges; nem
feltételezhetjük, hogy az A64 X0 felső fele eleve
megfelelő.
Az A64 X0 egyszerre lehet argumentum- és
eredményregiszter. A hívó tehát nem várhatja, hogy az első argumentum
eredeti értéke a visszatérés után is ott legyen.
Ez a „paraméterenként egy regiszter” szabály nem általánosítható
változtatás nélkül 128 bites egészekre vagy struktúrákra. Az A64
X0–X1 többregiszteres eredményátadásban is
részt vehet; az A64 X8 közvetett eredményátadási szerepét a
struktúra-visszaadás szerződése bontja ki.
Kilenc, egyenként uint64_t típusú paraméternél:
X0–X7 regiszterekbe
kerül;A hívó foglalja le az argumentumterületet, a szükséges igazítással
együtt. Egyetlen további 8 bájtos argumentumhoz a legegyszerűbb
hívóoldali foglalás 16 bájt, mert az A64 SP-nek a hívási
határon igazítottnak kell maradnia.
Ha a hívott ezután 32 bájtos saját keretet foglal, a kilencedik
argumentum az új A64 SP-hez képest már 32 bájtos eltolásnál
érhető el. Ezért fontos megkülönböztetni a belépéskori és az aktuális
veremmutatót.
Az alap AAPCS64 külön regisztersorozatot használ a lebegőpontos és megfelelő SIMD-argumentumokhoz.
| Regisztercsoport, A64/AArch64 | Alapszabály |
|---|---|
V0–V7 |
Lebegőpontos/SIMD argumentumok és eredmények; caller-saved |
V8–V15 |
Csak az alsó 64 bit megőrzése kötelező |
V16–V31 |
Caller-saved |
Az A64 Sn az adott Vn alsó 32 bites, a
Dn az alsó 64 bites nézete. Például a
double mix(uint32_t n, double x, const void *p, float y)
argumentumai:
n: A64 W0;x: A64 D0;p: A64 X1;y: A64 S1;D0.A két argumentumregiszter-sorozat kiosztása itt egymástól független. Emiatt a kilencedik összesített paraméter még nem feltétlenül kerül verembe.
Fontos előzetes korlát: egy teljes, 128 bites A64 V8
érték nem tekinthető megőrzöttnek. Részletes használatuk a lebegőpontos regiszterkészletnél következik.
Munkakönyvtár: ~/arm-labor/a64/a028/ —
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/a028
cd ~/arm-labor/a64/a028A feladat a uint64_t lab_score(uint64_t a, uint64_t b)
megvalósítása. A függvény meghívja a
uint64_t lab_twice(uint64_t a) segédfüggvényt, majd
kiszámítja:
Itt
,
és az eredmény mértékegység nélküli, előjel nélküli 64 bites egész
érték. A moduloaritmetika a uint64_t C-szemantikájával
egyezik.
Két megőrzési módszert szemléltetünk:
b az A64 X19-be kerül. A segédfüggvény
köteles ezt megőrizni, viszont a lab_score köteles
helyreállítani saját hívójának eredeti X19 értékét.a másolatát az A64 X9 tartalmazza. Mivel
ez caller-saved, a lab_score a segédhívás idejére külön
veremhelyre menti.Ez oktatási választás, nem minimális utasításszámú optimalizálás.
Mentsük a következő teljes programot lab-aapcs.S néven.
A blokk minden utasítása és regisztere A64 ISA, minden
direktívája GNU assembler, AArch64 ELF kontextusú.
// A64 ISA, AArch64 Linux, little-endian.
// Direktívák: GNU assembler, AArch64 ELF.
.section .text
.p2align 2
.global _start
.type _start, %function
_start:
mov X19, #99
mov X20, SP
mov X29, XZR
mov X0, #7
mov X1, #11
bl lab_score
lab_return:
cmp X0, #32
b.ne .Lfail
cmp X19, #99
b.ne .Lfail
mov X10, SP
cmp X10, X20
b.ne .Lfail
cbnz X29, .Lfail
mov X0, #0
b .Lexit
.Lfail:
mov X0, #1
.Lexit:
mov X8, #93
svc #0
.size _start, .-_start
.p2align 2
.global lab_score
.type lab_score, %function
lab_score:
.cfi_startproc
stp X29, X30, [SP, #-32]!
.cfi_def_cfa_offset 32
.cfi_offset 29, -32
.cfi_offset 30, -24
mov X29, SP
str X19, [SP, #16]
.cfi_offset 19, -16
mov X19, X1
mov X9, X0
str X9, [SP, #24]
bl lab_twice
lab_after_call:
ldr X9, [SP, #24]
add X0, X0, X9
add X0, X0, X19
ldr X19, [SP, #16]
.cfi_restore 19
ldp X29, X30, [SP], #32
.cfi_restore 29
.cfi_restore 30
.cfi_def_cfa_offset 0
ret
.cfi_endproc
.size lab_score, .-lab_score
.p2align 2
.global lab_twice
.type lab_twice, %function
lab_twice:
.cfi_startproc
mov X9, #0x55
add X0, X0, X0
ret
.cfi_endproc
.size lab_twice, .-lab_twice
.section .note.GNU-stack,"",%progbitsA .cfi_* direktívák a hibakeresőnek írják le a keretet;
jelentésüket és a könyv CFI-szabályát a Prológus és
epilógus alfejezet tárgyalja. A lab_twice szándékosan
felülírja az A64 X9-et. Ez teljesen szabályos: így a labor
nem egy véletlenül együttműködő segédfüggvényre támaszkodik.
Az _start a Linux folyamatbelépési pontja, nem szokásos,
visszatérő C-függvény. A végén az A64 X8 = 93,
X0 = státusz, SVC #0 a korábban tanult Linux syscall-konvenciót használja, nem az AAPCS64
függvényhívási mechanizmusát.
A lab_score belsejében, a foglalás utáni A64
SP-hez képest:
| Eltolás | Méret | Tartalom |
|---|---|---|
| 0 bájt | 8 bájt | A hívó A64 X29 értéke |
| 8 bájt | 8 bájt | Mentett A64 X30, a visszatérési cím |
| 16 bájt | 8 bájt | A hívó A64 X19 értéke: 99 |
| 24 bájt | 8 bájt | A segédhíváson át megőrzött a: 7 |
A teljes keret 32 bájt, tehát megtartja a 16 bájtos igazítást.
A várt állapotváltozások:
| Megfigyelési pont | A64 X0 |
A64 X9 |
A64 X19 |
|---|---|---|---|
| A segédhívás előtt | 7 | 7 | 11 |
lab_after_call címkénél |
14 | 85 | 11 |
Az A64 X9 visszatöltése után |
14 | 7 | 11 |
| Az összeadások után | 32 | 7 | 11 |
lab_return címkénél |
32 | 7 | 99 |
Ezek levezetett, várt értékek, nem végrehajtással igazolt mérési eredmények.
A példa minden konstansa és eltolása belefér a megfelelő A64-mezőbe;
a korlátokat az A64 immediate-korlátok
összefoglalója sorolja fel. A keretkezelés szempontjából a
STP/LDP számít: eltolása 8 bájttal skálázott,
−512…504 bájt.
A GNU assembler AArch64 ELF .p2align 2 direktívája 4
bájtos kódigazítást kér; ez nem végrehajtott utasítás.
Target: qemu-user, AArch64 Linux. Ubuntu 24.04-es laborhoston, a telepített kereszt-eszközlánccal:
# Target: qemu-user, AArch64 Linux; nem igényel libc-t.
aarch64-linux-gnu-as -EL -march=armv8-a -g \
-o lab-aapcs.o lab-aapcs.S
aarch64-linux-gnu-ld -e _start \
-o lab-aapcs lab-aapcs.o
aarch64-linux-gnu-objdump -d lab-aapcs
qemu-aarch64 ./lab-aapcs
printf 'kilepesi_statusz=%d\n' "$?"Várt kimenet: kilepesi_statusz=0. Maga
a program nem ír szöveget. A vizsgált eredmény vagy megőrzési feltétel
hibájánál a várt státusz 1.
A teszt ellenőrzi az eredményt, az A64 X19,
X29 és SP visszaállítását. Ez célzott
ellenőrzés, nem minden lehetséges ABI-sértés teljes tesztje.
A köztes állapot vizsgálatához az első terminálban:
# Target: qemu-user, AArch64; várakozás a debuggerre.
qemu-aarch64 -g 1234 ./lab-aapcsA második terminálban:
# Debugger-target: az előző qemu-user folyamat.
gdb-multiarch -q ./lab-aapcs \
-ex 'target remote localhost:1234' \
-ex 'break lab_after_call' \
-ex 'continue' \
-ex 'info registers x0 x9 x19 x29 x30 sp' \
-ex 'x/4gx $sp'A GDB parancsában a kisbetűs nevek ugyanazokat az A64 regisztereket
jelölik. A töréspont az A64 X9 visszatöltése
előtt áll meg. A négy veremszó várt tartalma
sorrendben: 0, a lab_return szimbolikus címe, 99, 7.
Rögzített abszolút címet nem feltételezünk. A futás GDB-ben a
continue paranccsal fejezhető be.
Rossz eredmény, ép verem. Ha kimarad az A64
X9 visszatöltése, az eredmény:
Hipotézis: egy caller-saved értéket változatlannak tekintettünk.
Adatgyűjtés: töréspont a lab_after_call címkénél, az A64
X9 és a mentési hely összevetése. Egyetlen javítás: a
hiányzó visszatöltés helyreállítása. Ellenőrzés: az eredmény újra 32, a
kilépési státusz 0.
Jó eredmény, hibás hívóállapot. Az A64
X19 visszatöltésének elhagyásakor az eredmény továbbra is
32 lehet, de a hívó 99 helyett 11-et kap vissza a regiszterben. Ezért
nem elegendő kizárólag az eredményregisztert tesztelni.
Hibás visszatérési út. Ha egy újabb hívást végző
függvény nem óvja meg az A64 X30-at, a RET
rossz helyre ugorhat. Ilyenkor a visszatérés előtti regiszterállapotot
és az objdumpban látható hívási helyeket kell összevetni.
32 bites írás megőrzött regiszterbe. Az A64
W19 módosítása is megköveteli az eredeti teljes
X19 megőrzését. Ellenpróbához olyan hívóoldali értéket
válasszunk, amelynek felső 32 bitje nem nulla.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Adjuk meg a
double evaluate(uint64_t count, double scale, const void *base, float bias)
argumentum- és eredményregisztereit. Miért nem az A64 X2
hordozza a base értékét?
(implementációs, középszint — cél: rutinépítés)
Készítsünk lab-aapcs-x20.S változatot, amely az
a értéket caller-saved regiszter és külön mentés helyett az
A64 X20-ban tartja a segédhíváson át. A keret maradjon 32
bájtos.
(hibakeresési, középszint — cél: rutinépítés)
Készítsünk lab-aapcs-bug.S másolatot, és csak az A64
X9 hívás utáni visszatöltését hagyjuk el. Jósoljuk meg az
eredményt, majd ellenőrizzük GDB-vel. Ezután külön kísérletben csak az
A64 X19 visszatöltését hagyjuk el.
1. megoldás — Segítség. Vedd sorra az argumentumokat típus szerint: az egész/mutató és a lebegőpontos paraméterek külön regisztercsaládot fogyasztanak, egymástól függetlenül sorszámozva.
1. megoldás — Teljes megoldás. A kiosztás: A64
X0 = count, D0 = scale,
X1 = base, S1 = bias; az eredmény A64
D0. A lebegőpontos argumentumok nem fogyasztanak általános
argumentumregisztert ebben az egyszerű esetben. A scale
ezért nem foglalja el az A64 X1-et.
2. megoldás — Segítség. Az A64 X20
callee-saved — gondold át, hol kell menteni és hol visszaállítani ahhoz,
hogy a hívó ne vegyen észre változást, és hogy a segédhívás se írja
felül idő előtt.
2. megoldás — Teljes megoldás. A keret 16 és 24
bájtos helyére az eredeti A64 X19 és X20
kerüljön. A segédhívás előtt X19 = b, X20 = a;
utána az eredményhez ezeket adjuk hozzá. Mindkét regisztert vissza kell
állítani. Az eredeti teszt ezt különösen jól ellenőrzi: az
_start az A64 X20-ban őrzi az eredeti
veremmutatót.
Target: qemu-user, AArch64 Linux; várt státusz: 0.
aarch64-linux-gnu-as -EL -march=armv8-a -g \
-o lab-aapcs-x20.o lab-aapcs-x20.S
aarch64-linux-gnu-ld -e _start \
-o lab-aapcs-x20 lab-aapcs-x20.o
qemu-aarch64 ./lab-aapcs-x20
printf 'kilepesi_statusz=%d\n' "$?"3. megoldás — Segítség. Az A64 X9
caller-saved (nincs megőrzési kötelezettség rá), az A64 X19
callee-saved (van) — a két hiba más-más szerződést sért, és más tünetet
ad.
3. megoldás — Teljes megoldás. Az első változat várt
eredménye 110, kilépési státusza 1. A másodiké 32, de az A64
X19 = 11 miatt szintén 1. A két hibát külön kell vizsgálni,
hogy az egyik ellenőrzés ne fedje el a másikat.
Target: qemu-user, AArch64 Linux; a szándékosan hibás változat várt státusza: 1.
aarch64-linux-gnu-as -EL -march=armv8-a -g \
-o lab-aapcs-bug.o lab-aapcs-bug.S
aarch64-linux-gnu-ld -e _start \
-o lab-aapcs-bug lab-aapcs-bug.o
qemu-aarch64 ./lab-aapcs-bug
printf 'kilepesi_statusz=%d\n' "$?"Az AAPCS64 a külön fordított függvények közös regiszterszerződése.
A64-ben az egyszerű egész és mutató argumentumok az
X0–X7 regisztercsaládot használják; az
egyszerű egész eredmény az X0 megfelelő szélességű
nézetében érkezik. A hívó a még szükséges ideiglenes értékeket védi, a
hívott pedig a módosított megőrzött regisztereket állítja helyre. A
visszatérési cím és a veremmutató kezelése ettől külön figyelmet
igényel. A helyes eredmény önmagában nem igazolja az ABI-helyességet. A
lebegőpontos argumentumok külön regisztersorozatot kapnak.
A linkek és a dokumentumverziók közös állapota az Olvasói útmutató forrásállapot-táblájában van; itt csak az alfejezet saját megállapításai szerepelnek.
Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is
fennmaradnak: a kód itt nem futott; minden kimenet várt,
levezetett eredmény. Az Ubuntu 24.04-es laborban telepített
eszközverziókat és a konkrét futást a hallgatónak kell rögzítenie. Az
ABI-dokumentum mozgó main változata helyett auditálható
projektben konkrét kiadást vagy commitot célszerű megjelölni (fentebb a
2025Q4-es kiadás a jelenlegi referencia).