6.1 Feltétel nélküli és feltételes elágazás

6.1.1 Motiváció: az értékválasztástól a végrehajtási út kiválasztásáig

Egy bemenetellenőrző rutin hibás adat esetén azonnal kilép, érvényes adat esetén pedig folytatja a feldolgozást. Itt nem elég két érték közül választani: bizonyos utasításokat egyáltalán nem szabad végrehajtani. Ehhez elágazásra (branch) van szükség.

A Feltételes végrehajtás: CSET/CSEL és feltételkódok alfejezetben az A64 feltételkódjai az eredményként kiválasztott adatot befolyásolták. Most ugyanezekkel a feltételekkel a következő végrehajtandó utasítást választjuk ki.

Az alfejezet teljes egészében A64 ISA, ARMv8-A, little-endian Linux környezetre vonatkozik. Előfeltétel az A64 CMP és az NZCV állapotjelzők értelmezése. A labor célja a vezérlés helyességének ellenőrzése, nem a teljesítménymérés.

6.1.2 Az elágazás működése

A vezérlésátadás (control transfer) megváltoztatja a következő végrehajtandó utasítás címét. Az A64 utasítások 4 bájtosak, ezért szokásos, szekvenciális végrehajtáskor a következő utasítás címe az aktuális cím plusz 4 bájt.

Az A64 közvetlen elágazásainál a cél az utasítás helyéhez viszonyított, előjeles eltolásból származik. A forráskódban ezt többnyire címkével (label) adjuk meg:

Ha egy feltételes elágazás nem teljesül, a végrehajtás a következő utasítással folytatódik. Ez a továbbhaladó ág (fall-through path). A64-en nincs késleltetett elágazási rés (branch delay slot): teljesülő ugrás után a közvetlenül következő utasítást nem kell még végrehajtani.

A64 utasítás Döntés alapja Olvassa az NZCV-t? Módosítja az NZCV-t?
B Nincs feltétel Nem Nem
B.cond A megadott feltételkód Igen Nem
CBZ Wn/Xn A regiszter értéke nulla Nem Nem
CBNZ Wn/Xn A regiszter értéke nem nulla Nem Nem

Az A64 B egyszerű ugrás: nem ment visszatérési címet. A függvényhívás ettől eltérő szerződését a Hívás és visszatérés: BL/RET bevezető mintája tárgyalja.

6.1.3 Feltételkód vagy közvetlen nullateszt?

Az A64 B.cond jelölésben a cond helyére konkrét feltételkód kerül, például EQ, NE, LT vagy HI. A GNU assemblerben kisbetűs írásmódot használunk: például A64 b.lt.

Az A64 cmp x0, #10 után:

A64 elágazás Jelentés
b.eq Az A64 X0 értéke egyenlő 10-zel
b.ne Az A64 X0 értéke nem egyenlő 10-zel
b.lt Az A64 X0 előjelesen kisebb 10-nél
b.gt Az A64 X0 előjelesen nagyobb 10-nél
b.lo Az A64 X0 előjel nélkül kisebb 10-nél
b.hi Az A64 X0 előjel nélkül nagyobb 10-nél

Az A64 CMP immediate-változata 12 bites, előjel nélküli konstanst fogad el, opcionális 12 bites balra tolással. A példában szereplő 10 közvetlenül kódolható.

Az összehasonlítás és az A64 B.cond között nem feltétlenül kell közvetlen szomszédságnak lennie. A követelmény az, hogy közben semmi ne írja felül a szükséges NZCV-állapotot. A közbeiktatott A64 ADDS például új jelzőket állítana elő, így az elágazás már nem az eredeti összehasonlításra válaszolna.

Nullatesztnél az A64 CBZ és CBNZ egyszerűbb mintázat:

Ez a megoldás külön összehasonlító utasítást takarít meg, de önmagában nem jelent bizonyított gyorsulást.

A regiszterszélesség a vizsgálat része. Az A64 cbz w0, cél csak az alsó 32 bitet vizsgálja; az A64 cbz x0, cél mind a 64 bitet. A 0x0000000100000000 bitmintánál az előbbi ugrik, az utóbbi nem. Ez olvasási művelet: az A64 W0 operandus használata itt nem nullázza az A64 X0 felső felét.

6.1.4 Relatív eltolás: a hatótávolság levezetése

Az A64 elágazás forrásbeli címkéje nem tetszőleges teljes címként kerül az utasításba. A kódolás egy előjeles mezőt tartalmaz, amelynek értékét 4 bájttal kell megszorozni.

