7.4 Bájtsorrend: endianness és miért releváns a határon

7.4.1 Miért a rendszerhatáron derül ki a bájtsorrend?

Egy C-program helyesen kezeli a uint32_t változókat, mégis hibás fájlt készít: a fogadó program minden hosszmezőt értelmetlenül nagynak lát. Hasonló jelenség hálózati kommunikációnál is előfordulhat. A számítás helyes, de a többbájtos érték memóriabeli bájtsorrendje nem egyezik a külső formátum előírásával.

A bájtsorrend (endianness) akkor válik láthatóvá, amikor ugyanazt az adatot egyszer többbájtos értékként, máskor bájtsorozatként értelmezzük. Ehhez a Load/store címzési módok ismerete szükséges: különösen a hozzáférési szélesség és az effektív cím megkülönböztetése.

Profil: az alfejezet minden assembly példája A64 ISA, AArch64, ARMv8-A, I. rész, little-endian Linux-felhasználói környezetben. A GNU assembler direktívái ehhez a célhoz tartozó eszközlánc-elemek, nem processzorutasítások. Az AArch64 architektúra adat-bájtsorrendjét nem szabad általánosan, minden rendszerre little-endianként rögzíteni; itt a könyv kiválasztott profilját használjuk.

7.4.2 Definíció: melyik bájt kerül a kisebb címre?

Egy többbájtos érték tárolásakor:

Legyen az előjel nélküli, 32 bites érték 0x12345678, és jelölje p az első bájt szimbolikus címét:

Cím Little-endian bájt Big-endian bájt
p + 0 0x78 0x12
p + 1 0x56 0x34
p + 2 0x34 0x56
p + 3 0x12 0x78

A táblázatban lefelé haladva nő a cím. A szám szokásos hexadecimális leírásában viszont balra vannak a nagyobb helyi értékű számjegyek. A kettő összekeverése gyakori hibaforrás.

Levezetés: ha a növekvő címeken található négy bájt rendre b0,b1,b2,b3b_0,b_1,b_2,b_3, little-endian értelmezéskor

V=b0+28b1+216b2+224b3. V=b_0+2^8b_1+2^{16}b_2+2^{24}b_3.

Itt VV dimenzió nélküli, előjel nélküli 32 bites szám; minden bib_i dimenzió nélküli bájtérték a 0–255 tartományban, az index pedig bájtonként növekvő címeltolást jelöl.

Ezért a 0x12 0x34 0x56 0x78 bájtsorozat little-endian betöltése 0x78563412 értéket ad, nem 0x12345678-at.

Három fontos következmény:

  1. A bájtsorrend nem az egy bájton belüli bitek megfordítását jelenti.
  2. A regiszterben lévő számértéknek nincs memóriacímek szerinti bájtsorrendje. A bájtsorrend a bájtsorozat és a többbájtos érték közötti leképezéshez tartozik.
  3. Az előjelkezelés külön kérdés. Ugyanannak a bitsorozatnak előjeles és előjel nélküli olvasata lehet, de ettől a tárolt bájtok sorrendje nem változik.

Az A64 LDR Wn négy bájtból képez 32 bites értéket; az A64 LDRB Wn egyetlen bájtot tölt be és nullával kiterjeszt. Egyetlen bájton nincs mit bájtsorrend szerint felcserélni.

7.4.3 Három gyakorlati hibaforrás

7.4.3.1 Fájlformátum: a natív objektum nem feltétlenül kész fájlmező

A fwrite(&value, sizeof value, 1, file) az objektum memóriabeli bájtjait írja ki. Ha a fájlformátum big-endian 32 bites mezőt követel, egy little-endian gépen ez önmagában hibás.

A formátum szabálya az irányadó, nem a processzoré. Rögzíteni kell:

Teljes C-struktúra közvetlen kiírásakor az igazítás és a kitöltő bájtok is problémát okozhatnak; ezt a Struktúrák, igazítás és mezőelérés tárgyalja.

7.4.3.2 Hálózati protokoll: a bájtfolyamnak saját szerződése van

Számos hálózati protokoll big-endian, úgynevezett hálózati bájtsorrendet (network byte order) használ. Ez azonban nem jelenti azt, hogy minden alkalmazási protokoll minden mezője ilyen.

A C htonl és ntohl függvényei 32 bites értékek gépi és hálózati bájtsorrendje között alakítanak. Az átalakítást a megfelelő határon egyszer kell elvégezni. Ha a beolvasó réteg már dekódolta a mezőt, a feldolgozó réteg újabb cseréje ismét elrontja.

