Egy assemblyben megírt függvény helyes eredményt számolhat, mégis elronthatja a hívóját: felülírhat egy megőrzendő regisztert, elveszítheti a visszatérési címet, vagy rossz helyen hagyhatja a veremmutatót. Ezek a hibák gyakran csak a következő függvényhíváskor jelentkeznek.
A prológus (function prologue) a függvény belépési műveletsora: létrehozza a szükséges veremkeretet, és elmenti a később visszaállítandó állapotot. Az epilógus (function epilogue) helyreállítja ezt az állapotot, felszabadítja a keretet, majd visszatér.
A nem-levél függvény (non-leaf function)
másik függvényt is hív. A64-ben a BL felülírja az
X30 kapcsolatregisztert, vagyis az LR-t (link
register). Ezért a hagyományos, hívás után tovább dolgozó nem-levél
függvénynek meg kell őriznie saját visszatérési címét.
Az előfeltétel Az AAPCS64 regiszterszerződés ismerete. Itt nem ismételjük meg a teljes regisztertáblázatot: azt mutatjuk meg, hogyan lesz a szerződésből működő mentési és visszaállítási kód.
Architektúrajelölés: az alfejezet minden utasítása és regisztere az A64 ISA, AArch64, I. rész kontextusához tartozik. A direktívák GNU assembler-, AArch64 ELF-környezetben értendők. A labor little-endian profilt használ; Cortex-M-kód nem szerepel benne.
Az A64 STP (store pair) két regisztert tárol
egymást követő memóriahelyekre. Az A64 LDP (load
pair) két regisztert tölt vissza. Két 64 bites regiszternél
összesen 16 bájt adat mozog.
A következő részlet 32 bájtos keretet foglal:
// A64 ISA, AArch64; GNU as szintaxis.
// Szemléltető részlet, a teljes program lejjebb következik.
STP X29, X30, [SP, #-32]!
MOV X29, SPA ! előindexelt, visszaíró címzést (pre-indexed
addressing with writeback) jelöl. Ha a belépési veremmutató értékét
S jelöli:
S − 32;X29 korábbi értéke az új veremmutatóhoz képest 0
bájtos eltolásra kerül;X30 korábbi értéke 8 bájtos eltolásra kerül;X29 felveszi az új keret alapcímét.Az S itt szimbolikus cím, nem konkrét
memóriatartomány.
A hozzá tartozó felszabadító részlet:
// A64 ISA, AArch64; GNU as szintaxis.
LDP X29, X30, [SP], #32
RETEz utóindexelt címzés (post-indexed addressing): előbb
megtörténik a betöltés a régi SP alapján, majd a
veremmutató 32 bájttal nő. Az operandus nélkül írt A64 RET
az X30 által megadott címre tér vissza.
Kódolási korlát: az itt használt, két 64 bites
általános célú regisztert mozgató A64 STP/LDP
eltolása előjeles, 7 bites, 8 bájttal skálázott mező. A tartomány
−512…504 bájt, 8 bájtos lépéssel. A negatív és pozitív szélsőérték tehát
nem szimmetrikus.
A STP X29, X30, [SP, #-n]! és
LDP X29, X30, [SP], #n egylépéses keretkezelő mintában,
pozitív és 16-tal osztható n mellett, a közös kódolható
tartomány 16…496 bájt. Nagyobb kerethez külön veremmutató-módosítás és
külön mentési művelet szükséges.
A páros utasítás csökkentheti a mentési kód utasításszámát. Ebből azonban nem következik garantált ciklusszám vagy kétszeres sebesség: a mikroarchitekturális végrehajtás külön kérdés.
Az AAPCS64 megőrzési követelménye a megőrzött, azaz callee-saved regiszterek módosítására vonatkozik.
X19 regisztert, emiatt nem
szükséges mentened.W19 nézetbe írsz, az X19 felső
32 bitje nullázódik. Ez is a teljes megőrzött regiszter módosítása.X29 regisztert keretmutatóként használod, a
régi értékét meg kell őrizned.X30 mentésének oka itt a további BL
hívás; nem egyszerűen az általános megőrzöttregiszter-lista
alkalmazása.Nem érdemes minden megőrzött regisztert automatikusan elmenteni. Ez felesleges memóriaforgalmat és nagyobb keretet okozna. A szükséges mentések a tényleges regiszterhasználatból következnek.
A labor a szokásos, két 64 bites elemből álló keretrekordot (frame record) használja: előző keretmutató, majd mentett visszatérési cím. Ez áttekinthető mintázat, de nem állítjuk, hogy minden optimalizált függvénynek pontosan ilyen kerete van. A platform keretlánc-szabályai és a fordító beállításai is számítanak.
A veremigazításról részletesebben A stack frame felépítése és a verem iránya szól. A példában a veremmutató végig 16 bájtra igazított; különösen minden hívási határon és minden veremmutatón keresztüli memóriaeléréskor.
Munkakönyvtár: ~/arm-labor/a64/a029/ —
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/a029
cd ~/arm-labor/a64/a029A megvalósítandó számítás C-modellje:
/* Szemantikai modell; a laborban nem külön fordítási egység. */
#include <stdint.h>
static uint64_t lab_double_model(uint64_t a)
{
return a + a;
}
uint64_t lab_eval_model(uint64_t a, uint64_t b)
{
return lab_double_model(a) + a + b;
}Az értékeket előjel nélküli, 64 bites számként kezeljük. Az összeadás modulo értendő; a tesztadatoknál nincs körbefordulás.
Az A64 assemblyváltozat:
X19 regiszterben megőrzi az a
argumentumot;X20 regiszterben megőrzi a b
argumentumot;3*a + b eredményt;A segédfüggvény szándékosan felülírja az A64 X1
regisztert. Ezt megteheti: a hívó nem támaszkodhat annak
megőrzésére.
Mentsd a következő teljes programot lab-frame.s
néven:
// A64 ISA, AArch64, ARMv8-A; GNU as, little-endian ELF.
.section .text
.global _start
.global lab_eval
.type _start, %function
_start:
// Magas biteket is tartalmazó megőrzési tesztminták.
MOVZ X19, #0x1234, LSL #48
MOVZ X20, #0x5678, LSL #48
MOV X29, XZR
MOV X21, SP
MOV X0, #7
MOV X1, #5
BL lab_eval
CMP X0, #26
B.NE .Lbad_value
MOV X9, SP
CMP X9, X21
B.NE .Lbad_stack
MOVZ X9, #0x1234, LSL #48
CMP X19, X9
B.NE .Lbad_saved
MOVZ X9, #0x5678, LSL #48
CMP X20, X9
B.NE .Lbad_saved
CBNZ X29, .Lbad_saved
MOV X0, #0
B .Lexit
.Lbad_value:
MOV X0, #1
B .Lexit
.Lbad_stack:
MOV X0, #2
B .Lexit
.Lbad_saved:
MOV X0, #3
.Lexit:
// AArch64 Linux exit rendszerhívás.
MOV X8, #93
SVC #0
.size _start, .-_start
.type lab_eval, %function
lab_eval:
.cfi_startproc
STP X29, X30, [SP, #-32]!
.cfi_def_cfa_offset 32 // CFA = SP + 32 (a belépési SP)
.cfi_offset 29, -32 // X29 a CFA − 32 címen
.cfi_offset 30, -24 // X30 a CFA − 24 címen
MOV X29, SP
STP X19, X20, [SP, #16]
.cfi_offset 19, -16
.cfi_offset 20, -8
MOV X19, X0
MOV X20, X1
BL lab_double
ADD X0, X0, X19
ADD X0, X0, X20
lab_eval_epilogue:
LDP X19, X20, [SP, #16]
.cfi_restore 19
.cfi_restore 20
LDP X29, X30, [SP], #32
.cfi_restore 29
.cfi_restore 30
.cfi_def_cfa_offset 0
RET
.cfi_endproc
.size lab_eval, .-lab_eval
.type lab_double, %function
lab_double:
// levélfüggvény: a kezdeti szabály (CFA = SP) végig érvényes
.cfi_startproc
ADD X0, X0, X0
MOV X1, #0
RET
.cfi_endproc
.size lab_double, .-lab_double
.section .note.GNU-stack,"",%progbitsA GNU .type és .size direktívák
függvényszimbólumokat írnak le, nem hajtanak végre mentést. A
.note.GNU-stack szekció a GNU ELF-eszközláncnak jelzi, hogy
a program nem igényel végrehajtható vermet.
A további immediate-ok: a MOV,
CMP és SVC konstansai kódolhatók (lásd az A64 immediate-korlátok összefoglalóját); ez a kis
pozitív konstansokra igaz, nem általános garancia tetszőleges 64 bites
értékre.
Legyen S a lab_eval belépési veremmutatója.
A prológus után az A64 SP és X29 értéke
egyaránt S − 32.
Eltolás az új A64 SP-hez képest |
Méret | Tartalom |
|---|---|---|
| 0 bájt | 8 bájt | A hívó mentett X29 értéke |
| 8 bájt | 8 bájt | A függvény belépési X30 értéke |
| 16 bájt | 8 bájt | A hívó mentett X19 értéke |
| 24 bájt | 8 bájt | A hívó mentett X20 értéke |
A középső STP nem módosítja a veremmutatót: nincs sem
!, sem utóindexelt operandus. A már lefoglalt keret felső
felét tölti ki.
A segédfüggvény visszatérésekor az A64 X0 értéke 14, az
X19 értéke 7, az X20 értéke 5. A két összeadás
eredménye 26. Ezután a munkára használt megőrzött regiszterek
visszakapják a hívó értékeit, nem a 7 és 5
argumentumot.
Host: Ubuntu 24.04 LTS x86_64, a könyv
cross-eszközláncával.
Target: AArch64 Linux, little-endian,
qemu-aarch64 user-mode.
# Target: AArch64 Linux, qemu-user.
aarch64-linux-gnu-as -EL -g -march=armv8-a \
-o lab-frame.o lab-frame.s
aarch64-linux-gnu-ld -EL -e _start \
-o lab-frame lab-frame.o
qemu-aarch64 ./lab-frame
lab_status=$?
printf 'kilepesi_allapot=%s\n' "$lab_status"
aarch64-linux-gnu-objdump -d ./lab-frameVárt, levezetett kimenet:
kilepesi_allapot=0. A program maga nem ír a terminálra.
| Kilépési állapot | Jelentés |
|---|---|
| 0 | A beépített eredmény- és megőrzési ellenőrzések sikeresek |
| 1 | Hibás számítási eredmény |
| 2 | Elcsúszott veremmutató |
| 3 | Hibásan visszaállított X19, X20 vagy
X29 |
Az A64 X21 a tesztelő kódban őrzi a belépési
veremmutatót; a vizsgált függvény nem módosítja. A magas biteket
tartalmazó tesztminták a véletlen 32 bites visszaállítást is
kimutathatják.
Ez célzott laborellenőrzés, nem általános ABI-validátor.
A lab_eval listájában a .cfi_* sorok nem
processzorutasítások, hanem az assemblernek szóló direktívák. Belőlük
készül az ELF .eh_frame szekciójának hívási
keretinformációja (Call Frame Information, CFI): egy
táblázat, amely a függvény minden utasításcímére megmondja, hol van a
hívó keretének kezdete (CFA, Canonical Frame Address —
itt a belépési SP), és hová mentettük a hívó regisztereit.
A GDB bt parancsa, a C++-kivételkezelés és a profilozók
ebből lépnek vissza a hívóhoz.
| Direktíva | Jelentés |
|---|---|
.cfi_startproc / .cfi_endproc |
a függvény CFI-leírásának kezdete és vége |
.cfi_def_cfa_offset 32 |
ettől az utasítástól CFA = SP + 32 |
.cfi_offset 30, -24 |
a 30-as DWARF-regiszter (X30) a CFA − 24 címen van
mentve |
.cfi_restore 30 |
az X30 újra a saját regiszterében van |
A DWARF-sorszámok AArch64-en: X0–X30 =
0–30, SP = 31, V0–V31 (így a
D8–D15 is) = 64–95.
CFI nélkül a hibakereső csak a keretláncból (X29 →
mentett X29 → …) vagy heurisztikából tud visszalépni. Ez a
prológus közepén, keretmutató nélküli függvényben vagy sérült veremben
megbízhatatlan. A könyv szabálya ezért a következő:
SP-t, vagy menti
az X29/X30/megőrzött regisztereket —, teljes
CFI-t kap.SP,
visszatérési cím az X30-ban) a függvény minden pontján
helyes. A .cfi_startproc/.cfi_endproc pár
ilyenkor is megengedett, a teljesség kedvéért._start-tal induló önálló program, biztonsági
demonstráció), a szöveg ezt jelöli: az ilyen kód hívási lánca
csak a keretlánc alapján követhető.A CFI helyességét nem a futás, hanem a táblázat igazolja:
# Hostoldali vizsgálat; a fájl célja AArch64 Linux, qemu-user.
aarch64-linux-gnu-readelf --debug-dump=frames-interp lab-frameA lab_eval FDE-jében négy sor várható: a belépéskor
CFA = sp+0; az első STP után
sp+32, x29 és x30 mentett
helyével (c-32, c-24); a második
STP után x19, x20 is
(c-16, c-8); az epilógus végén ismét
sp+0. Ha a táblázat egy mentést nem mutat, a
bt az adott ponton rossz hívót jelezhet, a program futása
viszont ettől még hibátlannak látszik.
lab_abi_callA lab-frame.s saját _start-ja csak a
lab_eval két megőrzött regiszterét ellenőrzi. A könyv
további C-ből hívható assembly függvényeihez közös
teszthívót használunk, amely a teljes AAPCS64 megőrzési
szerződést ellenőrzi:
X19–X28 és a
D8–D15 regiszterbe hívás előtt magas
biteket is tartalmazó mintát tölt
(0xa5a55a5a0000nnnn), így egy véletlen Wn-írás
— amely a felső 32 bitet nullázza — is lebukik;SP = X29) egy
statikus horgonyba menti, amelyhez a hívott nem fér
hozzá;SP-t és az X29-et külön-külön
a horgonnyal, és saját keretét csak a horgonyból állítja vissza, a
hívott által esetleg elrontott SP-ből vagy
X29-ből soha;X0-ját pedig külön kimeneti paraméterben.A horgony miatt a teszthívó nem reentráns és nem
szálbiztos: egyszerre csak egy lab_abi_call
futhat. Ezt nem csak dokumentáljuk, hanem ki is mutatjuk: ha a hívott
maga is lab_abi_call-t hív, a belső hívás nem indítja el a
függvényt, hanem LAB_ABI_BUSY hibabitet ad vissza. Ugyanezt
a megoldást használja a II. rész Thumb-2-es párja, a
lab_abi_call32 is.
A teszthívó maga is a fenti szabályt követi: teljes prológus–epilógus
és CFI. A fájlok ebben a könyvtárban
(~/arm-labor/a64/a029/) készülnek; a későbbi alfejezetek
innen másolják át őket.
A lab-abi-call.h tartalma:
/* lab-abi-call.h — közös AAPCS64 ABI-teszthívó; cél: AArch64 Linux, LP64. */
#ifndef LAB_ABI_CALL_H
#define LAB_ABI_CALL_H
#include <stdint.h>
/* Tetszőleges, legfeljebb hat egész/mutató argumentumú függvény címe.
* A hívás az assemblyben történik, ezért a C oldalon csak a címet adjuk át. */
typedef void (*lab_abi_fn)(void);
/* A hibamaszk bitjei; 0 = a hívott betartotta a megőrzési szerződést. */
#define LAB_ABI_X19 (1u << 0) /* bit 0..9: X19..X28 */
#define LAB_ABI_D8 (1u << 10) /* bit 10..17: D8..D15 */
#define LAB_ABI_SP (1u << 18) /* SP nem a hívás előtti */
#define LAB_ABI_FP (1u << 19) /* X29 nem a hívás előtti */
#define LAB_ABI_SP_FP (LAB_ABI_SP | LAB_ABI_FP)
#define LAB_ABI_BUSY (1u << 20) /* újrabelépés: fn nem hívódott meg */
/* Meghívja fn(a0, ..., a5)-öt úgy, hogy előtte X19–X28 és D8–D15 ismert,
* magas biteket is tartalmazó mintát kap; visszatérés után ellenőrzi
* a mintákat, valamint SP és X29 értékét. *result = a hívott X0-ja.
*
* NEM reentráns és nem szálbiztos: a hívás előtti keretcímet egyetlen
* statikus horgony őrzi. Egymásba ágyazott hívásnál a belső hívás nem
* hívja meg fn-t, LAB_ABI_BUSY-t ad vissza, *result = 0. */
uint64_t lab_abi_call(lab_abi_fn fn,
uint64_t a0, uint64_t a1, uint64_t a2,
uint64_t a3, uint64_t a4, uint64_t a5,
uint64_t *result);
#endifA lab-abi-call.S tartalma (nagybetűs .S: a
GCC előbb a C-preprocesszort futtatja rajta):
// lab-abi-call.S — A64 ISA, AArch64 Linux; GNU as, AAPCS64.
// Közös ABI-teszthívó: uint64_t lab_abi_call(fn, a0..a5, *result)
// Minta: [63:48] = 0xa5a5, [47:32] = 0x5a5a, [15:0] = a regiszter sorszáma.
// A felső fél nem nulla, így egy W-regiszteres írás is lebukik.
.macro LAB_PATTERN reg, n
movz \reg, #\n
movk \reg, #0x5a5a, lsl #32
movk \reg, #0xa5a5, lsl #48
.endm
// Ha \reg nem egyezik a mintával, a \bit sorszámú bit 1 lesz X10-ben.
.macro LAB_CHECK reg, n, bit
LAB_PATTERN x12, \n
cmp \reg, x12
cset x13, ne
orr x10, x10, x13, lsl #\bit
.endm
.macro LAB_CHECK_D dreg, n, bit
fmov x14, \dreg
LAB_CHECK x14, \n, \bit
.endm
.text
.p2align 2
.global lab_abi_call
.type lab_abi_call, %function
lab_abi_call:
.cfi_startproc
// Keret: X29/X30 (16) + X19–X28 (80) + D8–D15 (64) + result-mutató (8) -> 176
stp x29, x30, [sp, #-176]!
.cfi_def_cfa_offset 176
.cfi_offset 29, -176
.cfi_offset 30, -168
mov x29, sp
stp x19, x20, [sp, #16]
.cfi_offset 19, -160
.cfi_offset 20, -152
stp x21, x22, [sp, #32]
.cfi_offset 21, -144
.cfi_offset 22, -136
stp x23, x24, [sp, #48]
.cfi_offset 23, -128
.cfi_offset 24, -120
stp x25, x26, [sp, #64]
.cfi_offset 25, -112
.cfi_offset 26, -104
stp x27, x28, [sp, #80]
.cfi_offset 27, -96
.cfi_offset 28, -88
stp d8, d9, [sp, #96]
.cfi_offset 72, -80 // DWARF 72..79 = D8..D15
.cfi_offset 73, -72
stp d10, d11, [sp, #112]
.cfi_offset 74, -64
.cfi_offset 75, -56
stp d12, d13, [sp, #128]
.cfi_offset 76, -48
.cfi_offset 77, -40
stp d14, d15, [sp, #144]
.cfi_offset 78, -32
.cfi_offset 79, -24
str x7, [sp, #160] // result-mutató
// Horgony: a hívás előtti SP (= X29) egy statikus szóba kerül, amelyhez
// a hívott nem fér hozzá. 0 = szabad; nem nulla = egy hívás már fut.
adrp x11, lab_abi_anchor
add x11, x11, :lo12:lab_abi_anchor
ldr x12, [x11]
cbnz x12, 1f // újrabelépés: fn nem hívható
mov x12, sp
str x12, [x11]
// Argumentumok átrendezése: X0 = fn, X1..X6 = a0..a5.
mov x9, x0
mov x0, x1
mov x1, x2
mov x2, x3
mov x3, x4
mov x4, x5
mov x5, x6
// Megőrzendő regiszterek feltöltése mintával.
LAB_PATTERN x19, 19
LAB_PATTERN x20, 20
LAB_PATTERN x21, 21
LAB_PATTERN x22, 22
LAB_PATTERN x23, 23
LAB_PATTERN x24, 24
LAB_PATTERN x25, 25
LAB_PATTERN x26, 26
LAB_PATTERN x27, 27
LAB_PATTERN x28, 28
LAB_PATTERN x10, 0x108
fmov d8, x10
LAB_PATTERN x10, 0x109
fmov d9, x10
LAB_PATTERN x10, 0x10a
fmov d10, x10
LAB_PATTERN x10, 0x10b
fmov d11, x10
LAB_PATTERN x10, 0x10c
fmov d12, x10
LAB_PATTERN x10, 0x10d
fmov d13, x10
LAB_PATTERN x10, 0x10e
fmov d14, x10
LAB_PATTERN x10, 0x10f
fmov d15, x10
blr x9
// Ellenőrzés; X10 = hibamaszk. Az SP-t és az X29-et külön-külön a
// horgonyhoz hasonlítjuk, nem egymáshoz: azonos eltolásuk is lebukik.
mov x10, xzr
adrp x11, lab_abi_anchor
add x11, x11, :lo12:lab_abi_anchor
ldr x12, [x11] // a hívás előtti keretcím
mov x13, sp
cmp x13, x12
cset x13, ne
orr x10, x10, x13, lsl #18
cmp x29, x12
cset x13, ne
orr x10, x10, x13, lsl #19
mov sp, x12 // helyreállítás csak a horgonyból:
mov x29, x12 // sérült SP/X29 értékét nem használjuk
str xzr, [x11] // a horgony újra szabad
LAB_CHECK x19, 19, 0
LAB_CHECK x20, 20, 1
LAB_CHECK x21, 21, 2
LAB_CHECK x22, 22, 3
LAB_CHECK x23, 23, 4
LAB_CHECK x24, 24, 5
LAB_CHECK x25, 25, 6
LAB_CHECK x26, 26, 7
LAB_CHECK x27, 27, 8
LAB_CHECK x28, 28, 9
LAB_CHECK_D d8, 0x108, 10
LAB_CHECK_D d9, 0x109, 11
LAB_CHECK_D d10, 0x10a, 12
LAB_CHECK_D d11, 0x10b, 13
LAB_CHECK_D d12, 0x10c, 14
LAB_CHECK_D d13, 0x10d, 15
LAB_CHECK_D d14, 0x10e, 16
LAB_CHECK_D d15, 0x10f, 17
b 2f
1: mov x10, #(1 << 20) // LAB_ABI_BUSY; a horgonyt nem írjuk felül
mov x0, xzr
2: ldr x7, [sp, #160]
str x0, [x7] // a hívott eredménye
mov x0, x10 // visszatérési érték: hibamaszk
ldp d14, d15, [sp, #144]
ldp d12, d13, [sp, #128]
ldp d10, d11, [sp, #112]
ldp d8, d9, [sp, #96]
ldp x27, x28, [sp, #80]
ldp x25, x26, [sp, #64]
ldp x23, x24, [sp, #48]
ldp x21, x22, [sp, #32]
ldp x19, x20, [sp, #16]
.cfi_restore 72
.cfi_restore 73
.cfi_restore 74
.cfi_restore 75
.cfi_restore 76
.cfi_restore 77
.cfi_restore 78
.cfi_restore 79
.cfi_restore 19
.cfi_restore 20
.cfi_restore 21
.cfi_restore 22
.cfi_restore 23
.cfi_restore 24
.cfi_restore 25
.cfi_restore 26
.cfi_restore 27
.cfi_restore 28
ldp x29, x30, [sp], #176
.cfi_restore 29
.cfi_restore 30
.cfi_def_cfa_offset 0
ret
.cfi_endproc
.size lab_abi_call, .-lab_abi_call
.bss
.p2align 3
lab_abi_anchor: // helyi szimbólum: csak a teszthívó látja
.space 8
.section .note.GNU-stack,"",%progbitsA teszthívó önellenőrzéséhez egy helyes és hat szándékosan hibás
hívottat készítünk. Az SP/X29-szerződéshez
három külön változat tartozik: csak az SP-t, csak az
X29-et, illetve mindkettőt azonos mértékben eltoló hívott.
A lab-abi-demo.S tartalma:
// lab-abi-demo.S — A64 ISA, AArch64; hívottak az ABI-teszthívó kipróbálásához.
.text
.p2align 2
// Helyes: X19-et használja, de menti és visszaállítja. Eredmény: a0 + a1 + a2.
.global lab_demo_good
.type lab_demo_good, %function
lab_demo_good:
.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
add x19, x0, x1
add x0, x19, x2
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_demo_good, .-lab_demo_good
// HIBÁS: mentés nélkül felülírja X19-et.
.global lab_demo_bad_x19
.type lab_demo_bad_x19, %function
lab_demo_bad_x19:
.cfi_startproc
mov x19, x0
add x0, x19, x1
ret
.cfi_endproc
.size lab_demo_bad_x19, .-lab_demo_bad_x19
// HIBÁS: "ártalmatlannak" látszó W-írás; nullázza X20 felső 32 bitjét.
.global lab_demo_bad_w20
.type lab_demo_bad_w20, %function
lab_demo_bad_w20:
.cfi_startproc
mov w20, w20
add x0, x0, x1
ret
.cfi_endproc
.size lab_demo_bad_w20, .-lab_demo_bad_w20
// HIBÁS: D8 csak az alsó 64 bitjén megőrzendő, itt mégis felülíródik.
.global lab_demo_bad_d8
.type lab_demo_bad_d8, %function
lab_demo_bad_d8:
.cfi_startproc
fmov d8, xzr
add x0, x0, x1
ret
.cfi_endproc
.size lab_demo_bad_d8, .-lab_demo_bad_d8
// HIBÁS: 16 bájttal elállított SP-vel tér vissza.
.global lab_demo_bad_sp
.type lab_demo_bad_sp, %function
lab_demo_bad_sp:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
add x0, x0, x1
ret
.cfi_endproc
.size lab_demo_bad_sp, .-lab_demo_bad_sp
// HIBÁS: az SP rendben van, de az X29-et 16 bájttal elállítja.
.global lab_demo_bad_fp
.type lab_demo_bad_fp, %function
lab_demo_bad_fp:
.cfi_startproc
sub x29, x29, #16
add x0, x0, x1
ret
.cfi_endproc
.size lab_demo_bad_fp, .-lab_demo_bad_fp
// HIBÁS: az SP-t és az X29-et azonos mértékben tolja el. Az SP == X29
// összehasonlítás itt átmenne; csak a független horgony mutatja ki.
.global lab_demo_bad_spfp
.type lab_demo_bad_spfp, %function
lab_demo_bad_spfp:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
sub x29, x29, #16
add x0, x0, x1
ret
.cfi_endproc
.size lab_demo_bad_spfp, .-lab_demo_bad_spfp
.section .note.GNU-stack,"",%progbitsA lab-abi-test.c tartalma:
/* lab-abi-test.c — az ABI-teszthívó önellenőrzése; cél: AArch64 Linux. */
#include <inttypes.h>
#include <stdio.h>
#include "lab-abi-call.h"
extern void lab_demo_good(void);
extern void lab_demo_bad_x19(void);
extern void lab_demo_bad_w20(void);
extern void lab_demo_bad_d8(void);
extern void lab_demo_bad_sp(void);
extern void lab_demo_bad_fp(void);
extern void lab_demo_bad_spfp(void);
/* Szerződéstartó C-hívott, amely maga is meghívja a teszthívót: a belső
* hívásnak LAB_ABI_BUSY-t kell adnia (a horgony foglalt). */
static uint64_t lab_demo_reenter(void)
{
uint64_t inner = 0;
return lab_abi_call(lab_demo_good, 1, 2, 3, 0, 0, 0, &inner);
}
static int lab_expect(const char *name, lab_abi_fn fn,
uint64_t want_mask, uint64_t want_result)
{
uint64_t result = 0;
uint64_t mask = lab_abi_call(fn, 1, 2, 3, 0, 0, 0, &result);
int ok = (mask == want_mask) && (result == want_result);
printf("%-18s maszk=0x%05" PRIx64 " eredmeny=%" PRIu64 " %s\n",
name, mask, result, ok ? "OK" : "HIBA");
return ok;
}
int main(void)
{
int ok = 1;
ok &= lab_expect("lab_demo_good", lab_demo_good, 0, 6);
ok &= lab_expect("lab_demo_bad_x19", lab_demo_bad_x19, LAB_ABI_X19, 3);
ok &= lab_expect("lab_demo_bad_w20", lab_demo_bad_w20, LAB_ABI_X19 << 1, 3);
ok &= lab_expect("lab_demo_bad_d8", lab_demo_bad_d8, LAB_ABI_D8, 3);
ok &= lab_expect("lab_demo_bad_sp", lab_demo_bad_sp, LAB_ABI_SP, 3);
ok &= lab_expect("lab_demo_bad_fp", lab_demo_bad_fp, LAB_ABI_FP, 3);
ok &= lab_expect("lab_demo_bad_spfp", lab_demo_bad_spfp, LAB_ABI_SP_FP, 3);
ok &= lab_expect("lab_demo_reenter", (lab_abi_fn)lab_demo_reenter, 0, LAB_ABI_BUSY);
ok &= lab_expect("lab_demo_good", lab_demo_good, 0, 6);
return ok ? 0 : 1;
}Build és futtatás — target: AArch64 Linux, qemu-user:
aarch64-linux-gnu-gcc -std=c17 -O2 -Wall -Wextra -march=armv8-a -static \
lab-abi-test.c lab-abi-call.S lab-abi-demo.S -o lab-abi-test
qemu-aarch64 ./lab-abi-test
printf 'kilepesi_allapot=%d\n' "$?"Várt kimenet:
lab_demo_good maszk=0x00000 eredmeny=6 OK
lab_demo_bad_x19 maszk=0x00001 eredmeny=3 OK
lab_demo_bad_w20 maszk=0x00002 eredmeny=3 OK
lab_demo_bad_d8 maszk=0x00400 eredmeny=3 OK
lab_demo_bad_sp maszk=0x40000 eredmeny=3 OK
lab_demo_bad_fp maszk=0x80000 eredmeny=3 OK
lab_demo_bad_spfp maszk=0xc0000 eredmeny=3 OK
lab_demo_reenter maszk=0x00000 eredmeny=1048576 OK
lab_demo_good maszk=0x00000 eredmeny=6 OK
kilepesi_allapot=0
A legtanulságosabb sor a lab_demo_bad_w20: a
mov w20, w20 „semmit sem csináló” utasításnak tűnik, és kis
értékekkel tesztelve valóban nem okoz eltérést. A 64 bites minta felső
fele azonban nullázódik, és a teszthívó ezt kimutatja. Az
SP/X29-sorok azt mutatják, miért kell
független horgony. Egy korábbi változat az
SP-t az X29-hez hasonlította, és
mov sp, x29-cel állt vissza. Ez csak addig működik, amíg a
hívott az X29-et megőrzi. A lab_demo_bad_spfp
mindkettőt 16 bájttal tolja el, ezért az SP == X29
vizsgálat átmenne. A lab_demo_bad_fp esetén pedig a
mov sp, x29 egy 16 bájttal arrébb fekvő „keretből” töltené
vissza a mentett regisztereket és a visszatérési címet. A horgonyos
változat mindhárom hibát külön bittel jelzi (LAB_ABI_SP,
LAB_ABI_FP, illetve mindkettő), és a horgonyból mindig ép
kerettel tér vissza, így a hiba jelentés lesz, nem összeomlás. A
lab_demo_reenter sor eredménye 0x100000 =
LAB_ABI_BUSY. Ez a belső, egymásba ágyazott hívás maszkja,
amelyet a C-hívott továbbad. Az utána következő
lab_demo_good sor azt igazolja, hogy a horgony a külső
hívás végén felszabadult.
Korlátok. A teszthívó legfeljebb hat egész vagy
mutató argumentumot ad át, a hívott X0-ját olvassa vissza,
és nem ellenőrzi a lebegőpontos argumentumokat, a veremben átadott
argumentumokat, a V8–V15 felső 64 bitjét (ezt
az AAPCS64 nem is írja elő megőrzendőnek), sem a memóriabeli
mellékhatásokat. Ezeket a hívott függvény saját tesztje vizsgálja. Az
SP/X29-ellenőrzés csak a visszatéréskori
regiszterértékekre vonatkozik. Ha a hívott a hívó
keretét is felülírja (például a saját keretén túl ír, vagy előbb
megemeli, majd használja az SP-t), a teszthívó a mentett
regisztereket már sérült memóriából tölti vissza, és ezt nem ismeri fel.
A rossz X30-cal visszatérő hívott pedig el sem jut az
ellenőrzésig. Ezekre a debugger (watch a hívó keretére,
illetve töréspont a visszatérési címen) a megfelelő eszköz. Egyszerre
csak egy lab_abi_call futhat (statikus horgony); többszálú
vagy szignálkezelőből hívott tesztben ez a teszthívó nem
használható.
A szimmetria állapotbeli inverzt jelent, nem egyszerűen azonos számú mentő és betöltő utasítást.
A keretméretre vonatkozó levezetés:
Itt S bájtcím, a két n pedig bájtban
megadott keretméret. A hívó veremmutatója akkor áll helyre, ha a
foglalás és felszabadítás mérete azonos. Ez szükséges, de önmagában nem
elég: a regisztereket is a megfelelő rekeszekből kell visszatölteni.
Különösen alattomos módosítás:
// A64 ISA; szándékosan hibás epilógusrészlet.
LDP X19, X20, [SP, #16]
LDP X29, X30, [SP], #16
RETA visszatérési cím helyes helyről töltődik be, ezért a függvény
visszatérhet. A veremmutató viszont S − 16 marad. Még az
igazítása is helyes, tehát az igazításellenőrzés nem feltétlenül leplezi
le a hibát. A labor várt kilépési állapota ekkor 2.
| Tünet | Hipotézis | Ellenőrzés |
|---|---|---|
| Az eredmény helyes, később hibázik a program | Elmaradt megőrzöttregiszter-visszaállítás | Belépési és kilépési regiszterértékek összevetése |
| A visszatérés a függvény belsejébe vezet | A belépési LR elveszett | A64 X30 és a mentett visszatérési cím vizsgálata |
| Minden visszatérés után változik a veremhelyzet | Eltérő foglalási és felszabadítási méret | A64 SP különbségének kiszámítása |
| Csak a felső 32 bit hibás | Téves Wn művelet |
A teljes Xn érték és a disassembly ellenőrzése |
A következő GDB-menettel közvetlenül az epilógus előtt állunk meg.
Első terminál, target: AArch64 Linux, qemu-user:
qemu-aarch64 -g 1234 ./lab-frameA program a debugger kapcsolódására vár.
Második terminál, debugger: hoston futó
gdb-multiarch, target: qemu-user:
gdb-multiarch -q ./lab-frame \
-ex 'target remote localhost:1234' \
-ex 'break *lab_eval' \
-ex 'continue' \
-ex 'set $lab_sp0 = $sp' \
-ex 'break *lab_eval_epilogue' \
-ex 'continue' \
-ex 'p/d $lab_sp0 - $sp' \
-ex 'x/4gx $sp' \
-ex 'si' \
-ex 'si' \
-ex 'p/d $sp == $lab_sp0' \
-ex 'info registers x19 x20 x29 x30 sp' \
-ex 'continue' \
-ex 'quit'Várt állapot: az első különbség 32; a két A64
LDP után az egyenlőségvizsgálat eredménye 1. A memória négy
nyolcbájtos rekesze a fenti táblázatot követi. A mentett visszatérési
cím futásfüggő, ezért nem adunk hozzá számszerű címpéldát.
Hibakereséskor egyszerre egy dolgot módosíts: például csak a felszabadítás méretét. Ezután ismételd meg ugyanazt a regiszter- és veremellenőrzést. Ne következtess ABI-helyességre pusztán a számított eredményből.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Miért kell a példában az A64
X19, X20, X29 és X30
értékét megőrizni? Miért nem mentjük az X0 belépési értékét
visszaállítás céljából?
(implementációs, középszint — cél: rutinépítés)
Alakítsd át a lab_eval függvényt úgy, hogy két
segédfüggvény-hívással számítsa ki a 2*a + 2*b értéket. Az
első hívás eredményét őrizd meg a második hívás idejére. Maradjon 32
bájtos keret, és frissítsd az eredményellenőrzést.
(hibakeresési, középszint — cél:
megértés-ellenőrzés) Az eredeti példában változtasd az utolsó
A64 LDP utóindexét 32-ről 16-ra. Fordítsd újra a korábbi
assemble–link–run paranccsal. Jegyezd fel a kilépési állapotot, majd
GDB-ben ellenőrizd a veremmutató eltérését. Végül állítsd vissza az
eredeti kódot.
1. megoldás — Segítség. Két külön kérdést tegyél
fel. (1) Melyik regiszterek callee-saved-ek az AAPCS64
szerint, és ezek közül melyiket írja felül a lab_eval?
Ezeket minden hívó felé vissza kell állítania, akkor is, ha az éppen
bemutatott _start némelyiküket nem olvassa: a függvény nem
ismerheti a jövőbeli hívóit. (2) Mit veszélyeztet a
lab_eval saját belső BL-je? Gondolj az
X30-ra. A „csak azt, amit a hívó a hívás után még használ”
szempont a hívó saját döntésére való: a caller-saved
regisztereiből (X0–X18) azt kell a hívás előtt
elmentenie, amire a hívás után is szüksége van.
1. megoldás — Teljes megoldás. Az A64
X19 és X20 megőrzött regiszter, és a függvény
felülírja őket. Az X29 szintén megőrzendő, és új
keretmutatót kap. Az X30 belépési értékét a belső
BL felülírná, pedig arra kell majd visszatérni. Az
X0 eredményregiszter: kilépéskor az eredményt, nem a
belépési argumentumot kell tartalmaznia.
2. megoldás — Segítség. A két hívás eredményét nem
tarthatod egyszerre X0-ban — az egyiket a hívások között
egy megőrzött regiszterbe kell átmenteni, mielőtt a második
BL felülírja X0-t.
2. megoldás — Teljes megoldás. A teljes
lab_eval függvény helyére:
// A64 ISA, AArch64; a lab-frame.s cserefüggvénye.
.type lab_eval, %function
lab_eval:
STP X29, X30, [SP, #-32]!
MOV X29, SP
STP X19, X20, [SP, #16]
MOV X19, X1
BL lab_double
MOV X20, X0
MOV X0, X19
BL lab_double
ADD X0, X0, X20
lab_eval_epilogue:
LDP X19, X20, [SP, #16]
LDP X29, X30, [SP], #32
RET
.size lab_eval, .-lab_evalAz A64 X19 a második argumentumot, az X20
az első hívás eredményét őrzi. A tesztben a CMP X0, #26
helyett CMP X0, #24 szükséges. Az előző buildparancsokkal
újrafordítva a várt kilépési állapot ismét 0.
3. megoldás — Segítség. A veremmutató-eltérés és a
visszatérési cím helyessége két külön dolog — gondold végig, hogy a
LDP X29, X30, [SP], #16 melyiket állítja helyre, és
melyiket nem.
3. megoldás — Teljes megoldás. A várt kilépési állapot 2. Az epilógus előtti különbség továbbra is 32 bájt; a hibás felszabadítás után 16 bájt marad. A GDB egyenlőségvizsgálata 0-t ad. A visszatérési cím helyreállítása sikerült, a veremkereté nem. Ezért nincs szükség azonnali összeomlásra ahhoz, hogy a kód hibás legyen.
A nem-levél A64 függvénynek meg kell őriznie a további hívások által
veszélyeztetett visszatérési címét, valamint minden általa módosított
megőrzött regiszter belépési értékét. Az STP és
LDP tömör eszközt ad a páros mentéshez, de címzési és
immediate-korlátaikat figyelembe kell venni. A helyes epilógus nemcsak a
regisztereket, hanem a belépési veremmutatót is visszaállítja. A
szimmetria ezért állapotbeli azonosságot jelent. Egy függvény helyes
számítás mellett is megsértheti az ABI-t; ezt célzott regisztermintákkal
és veremmutató-ellenőrzéssel vizsgáljuk. A könyv C-ből hívható assembly
függvényeit a közös lab_abi_call teszthívó ellenőrzi; a
végleges függvények CFI-t is kapnak, amelyet a
readelf --debug-dump=frames-interp táblázata igazol.
.eh_frame táblázata arról, hol
a hívó kerete és hová kerültek a mentett regiszterek.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.
STP,
LDP, BL és RET architekturális
szemantikája. Ellenőrizve: a link él; a JavaScript-alapú
megjelenítés miatt a pontos tartalom automatikus lekéréssel nem volt
kinyerhető.Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is
ténylegesen fennmaradnak: a könyv referencia-eszközláncával
(GNU GCC/binutils, qemu-aarch64) a kódot itt nem futtattuk.
A lab-frame.s kilépési állapotát (0), a
lab-abi-test fenti kimenetét és a két függvény
CFI-táblázatát helyettesítő eszközlánccal mértük: zig cc
(Clang, integrált assembler, musl C-könyvtár,
aarch64-linux-musl cél), a futtatás pedig egy Unicorn-alapú
AArch64 user-mode emulátorban történt. Ugyanezzel a környezettel
ellenőriztük a horgony szükségességét is: a korábbi,
SP == X29 összehasonlítású és mov sp, x29-cel
visszaálló teszthívó a lab_demo_bad_fp és a
lab_demo_bad_spfp hívottnál a sérült keretből olvasott
result-mutató miatt szegmentálási hibával (139) állt le, a
horgonyos változat mindkettőnél a várt maszkot adta. A GNU assembler és
a glibc alatti eredmény a hallgatói laborban ellenőrizendő, csakúgy mint
a GDB-menet. A CFI a szinkron visszalépést (bt,
kivételkezelés hívási pontokon) írja le; az epilógus utasításai közötti
aszinkron visszafejtés pontosságát külön nem vizsgáltuk.