Legyen:

Ekkor:

T=P+4k. T=P+4k.

Fontos: az alap maga az elágazási utasítás, nem a következő utasítás címe. A cél és az elágazás különbségének 4 bájttal oszthatónak kell lennie.

Egy nn bites kettes komplemens mezőnél:

−2n−1≤k≤2n−1−1. -2^{n-1}\le k\le 2^{n-1}-1.

Ebből az alábbi, elméleti, levezetett tartományok adódnak:

A64 utasítás Eltolásmező Legkisebb eltolás [bájt] Legnagyobb eltolás [bájt]
B 26 bit −134217728 +134217724
B.cond 19 bit −1048576 +1048572
CBZ, CBNZ 19 bit −1048576 +1048572

Az A64 B tehát megközelítőleg ±128 MiB, a többi felsorolt elágazás megközelítőleg ±1 MiB távolságot ér el. A pozitív szélsőérték mindkét esetben 4 bájttal kisebb a kerek határnál. Itt 1 MiB = 1048576 bájt.

Ha egy feltételes cél kívül esik a rövid tartományon, gyakori átalakítás:

  1. a feltétel ellentettjével átugorjuk a következő A64 B utasítást;
  2. az A64 B a távolabbi célra ugrik.

Például az A64 b.gt lab_far helyett az A64 b.le .Lnear következhet, utána az A64 b lab_far, majd a .Lnear címke. Ez csak akkor elég, ha az új A64 B saját helyétől mérve eléri a célt.

Még nagyobb távolsághoz hosszabb, célcímet előállító és közvetett ugrást végző utasítássor kell. Ilyen köztes ugrókódot a linker bizonyos esetekben létrehozhat; ezt gyakran veneer vagy thunk néven említik. Nem szabad minden elágazásra automatikus javítást feltételezni. A feltételes elágazás túlcsordulása hibaüzenethez vezethet. A linker konkrét viselkedését és az esetleges segédregiszter-használatot külön ellenőrizni kell.

6.1.5 Címkék, assembler és linker

A címke szimbolikus nevet rendel egy helyhez. A GNU assembler A64-forrásában a .Lzero: sor nem utasítás, és önmagában nem foglal kódbájtot. Az utána következő helyet nevezi el.

Ha az ugrás és célja ugyanabban a szekcióban található, az assembler rendszerint már ismeri a különbségüket, és közvetlenül kódolhatja azt. Másik objektumfájlban definiált cél esetén relokációs bejegyzést (relocation entry) hagy az objektumban.

A linker ekkor:

  1. elrendezi a bemeneti szekciókat;
  2. feloldja a szimbólumokat;
  3. kiszámítja az elágazás eltolását;
  4. ellenőrzi, hogy az eltolás kódolható-e.

A GNU AArch64 ELF-eszközláncban az A64 B külső céljához tipikusan R_AARCH64_JUMP26 relokáció tartozik. A következő labor ezt külön objektumfájlokkal teszi megfigyelhetővé.

6.1.6 Kidolgozott példa: bemenetellenőrzés két objektumfájllal

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

A program szerződése:

A lab-branch.s fájl:

// A64 ISA, ARMv8-A; GNU as direktívák AArch64 ELF-hez.
.text
.p2align 2
.global _start
.type _start, %function

_start:
    movz    x0, #7
    movz    x2, #1

    cbz     x0, .Lzero

    cmp     x0, #0
    b.lt    .Lnegative

    cmp     x0, #10
    b.gt    .Llarge

    cbnz    x2, .Lenabled

    movz    w0, #3
    b       lab_exit

.Lenabled:
    movz    w0, #0
    b       lab_exit

.Lzero:
    movz    w0, #1
    b       lab_exit

.Lnegative:
    movz    w0, #2
    b       lab_exit

.Llarge:
    movz    w0, #4
    b       lab_exit

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

A lab-exit.s fájl:

// A64 ISA, ARMv8-A; GNU as direktívák AArch64 ELF-hez.
.text
.p2align 2
.global lab_exit
.type lab_exit, %function

lab_exit:
    movz    x8, #93
    svc     #0

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

A példában az A64 MOVZ 16 bites immediate-mezőjébe minden konstans belefér. Az A64 MOVZ megengedett eltolásai Wn cél esetén 0 vagy 16, Xn cél esetén 0, 16, 32 vagy 48 bit. Az A64 W0 írása nullázza az A64 X0 felső 32 bitjét, így a kilépési kód teljes regiszterértéke is megfelelő.

