5.2 Egész aritmetika: ADD/SUB/MUL/DIV és a kettes komplemens viselkedése

5.2.1 Mérnöki feladat: egy C-kifejezés gépi műveletei

Egy adatfeldolgozó modulban a következő értéket kell kiszámítani:

(a+7)⋅b−c.(a+7)\cdot b-c.

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.

5.2.2 Definíció: szélességhez kötött, moduláris aritmetika

Moduláris aritmetika (modular arithmetic) alatt itt azt értjük, hogy egy nn bites eredményregiszter csak a matematikai eredmény alsó nn bitjét őrzi meg. Az összeadás ezért:

r=(a+b)mod2n, r=(a+b)\bmod 2^n,

ahol nn a műveleti szélesség bitben, aa, bb és rr dimenzió nélküli egész számok. A64-en az itt tárgyalt egész műveleteknél n=32n=32 vagy n=64n=64.

A kettes komplemens (two’s complement) ábrázolásban egy negatív xx szám nn bites kódja 2n+x2^n+x. Ez ugyanabba a maradékosztályba tartozik, mint xx. Innen következik, hogy az összeadás áramköri megvalósításának nem kell tudnia az előjelről.

5.2.2.1 Levezetés: miért elég ugyanaz az összeadás?

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:

𝟶𝚡𝚏𝟿+𝟶𝚡𝟶𝚊=𝟶𝚡𝟷𝟶𝟹. \mathtt{0xf9}+\mathtt{0x0a}=\mathtt{0x103}.

Az alsó 8 bit 0x03. Előjel nélkül ez (249+10)mod256=3(249+10)\bmod256=3; előjelesen −7+10=3-7+10=3.

Általánosan egy negatív operandus kódjához hozzáadott 2n2^n 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:

r=(a−b)mod2n. r=(a-b)\bmod2^n.

Bitszinten ez megvalósítható a+∼b+1a+\mathord{\sim}b+1 összeadásként, ahol a bitenkénti invertálást nn 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 2642^{64} 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.

5.2.3 Az A64 ADD és SUB operandusformái

A 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, #57

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

5.2.3.1 Közvetlen operandus: 12 bit és opcionális eltolás

Az ADD/SUB közvetlen operandusa (immediate operand) nem tetszőleges egész szám. Kódolása:

k=imm12⋅2s,0≤imm12≤4095,s∈{0,12}. k=\mathrm{imm12}\cdot2^s, \qquad 0\leq\mathrm{imm12}\leq4095, \qquad s\in\{0,12\}.

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

Az ábrázolható értékek tehát:

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.

5.2.3.2 Eltolt regiszteroperandus

A regiszteres forma második forrása eltolható:

// A64 ISA, AArch64
ADD X3, X0, X1, LSL #3       // X3 = X0 + (X1 << 3), modulo 2^64

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

5.2.4 Szorzás és osztás: hol számít az előjel?

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ó nn bites eredménye előjeles és előjel nélküli operandusokra is azonos. Ennek oka ismét a modulo 2n2^n aritmetika. Ez az állítás nem vonatkozik automatikusan a teljes, 2n2n 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 r=a−qbr=a-qb összefüggéssel, külön szorzással és kivonással képezhetjük.

Két fontos, ISA által meghatározott szélső eset:

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.

5.2.5 Kidolgozott példa: kézi fordítás és állapotellenőrzés

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

A 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,"",%progbits

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

5.2.5.1 Fordítás és futtatás

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.

5.2.5.2 Regiszterek vizsgálata

Első terminál, qemu-user cél:

qemu-aarch64 -g 1234 ./lab-a015

Má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.

5.2.6 Tipikus hibák és ellenpróbák

5.2.7 Feladatok

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

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

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

  4. (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?

5.2.8 Megoldások

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 (255+2)mod256=1(255+2)\bmod256=1; előjelesen −1+2=1-1+2=1. 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, X14

Segédregiszter nélkül:

// A64 ISA, AArch64
ADD X3, X0, #1, LSL #12
ADD X3, X3, #904

Mindkettő 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, W15

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

5.2.9 Rövid összefoglaló

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.

5.2.10 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ó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.