Egy képfeldolgozó rutin, ellenőrzőösszeg-számítás vagy
protokollfeldolgozó rendszer gyakran tömbökön dolgozik. C-ben az
a[i] kifejezés elrejti az elemmérettel történő szorzást.
Assemblyben ezt nekünk kell helyesen kifejeznünk: a processzor
memóriacíme bájtot jelöl, nem tömbelemet.
Ebben az alfejezetben az egydimenziós indexeléstől eljutunk a sorfolytonos kétdimenziós tömbig és egy önellenőrző összegző programig. Előfeltétel a Load/store címzési módok ismerete.
Profil: minden alábbi utasítás és regiszter az
A64 ISA, AArch64, ARMv8-A környezetéhez tartozik. Az
assemblydirektívák GNU as szintaxisúak, AArch64 ELF célhoz.
A labor little-endian, Linux alatt, qemu-aarch64 user-mode
emulációval fut. Cortex-M-kódot itt nem használunk.
A skálázott indexelés (scaled indexing) az elemindexet az elemmérettel megszorozva alakítja bájteltolássá.
Legyen:
Ekkor:
A képlet használatának előfeltétele, hogy i a tömb
határain belül legyen, és a címképzés ne csorduljon túl. A hardver nem
ismeri a C-tömb hosszát: egy hozzáférhető cím még nem feltétlenül a
megfelelő objektumhoz tartozik.
Levezetés: ha az elemméret , akkor az szorzás az index bites balra tolásával állítható elő. Az A64 regisztereltolásos memóriautasítása ezt külön szorzóutasítás nélkül is kifejezheti.
| Elemméret | A64 betöltés / tárolás | Elemindexhez szükséges skálázás | Megengedett LSL eltolás a címzésben |
|---|---|---|---|
| 1 bájt | LDRB Wt / STRB Wt |
0 bit | 0 |
| 2 bájt | LDRH Wt / STRH Wt |
1 bit | 0 vagy 1 |
| 4 bájt | LDR Wt / STR Wt |
2 bit | 0 vagy 2 |
| 8 bájt | LDR Xt / STR Xt |
3 bit | 0 vagy 3 |
Az A64 címzésbe épített eltolás tehát nem tetszőleges
shift. Négybájtos hozzáférésnél például a LSL #1
nem kódolható ebben a címzési formában.
Ha az A64 X1 regiszter egy uint32_t tömb
báziscíme, az X2 pedig az előjel nélküli index:
// A64 ISA: X1 = báziscím, X2 = elemindex.
ldr w3, [x1, x2, lsl #2] // W3 = a[i]
str w3, [x1, x2, lsl #2] // a[i] = W3A hozzáférés négybájtos, a bázisregiszter változatlan marad. Az A64
LDR W3 a hozzá tartozó X3 felső 32 bitjét
nullázza.
Ha az index 32 bites előjel nélküli érték az A64 W2
regiszterben, a megfelelő címzési alak például
LDR W3, [X1, W2, UXTW #2]: előbb nullakiterjesztés történik
64 bitre, majd skálázás. Az index előjelkezelése és a betöltött adat
előjelkezelése két külön kérdés.
Ha minden elemet sorrendben dolgozunk fel, nem szükséges minden iterációban újra előállítani a báziscím és az index összegét. Fenntarthatunk egy aktuális mutatót, amely minden feldolgozás után a következő elemre lép.
A következő C-függvény a labor összegző szakaszának szemantikai referenciája:
#include <stddef.h>
#include <stdint.h>
uint64_t sum_u32(const uint32_t *p, size_t n)
{
uint64_t sum = 0;
while (n != 0) {
sum += *p++;
--n;
}
return sum;
}A megfelelő A64 post-index betöltés:
// A64 ISA: előbb olvasás az eredeti X1 címről, majd X1 += 4.
ldr w3, [x1], #4A #4 bájteltolás, nem négy elem. A
post-index immediate előjeles, 9 bites, ezért −256 és 255 bájt közötti
érték lehet; a négybájtos lépés belefér.
A ciklus állapotát három mennyiség írja le:
A ciklusinvariáns szerint feldolgozott elem után a mutató bájtcímet tartalmaz, a számláló , az akkumulátor pedig az első elem összege. Ez egyszerre segíti a program megírását és a hibakeresést.
Az üres bemenetet az első betöltés előtt kell
felismerni. Ha a nulla számlálót előbb csökkentjük, az A64 64 bites
regiszterében 0xffffffffffffffff keletkezik; ettől egy „nem
nulla” feltételű ciklus nem fog helyesen leállni.
A C valódi kétdimenziós tömbje, például a
uint32_t m[2][3], sorfolytonos elrendezésű
(row-major layout): először az első sor elemei következnek,
majd a második soré.
Ha az oszlopszám, a sorindex, az oszlopindex, akkor:
Itt , és dimenzió nélküli egész, az elemméret bájtban; feltétel a és tartomány, ahol a sorok száma.
Háromoszlopos, négybájtos elemekből álló tömbnél:
| Elem | Lineáris elemindex | Bájteltolás a bázishoz képest |
|---|---|---|
m[0][0] |
0 | 0 |
m[0][2] |
2 | 8 |
m[1][0] |
3 | 12 |
m[1][2] |
5 | 20 |
Az A64 MADD előállíthatja az
kifejezést, majd a skálázott betöltés elvégzi a bájteltolássá alakítást.
Soronkénti feldolgozáskor a belső ciklus négybájtos post-index
lépésekkel járhatja be az adott sort; a következő sor kezdete 12 bájttal
később található.
Ez nem ugyanaz, mint a uint32_t **: a
mutatótömb sorai külön memóriaterületeken is lehetnek, elérésükhöz
további mutatóbetöltés szükséges.
Munkakönyvtár: ~/arm-labor/a64/a023/ —
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/a023
cd ~/arm-labor/a64/a023Mentsük a következő teljes programot lab-array.s néven.
Három műveletet ellenőriz:
m[1][2] elemének elérését;LAB_N elem összegzését.A LAB_N fordításkor megadható, értéke 0–5 lehet. Az A64
X1 a két indexelt művelet során változatlan tömbbázis
marad, majd az összegzés aktuális mutatója lesz.
// A64 ISA, GNU as, AArch64 Linux, little-endian.
.ifndef LAB_N
.equ LAB_N, 5
.endif
.if (LAB_N < 0) || (LAB_N > 5)
.error "LAB_N must be in 0..5"
.endif
.equ LAB_EXPECTED, 5 * LAB_N * (LAB_N + 1)
.section .text
.global _start
.type _start, %function
_start:
adrp x1, lab_array
add x1, x1, :lo12:lab_array
// lab_output[3] = lab_array[3]
mov x2, #3
ldr w3, [x1, x2, lsl #2]
adrp x4, lab_output
add x4, x4, :lo12:lab_output
str w3, [x4, x2, lsl #2]
ldr w5, [x4, x2, lsl #2]
cmp w5, #40
b.ne lab_fail
// lab_matrix[1][2], három oszloppal.
adrp x4, lab_matrix
add x4, x4, :lo12:lab_matrix
mov x5, #1
mov x6, #2
mov x7, #3
madd x6, x5, x7, x6
ldr w3, [x4, x6, lsl #2]
cmp w3, #6
b.ne lab_fail
// Az első LAB_N elem összege.
mov x0, #0
mov x2, #LAB_N
cbz x2, lab_done
lab_loop:
ldr w3, [x1], #4
add x0, x0, x3
subs x2, x2, #1
cbnz x2, lab_loop
lab_done:
cmp x0, #LAB_EXPECTED
b.ne lab_fail
mov x0, #0
b lab_exit
lab_fail:
mov x0, #1
lab_exit:
mov x8, #93
svc #0
.size _start, .-_start
.section .data
.balign 4
lab_array:
.word 10, 20, 30, 40, 50
lab_array_end:
lab_guard:
.word 99
lab_output:
.word 0, 0, 0, 0, 0
lab_matrix:
.word 1, 2, 3
.word 4, 5, 6
.section .note.GNU-stack,"",%progbitsA GNU as .word direktívája itt 32 bites
elemeket helyez el; a .balign 4 négybájtos igazítást
biztosít. A lab_guard külön őrszó, nem része az ötelemű
logikai tömbnek.
A MOV, ADD, SUBS és
CMP konstansai mind kódolhatók (lásd az A64 immediate-korlátok összefoglalóját). A címképző
ADRP 4 KiB-os lapot választ; az utána következő
ADD a cím alsó 12 bitjét adja hozzá. A szimbólumokra
történő címképzést a linker oldja fel, nem kézzel választott
memóriacímeket használunk.
Az A64 LDR W3 nullakiterjesztése miatt az
ADD X0, X0, X3 a uint32_t elemet helyesen adja
a 64 bites előjel nélküli összeghez. Általános bemenetnél ez az összeg
modulo
értendő; a labor kis számainál nincs túlcsordulás.
A kilépés a korábban tanult Linux syscall-konvenciót követi: a 93-as hívásszám Linux AArch64 ABI-előírás, nem az A64 ISA része.
Cél: qemu-user, AArch64 little-endian Linux; host: Ubuntu 24.04 x86_64.
aarch64-linux-gnu-as -EL -march=armv8-a -g \
--defsym LAB_N=5 -o lab-array.o lab-array.s
aarch64-linux-gnu-ld -EL -e _start -o lab-array lab-array.o
qemu-aarch64 ./lab-array
printf 'exit=%d\n' "$?"Várt, nem itt mért kimenet: exit=0. A
program maga nem ír a terminálra; eltéréskor 1-es állapottal lép ki.
A lab_done címkénél, még az ellenőrzés előtt:
| Állapot, A64 környezet | LAB_N=5 |
LAB_N=0 |
|---|---|---|
X0, előjel nélküli összeg |
150 | 0 |
X1, aktuális mutató |
lab_array + 20 |
lab_array |
X2, hátralévő elemszám |
0 | 0 |
lab_output[3] |
40 | 40 |
Az elvárt összeg levezetése: az elemek
,
ezért összegük
.
Ezt számolja ki összeállításkor a LAB_EXPECTED
szimbólum.
Az üres eset külön buildje, ugyanezen qemu-user célhoz:
aarch64-linux-gnu-as -EL -march=armv8-a -g \
--defsym LAB_N=0 -o lab-empty.o lab-array.s
aarch64-linux-gnu-ld -EL -e _start -o lab-empty lab-empty.o
qemu-aarch64 ./lab-empty
printf 'exit=%d\n' "$?"Itt is exit=0 várható. Az összegző
szakasz nem olvas memóriát; az előtte álló indexelési
önellenőrzések továbbra is lefutnak.
Hiányzó skálázás. Ha az A64
LDR W3, [X1, X2, LSL #2] helyett eltolás nélküli alakot
írunk, az index bájteltolássá válik. A 3-as index ekkor nem 12, hanem 3
bájttal lép előre. Ez nem feltétlenül okoz szegmentálási hibát:
hozzáférhető, de hibás helyről olvashatunk.
Rossz hozzáférési szélesség. Az A64
LDR X3, [X1], #4 nyolc bájtot olvasna, miközben csak négy
bájttal léptet. A következő hozzáférés átfedne az előzővel. Előbb a
betöltés szélességét, utána a lépésközt ellenőrizzük.
Előjeles adat nullakiterjesztése.
int32_t elemek 64 bites előjeles összegzéséhez az A64
LDRSW X3, [X1], #4 adja a szükséges előjelkiterjesztést. A
ciklusszerkezet ettől még változatlan lehet.
Egy elemmel túlfutó ciklus (off-by-one error). A program a tömb utáni őrszót is elérheti, ezért a sikeres memóriahozzáférés nem igazolja a helyes ciklushatárt.
A hibakeresés menete: tünet → hipotézis → adatgyűjtés → egyetlen módosítás → új ellenőrzés. Például hibás összeg esetén előbb állapítsuk meg, hány bájttal mozdult el a mutató.
Cél: a qemu-user labor disassembly-vizsgálata és távoli hibakeresése. Az első terminálban:
aarch64-linux-gnu-objdump -d lab-array
qemu-aarch64 -g 1234 ./lab-arrayA második terminálban:
gdb-multiarch -q lab-array \
-ex 'target remote :1234' \
-ex 'break lab_done' \
-ex 'continue' \
-ex 'info registers x0 x1 x2' \
-ex 'p/d $x1 - (unsigned long)&lab_array' \
-ex 'x/6uw &lab_array' \
-ex 'x/5uw &lab_output'Öt elemnél a mutató különbségének várt értéke 20 bájt. A hat
kiolvasott szó rendre 10, 20, 30, 40, 50, 99; az utolsó a
szándékosan elhelyezett őrszó. A GDB continue parancsával a
helyes program 0 állapottal befejeződik.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Egy uint64_t a[7] tömbben
mekkora az a[5] bájteltolása? Milyen skálázás kell az A64
regisztereltolásos betöltéshez? Miért nem használható hozzá
LSL #2?
(implementációs, középszint — cél: rutinépítés)
Készíts lab-array-u64.s változatot kizárólag a post-index
összegzéshez, öt uint64_t elemmel:
10, 20, 30, 40, 50. Módosítsd az adatelhelyezést, az
igazítást és a betöltést. A fenti qemu-user munkafolyamattal,
lab-array-u64.o és lab-array-u64 nevekkel
ellenőrizd a nulla-, egy- és ötelemű esetet.
(hibakeresési, középszint — cél: rutinépítés)
Másold a programot lab-offby.s névre. Kizárólag az A64
CBNZ X2, lab_loop utasítást cseréld
B.PL lab_loop utasításra. Öt elemre fordítsd
lab-offby.o és lab-offby néven. A
lab_done ponton vizsgáld meg az összeget, a mutatóeltolást
és a számlálót. Miért nem szükségszerű a szegmentálási hiba?
1. megoldás — Segítség. Nézd meg a fejezet
táblázatát: nyolcbájtos elemnél melyik LSL eltolások
megengedettek a regisztereltolásos címzésben?
1. megoldás — Teljes megoldás. Az eltolás
bájt. A megfelelő A64 alak LDR X3, [X1, X2, LSL #3].
Nyolcbájtos hozzáférésnél a címzésben megengedett eltolás 0 vagy 3 bit;
a #2 nem egyszerűen hibás elemméretet jelent, hanem ebben
az alakban nem kódolható.
2. megoldás — Segítség. Csak az adatdirektívát (nyolcbájtos elemhez), az igazítást és a betöltés szélességét kell nyolcbájtosra cserélni — a ciklusvezérlés logikája változatlan maradhat.
2. megoldás — Teljes megoldás. A GNU as
adatdirektíva .xword, az igazítás .balign 8. A
ciklus betöltése A64 LDR X3, [X1], #8. A 64 bites
akkumulátor és a számláló kezelése változatlan. Nulla, egy, illetve öt
elem után a várt összeg 0, 10 és 150, a mutatóeltolás pedig 0, 8 és 40
bájt. Mindhárom esetben 0 kilépési állapot várható. A nyolcbájtos lépés
belefér a post-index előjeles 9 bites immediate mezőjébe.
3. megoldás — Segítség. A B.PL nem
ugyanazt a feltételt vizsgálja, mint a CBNZ — mi történik,
ha a számláló előjeles olvasatban még “nem negatív”, de ténylegesen már
0xffffffffffffffff?
3. megoldás — Teljes megoldás. A SUBS
után az A64 B.PL az N=0 feltételt vizsgálja.
Az ötödik elem után a számláló nulla, tehát N=0, és még egy
iteráció következik. Ez beolvassa a 99 értékű őrszót. A várt összeg
249, a mutatóeltolás 24 bájt, a
számláló 0xffffffffffffffff. Az utolsó csökkentés már
N=1 állapotot eredményez, így a ciklus kilép. Az
önellenőrzés 1-es kilépési állapotot ad. Nem szükségszerű a memóriahiba,
mert az őrszó a labor saját, hozzáférhető adatblokkjában található. A
javítás a hátralévő elemszám nemnulla feltételének visszaállítása.
A tömbindex elemben, a memóriacímzés bájtban értendő; a kettőt az elemméret kapcsolja össze. Az A64 skálázott regisztereltolása a hozzáférés méretéhez igazodó címképzést egy memóriautasításba sűríti, de nem végez határellenőrzést. Sorrendi feldolgozásnál a post-index betöltés egyszerre olvassa az aktuális elemet és lépteti a mutatót. Az üres bemenetet az első olvasás előtt kell kezelni. Kétdimenziós C-tömbnél az oszlopszám határozza meg a sor lépésközét. A helyességet az eredmény mellett a mutató és a számláló végállapota is ellenőrzi.
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.
--defsym. Tartalmilag ellenőrizve: a
jelenlegi kiadás binutils 2.47.Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is ténylegesen fennmaradnak: a programokat itt nem állítottuk össze és nem futtattuk. A közölt eredmények levezetett, várt állapotok. Az Ubuntu 24.04 alatt telepített konkrét binutils-, GDB- és QEMU-csomagverziót, valamint a teljes laborfutást a hallgatói környezetben kell rögzíteni és ellenőrizni.