Az A64 SVC 16 bites immediate-mezőjében a 0 kódolható. A 93-as rendszerhívásszám viszont nem ISA-tulajdonság, hanem a Linux AArch64 rendszerhívási felületének exit azonosítója.

Target: qemu-user, AArch64 little-endian Linux; host: Ubuntu 24.04 x86_64.

aarch64-linux-gnu-as -EL -march=armv8-a \
    -o lab-branch.o lab-branch.s

aarch64-linux-gnu-as -EL -march=armv8-a \
    -o lab-exit.o lab-exit.s

aarch64-linux-gnu-ld -EL -e _start \
    -o lab-branch lab-branch.o lab-exit.o

qemu-aarch64 ./lab-branch
lab_status=$?
printf 'status=%s\n' "$lab_status"

Várt, nem ténylegesen mért kimenet: status=0. A program saját maga nem ír szöveget; a megjelenített sort a shell állítja elő.

A levezetett végrehajtási út:

  1. Az A64 X0 értéke 7, ezért az A64 CBZ nem ugrik.
  2. A két A64 B.cond sem ugrik: 7 nem negatív és nem nagyobb 10-nél.
  3. Az A64 X2 értéke 1, ezért az A64 CBNZ a sikeres ágra ugrik.
  4. Az A64 W0 értéke 0 lesz.
  5. Az A64 B a külön objektumban definiált lab_exit célra ugrik.
Bemenet: A64 X0, előjeles Engedély: A64 X2, előjel nélküli Várt kilépési állapot
7 1 0
0 1 1
−1 1 2
7 0 3
11 1 4

A negatív ellenpróbához az A64 X0 inicializálását cseréljük A64 movz x0, #1, majd A64 sub x0, xzr, x0 utasításokra. Az eredmény előjelesen −1. Minden változtatás után ismételjük meg a teljes build–futtatás lépéssort.

6.1.7 Az elágazások ellenőrzése

Target: a qemu-user labor AArch64 objektumai és ELF-fájlja; hostoldali vizsgálat:

aarch64-linux-gnu-objdump -dr lab-branch.o
aarch64-linux-gnu-readelf -rW lab-branch.o
aarch64-linux-gnu-objdump -d lab-branch

Az első két parancs kimenetében a lab_exit szimbólumra hivatkozó R_AARCH64_JUMP26 bejegyzéseket várunk. A végleges ELF disassemblyjében az A64 B utasításoknak már a feloldott lab_exit célra kell mutatniuk.

A konkrét címeket nem kell előre feltételezni: a saját linkerünk kimenetét vizsgáljuk. A címkülönbségből ellenőrizhető az eltolás. A kiírt utasításcímek nem jelentenek általánosan használható memóriacímeket.

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

Hibás irányú feltétel. Ha a nulla bemenet nem a nullaágra jut, először ellenőrizzük, hogy nem A64 CBNZ szerepel-e A64 CBZ helyett. Adatgyűjtés: disassembly és kilépési állapot. Egyetlen mnemonic javítása után ismételjük meg a 0 és 7 bemenetes próbát.

Előjeles és előjel nélküli döntés összekeverése. A −1 bitmintája előjel nélkül nagy pozitív szám. Az A64 B.LT és B.LO ezért nem felcserélhető. A labor felső korlátjánál az A64 B.GT A64 B.HI-ra cserélése viszont nem feltétlenül látható: a korábbi negatívvizsgálat már kizárta az eltérő eseteket. A teszteket mindig az adott pontra ténylegesen eljutó értékek alapján válasszuk.

Helyes feltétel, rossz jelzők. Ha az összehasonlítás és az A64 B.cond közé jelzőfrissítő utasítás került, a disassemblyben keressük meg. Egyetlen változtatással távolítsuk el, majd futtassuk újra a határértékeket.

Túl távoli cél. A tartományhibát nem oldja meg a címke átnevezése. Vizsgáljuk meg a relokációt és a szekcióelrendezést; ezután válasszunk hosszabb ugrásmintát.

