5.1 MOV-variánsok és immediate-kódolás

5.1.1 Miért nem egyetlen utasítás minden konstansbetöltés?

Egy C-programban a uint64_t változó inicializálása egy nagy konstanssal egyetlen forrássor. A fordító azonban több gépi utasítást is kibocsáthat hozzá. Ez akkor válik mérnöki kérdéssé, amikor disassemblyben keresünk egy értéket, kézzel írunk rövid inicializáló kódot, vagy megmagyarázzuk egy függvény kódméretét.

Az utasításba kódolt közvetlen operandus (immediate operand) olyan érték, amelyet maga az utasításkód tartalmaz. Nem memóriából olvassuk, és nem egy másik regiszter szolgáltatja.

Hatáskör: az alfejezet minden utasítása és regisztere az A64 ISA, AArch64, I. rész kontextusában értendő. Az assemblydirektívák a GNU assembler AArch64 céljához tartoznak; nem CPU-utasítások. A labor little-endian profilt használ. A konstansokat előjel nélküli bitmintaként kezeljük, kivéve, ahol külön előjeles értelmezést adunk.

Az előfeltétel a Linux syscall-konvenció AArch64-en: a labor végén az ott megismert kilépést használjuk.

5.1.2 A64 MOV: kényelmes név, többféle kódolás

Az A64 utasítások hossza rögzítetten 32 bit. Ebből az utasítás fajtáját, az operandusokat és az esetleges közvetlen értéket is ki kell olvasni.

Levezetés: egy tetszőleges 64 bites konstansnak 2642^{64} lehetséges értéke van. Egy 32 bites utasításnak összesen csak 2322^{32} különböző bitmintája lehet, ráadásul nem mind konstansbetöltés. Ezért egyetlen A64 utasítás nem kódolhatja közvetlenül az összes lehetséges 64 bites konstanst.

Az A64 MOV kényelmi, gyakran pszeudoutasításként emlegetett írásmód. Pontosabban az itt használt formái más utasítások aliasnevei (instruction aliases). Az assembler a megfelelő gépi kódolást választja ki.

A64 forrásalak Mögöttes kódolás, illetve jelentés
mov X4, X3 orr X4, XZR, X3: regisztermásolás
mov W4, W3 orr W4, WZR, W3: 32 bites regisztermásolás
mov X3, #0x1234 Például MOVZ: közvetlen konstansbetöltés
mov X3, #-1 MOVN nulla közvetlen mezővel
mov X3, #0x00ff00ff00ff00ff Logikai immediate-et használó ORR

Az A64 XZR és WZR olvasáskor nullát szolgáltató nullaregiszter (zero register). A nullával végzett bitenkénti VAGY változatlanul továbbadja a másik operandust, ezért alkalmas másolásra.

A veremmutatót tartalmazó A64 MOV alakok külön esetek: például a mov X4, SP mögött ADD áll nulla közvetlen operandussal. Ezekre nem szabad automatikusan ráhúzni az ORR magyarázatát.

Fontos: a GNU assembler AArch64 célján a mov X3, #konstans nem általános, több utasítást előállító konstansbetöltő makró. Ha nincs megfelelő egyutasításos kódolás, az assembler hibát jelez.

A logikai immediate kódolása sem tetszőleges. Röviden: a szó egy 2, 4, 8, 16, 32 vagy 64 bites blokk ismétléséből állhat; a blokk körbeforgatott, összefüggő egyesbitsorozatot és legalább egy nullát tartalmaz. A 32 bites alak legfeljebb 32 bites blokkot használ. Emiatt a fenti ismétlődő maszk kódolható, de egy tetszőleges hexadecimális érték általában nem. Részletesen a Logikai alapműveletek és bitmaszkolás tárgyalja ezt.

5.1.3 A64 MOVZ, MOVK, MOVN: építkezés 16 bites mezőkből

A széles közvetlenérték-mozgatások (move wide immediate) 16 bites közvetlen mezőt tartalmaznak. Ennek megengedett tartománya 0x0000…0xffff.

A mező helyét az opcionális lsl #s módosító választja ki:

A64 célregiszter Megengedett s értékek, bitben
Wn, 32 bites művelet 0, 16
Xn, 64 bites művelet 0, 16, 32, 48

Itt az lsl az operandus módosítója, nem külön végrehajtott utasítás. Elhagyása nulla eltolást jelent. A kódolás kétbites mezőválasztót használ; a 32 bites változatban a felső két választás nem megengedett.

