Egy C-ben létrehozott rekordot assemblyben szeretnénk módosítani. Ismerjük a mezők nevét és típusát, de az A64 memóriautasításai nem ismerik sem a struktúrát, sem a mezőneveket: báziscímet és bájteltolást kapnak. A mérnöki feladat ezért az, hogy a C-fordító által választott adatelrendezést pontos címzési szerződéssé alakítsuk.
A mezők méretének egyszerű összeadása nem elegendő. A fordító kitöltő bájtokat helyezhet közéjük, és a struktúra végét is kiegészítheti. Egy hibás offset ettől még gyakran érvényes memóriacímre mutat: a program nem feltétlenül omlik össze, csak rossz mezőt olvas.
Az alfejezet profilja A64 ISA, AArch64 Linux, little-endian, LP64 adatmodell. Normál C-struktúrákat vizsgálunk, bitmezők, csomagolási attribútumok és külön előírt igazítás nélkül. Az előfeltétel a Tömbök és indexelt elérés assemblyben című alfejezet: a struktúramező címét szintén bájtokban számoljuk, de az eltolást itt a típus elrendezése határozza meg.
Az igazítás (alignment) azt írja elő, hogy egy objektum kezdőcíme milyen érték többszöröse legyen. Ha egy típus igazítása 8 bájt, akkor szabályosan elhelyezett objektumának címe 8-cal osztható.
A természetes igazítás (natural alignment) az adott típushoz az ABI által rendelt szokásos igazítás. A jelenlegi AArch64 LP64 profilban:
| C-típus | Méret [bájt] | Természetes igazítás [bájt] |
|---|---|---|
char |
1 | 1 |
int |
4 | 4 |
long |
8 | 8 |
| objektummutató | 8 | 8 |
Ezek nem általános C-nyelvi méretgaranciák. Például
a long más adatmodellben lehet 4 bájtos. A laborban
fordítási idejű ellenőrzésekkel rögzítjük a feltevéseket.
A C megőrzi a nem bitmező tagok deklarációs sorrendjét, és az első tag elé nem tesz kitöltést. A további tagok között, valamint a struktúra végén azonban lehet kitöltés (padding).
A szokásos, itt használt ABI-elrendezésben:
A harmadik szabály a struktúrákból álló tömbök miatt lényeges. Ha az
első elem megfelelően igazított, akkor a következő, sizeof
bájttal később kezdődő elemnek is annak kell lennie.
Vizsgáljuk ezt a C-típust:
/* C-adatelrendezés: AArch64 Linux, LP64, little-endian. */
struct lab_record {
char tag;
int count;
long total;
char state;
};Legyen az előző mező utáni első szabad bájt offsetje, a következő mező igazítása, pedig a mérete. Mindhárom mennyiség bájtban értendő. A következő mező kezdete:
A mező előtti kitöltés , az új szabad pozíció pedig . Ez a levezetés a fenti, normál ABI-elrendezést feltételezi.
| Mező | Korábbi [bájt] | Igazítás [bájt] | Offset [bájt] | Előtte padding [bájt] | Mező utáni pozíció [bájt] |
|---|---|---|---|---|---|
tag |
0 | 1 | 0 | 0 | 1 |
count |
1 | 4 | 4 | 3 | 8 |
total |
8 | 8 | 8 | 0 | 16 |
state |
16 | 1 | 16 | 0 | 17 |
A struktúra igazítása 8 bájt. A 17 bájtos felhasznált tartományt ezért 24 bájtra kell felfelé kerekíteni:
Az utolsó 7 bájt végi kitöltés (tail padding).
| Bájt-offsetek | Tartalom |
|---|---|
| 0 | tag |
| 1–3 | belső kitöltés |
| 4–7 | count |
| 8–15 | total |
| 16 | state |
| 17–23 | végi kitöltés |
A hasznos mezők összmérete csak bájt. A fennmaradó 10 bájt kitöltés.
Ha a nagyobb igazítású mezőket előrevesszük:
/* C-adatelrendezés: AArch64 Linux, LP64, little-endian. */
struct lab_record_compact {
long total;
int count;
char tag;
char state;
};akkor a mezőoffsetek rendre 0, 8, 12 és 13. A mezők 14 bájtot foglalnak el, majd 2 bájt végi kitöltés következik: a teljes méret 16 bájt.
Ez rekordonként 8 bájt megtakarítás. A nagyobb igazítású mezők előrevétele jó kiindulópont, de egy már publikált interfészben a sorrend megváltoztatása ABI-változás. A régi assembly offsetjei, illetve a struktúra nyers memóriaképére támaszkodó fájlformátumok többé nem egyeznek az új elrendezéssel.
A padding nem alkalmazásadat. Ne használjuk rejtett mezőként, és ne feltételezzük, hogy minden kitöltő bájt értéke nulla.
sizeof,
offsetof és az A64 mezőelérésA C sizeof operátora a teljes objektumméretet adja
vissza, a kitöltéssel együtt. Az
<stddef.h> fejléc offsetof makrója egy
tag bájteltolását adja meg a struktúra kezdetétől.
A példában:
sizeof(struct lab_record) értéke 24;offsetof(struct lab_record, count) értéke 4;offsetof(struct lab_record, total) értéke 8;_Alignof(struct lab_record) értéke 8.A count mező címe tehát:
A64 ISA, AArch64: ha az objektum címe az
X0 regiszterben van, a mezőelérés:
// A64 ISA, AArch64; X0 egy érvényes struct lab_record címe.
ldr w1, [x0, #4] // A count 4 bájtos bitmintája.
ldr x2, [x0, #8] // A total 8 bájtos bitmintája.
ldrb w3, [x0, #16] // A state 1 bájtja, nullakiterjesztéssel.Az utasítás szélességének egyeznie kell a mező szélességével. Az A64
LDR W1 4, az LDR X2 8, az LDRB W3
pedig 1 bájtot olvas. Az A64 LDR W1 az X1
felső 32 bitjét nullázza: ez nem előjeles
int → long konverzió. A negatív érték 32 bites
bitmintája ettől még helyesen kerül a W1 regiszterbe.
Az itt használt A64 unsigned-offset címzések korlátai (a teljes listát az A64 immediate-korlátok összefoglalója adja):
| A64 utasításcsalád | Immediate kódolása | Leírható bájteltolás |
|---|---|---|
LDR/STR Wn |
12 bites előjel nélküli érték, 4-gyel skálázva | 0–16380, 4-es lépéssel |
LDR/STR Xn |
12 bites előjel nélküli érték, 8-cal skálázva | 0–32760, 8-as lépéssel |
LDRB/STRB Wn |
12 bites előjel nélküli érték | 0–4095 |
Az assembly forrásban bájteltolást írunk: a
count esetében #4 szerepel, nem a kódolásban
tárolt 1.
Az offset kódolhatósága és a tényleges cím igazítása külön kérdés. Egy 8-cal osztható offset nem javítja meg a hibásan igazított báziscímet.
Munkakönyvtár: ~/arm-labor/a64/a024/ —
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/a024
cd ~/arm-labor/a64/a024A C-program létrehozza az objektumot, ellenőrzi az elrendezést, majd meghív egy rövid assemblyfüggvényt.
A hívási konvencióból most csak a szükséges szerződést használjuk:
A64/AAPCS64 szerint az első mutatóargumentum az
X0 regiszterben érkezik; az X1–X3
ideiglenesen felhasználható. A függvény nem hív más függvényt és nem
módosítja a vermet. Az A64 RET az X30
visszatérési címét használja. A teljes szabályrendszert Az AAPCS64 regiszterszerződés tárgyalja.
A lab-layout.c tartalma:
/* C-célprofil: AArch64 Linux, LP64, little-endian. */
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
struct lab_record {
char tag;
int count;
long total;
char state;
};
struct lab_record_compact {
long total;
int count;
char tag;
char state;
};
_Static_assert(CHAR_BIT == 8, "8 bites bajt szukseges");
_Static_assert(sizeof(int) == 4 && _Alignof(int) == 4,
"eltero int profil");
_Static_assert(sizeof(long) == 8 && _Alignof(long) == 8,
"LP64 long profil szukseges");
_Static_assert(offsetof(struct lab_record, tag) == 0, "tag offset");
_Static_assert(offsetof(struct lab_record, count) == 4, "count offset");
_Static_assert(offsetof(struct lab_record, total) == 8, "total offset");
_Static_assert(offsetof(struct lab_record, state) == 16, "state offset");
_Static_assert(sizeof(struct lab_record) == 24, "meret");
_Static_assert(_Alignof(struct lab_record) == 8, "igazitas");
_Static_assert(sizeof(struct lab_record_compact) == 16, "tomor meret");
extern void lab_update(struct lab_record *p);
int main(void)
{
struct lab_record record = {
.tag = 0x41,
.count = -3,
.total = 1000,
.state = 1
};
printf("size=%zu align=%zu compact=%zu\n",
sizeof record, _Alignof(struct lab_record),
sizeof(struct lab_record_compact));
printf("offsets=%zu,%zu,%zu,%zu\n",
offsetof(struct lab_record, tag),
offsetof(struct lab_record, count),
offsetof(struct lab_record, total),
offsetof(struct lab_record, state));
lab_update(&record);
printf("tag=0x%02x count=%d total=%ld state=%d\n",
(unsigned int)(unsigned char)record.tag,
record.count, record.total, (int)record.state);
return !(record.tag == 0x41 && record.count == 2 &&
record.total == 1024 && record.state == 2);
}A lab-layout.s tartalma:
// A64 ISA, ARMv8-A; GNU as direktívák AArch64 célhoz.
.text
.p2align 2
.global lab_update
.type lab_update, %function
lab_update:
ldr w1, [x0, #4]
add w1, w1, #5
str w1, [x0, #4]
ldr x2, [x0, #8]
add x2, x2, #24
str x2, [x0, #8]
ldrb w3, [x0, #16]
add w3, w3, #1
strb w3, [x0, #16]
lab_after_update:
ret
.size lab_update, .-lab_update
.section .note.GNU-stack,"",%progbitsAz ADD konstansai tolás nélkül kódolhatók (lásd az A64 immediate-korlátok összefoglalóját). A GNU as
.p2align 2 direktívája a kódot igazítja
bájtra; nem a C-struktúra elrendezését módosítja.
lab_update
teljes szerződéseA lab_update akkor helyettesíthető a következő
C-függvénnyel, ha a lenti előfeltételek teljesülnek:
/* C-referencia: a lab_update C-egyenértékű viselkedése a szerződés tartományán. */
void lab_update_ref(struct lab_record *p)
{
p->count += 5;
p->total += 24;
p->state += 1;
}| Feltétel | |
|---|---|
| Előfeltétel: mutató | p nem NULL, élő, írható,
struct lab_record típusú objektumra mutat (így a típus 8
bájtos igazítását is teljesíti); a hívás alatt más szál nem éri el az
objektumot |
Előfeltétel: count |
p->count <= INT_MAX - 5 |
Előfeltétel: total |
p->total <= LONG_MAX - 24 |
state |
nincs korlát: AArch64 Linuxon a sima char előjel
nélküli, a += 1 modulo 256 definiált, és ugyanazt a bájtot
adja, mint az ADD + STRB |
| Utófeltétel | count, total, state a régi
érték + 5, + 24, + 1; a tag mező és a kitöltő bájtok nem
változnak |
| Regiszterek és verem | csak X1–X3-at írja (caller-saved); a
vermet és az NZCV-t nem módosítja (ADD, nem
ADDS) |
Miért kell a két számtani előfeltétel? Az assembly
ADD W1, W1, #5 minden bemeneten definiált: a 32 bites
bitmintán modulo
számol, így például INT_MAX − 4-ből pontosan
INT_MIN lesz. A C p->count += 5 ugyanezen a
bemeneten előjeles túlcsordulás, vagyis definiálatlan
viselkedés. A véges bitmintát módosító assembly és a signed
C-összeadás ezért csak a C által definiált tartományon azonosítható;
azon kívül az assemblynek van eredménye, a C-nek nincs. Aki a
lab_update-et C-egyenértékűnek nevezi, annak az
előfeltételt is ki kell mondania, és a hívónak kell betartania: az
assembly nem ellenőrzi a C értéktartományait.
A tesztbemenet a tartomány belsejében van: a count
−3-ról 2-re, a total 1000-ről 1024-re változik, előjeles
túlcsordulás nélkül.
Target: AArch64 Linux ELF, QEMU user-mode. A statikus linkeléshez az AArch64 cross libc fejlesztői fájljai is szükségesek.
# Host: Ubuntu 24.04 x86_64.
# Target: AArch64 Linux, ARMv8-A, alapértelmezett little-endian.
aarch64-linux-gnu-gcc -std=c11 -O0 -g -Wall -Wextra \
-march=armv8-a -fno-pie \
-c lab-layout.c -o lab-layout-c.o
aarch64-linux-gnu-as -g -march=armv8-a -EL \
lab-layout.s -o lab-layout-asm.o
aarch64-linux-gnu-gcc -static -no-pie \
lab-layout-c.o lab-layout-asm.o -o lab-layout
# Target: qemu-user; várt kilépési állapot: 0.
qemu-aarch64 ./lab-layout
printf 'exit=%d\n' "$?"Várt, levezetett kimenet; itt nem történt futtatás:
size=24 align=8 compact=16offsets=0,4,8,16tag=0x41 count=2 total=1024 state=2exit=0A fordítási idejű ellenőrzések az ABI-feltevéseket, a visszatérési érték a mezőmódosításokat vizsgálja. A kettő külön védelmi réteg: a hibás elrendezést jobb már fordításkor észrevenni.
Igazítatlan memóriaelérésről (unaligned access) beszélünk, ha a hozzáférés címe nem többszöröse a hozzáférési szélességhez tartozó természetes igazításnak.
Az A64 alapprofilban nem igaz sem az, hogy minden ilyen elérés tiltott, sem az, hogy minden megengedett.
| Helyzet | A64/ARMv8-A alapprofil szerinti lényeg |
|---|---|
Szokásos LDR/STR, Normal memória, kikapcsolt általános
igazításellenőrzés |
Az igazítatlan elérés megengedett lehet. |
| Bekapcsolt általános igazításellenőrzés | A megfelelő igazítatlan hozzáférés igazítási hibát válthat ki. |
| Device memóriatípus | Az igazítatlan adatelérés nem megengedett. |
| Exkluzív és atomi hozzáférések | Külön, szigorúbb igazítási követelmények érvényesek; a sima
LDR/STR szabálya nem vihető át automatikusan. |
| Hozzáférés nem leképezett lapba is átnyúlik | Az igazítás megengedettségétől függetlenül memóriahiba keletkezhet. |
Az általános ellenőrzést többek között az AArch64 privilegizált
SCTLR_EL1.A vezérlőbit befolyásolja. Ezt Linux-felhasználói
programból nem állítjuk. QEMU user-mode alatt sem valódi
vendégkernel-konfigurációt módosítunk.
Három fontos különbség:
long *
dereferálása C-ben nem válik szabályossá attól, hogy egy A64 betöltés
azon a címen működne. Külső bájtpufferből igazított helyi objektumba
másoláshoz a memcpy megfelelő eszköz.LDUR
nevében az „unscaled” skálázatlan offsetet jelent, nem az
igazításellenőrzés megkerülését. Immediate-je előjeles, 9 bites
bájteltolás: −256–255.A GCC csomagolt, packed elrendezése megváltoztathatja az
offseteket és az igazítást. Ez külön szerződés, nem a normál
struktúraelrendezés „helytakarékos kapcsolója”, amely következmények
nélkül használható.
Tünet: a mező értéke hibás, de nincs összeomlás. Első hipotézisünk legyen a rossz offset vagy hozzáférési szélesség, ne rögtön a bájtsorrend.
Target: AArch64 objektumkód vizsgálata; nem futtatás.
aarch64-linux-gnu-objdump -dr lab-layout-asm.oEllenőrizzük, hogy az A64 kódban valóban a kívánt
Wn/Xn szélesség és bájteltolás szerepel-e.
A futás közbeni vizsgálathoz az első terminálban:
# Target: qemu-user, GDB-kapcsolatra vár.
# Folytatás után várt kilépési állapot: 0.
qemu-aarch64 -g 1234 ./lab-layoutA második terminálban:
# Target: a qemu-user alatt futó AArch64 program.
gdb-multiarch -q ./lab-layout \
-ex 'set pagination off' \
-ex 'target remote :1234' \
-ex 'break lab_after_update' \
-ex 'continue' \
-ex 'info registers x0 x1 x2 x3' \
-ex 'x/24bx $x0' \
-ex 'x/wd $x0+4' \
-ex 'x/gd $x0+8' \
-ex 'x/ub $x0+16' \
-ex 'continue'A töréspontnál az A64 X0 még az objektum báziscíme. A
várt értékek: X1=2, X2=1024,
X3=2; a mezők vizsgálata rendre 2, 1024 és 2 értéket mutat.
A padding bájtjaira nincs előírt várt érték.
Tünet: mezősorrend-változtatás után romlik el a
program. Hasonlítsuk össze az új offsetof
értékeket az assembly konstansaival. Egyetlen offsetet javítsunk, majd
ismételjük meg az ellenőrzést.
Tünet: ugyanazon mezőértékek mellett eltér a teljes
memóriakép. A padding eltérhet. A struktúrák logikai
egyenlőségét ezért általában nem helyettesíti a teljes objektumon
végzett memcmp.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Vezesd le a
lab_record_compact összes mezőoffsetjét és végi kitöltését!
Mennyi tárhelyet foglal 1000 eredeti, illetve 1000 átrendezett rekord?
Miért nem elég 14 bájtos lépésköz?
(implementációs, középszint — cél: rutinépítés)
Az eredeti lab_record mezősorrendjét változtasd a kompakt
sorrendre. Frissítsd a fordítási idejű ellenőrzéseket és az assembly
mezőeléréseit úgy, hogy az értékmódosítások változatlanok maradjanak.
Ugyanazokkal a laborparancsokkal építs és futtass.
(hibakeresési, középszint — cél: rutinépítés)
Állítsd vissza az eredeti elrendezést. Kizárólag az első A64 betöltést
módosítsd erre: ldr w1, [x0, #8]. A hozzá tartozó tárolás
maradjon #4 offsetű. Jósolj kimenetet, majd
objdump és GDB segítségével különítsd el az offsethibát a
bájtsorrendi problémától.
1. megoldás — Segítség. Rendezd sorba a
lab_record_compact mezőit igazítás szerint csökkenő
sorrendbe, és alkalmazd a fejezet align_up képletét egymás
után mindegyikre.
1. megoldás — Teljes megoldás. A total
offsetje 0, a count mezőé 8, a tag mezőé 12, a
state mezőé 13. A végpozíció 14, amelyet 8 bájtos
igazításhoz 16-ra kerekítünk. A két tömb mérete 24000, illetve 16000
bájt; a különbség 8000 bájt. A 14 bájtos lépésköz mellett a második elem
long mezője már nem lenne 8 bájtra igazítva.
2. megoldás — Segítség. Az offsetek megváltoznak, de
a MŰVELETEK (melyik mezőhöz mennyit adunk hozzá) nem — csak az assembly
#offset konstansait és a _Static_assert
értékeit kell a kompakt elrendezéshez igazítanod.
2. megoldás — Teljes megoldás. Az új ellenőrzött
offsetek: tag=12, count=8,
total=0, state=13; a méret 16, az igazítás
továbbra is 8. Az A64 count-betöltés és -tárolás offsetje
#8, a total műveleteié #0, a
state műveleteié #13. A szélességeket nem
változtatjuk meg. A kijelölt mezőneves C-inicializálás továbbra is
helyes. A mezőértékeket kiíró sor változatlan, az offseteket és
méreteket kiíró sor módosul; a várt kilépési állapot 0.
3. megoldás — Segítség. A #8 offset a
total mező ELSŐ 4 bájtjára mutat, nem a
count-ra — little-endian sorrendben milyen 32 bites értéket
adna ez vissza 1000-ből?
3. megoldás — Teljes megoldás. Little-endian
profilban a total mező első négy bájtjának előjel nélküli
32 bites értéke 1000. A hibás A64 betöltés ezt adja a W1
regiszternek, majd az összeadás után 1005 kerül a count
mezőbe. A total végértéke továbbra is 1024. A várt kilépési
állapot 1. Az objdump megmutatja az eltérő betöltési és
tárolási offsetet; GDB-ben a count és total
címén olvasott értékek alátámasztják a hipotézist. Egyetlen javítás
szükséges: az első betöltés offsetjét visszaállítani #4
értékre.
A struktúra mezőcíme a báziscím és a mező bájteltolásának összege. Az
eltolásokat nem pusztán a mezőméretek összeadásával kapjuk: minden mezőt
a saját igazításához, a teljes méretet pedig a struktúra igazításához
kell kerekíteni. A sizeof tartalmazza a kitöltést, az
offsetof közvetlenül ellenőrizhetővé teszi az assembly
konstansait. A mezősorrend csökkentheti a méretet, de megváltoztatja az
interfész elrendezési szerződését. Az A64 hozzáférési szélességet a
C-mező méretéhez választjuk. Az igazítatlan elérés ISA-szintű
megengedettsége nem helyettesíti a C szabályait, és nem garantál kedvező
teljesítményt.
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.
sizeof, _Alignof és offsetof.
Ellenőrizve: ez a WG14 utolsó nyilvános bizottsági
tervezete a C11 szabványosítás előtt; a gyakorlatban mindenhol „C11
szabványként” hivatkozzák, mert a fizetős ISO-szöveg nem nyilvános, de
technikailag tervezet, nem az eladott ISO/IEC 9899:2011 dokumentum (az
eltérés túlnyomórészt szerkesztési jellegű).Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is ténylegesen fennmaradnak: a programot itt nem fordítottuk és nem futtattuk. A telepített Ubuntu-csomagverziók, a statikus cross libc rendelkezésre állása és a GDB–QEMU munkafolyamat tényleges eredménye a hallgatói laborban ellenőrizendő. Az igazítatlan hozzáférés konkrét költségéről nem áll rendelkezésre mérési adat.