2.2 Számrendszerek és bitszintű reprezentáció

2.2.1 Belépőteszt: átugorhatod ezt az alfejezetet?

Ha a számrendszereket C-fejlesztőként már biztosan használod, ez az alfejezet nagyrészt ismétlés. A nyolc kérdést papíron, segédeszköz nélkül, legfeljebb tíz perc alatt válaszold meg, majd a megoldókulcs és az átugrási út alapján döntsd el, mit olvass el.

  1. Mennyi a 0x3C decimálisan, és mi a 8 bites bináris alakja?
  2. Hány bitet ír le egy hexadecimális számjegy, és hány hexadecimális jegy kell egy 32 bites szóhoz?
  3. Mi a 8 bites 0xF6 bitminta értéke előjel nélküli, illetve kettes komplemens olvasatban?
  4. Hogyan írjuk fel a −1-et 32 bites kettes komplemensben, hexadecimálisan?
  5. Mi lesz a 8 bites 0x80 16 bitre kiterjesztve nullakiterjesztéssel, illetve előjelkiterjesztéssel?
  6. Mennyi 8 bites előjel nélküli aritmetikában 200 + 100?
  7. Mi lesz 8 biten a 100 + 100 eredményének bitmintája, és mi ennek az előjeles olvasata?
  8. C-ben int x = INT_MAX; és unsigned u = UINT_MAX; mellett mi a x + 1, illetve az u + 1 státusza?

Megoldókulcs és átugrási út:

# Helyes válasz Ha hibáztál, olvasd el
1 60; 0b0011_1100 Átváltás bináris, hexadecimális és decimális alak között
2 4 bit; 8 jegy Méretek és helyi értékek; Hexadecimális jelölés a gyakorlatban
3 246, illetve −10 A kettes komplemens előjeles olvasata
4 0xFFFFFFFF A kettes komplemens előjeles olvasata
5 0x0080, illetve 0xFF80 Nullakiterjesztés és előjelkiterjesztés
6 44 (300 mod 256: körbefordulás) Túlcsordulás és körbefordulás: levezetés 8 bitre
7 0xC8, előjelesen −56 (előjeles túlcsordulás) Túlcsordulás és körbefordulás: levezetés 8 bitre
8 x + 1 definiálatlan viselkedés (előjeles túlcsordulás); u + 1 definiáltan 0 A C szemantikája külön szabályrendszer

2.2.2 Miért kell a bitmintát és az értékét külön kezelni?

Egy bináris fájlban 0xFF bájtot találsz. Ez 255, −1, nyolc bekapcsolt jelzőbit, vagy egy gépi utasítás része? Önmagában egyik értelmezés sem kötelező. A jelentést az adatformátum, a művelet és a választott bitszélesség határozza meg.

Ez gyakorlati probléma: egy hibás előjelkiterjesztésből rossz tömbindex, egy túlcsorduló méretszámításból túl kicsi memóriaterület lehet. Ebben az alfejezetben egész számok bitmintája és számértéke között alakítunk át. Levezetjük a kettes komplemens ábrázolást és a véges szélességű összeadás korlátait.

Profil: I. rész, AArch64, A64 ISA, ARMv8-A; little-endian bájtsorrend. A matematikai példák gyakran 8 bitesek, mert így minden bit áttekinthető. Ez nem jelenti azt, hogy az A64 általános célú regiszterei 8 bitesek.

2.2.3 Méretek és helyi értékek

A bit egy bináris számjegy: értéke 0 vagy 1. Az alábbi méretneveket különböztetjük meg:

Elnevezés Méret Megjegyzés
bit 1 bit Két lehetséges érték
félbájt (nibble) 4 bit Egy hexadecimális számjegy
bájt (byte) 8 bit A könyv ARM-célprofiljain
szó (word) 32 bit Az itt használt Arm-elnevezés
dupla szó (doubleword) 64 bit Két 32 bites szó