6.1.9 Feladatok

  1. Magyarázó (alapszint — cél: megértés-ellenőrzés): Miért megfelelő az A64 CBNZ az engedélyezéshez, és miért kell a felső korlát vizsgálatához más mintázat? Mi történik, ha a bemenet és az engedélyezési érték egyaránt nulla?

  2. Implementációs (alapszint — cél: rutinépítés): Módosítsd az elfogadott tartományt 1–20-ra, a hibakódok megtartásával. Ellenőrizd a 0, 1, 20 és 21 bemeneteket engedélyezett állapotban.

  3. Hibakeresési (középszint — cél: rutinépítés): Állítsd az engedélyt az A64 movz x2, #1, lsl #32 utasítással. Ezután szándékosan cseréld az A64 cbnz x2, .Lenabled utasítást A64 cbnz w2, .Lenabled alakra. Jósolj kilépési állapotot, majd a disassembly alapján magyarázd meg az eltérést.

  4. Kódolási (középszint — cél: megértés-ellenőrzés): Eléri-e az A64 B.cond a saját címéhez képest +1048572, illetve +1048576 bájtra lévő célt? Hogyan alakítanál át egy túl távoli A64 B.GT elágazást, ha a cél az A64 B tartományán belül van?

6.1.10 Megoldások

1. megoldás — Segítség. Az engedélyezés egy bináris (nulla/nem-nulla) kérdés, a felső korlát viszont egy reláció (kisebb/nagyobb) — melyik A64-mintázat olvassa csak a Z flaget, és melyik igényli NZCV teljes kiértékelését?

1. megoldás — Teljes megoldás. Az engedélyezés kizárólag nulla/nem-nulla kérdés; az A64 CBNZ ezt NZCV nélkül megoldja. A felső korlát relációt igényel, ezért A64 CMP és megfelelő A64 B.cond kell. Két nulla bemenetnél az első A64 CBZ már a nullaágra ugrik, ezért a várt állapot 1; az engedélyezést nem vizsgáljuk meg.

2. megoldás — Segítség. Csak a felső határt összehasonlító immediate kell cserélni; ellenőrizd, hogy 20 belefér-e a CMP immediate-kódolási korlátjába.

2. megoldás — Teljes megoldás. Az A64 cmp x0, #10 helyett A64 cmp x0, #20 szükséges. A 20 belefér a 12 bites immediate-mezőbe. A várt állapotok rendre 1, 0, 0 és 4. A felső határ elfogadott, ezért marad az A64 B.GT, nem A64 B.GE.

3. megoldás — Segítség. Nézd meg, mi kerül X2 alsó 32, illetve felső 32 bitjébe a movz ... lsl #32 után, és hogy a W2 nézet melyik felet látja.

3. megoldás — Teljes megoldás. Az A64 X2 értéke 0x0000000100000000, alsó 32 bitje nulla. Az A64 CBNZ X2 változattal a várt állapot 0; az A64 CBNZ W2 változattal 3. A hipotézist a disassembly operandusszélessége ellenőrzi. Az egyetlen javítás az A64 X2 nézet visszaállítása.

4. megoldás — Segítség. Az A64 B.cond 19 bites, előjeles, 4 bájtra skálázott eltolást kódol — számítsd ki ennek a mezőnek a legnagyobb pozitív értékét bájtban, és vesd össze a két jelölt céllal.

4. megoldás — Teljes megoldás. Az első cél elérhető, mert az eltolás osztva 4-gyel 262143, a 19 bites előjeles mező legnagyobb értéke. A másodikhoz 262144 kellene, ami már nem fér bele. Az átalakítás: A64 B.LE egy közeli címkére, utána A64 B a távoli célra, végül a közeli címke. Az új A64 B hatótávolságát a saját címétől számítjuk.

6.1.11 Rövid összefoglaló

Az A64 elágazás a végrehajtás következő helyét választja ki. A64 B esetén nincs feltétel, A64 B.cond esetén az NZCV dönt, míg az A64 CBZ/CBNZ közvetlenül egy regiszter nulla vagy nem nulla értékét vizsgálja. A regiszterszélesség és az előjeles értelmezés a helyesség része. A relatív cél az ugrás saját címéhez képest számítandó, 4 bájtos lépésekben. A külön objektumban található cél feloldása a linker feladata; a túl nagy távolságot nem feltétlenül javítja automatikusan. A laborban kilépési állapotokkal és disassemblyvel ellenőriztük az útvonalakat.

6.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 fennmaradnak: a kód nem került futtatásra; a kimenetek levezetett várakozások. Az Ubuntu 24.04-ben telepített konkrét binutils-verzió hibaüzeneteit és távoli ugrásokhoz generált esetleges köztes kódját a hallgatói laborban kell ellenőrizni.