Egy C-ben írt pufferfeldolgozó rutin gyakran egyszerre olvas adatot és lépteti a mutatót. Assemblyben külön kell döntenünk arról, hogy melyik címről történjen a hozzáférés, illetve változzon-e a címet tartalmazó regiszter. Egy rosszul választott címzési mód érvényes memóriacímet is előállíthat, miközben a program következetesen a szomszédos elemet dolgozza fel.
Az alfejezetben az A64 ISA, AArch64, ARMv8-A alapprofilját használjuk, little-endian bájtsorrenddel. Minden alábbi utasítás és regiszter ebben a kontextusban értendő; az assembly direktívái a GNU assembler AArch64-célú forrásához tartoznak. Cortex-M-kód itt nem szerepel.
Az előfeltétel a regiszterek, az alapvető aritmetika és a vezérlésátadás ismerete. A példaprogram szándékosan nem használ függvényhívást vagy vermet: kizárólag a memóriacímzésre összpontosít.
A betöltés (load) memóriából regiszterbe, a
tárolás (store) regiszterből memóriába mozgat
adatot. Az A64 LDR és STR egészregiszteres
alakjaiban a művelet szélességét az adatregiszter nézete határozza
meg:
// A64 ISA; szintaktikai minták, nem önálló program.
LDR W0, [X1] // 4 bájt betöltése
LDR X0, [X1] // 8 bájt betöltése
STR W0, [X1] // 4 bájt tárolása
STR X0, [X1] // 8 bájt tárolásaAz effektív cím (effective address, EA) a
memóriahozzáférés kezdőcíme. A bázisregiszter (base
register) a címképzés kiinduló értékét tartalmazza. A példákban ez
mindig 64 bites A64 Xn regiszter: a 32 bites adatbetöltés
sem teszi 32 bitessé a címet.
Az A64 betöltések és tárolások önmagukban nem módosítják az NZCV állapotjelzőket. A cím számításához nem tartozik automatikus objektumhatár-ellenőrzés sem: a programozó feladata, hogy a teljes hozzáférés érvényes, megfelelő jogosultságú memóriába essen.
Legyen a bázisregiszter eredeti értéke, pedig az eltolás bájtban. A következő táblázat sikeresen befejeződő A64 memóriahozzáférésre érvényes.
| A64 címzési mód | Példa | Effektív cím | Bázisregiszter utána |
|---|---|---|---|
| Immediate offset | LDR W0, [X1, #4] |
||
| Pre-index | LDR W0, [X1, #4]! |
||
| Post-index | LDR W0, [X1], #4 |
A visszaírás (writeback) a kiszámított új báziscím regiszterbe írása. A táblázat első alakja nem végez visszaírást; a másik kettő igen.
Figyelmeztetés: a bázis- és a célregiszter nem fedhet át visszaírásnál. A pre- és post-index alakban ugyanabba a regiszterbe két érték kerülne: a betöltött adat és az új báziscím. Az architektúra ezt nem rendezi el, hanem CONSTRAINED UNPREDICTABLE-nek nyilvánítja: a megvalósítás választhat például az utasítás érvénytelenként kezelése, a visszaírás elhagyása vagy meghatározatlan érték írása között. Hordozható kód ezért nem építhet egyikre sem.
| Alak | Tiltott átfedés (ha a bázis nem SP) |
|---|---|
LDR Xt, [Xn, #d]!, LDR Xt, [Xn], #d (és a
többi méretváltozat) |
t = n |
STR Xt, [Xn, #d]!, STR Xt, [Xn], #d |
t = n |
LDP Xt1, Xt2, [Xn, #d]!,
LDP Xt1, Xt2, [Xn], #d |
t1 = n vagy t2 = n |
STP Xt1, Xt2, [Xn, #d]!,
STP Xt1, Xt2, [Xn], #d |
t1 = n vagy t2 = n |
LDP Xt1, Xt2, [...] bármely címzéssel |
t1 = t2 |
Visszaírás nélkül az átfedés szabályos és gyakori: a
LDR X0, [X0, #8] (például láncolt lista bejárása,
p = p->next) jól definiált. A GNU assembler a tiltott
alakokra figyelmeztet, a Clang integrált assemblere hibát ad; a
figyelmeztetés mellett azonban az objektum elkészül, ezért a
build-naplót ellenőrizni kell, vagy -Wa,--fatal-warnings
kapcsolóval hibává kell tenni.
Az A64 LDR W0, [X1, #4] a bázis után négy bájttal
kezdődő adatot olvassa, de az A64 X1 értéke változatlan
marad. Ez alkalmas például ugyanazon objektum több mezőjének
elérésére.
Az eltolás assemblyben bájtban értendő, nem
elemszámban. Egy uint32_t sorozat következő eleméhez ezért
négyet adunk a címhez, nem egyet.
Az A64 LDR W0, [X1, #4]! a megnövelt címen olvas, és ezt
az új címet írja vissza az A64 X1 regiszterbe. A
felkiáltójel nem díszítés: ez jelöli a visszaírást.
C-szemléletű megfeleltetés egy uint32_t *p esetén: előbb
p léptetése, majd az új helyen levő érték olvasása.
Az A64 LDR W0, [X1], #4 a régi címen olvas, majd néggyel
növeli a bázisregisztert. Ez természetes választás előrefelé haladó
adatfolyam feldolgozásához.
Az „előbb” és „utána” itt az utasítás architekturális szemantikáját írja le. Nem a processzor belső időzítésére vonatkozó állítás, és hibás memóriahozzáférésre nem szabad belőle általános visszaírási szabályt következtetni.
Az A64 utasítás nem tartalmazhat tetszőleges méretű eltolást. A fő egészregiszteres alakok korlátai:
| A64 forma | Kódolt mező | Assemblyben megadható bájteltolás |
|---|---|---|
LDR/STR Wt, unsigned offset |
12 bites előjel nélküli érték, négyszeres skálázással | 0…16380, négy többszöröse |
LDR/STR Xt, unsigned offset |
12 bites előjel nélküli érték, nyolcszoros skálázással | 0…32760, nyolc többszöröse |
LDRB/STRB, unsigned offset |
12 bites előjel nélküli érték | 0…4095 |
LDRH/STRH, unsigned offset |
12 bites előjel nélküli érték, kétszeres skálázással | 0…8190, kettő többszöröse |
| Pre-index és post-index | 9 bites előjeles, skálázatlan érték | −256…255, bájtban |
Levezetés: a 12 bites előjel nélküli mező legnagyobb értéke 4095. Nyolcbájtos hozzáférésnél a legnagyobb eltolás ezért bájt. A 9 bites előjeles mező tartománya , ezért a visszaírásos alakok eltolása −256…255 bájt.
Például az A64 LDR X0, [X1, #24] kódolt mezője 3, de az
A64 LDR X0, [X1, #24]! mezője 24. A forrásban mindkettő 24
bájtot jelent.
Negatív vagy a hozzáférési mérettel nem osztható, visszaírás
nélküli eltoláshoz az A64 LDUR/STUR család ad
explicit skálázatlan alakot, ugyancsak −256…255 bájtos tartománnyal.
Egyes assemblerírásmódok automatikusan ezt a kódolást választják. A
tényleges utasítást ezért disassemblyben ellenőrizzük; ne kezeljük a
megengedő szintaxist új ISA-kódolásként.
Ha az index futás közben változik, az eltolás másik regiszterből is származhat:
// A64 ISA; címzési minták.
LDR W0, [X1, X2] // EA = X1 + X2
LDR W0, [X1, X2, LSL #2] // EA = X1 + X2 * 4
LDR X0, [X1, X2, LSL #3] // EA = X1 + X2 * 8A példákban az A64 X2 64 bites eltolást vagy indexet
tartalmaz. A címösszeg képzése nem módosítja sem az A64 X1,
sem az A64 X2 értékét.
Az itt használt A64 LSL indexmódosítás eltolásszáma nem
tetszőleges. Megengedett a nulla, illetve a hozzáférési méret kettes
alapú logaritmusa: négynél 2, nyolcnál 3. A skálázás tehát az elemszámot
bájteltolássá alakítja.
Ez a regiszteres forma nem kombinálható pre-index vagy post-index visszaírással. A tömbbejárás részletes mintái a következő alfejezetben következnek.
Keskeny adat betöltésekor azt is meg kell határozni, hogyan töltődjenek ki az adatregiszter felső bitjei.
| A64 betöltés | Olvasott méret | Kiterjesztés |
|---|---|---|
LDRB Wt, [...] |
1 bájt | Nullákkal 32 bitre |
LDRH Wt, [...] |
2 bájt | Nullákkal 32 bitre |
LDR Wt, [...] |
4 bájt | Nincs 32 biten belüli kiterjesztés |
LDR Xt, [...] |
8 bájt | Nincs kiterjesztés |
LDRSB Wt/Xt, [...] |
1 bájt | Előjellel 32/64 bitre |
LDRSH Wt/Xt, [...] |
2 bájt | Előjellel 32/64 bitre |
LDRSW Xt, [...] |
4 bájt | Előjellel 64 bitre |
Az előjelkiterjesztés (sign extension) a
forrás legfelső bitjét ismétli meg az új felső biteken. A
0x80 bájt előjel nélküli olvasata 128, nyolcbites előjeles
olvasata −128.
Fontos A64-szabály: bármely Wt-be írás nullázza
a megfelelő Xt felső 32 bitjét. Ezért:
LDRSB W10, [...]: W10 = 0xFFFFFF80, de
X10 = 0x00000000FFFFFF80;LDRSB X11, [...]:
X11 = 0xFFFFFFFFFFFFFF80.Az első eredmény 32 bites előjeles olvasatban −128, de a teljes 64 bites regiszter előjeles olvasata pozitív. A második 64 bites előjeles olvasatban is −128.
Tároláskor nincs előjelkiterjesztés: az A64 STRB az
adatregiszter alsó 8, az A64 STRH az alsó 16 bitjét írja
ki.
Munkakönyvtár: ~/arm-labor/a64/a022/ —
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/a022
cd ~/arm-labor/a64/a022A lab-loadstore.s önálló Linux/AArch64 program. A
címeket saját ELF-szimbólumaiból képezi; nem használ rögzített
memóriacímeket. Jelölje
a lab_words,
a lab_output címét.
// A64 ISA, ARMv8-A; GNU as direktívák; little-endian.
.section .text
.global _start
.type _start, %function
_start:
ADR X1, lab_words
LDR W2, [X1, #8] // W2 = 30; X1 = B
LDR W3, [X1, #4]! // W3 = 20; X1 = B+4
LDR W4, [X1], #4 // W4 = 20; X1 = B+8
MOVZ X5, #1
LDR W6, [X1, X5, LSL #2] // W6 = 40; X1 változatlan
ADR X7, lab_output
STR W2, [X7, #8] // D+8 címre 30
STR W3, [X7], #4 // D címre 20; X7 = D+4
STR W6, [X7, #8]! // D+12 címre 40; X7 = D+12
ADR X8, lab_byte
LDRB W9, [X8]
LDRSB W10, [X8]
LDRSB X11, [X8]
ADR X12, lab_half
LDRH W13, [X12]
LDRSH X14, [X12]
ADR X15, lab_signed_word
LDRSW X16, [X15]
lab_observe:
CMP W2, #30
B.NE lab_fail
CMP W3, #20
B.NE lab_fail
CMP W4, #20
B.NE lab_fail
CMP W6, #40
B.NE lab_fail
ADR X17, lab_words
ADD X17, X17, #8
CMP X1, X17
B.NE lab_fail
ADR X17, lab_output
LDR W0, [X17]
CMP W0, #20
B.NE lab_fail
LDR W0, [X17, #4]
CBNZ W0, lab_fail
LDR W0, [X17, #8]
CMP W0, #30
B.NE lab_fail
LDR W0, [X17, #12]
CMP W0, #40
B.NE lab_fail
ADD X17, X17, #12
CMP X7, X17
B.NE lab_fail
MOVZ X0, #0
B lab_exit
lab_fail:
MOVZ X0, #1
lab_exit:
MOVZ X8, #93 // Linux/AArch64: exit
SVC #0
.size _start, .-_start
.section .data
.balign 8
lab_words:
.word 10, 20, 30, 40
lab_output:
.zero 16
lab_byte:
.byte 0x80
.balign 2
lab_half:
.hword 0x8001
.balign 4
lab_signed_word:
.word 0x80000000
.section .note.GNU-stack,"",@progbitsA kiegészítő utasítások (ADR, ADD,
CMP, MOVZ, SVC) konstansai mind
kódolhatók (lásd az A64 immediate-korlátok
összefoglalóját). A labor kis ELF-fájljának szimbólumai várhatóan az
ADR ±1 MiB-os hatótávolságán belül lesznek; az esetleges
tartományhibát a linker jelzi. A példa egyetlen visszaíró utasítása sem
fedi át a bázist és a célregisztert.
Build és futtatás — cél: qemu-user, Linux/AArch64, little-endian.
aarch64-linux-gnu-as -g -EL -march=armv8-a \
-o lab-loadstore.o lab-loadstore.s
aarch64-linux-gnu-ld -EL -e _start \
-o lab-loadstore lab-loadstore.o
aarch64-linux-gnu-objdump -d lab-loadstore
qemu-aarch64 ./lab-loadstore
printf 'kilepesi_allapot=%d\n' "$?"Várt, nem ténylegesen mért eredmény: nincs
programkimenet, a shell kilepesi_allapot=0 sort ír. Az 1
kilépési állapot valamely beépített címzési ellenőrzés hibáját jelzi. A
méretvariánsok eredményeit külön, GDB-vel vizsgáljuk.
Állapot a lab_observe címkénél |
Várt érték |
|---|---|
A64 X1 |
|
A64 X7 |
|
lab_output, négy előjel nélküli 32 bites szóként |
20, 0, 30, 40 |
A64 X9 |
0x0000000000000080 |
A64 X10 |
0x00000000FFFFFF80 |
A64 X11 |
0xFFFFFFFFFFFFFF80 |
A64 X13 |
0x0000000000008001 |
A64 X14 |
0xFFFFFFFFFFFF8001, előjelesen −32767 |
A64 X16 |
0xFFFFFFFF80000000, előjelesen −2147483648 |
Cél: ugyanaz a qemu-user program, GDB-vel. Az első terminálban:
qemu-aarch64 -g 1234 ./lab-loadstoreA másodikban:
gdb-multiarch -q ./lab-loadstore \
-ex 'target remote localhost:1234' \
-ex 'break lab_observe' \
-ex 'continue' \
-ex 'info registers x1 x7 x9 x10 x11 x13 x14 x16' \
-ex 'x/4uw &lab_output'A töréspontnál a táblázat szerinti állapot várható. Az ellenőrzés
után a GDB continue parancsával a programnak 0 állapottal
kell befejeződnie.
Tipikus hibakeresési útvonalak:
LDUR:
ellenőrizd az immediate tartományát és skálázhatóságát az
objektumkódban.Visszaírásos betöltésnél ne használd ugyanazt a regisztert
adatcélként és bázisként, például A64 LDR X1, [X1], #8
alakban. Ez architekturálisan korlátozottan meghatározatlan
(constrained unpredictable) eset; nem hordozható
mutatóléptetési technika.
Az igazítatlan hozzáférés sem feltétlenül okoz hibát. A labor szokásos Linux/QEMU normálmemóriájában gyakran végrehajtható, ezért egy hibás index inkább rossz adatként, mint összeomlásként jelentkezhet.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Az A64 X1 kezdetben
.
Külön-külön ebből az állapotból indulva mi az effektív cím és az új
bázisérték az A64 LDR W0, [X1, #-4]!, illetve
LDR W0, [X1], #-4 esetén?
(implementációs, középszint — cél: rutinépítés) Egészítsd ki a labort a négy bemeneti érték fordított sorrendű összegzésével. Indulj az objektum vége utáni címből, használj pre-index betöltést, és ellenőrizd a 100 eredményt. A kiegészítést a meglévő ellenőrzések után, a sikeres kilépési érték beállítása elé illeszd.
(hibakeresési, középszint — cél: rutinépítés)
Változtasd a regiszteres betöltésben az A64 LSL #2
módosítást LSL #0 alakra. Jósolj eredményt little-endian
bájtsorrend mellett, majd objdump és GDB segítségével keresd meg az
eltérést. Egyetlen módosítással javítsd a hibát.
1. megoldás — Segítség. Nézd meg a fejezet első táblázatát: a pre-index és a post-index közül melyik írja vissza a MÓDOSÍTOTT, melyik az EREDETI bázist — és ez hogyan hat a negatív eltolásra?
1. megoldás — Teljes megoldás. A pre-index alak
effektív címe
,
és az A64 X1 új értéke is
.
A post-index alak effektív címe
,
az új bázisérték viszont szintén
.
A végső mutató tehát azonos, az olvasott adat nem.
2. megoldás — Segítség. Az objektum vége utáni cím ; pre-index betöltéssel minden lépés ELŐBB csökkenti a bázist néggyel, utána olvas — milyen sorrendben éred el így a négy elemet?
2. megoldás — Teljes megoldás. A beilleszthető A64-részlet:
// A64 ISA; a lab-loadstore.s meglévő ellenőrzései után.
ADR X1, lab_words
ADD X1, X1, #16
MOVZ W0, #0
MOVZ W5, #4
lab_reverse:
LDR W2, [X1, #-4]!
ADD W0, W0, W2
SUBS W5, W5, #1
B.NE lab_reverse
CMP W0, #100
B.NE lab_failAz A64 SUBS immediate korlátja az ADD fent
ismertetett korlátjával egyezik. Az effektív címek sorrendje
,
,
,
.
A kezdeti, objektum utáni címet nem dereferáljuk. Az eredeti build–run
lépéssorral a várt kilépési állapot továbbra is 0.
3. megoldás — Segítség. LSL #0 azt
jelenti, hogy az index regiszter értékét byte-egységben, skálázás NÉLKÜL
adja a bázishoz — ha X5 = 1, ez hány bájttal tolja el a
címet a szándékolt 4 helyett?
3. megoldás — Teljes megoldás. Az A64
X1 értéke
,
az A64 X5 értéke 1. Skálázás nélkül ezért a betöltés
címen kezdődik. A négy bájt 0x00, 0x00,
0x00, 0x28, így a várt A64 W6
értéke 0x28000000, nem 40. A hozzáférés az objektumon belül
marad; a szokásos laborbeállítás mellett az ellenőrzés 1 kilépési
állapotot eredményez. Az A64 LSL #2 visszaállítása
helyreállítja a helyes bájteltolást.
Az A64 memóriahozzáférésnél külön kérdés az elért cím, a mozgatott adatmennyiség és a bázisregiszter mellékhatása. Az immediate offset változatlanul hagyja a bázist; a pre-index a módosított, a post-index az eredeti címen fér hozzá. Visszaírásnál a bázisregiszter nem lehet egyben a betöltés vagy tárolás adatregisztere (páros alaknál egyik sem): az ilyen kód viselkedése architekturálisan nem meghatározott. Az eltolások a forrásban bájtban szerepelnek, de kódolásuk lehet méretskálázott vagy skálázatlan. Regiszteres indexnél a megfelelő skálázás elhagyása érvényes címen is hibás adatot eredményezhet. A betöltés szélessége és előjelkezelése egymástól külön döntés. Hibakereséskor ezért a regiszterérték mellett a disassemblyt és a memóriabájtokat is ellenőrizzük.
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 ténylegesen fennmaradnak: a kód összeállítása, linkelése és futtatása itt nem történt meg. A várt állapotok az utasítások szemantikájából levezetettek. A konkrét Ubuntu 24.04 csomagverziókat, az assembler alternatív címzési írásmódjait és a GDB/QEMU együttműködését a hallgatói laborban kell ellenőrizni.