9.3 Bitfield-utasítások: BFI/UBFX/SBFX és csomagolt adatszerkezetek

9.3.1 Miért érdemes külön utasításokkal kezelni a bitmezőket?

Egy állapotszó gyakran több, egymástól független adatot hordoz: néhány bit üzemmódot, más bitek előjeles korrekciót, további bitek számlálót jelentenek. C-ben ezeket eltolásokkal és maszkolással vagy struktúrába szervezett bitmezőkkel kezelhetjük. Assemblyben a feladat két alapműveletre egyszerűsödik:

A bitmező (bit field) egy rögzített szélességű érték összefüggő bittartománya. A legkisebb helyi értékű bit sorszáma 0. Egy mezőt kezdőpozíciója és bitszélessége határoz meg.

Az előfeltétel a Shift és rotate: LSL/LSR/ASR/ROR gyakorlati mintázatok ismerete. Itt már nem általános bitmozgatásokat építünk fel, hanem azok gyakori összetett mintáit fejezzük ki közvetlenül.

Architektúra és profil: az alfejezet minden assembly utasítása és regisztere az A64 ISA-hoz, az AArch64 I. részhez tartozik. A cél ARMv8-A, Linux, little-endian. A példák nem Cortex-M-perifériákat érnek el.

9.3.2 A három alapművelet pontos szerződése

Az A64 utasítások szintaxisa:

A64 utasítás Jelentés
UBFX Wd, Wn, #lsb, #width Mezőkinyerés, nullakiterjesztéssel 32 bitre
SBFX Wd, Wn, #lsb, #width Mezőkinyerés, előjelkiterjesztéssel 32 bitre
BFI Wd, Wn, #lsb, #width A forrás alsó bitjeinek beillesztése a célmezőbe

Mindháromnak van 64 bites, Xd és Xn operandusokat használó változata. Egy utasításon belül az adatoperandusok szélessége azonos.

Legyen:

Az immediate operandusok megengedett tartománya:

0≤p<N,1≤k≤N−p. 0 \le p < N,\qquad 1 \le k \le N-p.

A mező tehát nem lóghat túl a regiszter felső szélén. A pozíció és a szélesség fordításkor ismert konstans, nem regiszterből vett futásidejű paraméter. Például a 32 bites UBFX W0, W1, #28, #5 érvénytelen: a mező utolsó bitje a nem létező 32 sorszámú bit lenne.

9.3.2.1 Előjel nélküli kinyerés: UBFX

Az A64 UBFX a kiválasztott mezőt a cél alsó bitjeire helyezi, a cél többi bitjét nullázza.

Levezetés, előjel nélküli értelmezéssel: ha SS a forrásregiszter értéke, akkor

u=(SLSRp)&(2k−1). u=(S \mathbin{\mathrm{LSR}} p)\mathbin{\&}(2^k-1).

Az eredmény tartománya 0…2k−10 \ldots 2^k-1. Az eltolás eltávolítja a mező alatti biteket, a maszk pedig a mező feletti biteket.

Az A64 UBFX W0, W0, #8, #8 tehát a forrás 15:8 bitjeit emeli ki. Az azonos forrás- és célregiszter megengedett.

9.3.2.2 Előjeles kinyerés: SBFX

Az A64 SBFX ugyanazt a bittartományt választja ki, de annak legfelső bitjét előjelbitként kezeli. Az előjelkiterjesztés (sign extension) ezt a bitet másolja a cél magasabb bitjeire.

Ha az előjel nélküli mezőérték uu, akkor a kettes komplemens szerinti előjeles érték:

s={u,u<2k−1,u−2k,u≥2k−1. s= \begin{cases} u, & u < 2^{k-1},\\ u-2^k, & u \ge 2^{k-1}. \end{cases}

Az eredmény tartománya −2k−1…2k−1−1-2^{k-1} \ldots 2^{k-1}-1. Az előjelet a mező legfelső bitje, nem a teljes forrásregiszter legfelső bitje határozza meg.

Fontos A64-szélességi szabály: az SBFX W0, ... csak 32 bitre terjeszti ki az előjelet. A W0 írása az X0 felső 32 bitjét nullázza.

A64 művelet eredménye W0 bitmintája X0 bitmintája
32 bites előjeles -3 0xFFFFFFFD 0x00000000FFFFFFFD

Ha eleve 64 bites, előjeles eredmény kell, a megfelelő 64 bites forráson az SBFX X0, X0, #3, #5 változatot használhatjuk. Ennek -3 eredménye 0xFFFFFFFFFFFFFFFD.

9.3.2.3 Beillesztés: BFI

Az A64 BFI a forrás alsó kk bitjét másolja a cél p+k-1:p tartományába. A cél eredeti értéke tehát bemenet is.

Legyen a célmező maszkja:

M=(2k−1)≪p. M=(2^k-1)\ll p.

Ekkor, NN bites műveletként értelmezve:

D′=(D&¬NM)|((S&(2k−1))≪p). D'=(D\mathbin{\&}\neg_N M) \mathbin{|} ((S\mathbin{\&}(2^k-1))\ll p).

Itt ¬N\neg_N pontosan NN biten képzett komplementert jelent. Az első tag megőrzi a célmezőn kívüli biteket, a második a helyükre teszi az új mezőértéket.

Az A64 BFI W0, W1, #8, #8:

A túl nagy bemenetet nem utasítja el: csonkolja. A 0x123 forrásból 8 bites mezőbe 0x23 kerül.

A megőrzési ígéret itt csak a 32 bites célra vonatkozik: a W0 írása továbbra is nullázza az X0 felső felét.

9.3.2.4 Kódolási háttér és állapotjelzők

Az A64 UBFX, SBFX és BFI rendre az UBFM, SBFM és BFM utasítások alternatív nevei, azaz aliasai. Nem több utasításra kibontott makrók.

A kódolt immediate mezők:

A64 alias immr imms
UBFX, SBFX pp p+k−1p+k-1
BFI (−p)modN(-p)\bmod N k−1k-1

A kódolás mindkét immediate mezője 6 bites; 32 bites alaknál csak a megfelelő 5 bites értéktartomány érvényes. Az assembler az olvasható pozíció–szélesség párból állítja elő ezeket.

E három művelet nem módosítja az A64 NZCV állapotjelzőket. A disassembler más aliast is megjeleníthet, különösen szélső paramétereknél; ez önmagában nem jelent eltérő működést.

9.3.3 Kidolgozott példa: csomagolt állapotszó

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

Egy programon belüli, 32 bites állapotszó formátuma legyen:

Bittartomány Szélesség Mező Értelmezés
2:0 3 bit mode Előjel nélküli üzemmód
7:3 5 bit delta Előjeles korrekció
15:8 8 bit count Előjel nélküli számláló
31:16 16 bit tag Előjel nélküli azonosító

A bemenet 0xA55A3CED.

Levezetett mezőértékek:

A count mezőbe 0x123 értéket illesztünk. A várt eredmény 0xA55A23ED: csak a mező alsó 8 bitnyi új értéke kerül be.

9.3.3.1 Assembly modul

A lab-bitfield.S fájl teljes tartalma:

// A64 ISA, AArch64, ARMv8-A.
// A direktívák GNU as / ELF környezethez tartoznak.
.arch armv8-a
.text
.p2align 2

.global lab_count
.type lab_count, %function
lab_count:
    UBFX W0, W0, #8, #8
    RET X30
.size lab_count, .-lab_count

.global lab_delta
.type lab_delta, %function
lab_delta:
    SBFX W0, W0, #3, #5
    RET X30
.size lab_delta, .-lab_delta

.global lab_put_count
.type lab_put_count, %function
lab_put_count:
    BFI W0, W1, #8, #8
    RET X30
.size lab_put_count, .-lab_put_count

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

Az AAPCS64 szerint a 32 bites argumentumok itt a W0, illetve W1 regiszterben érkeznek, az eredmény a W0 regiszterben távozik. A függvények nem hívnak más függvényt, és nem használnak megőrzendő regisztert, ezért nincs szükség veremkeretre.

9.3.3.2 C bitmezők és reprezentáció

A C bitmezős struktúra nem hordozható fájl- vagy perifériaformátum-leírás. A mezők tárolóegységen belüli elhelyezése, a kitöltés és bizonyos típusválasztási részletek implementációfüggők; a konkrét ABI további szabályokat ad.

Különösen:

A labor az AArch64 GNU/Linux little-endian profil várható kiosztását ellenőrzi, nem általános C-szabályként feltételezi. A reprezentációt memcpy segítségével vizsgáljuk, nem inkompatibilis mutatótípuson keresztül.

A lab-bitfield.c fájl:

/* Cel: AArch64 GNU/Linux, ARMv8-A, little-endian. */
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

uint32_t lab_count(uint32_t word);
int32_t lab_delta(uint32_t word);
uint32_t lab_put_count(uint32_t word, uint32_t value);

struct lab_fields {
    unsigned int mode : 3;
    signed int delta : 5;
    unsigned int count : 8;
    unsigned int tag : 16;
};

_Static_assert(sizeof(struct lab_fields) == 4,
               "A labor 4 bajtos strukturakiosztast var");

uint32_t lab_c_count(const struct lab_fields *p)
{
    return p->count;
}

int32_t lab_c_delta(const struct lab_fields *p)
{
    return p->delta;
}

void lab_c_set_count(struct lab_fields *p, uint32_t value)
{
    p->count = value;
}

uint32_t lab_c_put_count_word(uint32_t word, uint32_t value)
{
    return (word & UINT32_C(0xFFFF00FF))
         | ((value & UINT32_C(0xFF)) << 8);
}

int main(void)
{
    const uint32_t word = UINT32_C(0xA55A3CED);
    const uint32_t value = UINT32_C(0x123);
    const uint32_t updated = lab_put_count(word, value);

    if (lab_count(word) != 60 || lab_delta(word) != -3)
        return 1;

    if (updated != UINT32_C(0xA55A23ED) ||
        ((updated ^ word) & UINT32_C(0xFFFF00FF)) != 0)
        return 2;

    struct lab_fields fields = {
        .mode = 5, .delta = -3, .count = 0x3C, .tag = 0xA55A
    };
    uint32_t raw;
    memcpy(&raw, &fields, sizeof raw);

    if (raw != word ||
        lab_c_count(&fields) != 60 || lab_c_delta(&fields) != -3)
        return 3;

    lab_c_set_count(&fields, value);
    memcpy(&raw, &fields, sizeof raw);

    if (raw != updated ||
        lab_c_put_count_word(word, value) != updated)
        return 4;

    printf("count=%" PRIu32 " delta=%" PRId32
           " updated=0x%08" PRIx32 "\n",
           lab_count(word), lab_delta(word), updated);
    return 0;
}

A C-szintű maszkolás itt biztonságos: előjel nélküli, 32 bites értékeken dolgozik, és az eltolásszám 8. Általános segédfüggvényben az (1u << width) - 1 kifejezés width == 32 esetén már hibás lenne; a teljes szélességű mezőt külön kell kezelni.

9.3.3.3 Assemble–link–run

Az alábbi parancsok célja qemu-user, AArch64. A statikus linkeléshez a cross környezet céloldali C fejlesztői könyvtárai is szükségesek.

# Target: qemu-user / AArch64, ARMv8-A, little-endian.
aarch64-linux-gnu-as -EL -g \
    -o lab-bitfield.o lab-bitfield.S

aarch64-linux-gnu-gcc -std=c11 -O2 -g -Wall -Wextra \
    -march=armv8-a -mlittle-endian \
    -c lab-bitfield.c -o lab-bitfield-c.o

aarch64-linux-gnu-gcc -static -no-pie \
    lab-bitfield-c.o lab-bitfield.o -o lab-bitfield

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

# Az A64 kodolas es a C-bol generalt kod vizsgalata.
aarch64-linux-gnu-objdump -d -M no-aliases lab-bitfield.o
aarch64-linux-gnu-objdump -d lab-bitfield-c.o

Várt, nem ténylegesen mért kimenet:
count=60 delta=-3 updated=0xa55a23ed, majd exit=0.

A nem nulla kilépési állapot jelentése:

Állapot Elsődleges vizsgálati irány
1 Kinyerési pozíció, szélesség vagy előjelkezelés
2 Beillesztés és a többi bit megőrzése
3 C-struktúra kezdeti kiosztása vagy kiolvasása
4 C-mezőírás és assembly eredményének eltérése

Little-endian memóriában a bemeneti szó bájtjai, növekvő címek sorrendjében: 0xED, 0x3C, 0x5A, 0xA5. Ez nem változtatja meg a regiszteren belüli bitsorszámokat.

9.3.4 Mit várhatunk a C-fordítótól?

A forráskód szerkezete nem ír elő egyetlen kötelező utasítássorozatot.

Az A64 célra fordított lab_c_delta esetében természetes minta egy betöltést követő SBFX. A count azonban bájthatárra esik, ezért a fordító közvetlen LDRB betöltést vagy STRB tárolást is választhat: ilyenkor nincs szükség külön regiszteres mezőműveletre.

A lab_c_put_count_word ezzel szemben már regiszterben érkező egész értéket módosít; kézenfekvő A64 megvalósítása a BFI. A konkrét GCC-verzió választását az objektumkód mutatja meg.

A helyes ellenőrzési kérdés tehát nem az, hogy „megjelent-e a kívánt mnemonic”, hanem hogy azonos-e a kinyert érték, az előjel és a megőrzött bittartomány. Az egyutasításos alakból önmagában nem következik konkrét ciklusszám vagy gyorsulás; QEMU-val itt funkcionális helyességet vizsgálunk.

9.3.5 Kapcsolódás perifériaregiszterekhez

A példánk perifériastílusú állapotszó, de kizárólag normál programadat. A valódi perifériaregiszter kezelése további szerződést igényel, amelyet a Memory-mapped I/O alapelve tárgyal.

Az A64 BFI csak CPU-regisztert módosít. A memóriából betöltés, módosítás és visszaírás együtt olvasás–módosítás–írás (read-modify-write) műveletsor; nem válik atomivá attól, hogy középen egyetlen utasítás áll.

Perifériánál egy visszaírt 1 akár állapotbitet törölhet, az olvasásnak pedig mellékhatása lehet. A fenntartott biteket és a megengedett hozzáférési szélességet is a periféria dokumentációja szabályozza. A C volatile minősítő önmagában sem az atomiságot, sem a megfelelő bitmező-hozzáférést nem garantálja.

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

Tünet: -3 helyett 29.
Hipotézis: az előjeles mezőt A64 UBFX emeli ki. Az objdump kimenetében ellenőrizzük az utasítást és a #3, #5 operanduspárt. Egyetlen változtatásként cseréljük SBFX alakra, majd futtassuk újra a tesztet.

Tünet: a számláló helyes, az azonosító mégis megváltozik.
Hipotézis: túl széles a célmező. A mező visszaolvasása önmagában elégtelen ellenpróba: a mezőn kívüli biteket is össze kell hasonlítani.

Tünet: egy korábbi 64 bites érték felső fele eltűnik.
Hipotézis: A64 W célregisztert használó művelet történt. A debuggerben az X nézetet is vizsgáljuk; a szélességválasztást javítsuk, ne a bitmaszkot.

Tünet: néhány negatív érték jó, mások hibásak.
Hipotézis: rossz a mezőszélesség. A -3 véletlenül 4 és 5 bites előjeles mezőként is helyesen kijöhet. Ellenpróbának az 5 bites tartomány széleit, -16 és 15 értéket használjuk.

9.3.7 Feladatok

  1. (magyarázó, alapszint — cél: megértés-ellenőrzés) Miért ad a 0xA55A3CED szó 7:3 mezőjére az A64 UBFX 29, az SBFX pedig -3 értéket? Mi lesz az X0 teljes bitmintája, ha az eredményt SBFX W0, ... állítja elő?

  2. (implementációs, középszint — cél: rutinépítés) Készíts lab_put_delta(uint32_t word, int32_t value) függvényt, amely a 7:3 mezőt írja át. Teszteld az összes -16 és 15 közötti értékkel; ellenőrizd a visszaolvasást és a többi bit megőrzését is.

  3. (hibakeresési, középszint — cél: rutinépítés) A lab_put_count A64 BFI utasításában változtasd a szélességet #8 helyett #9 értékre. Jósolj eredményt, majd debuggerrel vizsgáld meg a beillesztés előtti és utáni regiszterállapotot. Miért fogadja el ezt az assembler?

9.3.8 Megoldások

1. megoldás — Segítség. Nézd meg a mező legfelső bitjét (a 7:3 mező 5 bites, tehát a 4. relatív bit) — ha ez 1, az SBFX kettes komplemens előjelbitként kezeli.

1. megoldás — Teljes megoldás. A mező bitmintája 0b11101, előjel nélkül 29. Öt biten az előjelbit értéke 1, ezért az előjeles érték 29−25=−329-2^5=-3. Az A64 SBFX W0, ... eredménye W0 = 0xFFFFFFFD, de X0 = 0x00000000FFFFFFFD, mert a 32 bites célírás nullázza a felső 32 bitet.

2. megoldás — Segítség. A lab_put_count mintáját egyszerűen másolhatod: csak a pozíció és a szélesség immediate paramétere változik #8,#8-ról #3,#5-re.

2. megoldás — Teljes megoldás. Az assembly fájlhoz hozzáadható:

// A64 ISA; GNU as / ELF direktivak.
.text
.global lab_put_delta
.type lab_put_delta, %function
lab_put_delta:
    BFI W0, W1, #3, #5
    RET X30
.size lab_put_delta, .-lab_put_delta

A C-fájl deklarációihoz:

/* Cel: AArch64 GNU/Linux. */
uint32_t lab_put_delta(uint32_t word, int32_t value);

A main függvénybe, a kiírás elé:

/* Cel: AArch64 GNU/Linux; a main tovabbi ellenorzese. */
for (int32_t v = -16; v <= 15; ++v) {
    uint32_t changed = lab_put_delta(word, v);
    if (lab_delta(changed) != v)
        return 5;
    if (((changed ^ word) & UINT32_C(0xFFFFFF07)) != 0)
        return 6;
}

Az új mező maszkja 0x000000F8, ennek 32 bites komplementere 0xFFFFFF07. A negatív bemenet alsó öt bitje a kívánt kettes komplemens mezőkód. Az eredeti buildlépések megismétlésével a várt kilépési állapot továbbra is 0. A tartományon kívüli bemeneteket ez az interfész csonkolná; az elutasításhoz külön ellenőrzés kellene.

3. megoldás — Segítség. Számold ki, melyik bitre esik a #8, #9 mező teteje (8+9-1), és nézd meg, hogy a 0x123 forrás kilencedik bitje (index 8) nulla vagy egy.

3. megoldás — Teljes megoldás. A hibás A64 művelet a 16:8 biteket írja. A 0x123 kilencedik bitje 1, ezért az eredmény 0xA55B23ED lesz: az azonosító egyik bitje is megváltozik. A program várt kilépési állapota 2. A mező kódolható, mert 8 + 9 <= 32; a hiba szemantikai, nem szintaktikai.

Újraépítés után az első terminálban:

# Target: qemu-user / AArch64; varakozas GDB-kapcsolatra.
qemu-aarch64 -g 1234 ./lab-bitfield

A második terminálban:

# Target: a QEMU altal futtatott AArch64 program.
gdb-multiarch -q ./lab-bitfield \
    -ex 'target remote :1234' \
    -ex 'break lab_put_count' \
    -ex 'continue' \
    -ex 'x/2i $pc' \
    -ex 'info registers x0 x1' \
    -ex 'si' \
    -ex 'info registers x0 x1'

A léptetés után az A64 X0 várt értéke 0x00000000A55B23ED. Állítsuk vissza kizárólag a szélességet #8 értékre, építsünk újra, és ellenőrizzük a 0 kilépési állapotot.

9.3.9 Rövid összefoglaló

Az A64 UBFX előjel nélküli, az SBFX előjeles mezőt emel ki, a BFI pedig a forrás alsó bitjeivel helyettesíti a cél kijelölt tartományát. A pozíció és a szélesség immediate operandus, és a mezőnek bele kell férnie a választott regiszterszélességbe. A 32 bites célírás a kapcsolódó 64 bites regiszter felső felét nullázza. A C bitmezős struktúrák kiosztását az implementáció és az ABI határozza meg; rögzített külső formátumhoz explicit szerződés szükséges. Teszteléskor a kinyert érték mellett a megőrzendő biteket és a szélső értékeket is ellenőrizzük.

9.3.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 programokat és parancsokat itt nem futtattuk. A helyi Ubuntu 24.04 csomagverziók, a tényleges GCC-utasításválasztás és a struktúrakiosztási teszt eredménye a hallgatói laborban ellenőrizendő. Minden megadott kimenet és regiszterállapot levezetett, várt eredmény.