7.1 Load/store címzési módok

7.1.1 Miért számít, mikor változik meg a mutató?

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.

7.1.2 Effektív cím és bázisregiszter

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ása

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

7.1.3 Három immediate címzési mód

Legyen BB a bázisregiszter eredeti értéke, dd 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] B+4B+4 BB
Pre-index LDR W0, [X1, #4]! B+4B+4 B+4B+4
Post-index LDR W0, [X1], #4 BB B+4B+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.

7.1.3.1 Immediate offset: hozzáférés a mutató megváltoztatása nélkül

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.

7.1.3.2 Pre-index: előbb léptetett címet használunk

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.

7.1.3.3 Post-index: az eredeti címen férünk hozzá

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.

7.1.4 Az immediate mezők kódolási korlátai

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 4095⋅8=327604095\cdot8=32760 bájt. A 9 bites előjeles mező tartománya −28…28−1-2^8\ldots2^8-1, 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.

7.1.5 Regiszteres és skálázott eltolás

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 * 8

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

7.1.6 Méretvariánsok és előjelkiterjesztés

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:

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.

7.1.7 Kidolgozott példa: címzés és visszaírás ellenőrzése

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

A 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 BB a lab_words, DD 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,"",@progbits

A 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 B+8B+8
A64 X7 D+12D+12
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

7.1.8 Hibakeresés és ellenpróbák

Cél: ugyanaz a qemu-user program, GDB-vel. Az első terminálban:

qemu-aarch64 -g 1234 ./lab-loadstore

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

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.

7.1.9 Feladatok

  1. (magyarázó, alapszint — cél: megértés-ellenőrzés) Az A64 X1 kezdetben B+8B+8. 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?

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

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

7.1.10 Megoldások

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 B+4B+4, és az A64 X1 új értéke is B+4B+4. A post-index alak effektív címe B+8B+8, az új bázisérték viszont szintén B+4B+4. 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 B+16B+16; 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_fail

Az A64 SUBS immediate korlátja az ADD fent ismertetett korlátjával egyezik. Az effektív címek sorrendje B+12B+12, B+8B+8, B+4B+4, BB. 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 B+8B+8, az A64 X5 értéke 1. Skálázás nélkül ezért a betöltés B+9B+9 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.

7.1.11 Rövid összefoglaló

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.

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