Legyen V a közvetlen 16 bites mező s bittel balra tolva. A műveleti szélesség 32 vagy 64 bit.

A64 utasítás Hatás a műveleti szélességen belül
MOVZ Beírja V értékét; minden más bit nulla lesz.
MOVK Csak a kiválasztott 16 bites mezőt cseréli ki; a többi bit megmarad.
MOVN A teljes szélességen bitenként invertálja V értékét.

A nevekben a Z a nullázásra (zero), a K a megtartásra (keep), az N a bitenkénti negálásra (NOT) utal.

Két fontos következmény:

Például:

// A64 ISA, AArch64: önálló szemléltető utasítások.
movn X5, #0
movn W6, #0

Az első eredménye az A64 X5 = 0xffffffffffffffff. Ez 64 bites előjeles, kettes komplemens értelmezésben −1.

A második eredménye az A64 W6 = 0xffffffff, de a teljes X6 = 0x00000000ffffffff. Ennek oka az általános A64 szabály: egy Wn célregiszter írása nullázza a hozzá tartozó Xn felső 32 bitjét.

Ez az A64 MOVK Wn, ... esetében is igaz. A „megtartás” csak a 32 bites műveleti szélesség érintetlen mezőire vonatkozik.

Az itt tárgyalt A64 mozgatások nem módosítják az NZCV állapotjelzőket.

5.1.4 Levezetés: tetszőleges 64 bites érték felépítése

Legyen C egy előjel nélküli, 64 bites, dimenzió nélküli egész. Bontsuk négy 16 bites mezőre:

C=h0+h1216+h2232+h3248,0≤hj≤65535. C=h_0+h_1\,2^{16}+h_2\,2^{32}+h_3\,2^{48}, \qquad 0\le h_j\le 65535.

A j mezőindex értéke 0–3; az egyes eltolások bitszámot jelentenek. A mezők kinyerése C-szemlélettel: jobbra tolás, majd maszkolás 0xffff értékkel.

A 0x0123456789abcdef konstans felépítése:

Lépés A64 utasítás Várt X3 tartalom
1 movz X3, #0xcdef 0x000000000000cdef
2 movk X3, #0x89ab, lsl #16 0x0000000089abcdef
3 movk X3, #0x4567, lsl #32 0x0000456789abcdef
4 movk X3, #0x0123, lsl #48 0x0123456789abcdef

Az első utasítás ismert alapállapotot hoz létre, a továbbiak különálló mezőket írnak át. Így legfeljebb négy utasítás elegendő bármely 64 bites konstanshoz.

Ez nem állítás minden konstans minimális utasításszámáról. A nulla mezőket nem kell külön beírni a MOVZ után, sok egyesbit esetén pedig a MOVN adhat jobb kezdőállapotot. Például az A64 movn X3, #0xedcb önmagában előállítja a 0xffffffffffff1234 bitmintát.

5.1.5 Kidolgozott példa: konstansok ellenőrzése QEMU-ban

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

A következő teljes program nem használ C-futtatókörnyezetet. A konstansok adatok, nem memóriacímek. A lab_check szimbólumnál debuggerrel ellenőrizhető az eredmény.

A fájl neve legyen lab-mov.s:

// A64 ISA, AArch64, I. rész; GNU as; little-endian cél.
.section .text
.p2align 2
.global _start
.global lab_check
.type _start, %function

_start:
    movz X3, #0xcdef
    movk X3, #0x89ab, lsl #16
    movk X3, #0x4567, lsl #32
    movk X3, #0x0123, lsl #48

    mov  X4, X3
    movn X5, #0
    movn W6, #0
    mov  W7, W4

    mov  X9, #0x00ff00ff00ff00ff
    ldr  X10, =0x0123456789abcdef

lab_check:
    mov X0, #0
    mov X8, #93
    svc #0

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

A GNU as AArch64 .p2align 2 direktívája 22=42^2=4 bájtos igazítást kér. A szekció- és szimbólumdirektívák a tárgyfájlt írják le; nem végrehajtandó műveletek.

Build és futtatás — cél: qemu-user, AArch64 little-endian; host: Ubuntu 24.04 x86_64.

aarch64-linux-gnu-as -EL -march=armv8-a \
    -o lab-mov.o lab-mov.s &&
