5.3 Az NZCV állapotjelzők és eredetük

5.3.1 Miért kell az eredmény mellé négy további bit?

Egy előjel nélküli számláló növelésekor nemcsak az új érték érdekes, hanem az is, hogy a számláló átfordult-e nullára. Előjeles koordináták összeadásakor más kérdés merül fel: belefér-e a matematikai eredmény az ábrázolható tartományba? A két probléma ugyanahhoz az összeadóhoz kapcsolódik, de nem ugyanazt a jelzést igényli.

Az Egész aritmetika: ADD/SUB/MUL/DIV és a kettes komplemens viselkedése alfejezetben az eredmény alsó bitjeit vizsgáltuk. Most az eredmény mellett keletkező állapotjelzőket (condition flags) vezetjük le.

Architektúra és hatókör: az alfejezet valamennyi utasítása, regisztere és assembly példája A64 ISA, AArch64, ARMv8-A környezetre vonatkozik. A direktívák GNU assemblerhez, AArch64 ELF-célhoz tartoznak; nem processzorutasítások. A labor little-endian Linux-program, qemu-aarch64 user-mode céllal. Cortex-M-szabályokat itt nem használunk.

5.3.2 Az NZCV jelentése és a műveleti szélesség

Az A64 architekturális processzorállapotának, a PSTATE-nek négy jelzőjét együtt NZCV néven említjük. Egész összeadásnál és kivonásnál:

A64-jelző Név Jelentés
N negatív, negative A műveleti szélesség szerinti eredmény legfelső bitje
Z nulla, zero 1, ha a teljes, adott szélességű eredmény nulla
C átvitel, carry Összeadásnál a legfelső biten túli átvitel; kivonásnál a kölcsönzés hiánya
V előjeles túlcsordulás, overflow 1, ha a matematikai előjeles eredmény nem ábrázolható az adott szélességen

Az A64 ADDS és SUBS a számítás mellett mind a négy jelzőt frissíti. Az A64 ADD és SUB ezzel szemben változatlanul hagyja őket. Az S tehát itt nem „signed”, hanem a jelzőfrissítésre utal.

A műveleti szélességet az operandusok határozzák meg:

Az A64 Wn célregiszter írása továbbra is nullázza a hozzá tartozó Xn felső 32 bitjét. Ettől azonban az N még lehet 1: a jelzők a műveleti szélességet követik, nem a nullakiterjesztett 64 bites tároló legfelső bitjét.

A jelzők nem hordoznak C-típust. Ugyanaz az összeadás egyszerre képezi az előjel nélküli értelmezéshez használható C-t és az előjeles értelmezéshez használható V-t.

5.3.3 Levezetés: hogyan keletkezik a carry?

Legyen nn a műveleti szélesség bitben, aa és bb pedig két nn bites operandus előjel nélküli, dimenzió nélküli értéke. A matematikai összeg és a tárolt eredmény:

t=a+b,r=tmod2n. t=a+b,\qquad r=t\bmod 2^n.

Mivel 0≤a,b≤2n−10\leq a,b\leq 2^n-1, az összeg legfeljebb egy további bitet igényel:

C={1,t≥2n,0,t<2n. C= \begin{cases} 1,&t\geq 2^n,\\ 0,&t<2^n. \end{cases}

Ez architekturális eredményszabály, nem állítás arról, milyen kapuhálózatot használ egy konkrét processzor.

A bitszintű levezetéshez legyen aia_i, bib_i és rir_i az operandusok, illetve az eredmény ii-edik bitje. A cic_i az adott bithelyre belépő átvitel; egyszerű összeadásnál c0=0c_0=0:

ri=ai⊕bi⊕ci, r_i=a_i\oplus b_i\oplus c_i,

ci+1=(ai∧bi)∨(ai∧ci)∨(bi∧ci). c_{i+1}=(a_i\land b_i)\lor(a_i\land c_i)\lor(b_i\land c_i).

A kimenő átvitel akkor 1, ha a három bemeneti bit közül legalább kettő 1. A végső C jelző cnc_n.

Vizsgáljuk az alábbi 32 bites A64 műveletet:

Bithely Első operandus bitje Második operandus bitje Belépő átvitel Eredménybit Kilépő átvitel
0 1 1 0 0 1
1–30, mindegyik külön 1 0 1 0 1
31 1 0 1 0 1

Az átvitel végighalad az összes bithelyen. A matematikai összeg 0x100000000, de a 32 bites eredmény 0x00000000. Ezért:

Előjeles kettes komplemens olvasatban viszont ugyanez −1 + 1 = 0, ami ábrázolható. Így V = 0, a teljes NZCV pedig 0b0110.

A carry nem azonos az előjeles túlcsordulással.

5.3.4 Levezetés: mikor keletkezik overflow?

nn bites kettes komplemens ábrázolásban az előjeles tartomány:

−2n−1≤x≤2n−1−1. -2^{n-1}\leq x\leq 2^{n-1}-1.

Különböző előjelű számok összeadásakor az eredmény ebben a tartományban marad. Túlcsordulás akkor lehetséges, ha:

  1. a két operandus előjele azonos;
  2. a tárolt eredmény előjele ettől eltér.

Legyen sas_a, sbs_b és srs_r a két operandus, illetve az eredmény előjelbitje. Ekkor összeadásra:

V=¬(sa⊕sb)∧(sa⊕sr). V=\neg(s_a\oplus s_b)\land(s_a\oplus s_r).

Konkrét 32 bites A64 példa:

A matematikai eredmény 2147483648 lenne, amely nem fér el előjeles 32 biten.

Bitszinten a 0. bithelyen átvitel keletkezik, amely végighalad az 1–30. bithelyeken. Az előjelbithez tehát c31=1c_{31}=1 érkezik. Ott viszont:

0+0+1=1, 0+0+1=1,

így az eredmény előjelbitje 1, a kilépő átvitel pedig c32=0c_{32}=0. Következésképpen C = 0, miközben V = 1.

Az overflow másik, ezzel egyenértékű alakja:

V=cn−1⊕cn. V=c_{n-1}\oplus c_n.

Az indoklás az előjelbit összeadásából következik:

Az előző carry-példában c31=c32=1c_{31}=c_{32}=1, tehát V = 0. A mostani példában különböznek, ezért V = 1.

Az alábbi elméleti, ISA-szemantikából levezetett, 32 bites A64 eredmények mind a négy C/V-kombinációt megmutatják:

Operandusok bitmintája Eredmény bitmintája C V NZCV
0x00000001 + 0x00000001 0x00000002 0 0 0b0000
0xffffffff + 0x00000001 0x00000000 1 0 0b0110
0x7fffffff + 0x00000001 0x80000000 0 1 0b1001
0x80000000 + 0x80000000 0x00000000 1 1 0b0111

Az utolsó sor előjeles olvasata −2147483648 + −2147483648: a matematikai eredmény túl kicsi. Előjel nélkül ugyanakkor az összeg pontosan 2322^{32}, ezért carry is keletkezik.

C-ben az előjel nélküli összeadás moduláris viselkedése meghatározott. A túlcsorduló előjeles összeadás viszont nem definiált viselkedés: egy C-programban nem szabad előbb túlcsorduló előjeles összeadást végezni, majd az eredmény előjeléből biztonságot ellenőrizni. Az ISA szabálya nem írja felül a C nyelv szabályát.

5.3.5 Kivonás és összehasonlítás: a C jelző itt nem „borrow”

Az A64 SUBS is frissíti az NZCV-t. Kivonáskor azonban C = 1 azt jelenti, hogy nem kellett kölcsönözni (no borrow).

Ennek levezetéséhez az nn bitre korlátozott bitenkénti komplementer értéke 2n−1−b2^n-1-b. Az összeadóval végzett kivonás:

a+NOTn(b)+1=a+(2n−1−b)+1=a+2n−b. a+\operatorname{NOT}_n(b)+1 =a+(2^n-1-b)+1 =a+2^n-b.

Ez akkor éri el 2n2^n értékét, ha a≥ba\geq b. Következésképpen, előjel nélküli olvasatban:

A 32 bites 0 − 1 eredménye 0xffffffff, C = 0, V = 0. A 0 − 0 eredménye nulla, C = 1.

Kivonásnál az előjeles overflow feltétele:

V=(sa⊕sb)∧(sa⊕sr). V=(s_a\oplus s_b)\land(s_a\oplus s_r).

Itt eltérő előjelű operandusoknál lehet túlcsordulás. Az N önmagában ezért nem általános előjeles összehasonlítási eredmény; a feltételkódok ezt a V figyelembevételével kezelik majd a Feltételes végrehajtás: CSET/CSEL és feltételkódok alfejezetben.

Az A64 CMP alias, nem külön összehasonlító számítás:

// A64 ISA: a két sor azonos utasításkódolást jelent.
CMP  W1, W2
SUBS WZR, W1, W2

Az A64 WZR nullaregiszterbe írt eredmény elvész, de a jelzők frissülnek. A CMP nem módosítja az operandusait. Az A64 64 bites változatában Xn operandusok és XZR szerepelnek.

Immediate-korlát: az immediate-os ADDS/SUBS és a CMP ugyanazt a mezőt használja, mint az ADD/SUB (lásd az A64 immediate-korlátok összefoglalóját); tetszőleges 32 vagy 64 bites konstans közvetlenül nem írható beléjük.

5.3.6 Összefoglaló táblázat: mely utasítások írják az NZCV-t?

A könyv további alfejezetei erre a táblázatra hivatkoznak, ha az NZCV-t kell követni. A szabály egyszerű: az A64-ben csak a kifejezetten jelzőállító utasítások írják az NZCV-t — a legtöbb ilyen nevének S a végződése, illetve összehasonlító alias (CMP, CMN, TST). Minden más utasítás változatlanul hagyja, akkor is, ha az eredménye nulla vagy negatív.

Csoport A64-utasítások Hatás az NZCV-re
Aritmetika jelzőállítással ADDS, SUBS, ADCS, SBCS, NEGS, NGCS; aliasok: CMP (= SUBS nullaregiszterbe), CMN (= ADDS nullaregiszterbe) mind a négy jelzőt írja az eredmény szerint
Logika jelzőállítással ANDS, BICS; alias: TST (= ANDS nullaregiszterbe) N és Z az eredmény szerint; C = 0, V = 0
Feltételes összehasonlítás CCMP, CCMN ha a feltétel teljesül, az összehasonlítás szerint; különben az utasításban megadott #nzcv értéket írja
Lebegőpontos összehasonlítás FCMP, FCMPE az összehasonlítás szerint (rendezetlen eredménynél 0011); lásd a skaláris lebegőpontos utasításokat
Közvetlen írás MSR NZCV, Xn a regiszter 31–28. bitjét írja be
Nem írja ADD, SUB, ADC, SBC, NEG, MUL, MADD, MSUB, SDIV, UDIV, AND, ORR, EOR, BIC, MVN, LSL/LSR/ASR/ROR, MOV/MOVZ/MOVK/MOVN, bitfield-utasítások, LDR/STR és társaik, ADR/ADRP, B, BL, BR, BLR, RET, CBZ/CBNZ, TBZ/TBNZ változatlanul hagyja
Olvassa, de nem írja B.cond, CSEL, CSET, CSINC, CSINV, CSNEG, ADC, SBC (a C jelzőt) a jelzőkből dönt vagy számol, de nem módosítja őket

Két következmény, amelyre a későbbi alfejezetek építenek:

5.3.7 Kidolgozott labor: az NZCV pillanatfelvétele

Munkakönyvtár: ~/arm-labor/a64/a016/ — 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/a016
cd ~/arm-labor/a64/a016

A program minden vizsgált művelet után kiolvassa a jelzőket. Az A64 MRS Xn, NZCV az NZCV rendszerregiszteren keresztül másolja őket általános célú regiszterbe, és maga nem módosítja őket.

A64 NZCV kiolvasott bithelye Tartalom
31 N
30 Z
29 C
28 V
Többi bit 0

Így például az NZCV = 0b0110 kiolvasott értéke 0x0000000060000000, nem 0x6. Ezek regiszterértékek, nem memóriacímek.

Mentsük a következő teljes forrást lab-nzcv.s néven:

// A64 ISA, ARMv8-A; GNU as, AArch64 Linux ELF.
.text
.global _start
.global lab_inspect
.type _start, %function

_start:
    // A64 A: 0xffffffff + 1, 32 bit.
    MOVZ W0, #0xffff
    MOVK W0, #0xffff, LSL #16
    MOVZ W1, #1
    ADDS W2, W0, W1
    ADD  W9, W1, W1       // Nem modosítja az NZCV-t.
    MRS  X10, NZCV

    // A64 B: 0x7fffffff + 1, 32 bit.
    MOVZ W0, #0xffff
    MOVK W0, #0x7fff, LSL #16
    ADDS W3, W0, W1
    MRS  X11, NZCV

    // A64 C: 0x80000000 + 0x80000000, 32 bit.
    MOVZ W0, #0x8000, LSL #16
    ADDS W4, W0, W0
    MRS  X12, NZCV

    // A64 D: 1 + 1, 32 bit.
    ADDS W5, W1, W1
    MRS  X13, NZCV

    // A64 E: 0 - 1, 32 bit.
    SUBS W6, WZR, W1
    MRS  X14, NZCV

    // A64 F: osszehasonlitas; W1 erteke nem valtozik.
    CMP  W1, W1
    MRS  X15, NZCV

lab_inspect:
    // A64 Linux exit(0).
    MOVZ W0, #0
    MOVZ W8, #93
    SVC  #0

.size _start, .-_start
.section .note.GNU-stack,"",%progbits

A konstansépítő MOVZ/MOVK és az SVC #0 immediate-jai kódolhatók (lásd az A64 immediate-korlátok összefoglalóját).

Build és futtatás — target: AArch64 Linux, qemu-user; host: Ubuntu 24.04 x86_64.

aarch64-linux-gnu-as -g -march=armv8-a -EL \
  -o lab-nzcv.o lab-nzcv.s
aarch64-linux-gnu-ld -EL -e _start \
  -o lab-nzcv lab-nzcv.o

aarch64-linux-gnu-objdump -d -M no-aliases lab-nzcv

qemu-aarch64 ./lab-nzcv
printf 'exit=%d\n' "$?"

Várt, nem itt mért eredmény: a program nem ír a szabványos kimenetre; a shell exit=0 sort jelez. Ez csak a szabályos befejezést ellenőrzi, nem a jelzők helyességét.

Az aliasok nélküli disassemblyben az A64 CMP W1, W1 helyén subs wzr, w1, w1 alak várható.

A regiszterellenőrzéshez az első terminálban indítsuk újra a qemu-user célprogramot:

qemu-aarch64 -g 1234 ./lab-nzcv
printf 'exit=%d\n' "$?"

Második terminálban csatlakozzunk:

gdb-multiarch -q ./lab-nzcv \
  -ex 'target remote localhost:1234' \
  -ex 'break lab_inspect' \
  -ex 'continue' \
  -ex 'info registers x2 x3 x4 x5 x6 x9' \
  -ex 'info registers x10 x11 x12 x13 x14 x15'

A töréspontban a következő levezetett, várt A64 állapot ellenőrizendő:

Eset Eredmény 32 bites nézetben NZCV Pillanatfelvétel
A W2 = 0x00000000 0b0110 X10 = 0x0000000060000000
B W3 = 0x80000000 0b1001 X11 = 0x0000000090000000
C W4 = 0x00000000 0b0111 X12 = 0x0000000070000000
D W5 = 0x00000002 0b0000 X13 = 0x0000000000000000
E W6 = 0xffffffff 0b1000 X14 = 0x0000000080000000
F Nincs tárolt kivonási eredmény 0b0110 X15 = 0x0000000060000000

Az A64 X2–X6 felső 32 bitje nulla. Különösen: X3 = 0x0000000080000000 mellett a B eset N jelzője mégis 1. Az A64 W9 = 2, de a közbeiktatott ADD nem írta felül az A eset jelzőit.

A GDB-ben kiadott continue után a program várt kilépési állapota 0.

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

Tünet Hipotézis Adatgyűjtés és egyetlen változtatás
Az eredmény helyes, a jelző régi A64 ADD szerepel ADDS helyett objdump segítségével ellenőrizzük a kódot; csak a mnemonicot javítsuk
A várt 32 bites átfordulás elmarad A64 Xn operandusokkal számoltunk Ellenőrizzük az utasítás szélességét; csak a hibás összeadást módosítsuk
A kivonás C-je „fordítottnak” látszik A kölcsönzést és annak hiányát összekevertük Vessük össze a 0 − 1 és 0 − 0 esetet; a kód helyett először az elvárást javítsuk
Több eredményhez ugyanaz a jelző tartozik A kiolvasás előtt egy későbbi művelet frissítette az NZCV-t Helyezzük az A64 MRS utasítást közvetlenül a vizsgált művelet után

A jelzők egyetlen közös, felülírható állapotot alkotnak; nem kapcsolódik minden eredményregiszterhez külön NZCV. Hibakereséskor ezért egyszerre ellenőrizzük az operandusokat, a szélességet, az eredményt és a kiolvasás helyét.

5.3.9 Feladatok

  1. (magyarázó, alapszint — cél: megértés-ellenőrzés) Határozd meg a 32 bites A64 0x80000000 + 0xffffffff eredményét és NZCV-jét. Külön indokold C és V értékét előjel nélküli, illetve előjeles értelmezéssel.

  2. (implementációs, középszint — cél: megértés-ellenőrzés) A labor első összeadását módosítsd egyetlen sorban 64 bitesre, ADDS X2, X0, X1 alakra. A konstansépítést hagyd változatlanul. A fenti qemu-user builddel és GDB-ellenőrzéssel vizsgáld meg X2, annak alsó 32 bites W2 nézete, valamint X10 értékét. Miért nem következik a nulla W2 értékből Z = 1?

  3. (hibakeresési, középszint — cél: rutinépítés) Az eredeti program B esetében szándékosan cseréld az A64 ADDS W3, W0, W1 utasítást ADD W3, W0, W1 alakra. Előre jelezd meg az eltérő regiszterértéket, majd objdump és GDB segítségével igazold a hipotézist. Egyetlen módosítással javítsd vissza.

5.3.10 Megoldások

1. megoldás — Segítség. Számítsd ki az előjel nélküli összeget binárisan (hány bitre nő), majd külön az előjeles összeget — fér-e a 32 bites előjeles tartományba?

1. megoldás — Teljes megoldás. Eredmény: 0x7fffffff; NZCV: 0b0011. Előjel nélkül az összeg 0x17fffffff, ezért a 32. biten átvitel keletkezik: C = 1. Előjelesen −2147483648 + −1 adódik, ami kisebb a minimumértéknél: V = 1. A tárolt eredmény előjelbitje 0, és nem nulla, ezért N = 0, Z = 0.

2. megoldás — Segítség. Nézd meg, mit csinál a konstansépítő MOVZ W0, .../MOVZ W1, ... pár az X0/X1 FELSŐ 32 bitjével, mielőtt egyáltalán az ADDS-hoz érnél.

2. megoldás — Teljes megoldás. X2 = 0x0000000100000000, W2 = 0, X10 = 0. A konstansépítés W0 és W1 írásával már nullázta az A64 X0 és X1 felső felét. A 64 bites összeadás ezért 4294967295 + 1 értéket számít, amely mindkét 64 bites értelmezésben ábrázolható. Nincs carry és overflow. Z a teljes 64 bites eredményt vizsgálja, amely nem nulla; az alsó 32 bites nézet önmagában nem döntő.

3. megoldás — Segítség. Ha az ADD nem frissíti az NZCV-t, melyik KORÁBBI eset jelzői maradnak érvényben az MRS X11, NZCV pillanatában?

3. megoldás — Teljes megoldás. Az A64 W3 továbbra is 0x80000000, de X11 = 0x0000000060000000 lesz. A hibás ADD nem frissíti az NZCV-t, így a pillanatfelvétel az A eset 0b0110 jelzőit őrzi. A közbeeső konstansépítés sem változtatott rajtuk. Az ADDS visszaállítása után a várt X11 = 0x0000000090000000. A helyes eredmény tehát önmagában nem igazolja a helyes jelzőkezelést.

5.3.11 Rövid összefoglaló

Az A64 aritmetikában az eredmény és a jelzők együtt írják le, mi történt az adott műveleti szélességen. Az N az eredmény legfelső bitje, a Z a teljes eredmény nullaságát jelzi. Az összeadás C jelzője az előjel nélküli tartományból kilépést, V jelzője az előjeles tartomány megsértését mutatja; egyikből sem következik a másik. A kivonás C-je a kölcsönzés hiányát jelenti. Az A64 CMP eldobott eredményű SUBS, a jelzőket viszont megtartja. Az NZCV-t csak a jelzőállító utasítások írják (…S végződés, CMP, CMN, TST, CCMP, FCMP); a teljes áttekintést az alfejezet összefoglaló táblázata adja. A laborban közvetlen kiolvasással ellenőriztük a levezetett állapotokat.

5.3.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 forrást itt nem fordítottuk és nem futtattuk. Minden közölt eredmény ISA-szemantikából levezetett, várt állapot. Az Ubuntu 24.04 helyben telepített csomagverzióit, a disassembly konkrét megjelenését és a QEMU–GDB kapcsolat működését a laborban kell ellenőrizni. Teljesítményadatot nem közöltünk; a bitenkénti levezetés nem mikroarchitekturális időzítési modell.