7.4.3.3 Union-alapú trükk: helyi megfigyelésből nem lesz hordozható formátum

Tegyük fel, hogy egy C-union uint32_t value és unsigned char byte[4] tagot tartalmaz. A value = 0x12345678 beállítás után a jelenlegi little-endian profilban byte[0] értéke 0x78.

A téves feltételezés az, hogy az első tömbelem mindig a hexadecimális leírás bal szélső bájtját tartalmazza. Nem: a legkisebb című bájtot tartalmazza.

Az objektumábrázolás unsigned char bájtokon keresztül megvizsgálható; a hiba itt a platformfüggő sorrendre épített szerződés. A memcpy sem végez bájtsorrend-konverziót. Egy bájtpuffer uint32_t * mutatóvá alakítása pedig a bájtsorrenden túl igazítási és C-típuselérési problémákat is okozhat.

7.4.4 Bájtcserélő utasítások: a csoportméret számít

Az alábbi táblázat minden utasítása A64 ISA. A bemenetek előjel nélküli bitmintaként értendők.

Utasítás Bemenet Eredmény Művelet
REV Wd, Wn 0x12345678 0x78563412 Mind a négy bájt sorrendjét megfordítja
REV16 Wd, Wn 0x12345678 0x34127856 Minden 16 bites csoporton belül cserél
REV Xd, Xn 0x1122334455667788 0x8877665544332211 Mind a nyolc bájt sorrendjét megfordítja
REV16 Xd, Xn 0x1122334455667788 0x2211443366558877 Minden 16 bites csoporton belül cserél
REV32 Xd, Xn 0x1122334455667788 0x4433221188776655 Minden 32 bites csoporton belül cserél

Az A64 REV32 Xd, Xn nem cseréli fel a két 32 bites csoport helyét. Nincs külön A64 REV32 Wd, Wn alak: egy 32 bites érték teljes bájtcseréjéhez az A64 REV Wd, Wn használható.

Ezek regiszterutasítások: nincs immediate operandusuk, nem olvasnak memóriát, és nem módosítják az NZCV állapotjelzőket. A W-regiszterbe író alakok a hozzá tartozó X-regiszter felső 32 bitjét nullázzák.

7.4.5 Kidolgozott példa: natív érték és külső bájtsorozat

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

A labor három dolgot ellenőriz:

  1. a natív 32 bites érték little-endian tárolását;
  2. egy big-endian bájtsorozat dekódolását;
  3. a különböző A64 bájtcserék eredményét.

Mentsük a következőt lab-endian.s néven:

// A64 ISA, ARMv8-A; GNU as; little-endian Linux, I. rész.
.section .text
.global _start
.type _start, %function