aarch64-linux-gnu-ld -EL -e _start \
    -o lab-mov lab-mov.o &&
qemu-aarch64 ./lab-mov
printf 'Kilépési állapot: %d\n' "$?"

Levezetett, várt eredmény: a program nem ír a szabványos kimenetre; a shell Kilépési állapot: 0 sort ír ki. A nulla kilépés önmagában még nem igazolja a konstansok helyességét: a program nem végez önellenőrzést.

A tényleges kódolások megtekintése:

# Cél: a qemu-user számára készült AArch64 ELF statikus vizsgálata.
aarch64-linux-gnu-objdump -d -M no-aliases lab-mov

A no-aliases kapcsolóval a regisztermásolás mögötti A64 ORR, illetve az egyszerű immediate-mozgatások mögötti kódolás látható. A megjelenítés pontos formája binutils-verziótól függhet.

A regiszterellenőrzéshez az első terminálban:

# Cél: qemu-user, AArch64; várakozás GDB-kapcsolatra.
qemu-aarch64 -g 1234 ./lab-mov

A második terminálban:

# Cél: a fenti qemu-user folyamat A64 regisztereinek ellenőrzése.
gdb-multiarch -q ./lab-mov \
    -ex 'set architecture aarch64' \
    -ex 'target remote :1234' \
    -ex 'break lab_check' \
    -ex 'continue' \
    -ex 'info registers x3 x4 x5 x6 x7 x9 x10' \
    -ex 'continue' \
    -ex 'quit'

Várt állapot a töréspontnál; nem ténylegesen mért laboreredmény:

A64 regiszter Várt bitminta Ellenőrzött tulajdonság
X3 0x0123456789abcdef Négymezős felépítés
X4 0x0123456789abcdef Teljes regisztermásolás
X5 0xffffffffffffffff 64 bites MOVN
X6 0x00000000ffffffff 32 bites írás felsőfél-nullázása
X7 0x0000000089abcdef W nézetű másolás
X9 0x00ff00ff00ff00ff Logikai immediate
X10 0x0123456789abcdef Assembler-segített betöltés

5.1.6 Az A64 LDR =konstans előzetese

A labor A64 ldr X10, =0x0123456789abcdef sora valódi assembler-pszeudoutasítás (pseudo-instruction). Az egyenlőségjeles alak nem gépi operandusformátum.

A GNU assembler AArch64 célján ez a következő elvet használja:

  1. A konstans bekerül egy konstanstárolóba (literal pool).
  2. Az assembler egy PC-relatív A64 LDR utasítást állít elő.
  3. A processzor végrehajtáskor memóriából tölti be az értéket.

A GNU as AArch64 .ltorg direktívája az aktuális helyre bocsátja ki a függőben lévő konstanstárolót. A példában ez a kilépés után található, így a normál vezérlés nem fut bele az adatba.

A valódi A64 LDR literal kódolás előjeles, 19 bites mezőt használ, négyszeres skálázással: a bájteltolás −1048576 és 1048572 közötti, négybájtos lépésekben. Ezért a tárolónak elérhető távolságban kell lennie.

Az egyetlen forrássor tehát itt egy betöltőutasítást és külön tárolt adatot jelent. Nem következik belőle, hogy gyorsabb a MOVZ/MOVK sorozatnál. Utóbbi nem végez adatmemória-olvasást; előbbi igen. Teljesítményt ebből mérés nélkül nem állítunk, QEMU pedig nem garantál hardverhű ciklusszámot.

5.1.7 Hivatkozható összefoglaló: A64 immediate-korlátok

Az A64 minden utasításformája saját, rögzített szélességű mezőben tárolja a közvetlen operandust vagy eltolást. A könyv későbbi alfejezetei nem sorolják fel újra ezeket a korlátokat, hanem erre a táblázatra hivatkoznak, és csak a példára jellemző eltérést emelik ki. A részletes levezetést az utolsó oszlopban megadott alfejezet adja.

