8.2 Az AAPCS64 regiszterszerződés

8.2.1 Miért kell a regiszterekre szerződés?

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.

8.2.2 A megőrzés két felelőse

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.

8.2.2.1 Mit tesz a hardver, és mit ír elő az ABI?

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.

8.2.3 Argumentumok és visszatérési értékek

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.

8.2.3.1 Nyolcnál több egész argumentum

Kilenc, egyenként uint64_t típusú paraméterné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.

8.2.3.2 Előzetes: lebegőpontos és NEON-argumentumok

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:

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.

8.2.4 Kidolgozott példa: mindkét megőrzési felelősség egy hívásban

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/a028

A 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:

score(a,b)=(2a+a+b)mod264. \operatorname{score}(a,b)=(2a+a+b)\bmod 2^{64}.

Itt aa, bb é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:

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,"",%progbits

A .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.

8.2.4.1 A keret és az eredmény levezetése

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.

8.2.4.2 A példában használt immediate-korlátok

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.

8.2.5 Összeállítás, futtatás és ellenőrzé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-aapcs

A 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.

8.2.6 Tipikus hibák és célzott ellenpróbák

Rossz eredmény, ép verem. Ha kimarad az A64 X9 visszatöltése, az eredmény:

14+85+11=110. 14+85+11=110.

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.

8.2.7 Feladatok

  1. (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?

  2. (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.

  3. (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.

8.2.8 Megoldások

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' "$?"

8.2.9 Rövid összefoglaló

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.

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

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).