Egy adatfeldolgozó modulban a következő értéket kell kiszámítani:
C-ben ez egyetlen kifejezés. Assemblyben nekünk kell kijelölni a részeredmények helyét, kiválasztani a műveleti szélességet, és eldönteni, hogyan értelmezzük a túlcsorduló eredményt. Ugyanilyen fontos kérdés: mi történik, ha később az eredményt negatív számmal vagy nullával osztjuk?
Hatókör: az alfejezet minden utasítása és regisztere
az A64 ISA, AArch64, ARMv8-A alapprofil környezetéhez
tartozik. Az assemblyblokkok GNU assembler-szintaxist használnak;
direktíváik ebben az AArch64-eszközláncban értendők. A labor
little-endian Linux-program, célja qemu-aarch64 user-mode
emuláció. Cortex-M-utasítás itt nem szerepel.
Előfeltétel a MOV-variánsok és
immediate-kódolás ismerete. Most nem az állapotjelzőket vizsgáljuk:
az itt használt ADD, SUB, MUL,
UDIV és SDIV nem módosítja az
NZCV állapotjelzőket. Ezekkel Az NZCV
állapotjelzők és eredetük foglalkozik.
Moduláris aritmetika (modular arithmetic) alatt itt azt értjük, hogy egy bites eredményregiszter csak a matematikai eredmény alsó bitjét őrzi meg. Az összeadás ezért:
ahol a műveleti szélesség bitben, , és dimenzió nélküli egész számok. A64-en az itt tárgyalt egész műveleteknél vagy .
A kettes komplemens (two’s complement) ábrázolásban egy negatív szám bites kódja . Ez ugyanabba a maradékosztályba tartozik, mint . Innen következik, hogy az összeadás áramköri megvalósításának nem kell tudnia az előjelről.
Legyen egy szemléltető, 8 bites példában az egyik operandus −7, a másik 10. Ez nem külön 8 bites A64-utasítás, csak rövid bitszemléltetés.
| Bitminta | Előjel nélküli olvasat | Előjeles olvasat |
|---|---|---|
0xf9 |
249 | −7 |
0x0a |
10 | 10 |
Eredmény: 0x03 |
3 | 3 |
A bitminták összege:
Az alsó 8 bit 0x03. Előjel nélkül ez
;
előjelesen
.
Általánosan egy negatív operandus kódjához hozzáadott
a végső modulo műveletben eltűnik. Ugyanaz az ADD
utasításkód és ugyanazok az operandusbitminták ugyanazt az eredményt
adják, függetlenül az előjeles vagy előjel nélküli
értelmezéstől.
A kivonás hasonlóan:
Bitszinten ez megvalósítható összeadásként, ahol a bitenkénti invertálást bitre értjük.
Ez azonban nem teszi azonossá az előjeles és előjel nélküli
tartományellenőrzést. Nyolcbites szemléltetésben
0x7f + 1 = 0x80: előjel nélkül 128, előjelesen −128,
miközben a matematikai összeg 128.
Kapcsolat C-vel: a uint64_t aritmetika
modulo
működik. Az előjeles C-összeadás, -kivonás és -szorzás
tartománytúllépése viszont nem definiált viselkedés (undefined
behavior). Az assembly kiszámítható körbefordulása nem jogosít fel
arra, hogy ugyanezt egy túlcsorduló előjeles C-kifejezéstől is
elvárjuk.
ADD és
SUB operandusformáiA célregiszter az első operandus; a másik kettő a bemenet:
// A64 ISA, AArch64: egymástól független szintaktikai példák.
ADD X3, X0, X1 // X3 = X0 + X1, modulo 2^64
SUB W3, W0, W1 // W3 = W0 - W1, modulo 2^32
ADD X4, X0, #7
SUB X5, X4, #57A Wn és Xn szélességeket egy ilyen
utasításon belül nem keverjük. A W3 írása az egész
X3 felső 32 bitjét nullázza; nem végez
előjelkiterjesztést.
Az ADD/SUB közvetlen operandusa
(immediate operand) nem tetszőleges egész szám. Kódolása:
A konstans és az eltolás dimenzió nélküli; az eltolás mértéke bit.
// A64 ISA, AArch64: minden sor külön példa.
ADD X3, X0, #4095
ADD X3, X0, #1, LSL #12 // hozzáadott érték: 4096
SUB X3, X0, #4095, LSL #12 // kivont érték: 16773120Az ábrázolható értékek tehát:
0xfff000
értékig.Nem egy összefüggő, 24 bites konstansmezőről van szó. Az 5000 például nem fér el ebben az immediate-formában. Betölthetjük külön regiszterbe, vagy felbonthatjuk 4096 és 904 összegére.
Negatív immediate helyett a szándékot egyértelműen az ellenkező
művelettel írjuk: 7 hozzáadása helyett −7 hozzáadása legyen
SUB ..., #7. Egyes assembler-átírások elfogadása nem
változtatja meg az ISA kódolási korlátját.
A regiszteres forma második forrása eltolható:
// A64 ISA, AArch64
ADD X3, X0, X1, LSL #3 // X3 = X0 + (X1 << 3), modulo 2^64Az itt tárgyalt shifted register forma LSL,
LSR vagy ASR eltolást enged. Az eltolás 32
bites műveletnél 0–31, 64 bitesnél 0–63 bit. Ez más
kódolás, mint az immediate-forma kizárólag 0 vagy 12 bites
eltolása. Az eltolások részletes szemantikáját a Shift és rotate tárgyalja.
Az A64 alaputasítások:
| A64-utasítás | Jelentés | Operanduskorlát |
|---|---|---|
MUL Xd, Xn, Xm |
A szorzat alsó 64 bitje | Csak regiszterek |
MUL Wd, Wn, Wm |
A szorzat alsó 32 bitje | Csak regiszterek |
UDIV Xd, Xn, Xm |
Előjel nélküli egész hányados | Csak regiszterek |
SDIV Xd, Xn, Xm |
Előjeles egész hányados | Csak regiszterek |
Az osztásoknak is van Wn operandusú, 32 bites
változatuk. Nincs ezekhez általános immediate-forma: a
MUL X3, X0, #9 nem érvényes utasítás. A 9-et előbb
regiszterbe kell tölteni.
A MUL alsó
bites eredménye előjeles és előjel nélküli operandusokra is azonos.
Ennek oka ismét a modulo
aritmetika. Ez az állítás nem vonatkozik automatikusan
a teljes,
bites szorzat felső felére. A MUL valójában a
MADD nulla összeadandóval használt aliasa; használatához
most elegendő a háromoperandusú alak.
Az osztás más: az előjeles értelmezés megváltoztatja a hányadost.
Csonkolás nulla felé (truncation toward
zero): az SDIV a matematikai hányados törtrészét
elhagyja, nem lefelé kerekít.
| Művelet | A64-utasítás | Várt hányados |
|---|---|---|
| 19 / 9, előjel nélkül | UDIV |
2 |
| −19 / 9, előjelesen | SDIV |
−2 |
| 19 / −9, előjelesen | SDIV |
−2 |
| −19 / −9, előjelesen | SDIV |
2 |
A hányados nem tartalmazza a maradékot. Ha szükséges, azt a összefüggéssel, külön szorzással és kivonással képezhetjük.
Két fontos, ISA által meghatározott szélső eset:
UDIV és SDIV nulla osztó mellett 0
eredményt ad; az utasítás nem vált ki egészosztási kivételt.SDIV legkisebb előjeles érték / −1 esetén a
legkisebb előjeles érték bitmintáját adja vissza. Például 32 biten
0x80000000 / 0xffffffff eredménye
0x80000000.C-ben a nullával osztás, illetve az adott előjeles eredménytípusban nem ábrázolható hányados nem definiált viselkedés. Ezeket ne használjuk C-referenciatesztként.
Munkakönyvtár: ~/arm-labor/a64/a015/ —
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/a015
cd ~/arm-labor/a64/a015A számítás C-beli specifikációja legyen a következő,
lab-a015-ref.c fájlban:
#include <stdint.h>
uint64_t lab_expr(uint64_t a, uint64_t b, uint64_t c)
{
return (a + UINT64_C(7)) * b - c;
}A bemeneteket a laborban közvetlenül regiszterekbe tesszük. Ez még nem C-ből hívható függvény, hanem önálló Linux-program.
Fájl: lab-a015.s.
// A64 ISA, ARMv8-A; GNU as; Linux; little-endian.
.text
.global _start
.global lab_check
.type _start, %function
_start:
MOVZ X0, #19
MOVZ X1, #9
MOVZ X2, #57
ADD X3, X0, #7
MUL X4, X3, X1
SUB X5, X4, X2
UDIV X6, X5, X1
MOVN X7, #18 // ~18 = -19, 64 bites kettes komplemens
SDIV X9, X7, X1
MOVN W10, #6 // 0xfffffff9: előjelesen -7
MOVZ W11, #10
ADD W12, W10, W11
UDIV X13, X5, XZR // nulla osztó: eredmény 0
lab_check:
MOV X0, X5 // Linux kilépési állapot: 177
MOVZ X8, #93 // Linux AArch64 exit syscall
SVC #0
.size _start, .-_start
.section .note.GNU-stack,"",%progbitsA használt MOVZ/MOVN és SVC
immediate-ok mind kódolhatók (lásd az A64
immediate-korlátok összefoglalóját); a Linux-rendszerhívás száma az
X8 regiszterben van. Az XZR az A64
nullaregisztere.
A kézi leképezés függőségi lánca: X3 = 26, majd
X4 = 234, végül X5 = 177. Nem kell és nem is
szabad minden részeredményt memóriába kiírni csak azért, mert C-ben egy
összetett kifejezésből indultunk.
Az alábbi parancsok célja AArch64 Linux, qemu-user, Ubuntu 24.04 x86_64 hostról:
# A C-specifikáció külön fordításellenőrzése; nem része az executable-nek.
aarch64-linux-gnu-gcc -std=c17 -Wall -Wextra -O2 -march=armv8-a \
-c lab-a015-ref.c -o lab-a015-ref.o
# Assembly -> objektum -> önálló ELF -> qemu-user.
aarch64-linux-gnu-as -g -EL -march=armv8-a \
-o lab-a015.o lab-a015.s
aarch64-linux-gnu-ld -EL -e _start -o lab-a015 lab-a015.o
aarch64-linux-gnu-objdump -d lab-a015
qemu-aarch64 ./lab-a015
lab_status=$?
printf 'exit=%s\n' "$lab_status"Levezetett, nem ténylegesen futtatott elvárás: nincs
programkimenet; a shell exit=177 sort ír ki. Ez szándékos,
nem hibát jelző laboreredmény. A shellben megfigyelhető kilépési kód
csak az alsó 8 bit, ezért nagyobb eredmények ellenőrzésére debugger
szükséges.
Első terminál, qemu-user cél:
qemu-aarch64 -g 1234 ./lab-a015Második terminál, ugyanennek az emulált AArch64-programnak a vizsgálata:
gdb-multiarch -q ./lab-a015 \
-ex 'target remote localhost:1234' \
-ex 'break lab_check' \
-ex 'continue' \
-ex 'info registers x3 x4 x5 x6 x7 x9 x10 x12 x13' \
-ex 'p/d $x9'A lab_check címkénél, a kilépési kód beállítása
előtt:
| A64-regiszter | Várt bitminta | Értelmezés |
|---|---|---|
X3 |
0x1a |
26 |
X4 |
0xea |
234 |
X5 |
0xb1 |
177 |
X6 |
0x13 |
Előjel nélküli hányados: 19 |
X7 |
0xffffffffffffffed |
Előjelesen −19 |
X9 |
0xfffffffffffffffe |
Előjelesen −2 |
X10 |
0x00000000fffffff9 |
A W10 írása nullázta a felső felet |
X12 |
0x3 |
32 bites összeadás eredménye: 3 |
X13 |
0x0 |
Nulla osztó eredménye |
A vizsgálat után GDB-ben a continue futtatja le a
kilépést; a QEMU-folyamat várt kilépési állapota ismét 177.
objdump kimenetét.SDIV helyett UDIV szerepel. GDB-ben
először hexadecimálisan vizsgáljuk meg a bemenetet, majd csak az osztási
utasítást változtassuk.W10 írása után X10 nem −7. Ne keverjük a
műveleti szélességeket; a labor szándékosan W10-zel
számol.lab_check ponton, és hasonlítsuk össze a teljes
regisztertáblát.(magyarázó, alapszint — cél:
megértés-ellenőrzés) A 8 bites szemléltető modellben számítsd
ki 0xff + 0x02 eredményét. Add meg mindkét operandus
előjeles és előjel nélküli értelmezését, és magyarázd meg az azonos
eredménybitmintát.
(implementációs, középszint — cél: rutinépítés)
A labor ADD X3, X0, #7 sorát helyettesítsd úgy, hogy
X3 = X0 + 5000 legyen. Adj meg egy új segédregisztert
használó és egy segédregiszter nélküli változatot. Mennyi lesz
X5, és mit mutat a shell kilépési kódja?
(hibakeresési, középszint — cél: rutinépítés)
Cseréld a labor SDIV utasítását UDIV-re. A
korábbi parancsokkal építsd újra és vizsgáld meg. Mely ellenőrzés jelzi
a hibát, és miért nem jelzi a kilépési kód?
(szélsőérték-próba, középszint — cél:
megértés-ellenőrzés) Egészítsd ki a labort a 32 bites legkisebb
előjeles érték −1-gyel történő osztásával. A lab_check
előtt ellenőrizd a hányadost. Miért nem megfelelő hozzá egy túlcsorduló
int32_t C-kifejezés referenciaként?
1. megoldás — Segítség. Add össze a két bitmintát hexadecimálisan, majd nézd meg, mi marad a 8 biten belül — az eredmény bitmintája ugyanaz marad, csak az OLVASAT (előjeles/előjel nélküli) változik.
1. megoldás — Teljes megoldás. Az összeg
0x101, amelyből 8 biten 0x01 marad. Előjel
nélkül
;
előjelesen
.
Az előjel az értelmezést változtatja meg, nem az összeadó bemeneti
bitmintáit.
2. megoldás — Segítség. Nézd meg az immediate-kódolási szabályt: 5000 nem fér bele a 12 bites, 0/12 eltolású mezőbe egyben — bontsd fel a 4096 többszöröse és egy 0–4095 közötti maradék összegére.
2. megoldás — Teljes megoldás. Segédregiszterrel:
// A64 ISA, AArch64
MOVZ X14, #5000 // 16 bites immediate, eltolás nélkül
ADD X3, X0, X14Segédregiszter nélkül:
// A64 ISA, AArch64
ADD X3, X0, #1, LSL #12
ADD X3, X3, #904Mindkettő 5019-et állít elő. Ezután
X5 = 5019*9-57 = 45114, míg a shellben
45114 mod 256 = 58 jelenik meg. A két utasításos
változatokból önmagában nem következik azonos hardveres futási idő.
3. megoldás — Segítség. Az X7
bitmintáját UDIV nagy pozitív számként olvassa — mi
történik egy ilyen nagyságú előjel nélküli osztással 9-cel, és ez hogyan
jelenik meg X9-ben, de NEM jelenik meg a kilépési kódban
(ami X5-ből jön)?
3. megoldás — Teljes megoldás. Az X7
bitmintáját az UDIV nagy pozitív, előjel nélküli számként
értelmezi. X9 ezért nem 0xfffffffffffffffe
lesz. A GDB-s regiszterellenőrzés és az objdump
utasításlistája kimutatja a hibát. A kilépési állapot változatlanul 177,
mert azt X5 adja, nem X9. Egyetlen javításként
állítsuk vissza az SDIV-et, majd ismételjük meg az
ellenőrzést.
4. megoldás — Segítség. A legkisebb előjeles 32
bites érték 0x80000000; ennek -1-gyel osztott
matematikai hányadosa nagyobb, mint a legnagyobb ábrázolható pozitív 32
bites előjeles szám — mire utal ez az ISA-szabály (lásd a fejezet “két
fontos szélső eset” pontját)?
4. megoldás — Teljes megoldás. A
lab_check címke elé illeszthető:
// A64 ISA, AArch64
MOVZ W14, #0x8000, LSL #16
MOVN W15, #0
SDIV W16, W14, W15A MOVZ 16 bites mezője itt 16 bittel eltolt. A várt
W16 érték 0x80000000; a teljes
X16 érték 0x0000000080000000. Az előjeles
matematikai hányados 2147483648, amely 32 bites előjeles eredményként
nem ábrázolható. A megfelelő C-osztás emiatt nem definiált viselkedésű
lenne, nem megbízható referencia.
Az A64 egész aritmetikájában először a műveleti szélességet
választjuk ki. Az összeadás, kivonás és az alsó szorzatbitet megtartó
szorzás ugyanazzal az utasítással szolgálja az előjeles és előjel
nélküli értelmezést. Osztáskor viszont külön UDIV és
SDIV szükséges. Az ADD/SUB
immediate-ja korlátozott: 12 bites mező, opcionálisan 12 bites balra
tolással. A C-kifejezés kézi fordításánál a részeredményeket és az
adatfüggőségeket követjük. A regiszterállapot erősebb ellenőrzés, mint a
csonkolt kilépési kód. Az ISA meghatározott viselkedését mindig
különválasztjuk a C nem definiált eseteitől.
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.
ADD,
SUB, MADD/MUL, UDIV
és SDIV leírásai. Ellenőrizve: a link él; a
fejezetszám és a kiadásbetű az Arm-oldalak közös korlátja miatt nem
ellenőrzött.Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is ténylegesen fennmaradnak: a kódot itt nem fordítottuk és nem futtattuk. A kimenetek és regiszterértékek ISA-szemantikából levezetett elvárások. A helyi Ubuntu 24.04 csomagverziókat, a parancsok elfogadását és a debugger megjelenítését a hallgatói laborban kell ellenőrizni; teljesítménymérést ez a gyakorlat nem tartalmaz.