_start:
    adr     x0, lab_native
    ldr     w1, [x0]

    adr     x2, lab_wire
    ldr     w3, [x2]
    rev     w4, w3

    rev16   w5, w1

    adr     x6, lab_wide
    ldr     x7, [x6]
    rev     x10, x7
    rev16   x11, x7
    rev32   x12, x7

    adr     x13, lab_output
    str     w1, [x13]
    rev     w14, w1
    str     w14, [x13, #4]

lab_snapshot:
    cmp     w1, w4
    b.ne    .Lfail

    ldrb    w15, [x13]
    cmp     w15, #0x78
    b.ne    .Lfail

    ldrb    w15, [x13, #4]
    cmp     w15, #0x12
    b.ne    .Lfail

    movz    w0, #0
    b       .Lexit

.Lfail:
    movz    w0, #1

.Lexit:
    movz    x8, #93
    svc     #0

.size _start, . - _start

.section .rodata
.balign 4
lab_native:
    .word   0x12345678

lab_wire:
    .byte   0x12, 0x34, 0x56, 0x78

.balign 8
lab_wide:
    .quad   0x1122334455667788

.section .bss
.balign 4
lab_output:
    .skip   8

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

Az AArch64 GNU assembler .word direktívája itt négybájtos értéket bocsát ki a kiválasztott bájtsorrendben. A .byte viszont a felsorolt bájtokat adja ki, sorrendben. Ezért ugyanannak a logikai számnak a natív és a külső ábrázolása különböző lesz.

Immediate-ok: a példa minden konstansa és eltolása kódolható (lásd az A64 immediate-korlátok összefoglalóját); a STR Wn eltolása négy többszöröse, és a kis laboron belüli címkék az ADR hatótávolságán belül vannak. Az elágazások címkés, korlátozott PC-relatív eléréséről lásd a Feltétel nélküli és feltételes elágazás alfejezetet.

7.4.5.1 Összeállítás, linkelés, futtatás

Ubuntu 24.04 x86_64 hoston, qemu-user céllal:

# Target: qemu-user, AArch64 little-endian.
aarch64-linux-gnu-as -EL -march=armv8-a -g \
    -o lab-endian.o lab-endian.s

aarch64-linux-gnu-ld -EL -static -e _start \
    -o lab-endian lab-endian.o

aarch64-linux-gnu-readelf -h lab-endian

qemu-aarch64 ./lab-endian
printf 'kilépés=%d\n' "$?"

Várt, nem itt mért eredmény: a program nem ír a szabványos kimenetre, a shell pedig kilépés=0 sort jelez. Az 1 kilépési állapot valamelyik beépített összehasonlítás hibáját jelenti. Az ELF-fejléc adatábrázolásának little-endiant, géptípusának AArch64-et kell mutatnia.

A 0 állapot csak a beépített feltételeket igazolja; a 64 bites cserék részletes ellenőrzése a következő lépés.

7.4.6 Bájtsorrend ellenőrzése GDB-memóriadumpban

Első terminál:

# Target: qemu-user, AArch64; várakozás GDB-kapcsolatra.
qemu-aarch64 -g 1234 ./lab-endian

Második terminálban készítsünk név szerint megadott GDB-parancsfájlt, majd indítsuk a hibakeresőt:

# Target: az előző qemu-user folyamat AArch64 programja.
cat > lab-endian.gdb <<'EOF'
set pagination off
target remote :1234
break *lab_snapshot
continue
show endian
info registers x1 x3 x4 x5 x7 x10 x11 x12 x14
x/4bx $x0
x/4bx $x2
x/8bx $x13
x/wx $x2
x/2wx $x13
continue
quit
EOF

gdb-multiarch -q -x lab-endian.gdb ./lab-endian

A lab_snapshot címnél a tárolások már végrehajtódtak, a kilépési út még nem módosította az A64 X0 regisztert.

Várt regiszterértékek — A64:

Regiszter Előjel nélküli érték Jelentés
W1 0x12345678 Natív betöltés
W3 0x78563412 Külső bájtok nyers betöltése
W4 0x12345678 Dekódolt külső érték
W5 0x34127856 16 bites csoportonkénti csere
X7 0x1122334455667788 64 bites bemenet
X10 0x8877665544332211 Teljes 64 bites bájtcsere
X11 0x2211443366558877 16 bites csoportonkénti csere
X12 0x4433221188776655 32 bites csoportonkénti csere
W14 0x78563412 Big-endian tároláshoz előkészített érték

A GDB-parancs X-nézetben listázza a regisztereket. A W-nézetben írt értékek felső 32 bitje nulla.

Várt memóriaállapot növekvő címek szerint:

Terület Bájtsorozat
lab_native 0x78 0x56 0x34 0x12
lab_wire 0x12 0x34 0x56 0x78
lab_output, első négy bájt 0x78 0x56 0x34 0x12
lab_output, következő négy bájt 0x12 0x34 0x56 0x78

A GDB x/4bx parancsa négy egybájtos egységet jelenít meg hexadecimálisan. Az x/wx ezzel szemben egy négybájtos egységet a hibakereső aktuális cél-bájtsorrendje szerint értelmez.

Ezért az x/wx $x2 várt eredménye 0x78563412. A debugger nem tudja, hogy a puffer egy protokoll big-endian mezője: csak négy bájtot lát egy little-endian célon.

Ne állítsuk át a GDB bájtsorrendjét azért, hogy a protokollmező „szebben” jelenjen meg. A célgép és a külső formátum bájtsorrendje külön tulajdonság.

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

Tünet Hipotézis Adatgyűjtés és egyetlen javítás
0x12345678 helyett 0x78563412 Hiányzó bájtcsere GDB-bájtdump; egy A64 REV Wd, Wn beillesztése a dekódolási határon
A bájtpárok jók, a teljes érték hibás A64 REV16 került REV helyére Disassembly; csak az utasítás cseréje
Egy rétegben jó, a következőben hibás Kétszeres konverzió Regiszterérték vizsgálata mindkét réteghatáron; egy fölösleges csere eltávolítása
Csak bizonyos puffercímeken jelentkezik hiba Nem pusztán bájtsorrend-probléma Cím, igazítás és hozzáférési szélesség ellenőrzése

A tényleges utasítások vizsgálata:

# Target: a qemu-user számára épített AArch64 ELF.
aarch64-linux-gnu-objdump -d lab-endian

Ellenpróbához ne kizárólag 0x00000000 vagy 0xffffffff értéket használjunk: ezek bájtcserére változatlanok. A különböző bájtokat tartalmazó tesztminta sokkal informatívabb.

7.4.8 Feladatok

  1. (magyarázó, alapszint — cél: megértés-ellenőrzés) Egy négybájtos puffer tartalma növekvő címeken 0x01 0x02 0x03 0x04. Milyen előjel nélküli értéket ad az A64 LDR Wn a jelenlegi profilban? Miért ad mást a big-endian értelmezés?

  2. (implementációs, középszint — cél: rutinépítés) Egészítsük ki a labort úgy, hogy az A64 X7 regiszterben lévő értéket big-endian bájtsorozatként is eltárolja. Használjunk új, nyolcbájtos puffert. Ellenőrizzük GDB-vel.

  3. (hibakeresési, középszint — cél: rutinépítés) A labor dekódoló A64 rev w4, w3 utasítását cseréljük rev16 w4, w3 alakra. Jósoljuk meg W4 értékét és a kilépési állapotot, majd végezzünk bájtdumpot és disassembly-ellenőrzést.

7.4.9 Megoldások

1. megoldás — Segítség. Rendeld hozzá a négy bájtot a megfelelő helyi értékekhez a little-endian szabály szerint (b0+28b1+...b_0 + 2^8 b_1 + ...), majd ismételd meg fordított hozzárendeléssel.

1. megoldás — Teljes megoldás. Little-endian betöltéskor az eredmény 0x04030201; big-endian értelmezéssel 0x01020304. Az eltérés oka, hogy más helyi értéket rendelünk ugyanahhoz a cím szerinti bájtsorozathoz. Az egyes bájtok bitmintája nem változik.

2. megoldás — Segítség. Egyetlen REV-fel teljes 64 bites bájtcserét kapsz; ha ezt az eredményt little-endian tárolod, a memóriabeli bájtsorrend éppen a kívánt big-endian lesz. Hol kell ezt a két lépést (csere, tárolás) a meglévő kódba illeszteni?

2. megoldás — Teljes megoldás. A meglévő .bss részhez, az AArch64 GNU assembler környezetében:

// AArch64 GNU as direktívák; I. rész.
.balign 8
lab_output64:
    .skip 8

Az A64 kódban a lab_snapshot címke elé:

// A64 ISA, little-endian cél.
adr     x16, lab_output64
rev     x17, x7
str     x17, [x16]

Az előző assemble–link–run parancsokkal építsük újra. A GDB-parancsfájlban a második continue elé kerüljön az x/8bx $x16 vizsgálat. A várt bájtok: 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88. A regiszterbeli bájtcsere és a little-endian tárolás együtt állítja elő a kívánt külső sorrendet.

3. megoldás — Segítség. A REV16 16 bites csoportokon belül cserél — nézd meg W3 két 16 bites felét külön-külön, és csak azokon belül fordítsd meg a bájtokat.

3. megoldás — Teljes megoldás. Az A64 W3 értéke 0x78563412, ezért az A64 REV16 eredménye W4-ben 0x56781234. Ez nem egyezik W1 értékével; a várt kilépési állapot 1. A lab_wire bájtjai változatlanok, tehát a hiba a dekódoló műveletben van. Egyetlen javításként állítsuk vissza az A64 REV utasítást, majd ismételjük meg az ellenőrzést.

7.4.10 Rövid összefoglaló

A bájtsorrend a többbájtos érték és a növekvő memóriacímeken tárolt bájtok kapcsolatát határozza meg. A könyv AArch64 laborprofilja little-endian, de a fájlok és protokollok ettől független ábrázolást írhatnak elő. Az A64 REV teljes regiszterszélességen, a REV16 és REV32 meghatározott csoportokon belül cserél bájtokat. A helyes konverzióhoz ezért a mező szélességét is ismerni kell. Hibakereséskor a GDB bájtonkénti nézetét vessük össze a regiszterértékkel; egy többbájtos memóriamegjelenítés már értelmezést is végez. A konverzió helyét dokumentáljuk, és különböző bájtokat tartalmazó mintával teszteljünk.

7.4.11 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: az összeállítás, linkelés, QEMU-futtatás és GDB-vizsgálat itt nem történt meg. A közölt állapotok levezetett, várt eredmények. A konkrét Ubuntu 24.04 csomagverziókat és a labor tényleges működését a hallgatói környezetben kell rögzíteni és ellenőrizni.