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.
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 , little-endian értelmezéskor
Itt dimenzió nélküli, előjel nélküli 32 bites szám; minden 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:
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.
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.
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.
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.
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.
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/a025A labor három dolgot ellenőriz:
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,"",%progbitsAz 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.
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.
Első terminál:
# Target: qemu-user, AArch64; várakozás GDB-kapcsolatra.
qemu-aarch64 -g 1234 ./lab-endianMá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-endianA 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.
| 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-endianEllenpró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.
(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?
(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.
(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.
1. megoldás — Segítség. Rendeld hozzá a négy bájtot a megfelelő helyi értékekhez a little-endian szabály szerint (), 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 8Az 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.
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.
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.
REV, REV16, REV32.
Ellenőrizve: a link él; a fejezetszám és a kiadásbetű az
Arm-oldalak közös korlátja miatt nem ellenőrzött.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.