A „szó” nem minden architektúrán jelent ugyanannyit. A64 ISA-környezetben az általános célú Wn regiszternézet 32 bites, az Xn nézet 64 bites. Ettől az Arm terminológiájában a word még 32 bit marad. A C int vagy long típusának méretét pedig a megvalósítás és az ABI szabályai határozzák meg, nem a szó hétköznapi jelentése.

Egy nn bites, előjel nélküli (unsigned) szám értéke:

U=∑i=0n−1bi2i. U=\sum_{i=0}^{n-1} b_i2^i.

Itt nn a bitszélesség bitben, ii a jobbról számozott bithely, bib_i pedig 0 vagy 1; UU dimenzió nélküli egész szám. A legkisebb helyi értékű bit (least significant bit, LSB) indexe 0, a legnagyobbé (most significant bit, MSB) n−1n-1.

Például, 8 bites előjel nélküli olvasatban:

𝟶𝚋𝟶𝟶𝟷𝟶𝟷𝟷𝟶𝟷=32+8+4+1=45. \texttt{0b00101101}=32+8+4+1=45.

A számjegyek számától eltérő fogalom az érték: a 0b101 és a 0b00000101 előjel nélküli értéke egyaránt 5, de a második jelölés egy 8 bites mezőt szemléltet.

2.2.4 Átváltás bináris, hexadecimális és decimális alak között

A hexadecimális (hexadecimal) számrendszer alapja 16. Számjegyei 0–9, majd A–F; utóbbiak rendre a 10–15 értéket jelölik. Mivel 16=2416=2^4, minden hexadecimális számjegy pontosan négy bitnek felel meg.

Hexadecimális számjegy Négybites alak Előjel nélküli decimális érték
0x2 0b0010 2
0x7 0b0111 7
0xA 0b1010 10
0xD 0b1101 13
0xF 0b1111 15

Binárisból hexadecimálisra váltáskor jobbról négyes csoportokat képzünk:

Az aláhúzás itt csak olvasást segítő csoportosítás, nem a labor C-kódjának szintaxisa.

Hexadecimálisból decimálisra ugyanúgy helyi értékeket összegzünk:

𝟶𝚡𝟸𝙳=2⋅16+13=45. \texttt{0x2D}=2\cdot16+13=45.

Decimálisból binárisra ismételt, maradékos osztás használható:

Osztandó 2-vel osztás hányadosa Maradék
45 22 1
22 11 0
11 5 1
5 2 1
2 1 0
1 0 1

A maradékokat alulról felfelé olvasva 0b101101 adódik. Nyolc bitre nullákkal kiegészítve: 0b00101101.

Hexadecimális átváltáshoz 16-tal osztunk: 45 hányadosa 2, maradéka 13, ezért az eredmény 0x2D. Ezek az eljárások tetszőleges nemnegatív egészre működnek; véges mezőbe tárolás előtt külön ellenőrizni kell, hogy az érték belefér-e.

2.2.5 A kettes komplemens előjeles olvasata

Ugyanannak az nn bites mintának a kettes komplemens (two’s complement) előjeles értéke:

S={U,U<2n−1,U−2n,U≥2n−1. S= \begin{cases} U, & U<2^{n-1},\\ U-2^n, & U\ge 2^{n-1}. \end{cases}

UU az előjel nélküli, SS az előjeles, dimenzió nélküli érték; n≥1n\ge1 a bitszélesség. Ezzel egyenértékű, ha a legfelső bit helyi értékét negatívnak vesszük:

S=−bn−12n−1+∑i=0n−2bi2i. S=-b_{n-1}2^{n-1}+\sum_{i=0}^{n-2}b_i2^i.

Nyolc biten ezért a legfelső bit súlya −128, nem +128.

8 bites minta Előjel nélküli érték Kettes komplemens előjeles érték
0x00 = 0b00000000 0 0
0x7F = 0b01111111 127 127
0x80 = 0b10000000 128 −128
0xD3 = 0b11010011 211 −45
0xFF = 0b11111111 255 −1

Az ábrázolható tartományok:

Bitszélesség Előjel nélküli tartomány Kettes komplemens előjeles tartomány
nn bit 0…2n−10\ldots 2^n-1 −2n−1…2n−1−1-2^{n-1}\ldots 2^{n-1}-1
8 bit 0…255 −128…127
16 bit 0…65535 −32768…32767
32 bit 0…4294967295 −2147483648…2147483647

Negatív szám kódolásakor előbb bitszélességet választunk. A −45 érték 8 bites mintája:

28−45=211=𝟶𝚡𝙳𝟹. 2^8-45=211=\texttt{0xD3}.

Ugyanezt adja a pozitív érték bitjeinek megfordítása és 1 hozzáadása:

  1. +45 mintája: 0b00101101;
  2. bitenkénti megfordítás: 0b11010010;
  3. hozzáadunk 1-et: 0b11010011.

Levezetés: az nn bites megfordítás 2n−1−U2^n-1-U értéket ad. Ehhez 1-et hozzáadva 2n−U2^n-U keletkezik, amelynek kettes komplemens olvasata −U-U.

A -0x2D matematikailag −45; a 0xD3 önmagában egy pozitív hexadecimális számjelölés. Utóbbi csak 8 bites kettes komplemens mintaként értelmezve jelenti ugyanazt.

A tartomány aszimmetrikus: −128 ábrázolható 8 biten, +128 előjelesen nem. Ezért a legkisebb negatív érték negálása ugyanazon a bitszélességen nem adhat helyes, ábrázolható pozitív eredményt.

2.2.6 Nullakiterjesztés és előjelkiterjesztés

Szélesebb mezőre váltáskor meg kell választani, mivel töltjük ki az új felső biteket.

Kiinduló 8 bites minta 16 bites eredmény Művelet Megőrzött érték
0x2D 0x002D Mindkettő 45
0xD3 0x00D3 Nullakiterjesztés Előjel nélküli 211
0xD3 0xFFD3 Előjelkiterjesztés Előjeles −45

Levezetés: negatív szám esetén az nn bites előjel nélküli minta UU, előjeles értéke U−2nU-2^n. Ha m>nm>n bitre terjesztünk előjellel, az új felső egyesek értéke 2m−2n2^m-2^n. Az új előjeles olvasat tehát:

(U+2m−2n)−2m=U−2n. \bigl(U+2^m-2^n\bigr)-2^m=U-2^n.

Az érték változatlan. A kiterjesztés módját tehát nem a hexadecimális számjegyek „kinézete”, hanem a kívánt értelmezés dönti el.

2.2.7 Túlcsordulás és körbefordulás: levezetés 8 bitre

A túlcsordulás (overflow) azt jelenti, hogy a pontos matematikai eredmény kívül esik a választott értéktartományon. A körbefordulás (wraparound) a véges szélességű, modulo szerinti eredményképzés.

Ha egy összeadásból csak az alsó nn bitet tartjuk meg, akkor:

R=(A+B)mod2n. R=(A+B)\bmod 2^n.

Itt AA és BB az operandusminták előjel nélküli értéke, RR a tárolt minta előjel nélküli értéke; valamennyi dimenzió nélküli egész.

Nyolc biten az eredményt 256-tal vett maradékra korlátozzuk.

Operandusminták Pontos előjel nélküli összeg Pontos előjeles összeg Alsó 8 bit Előjel nélküli túlcsordulás Előjeles túlcsordulás
0xFF + 0x01 256 −1 + 1 = 0 0x00 Igen Nem
0x7F + 0x01 128 127 + 1 = 128 0x80 Nem Igen
0x80 + 0xFF 383 −128 − 1 = −129 0x7F Igen Igen

Az első sorban az előjel nélküli összeg kilenc bitet igényel: 0b1_00000000. A kilencedik bit elhagyása után nulla marad.

A második sorban nincs kilencedik bit, mégis előjeles túlcsordulás történik: két nemnegatív operandusból negatívnak olvasott eredmény keletkezik.

Ebből következik az előjeles összeadás szabálya:

Utóbbi oka, hogy a negatív operandus csökkenti a pozitív értéket, és fordítva: az összeg nem kerülhet az operandusok által megengedett szélsőértékeken túlra.

Kivonásnál az előjeles túlcsordulás akkor lehetséges, ha az operandusok előjele különböző, és az eredmény előjele eltér a kisebbítendőétől. Például 8 biten 127−(−1)=128127-(-1)=128 nem ábrázolható előjelesen.

2.2.7.1 A C szemantikája külön szabályrendszer

A gépi alsó-bites eredményből nem következik, hogy minden hasonló C-kifejezés szabályos:

Így két uint8_t összeadása az AArch64 laborprofilban nem automatikusan „8 bites összeadás”. A 255 + 1 összeg int típusban 256; a későbbi uint8_t konverzió eredménye lesz 0.

2.2.8 Hexadecimális jelölés a gyakorlatban

A hexadecimális alak tömör, de nem jelöl önmagában adattípust.

Felhasználás Példa Helyes olvasási kérdés
Bájtcímhez viszonyított eltolás 0x20 bájt 32 bájttal arrébb?
8 bites bitmaszk (bit mask) 0xF0 A felső négy bitet jelöli?
A64 ISA utasításkódolás 0xD503201F Ez az A64 NOP kódolása?

A címeltolás itt nem konkrét memóriacím: csak már érvényes objektum határain belül alkalmazható. A maszkot nem előjeles számként, az utasításkódot pedig nem aritmetikai adatként értelmezzük.

A szám leírása és memóriabeli bájtsorrendje is különbözik. A 32 bites 0x12345678 minta little-endian tárolásakor növekvő bájteltolás mellett 0x78, 0x56, 0x34, 0x12 következik. Ez nem változtatja meg a szám helyi értékeit. A részletes tárgyalás: Bájtsorrend: endianness és miért releváns a határon.

2.2.9 Kidolgozott labor: ugyanazok a bitek, két olvasat

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

A program tudatosan szélesebb típusokkal számítja ki a pontos összegeket, majd külön képezi a 8 bites eredményt. Így nem idéz elő előjeles C-túlcsordulást.

A lab_signed8 nem támaszkodik tartományon kívüli, előjeles típusra történő konverzióra. A labor C17-et használ; a bináris 0b alakot kiírja, a forrás konstansait azonban hexadecimálisan vagy decimálisan adja meg.

Fájl: lab-bits.c. C17, AArch64 célprofil; little-endian.

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

static int32_t lab_signed8(uint8_t x)
{
    int32_t u = x;
    return u < 128 ? u : u - 256;
}

static void lab_show8(uint8_t x)
{
    printf("0b");
    for (int bit = 7; bit >= 0; --bit)
        putchar((((unsigned)x >> bit) & 1u) ? '1' : '0');

    printf(" hex=0x%02x unsigned=%u signed=%" PRId32 "\n",
           (unsigned)x, (unsigned)x, lab_signed8(x));
}

static int lab_add8(uint8_t a, uint8_t b, uint8_t expected,
                    int expected_uo, int expected_so)
{
    unsigned wide_u = (unsigned)a + (unsigned)b;
    int32_t wide_s = lab_signed8(a) + lab_signed8(b);
    uint8_t result = (uint8_t)wide_u;

    int uo = wide_u > 255u;
    int so = wide_s < -128 || wide_s > 127;

    printf("add 0x%02x + 0x%02x -> 0x%02x UO=%d SO=%d\n",
           (unsigned)a, (unsigned)b, (unsigned)result, uo, so);

    return result == expected
        && uo == expected_uo
        && so == expected_so;
}

int main(void)
{
    lab_show8(0x2d);
    lab_show8(0xd3);
    lab_show8(0xff);

    uint8_t small = 0xd3;
    uint32_t zero = small;
    int32_t sign = lab_signed8(small);

    printf("zero=0x%08" PRIx32 " sign=0x%08" PRIx32 "\n",
           zero, (uint32_t)sign);

    int ok = zero == 211u && sign == -45;
    ok &= lab_add8(0xff, 0x01, 0x00, 1, 0);
    ok &= lab_add8(0x7f, 0x01, 0x80, 0, 1);
    ok &= lab_add8(0x80, 0xff, 0x7f, 1, 1);

    puts(ok ? "PASS" : "FAIL");
    return ok ? 0 : 1;
}

A negatív sign érték uint32_t típusra alakítása definiált, modulo 2322^{32} szerinti konverzió; ezért alkalmas a kívánt 32 bites minta hexadecimális megjelenítésére.

2.2.9.1 Fordítás, összeállítás, linkelés, futtatás

Cél: qemu-user, qemu-aarch64; host: Ubuntu 24.04 x86_64. A parancsok a cross-eszközláncot és a statikus linkeléshez szükséges céloldali C-könyvtárat feltételezik. Telepítésük később, a Labor-alapkörnyezet: eszközlánc telepítése és verifikáció alfejezetben szerepel.

# C -> A64 ISA assembly; explicit little-endian célprofil.
aarch64-linux-gnu-gcc -std=c17 -O0 -g \
    -Wall -Wextra -Wconversion \
    -march=armv8-a -mlittle-endian \
    -S lab-bits.c -o lab-bits.s

# A64 ISA assembly -> objektum.
aarch64-linux-gnu-as -EL -march=armv8-a \
    lab-bits.s -o lab-bits.o

# Statikus AArch64 Linux-program.
aarch64-linux-gnu-gcc -static lab-bits.o -o lab-bits

# Futtatás: qemu-user.
qemu-aarch64 ./lab-bits
printf 'exit=%d\n' "$?"

Várt, levezetett eredmények; a kód itt nem volt futtatva:

Ellenőrzés Várt adat
0x2D 0b00101101, előjel nélkül 45, előjelesen 45
0xD3 0b11010011, előjel nélkül 211, előjelesen −45
0xFF 0b11111111, előjel nélkül 255, előjelesen −1
Kiterjesztések zero=0x000000d3 sign=0xffffffd3
Első összeadás 0x00, UO=1, SO=0
Második összeadás 0x80, UO=0, SO=1
Harmadik összeadás 0x7F, UO=1, SO=1
Önteszt és kilépés PASS, majd exit=0

Az UO és SO a program által kiszámított, 8 bites előjel nélküli, illetve előjeles túlcsordulási feltétel. Nem a processzor állapotjelzőinek kiolvasása.

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

Tünet Hipotézis Ellenpróba
A várt −45 helyett 211 jelenik meg Rossz előjeles értelmezés A 0xD3 mintából vond ki a 256-ot
Szélesítés után 0x00D3 látható Nullakiterjesztést alkalmaztál Vizsgáld meg a forrás típusát és a kívánt értéket
„Nincs kilencedik bit, tehát nincs túlcsordulás” Összekevered a két értéktartományt Próbáld ki a 0x7F + 0x01 esetet
A fordító nem 8 bites összeadást készít Figyelmen kívül maradt az egészpromóció Vizsgáld meg a generált kódot

Cél: qemu-user programjának statikus vizsgálata, A64 ISA disassembly:

aarch64-linux-gnu-objdump -d --disassemble=lab_add8 ./lab-bits

Ez a parancs nem futtatja a programot. Egyelőre azt figyeld meg, hogy a C-szintű bájtos adattípus nem követel minden köztes művelethez bájtszélességű utasítást.

Hibakereséskor előbb rögzítsd a bitmintát, a bitszélességet és a kívánt olvasatot. Ezután csak egy értelmezést vagy konverziót módosíts, majd ismételd meg az öntesztet. Pusztán más kiírási formátum választása nem javítja meg a hibás számítást.

2.2.11 Feladatok

  1. (magyarázó, alapszint — cél: megértés-ellenőrzés) Alakítsd át a 173 előjel nélküli értéket 8 bites bináris és hexadecimális alakra! Mi ugyanezen minta előjeles értéke? Add meg a −83 érték 8 bites kódját és 16 bites előjelkiterjesztését!

  2. (implementációs, középszint — cél: rutinépítés) Egészítsd ki a labort a 0xC8 + 0x64 és a 0x9C + 0xCE esetekkel! Előbb kézzel határozd meg az eredménymintát és a két túlcsordulási feltételt.

  3. (hibakeresési, középszint — cél: megértés-ellenőrzés) A lab_signed8 törzsét ideiglenesen cseréld return (int32_t)x; sorra. Mely eredmények romlanak el? Fordítsd újra ugyanazokkal a parancsokkal, vizsgáld meg a kimenetet és az objdump eredményét, majd egyetlen javítással állítsd helyre a programot.

2.2.12 Megoldások

1. megoldás — Segítség. Bontsd 173-at kettőhatványok összegére (128+…), majd a kapott 8 bites mintát olvasd le a kettes komplemens képlet szerint: ha a legfelső bit 1, vond ki 256-ot.

1. megoldás — Teljes megoldás. 173=128+32+8+4+1173=128+32+8+4+1, ezért a minta 0b10101101, azaz 0xAD. Nyolc bites kettes komplemens olvasata 173−256=−83173-256=-83. A −83 kódja tehát 0xAD, 16 bites előjelkiterjesztése 0xFFAD. A 0x00AD nem −83-at, hanem 173-at jelentene.

2. megoldás — Segítség. Számítsd ki mindkét esetben külön az előjel nélküli és az előjeles pontos összeget, majd vesd össze a 8 bitbe még beférő tartománnyal (0–255, illetve −128–127) — a túlcsordulás pontosan akkor áll fenn, ha a pontos érték kilóg ebből.

2. megoldás — Teljes megoldás. Az első esetben az előjel nélküli összeg 200+100=300200+100=300, maradéka 44, vagyis 0x2C: UO=1. Előjelesen −56+100=44-56+100=44, ezért SO=0.

A második esetben 156+206=362156+206=362, maradéka 106, vagyis 0x6A: UO=1. Előjelesen −100−50=−150-100-50=-150, ami kisebb −128-nál, ezért SO=1. A main öntesztje így bővíthető:

/* C17, AArch64 célprofil; beszúrás a végső puts elé. */
ok &= lab_add8(0xc8, 0x64, 0x2c, 1, 0);
ok &= lab_add8(0x9c, 0xce, 0x6a, 1, 1);

3. megoldás — Segítség. A (int32_t)x egyszerű konverzió nem vizsgálja, hogy a 8 bites minta legfelső bitje 1-e — gondold végig, melyik bemenet (0x2d vagy 0xd3/0xff) ad emiatt eltérő eredményt az eredeti függvényhez képest.

3. megoldás — Teljes megoldás. A hibás változat a uint8_t numerikus értékét őrzi meg: 0xD3 esetén 211-et, 0xFF esetén 255-öt ad. Emiatt az előjeles kiírás, a kiterjesztés és több túlcsordulási ellenőrzés hibás. Várt eredmény: FAIL, kilépési állapot 1. Az objdump a megváltozott megvalósítást mutatja, de az értelmezési hiba oka már a típusokból következik. A javítás a 128-tól kezdődő mintákból 256 kivonásának visszaállítása.

2.2.13 Rövid összefoglaló

A bitszélesség és az értelmezés együtt határozza meg egy bitminta számértékét. A hexadecimális jelölés négybites csoportokat tömörít, de önmagában nem mond semmit az előjelről vagy az adat szerepéről. Kettes komplemensben a legfelső bit negatív helyi értékű; ezért a pozitív és negatív tartomány nem szimmetrikus. Szélesítéskor a nullakiterjesztés az előjel nélküli, az előjelkiterjesztés az előjeles értéket őrzi meg. A labor szélesebb köztes típusokkal választotta külön a pontos összeget és a tárolt mintát. A gépi körbefordulásból nem következik az előjeles C-túlcsordulás megengedettsége.

2.2.14 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 helyi cross-libc statikus linkelhetősége, a ténylegesen telepített eszközláncverziók (gcc, binutils, QEMU), és a bemutatott laborkimenet tényleges, lefuttatott reprodukálása — ezek egyike sem történt meg ebben a munkafolyamatban.