Utasításcsalád Mező Megengedett érték Részletesen
MOVZ, MOVN, MOVK (és a MOV #konstans alias) 16 bit, előjel nélküli + lsl #s 0…0xffff; s = 0, 16 (Wn), illetve 0, 16, 32, 48 (Xn) ez az alfejezet
ADD, SUB, ADDS, SUBS; aliasok: CMP, CMN 12 bit, előjel nélküli + opcionális lsl #12 0…4095, vagy ennek 4096-szorosa (4096…16773120, 4096-os lépésben) Egész aritmetika
AND, ORR, EOR, ANDS; alias: TST, bitmaszkos MOV logikai (bitmaszk-) immediate ismétlődő, forgatott, összefüggő egyesbit-blokk; a csupa 0 és a csupa 1 nem kódolható Logikai alapműveletek
LSL, LSR, ASR, ROR immediate; eltolt regiszteroperandus eltolásszám 0–31 (Wn), 0–63 (Xn) Shift és rotate
UBFX, SBFX, BFI pozíció és szélesség, 6–6 bit a mező a műveleti szélességen belül marad (Wn: 0–31) Bitfield-utasítások
CCMP, CCMN immediate 5 bit + 4 bites #nzcv 0…31 NZCV-összefoglaló
MUL, MADD, SDIV, UDIV, CSEL nincs immediate-forma a konstanst előbb regiszterbe kell tölteni Egész aritmetika, CSET/CSEL
LDR/STR unsigned offset 12 bit, a hozzáférés méretével skálázva 0…4095 × méret bájt, a méret többszöröse (pl. Wn: 0–16380, Xn: 0–32760) Load/store címzési módok
LDUR/STUR; pre- és post-index 9 bit, előjeles, skálázatlan −256…255 bájt Load/store címzési módok
LDP/STP 7 bit, előjeles, skálázva Wn: −256…252, Xn: −512…504 bájt Load/store címzési módok
LDR literal (az LDR =konstans mögött is) 19 bit, előjeles, ×4 −1048576…1048572 bájt (±1 MiB) ez az alfejezet
ADR 21 bit, előjeles, bájtos −1048576…1048575 bájt (±1 MiB) Load/store címzési módok
ADRP 21 bit, előjeles, 4 KiB-os lapokban ±4 GiB; az alsó 12 bitet ADD …, :lo12: adja Load/store címzési módok
B, BL 26 bit, előjeles, ×4 ±128 MiB Elágazás
B.cond, CBZ, CBNZ 19 bit, előjeles, ×4 ±1 MiB Elágazás
TBZ, TBNZ 14 bit, előjeles, ×4; bitszám 6 bit ±32 KiB; bitszám 0–63 Elágazás
SVC 16 bit, előjel nélküli 0…65535; Linuxon mindig #0, a hívásszám az X8-ban van Linux syscall-konvenció

Ha egy konstans vagy eltolás nem fér bele a mezőbe, az assembler hibát jelez (vagy — LDR/STR esetén — más kódolást, például LDUR-t választhat). A megoldás ilyenkor a konstans regiszterbe töltése (MOVZ/MOVK sorozat vagy LDR =konstans), illetve a cím előzetes kiszámítása.

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

„A MOV nem fogadja el a számot.” Először azt vizsgáljuk, belefér-e egyetlen támogatott kódolásba. Kontrollált negatív próba:

# Cél: AArch64/qemu-user labor; szándékos assemblerhiba, nincs futtatás.
printf '%s\n' '.text' 'mov X3, #0x0123456789abcdef' \
    > lab-mov-bad.s
aarch64-linux-gnu-as -EL -march=armv8-a \
    -o lab-mov-bad.o lab-mov-bad.s
printf 'Assembler kilépési állapota: %d\n' "$?"

A várt eredmény nem nulla állapot és olyan diagnosztika, hogy az immediate nem mozgatható egyetlen utasítással. Pontos szövege verziófüggő. Linkelésre és futtatásra ennél a negatív próbánál nem kerül sor.

Eltűnik egy korábban beírt mező. Hipotézis: MOVK helyett újabb MOVZ szerepel. GDB-ben utasításonként léptessünk, és keressük meg az első hibás regiszterállapotot. Csak ezt az utasítást javítsuk, majd ismételjük meg az ellenőrzést.

Nulla lesz a felső 32 bit. Keressünk Wn célregiszteres írást. A MOVK Wn sem őrzi meg az Xn felső felét.

Érvénytelen eltolás. A 16 bites mező nem helyezhető tetszőleges bitpozícióra: például az A64 lsl #8 nem megengedett ezeknél az utasításoknál. A Wn alakhoz a lsl #32 sem használható.

5.1.9 Feladatok

  1. (magyarázó, alapszint — cél: megértés-ellenőrzés) Miért nem megfelelő magyarázat, hogy „a regiszter 64 bites, tehát a MOV bármely 64 bites számot elfogad”? Add meg az A64 movn X2, #0x1234, lsl #16 eredményét is.

  2. (implementációs, középszint — cél: rutinépítés) Egészítsd ki a labort a lab_check előtt. Az A64 X11 kapja a 0xfedcba9876543210 értéket MOVZ/MOVK segítségével, az X12 pedig a 0xffffffffffff1234 értéket egyetlen utasítással. Ellenőrizd mindkettőt GDB-ben.

  3. (hibakeresési, középszint — cél: rutinépítés) A labor második A64 utasításában cseréld a MOVK mnemonicot MOVZ-re, a többi operandust hagyd változatlanul. Jósolj végső X3 értéket, majd keresd meg az első eltérést utasításonkénti léptetéssel. Egyetlen módosítással javítsd a hibát.

5.1.10 Megoldások

1. megoldás — Segítség. A regiszter szélessége és az UTASÍTÁSKÓDBAN tárolható mező mérete két külön korlát — melyik szabja meg, mit tud egyetlen MOVN ténylegesen beírni? A megadott MOVN előbb egy 16 bites mezőt helyez el eltolva, majd a TELJES 64 bitet invertálja.

1. megoldás — Teljes megoldás. A regiszter szélessége a tárolható értéket határozza meg, nem az utasítás közvetlen mezőjének méretét. A megadott A64 MOVN először a 0x0000000012340000 mintát képezi, majd 64 biten invertálja. Eredménye 0xffffffffedcbffff.

2. megoldás — Segítség. Az X11-hez négy MOVZ/MOVK lépés kell (ugyanaz a minta, mint a fejezet 0x0123456789abcdef példájában). Az X12-nél nézd meg: a 0xffffffffffff1234 mintázat egyetlen MOVN bitenkénti negáltjaként kódolható-e.

2. megoldás — Teljes megoldás. A laborba illeszthető A64 kód:

// A64 ISA, AArch64: beszúrás a lab_check címke elé.
movz X11, #0x3210
movk X11, #0x7654, lsl #16
movk X11, #0xba98, lsl #32
movk X11, #0xfedc, lsl #48

movn X12, #0xedcb

Az első sorozat minden 16 bites mezőt kitölt. A második a 0xedcb bitenkénti komplementereként 0x1234 alsó mezőt és csupa egyes felső biteket állít elő. Ugyanazzal a builddel futtatható; a GDB regiszterlistájához add hozzá az x11 x12 neveket.

3. megoldás — Segítség. A MOVZ mindig NULLÁZZA a teljes műveleti szélességet a beírt mezőn kívül — mi történik emiatt az ELŐZŐ utasítás által már beírt mezővel, ha a másodikat is MOVZ-re cseréled?

3. megoldás — Teljes megoldás. A végső A64 X3 = 0x0123456789ab0000. A második utasítás lenullázza az első által beírt 0xcdef mezőt. A GDB-ben _start törésponttól két stepi után már 0x0000000089ab0000 látható az elvárt 0x0000000089abcdef helyett. A javítás kizárólag a második mnemonic visszaállítása MOVK-re; utána új build és azonos ellenőrzés szükséges.

5.1.11 Rövid összefoglaló

Az A64 konstansbetöltésnél külön kell választani a regiszter szélességét és az utasításban rendelkezésre álló kódolómezőket. A MOV kényelmes alias, de nem ígéret tetszőleges konstans egyutasításos előállítására. A MOVZ ismert, nullázott alapot ad, a MOVK kiválasztott mezőket cserél, a MOVN teljes műveleti szélességen invertál. Tetszőleges 64 bites érték legfeljebb négy ilyen utasítással felépíthető. A Wn célregiszteres írás mindig nullázza az Xn felső felét. Az LDR =konstans kényelme mögött külön tárolt adat és memóriaolvasás áll; működését disassemblyvel és regiszterellenőrzéssel igazoljuk. Az A64 immediate-mezők korlátait az alfejezet hivatkozható összefoglaló táblázata gyűjti egybe; a könyv későbbi alfejezetei erre utalnak vissza.

5.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 ténylegesen fennmaradnak: laborfuttatás nem történt. A regiszterértékek levezetett várakozások. Az Ubuntu 24.04 telepített csomagverzióit, az assembler pontos hibaüzenetét és az objdump megjelenítését a hallgatói laborban kell rögzíteni.