Egy C-függvény helyes eredményt ad, mégis túl lassú. Egy külső könyvtár meghívása után látszólag sértetlen változók változnak meg. Egy mikrokontroller perifériája nem reagál a programra. Egy hibás memóriahozzáférés pedig nem ott okoz összeomlást, ahol a forráskód alapján várnád.
Ezekben a helyzetekben ugyanaz a kérdés merül fel: milyen gépi műveletek valósítják meg a C-programot, és milyen szerződésekre támaszkodnak?
Az assembly ismerete nem azt jelenti, hogy ezentúl minden függvényt kézzel kell megírnod. Gyakrabban fogsz fordító által előállított kódot olvasni, mint saját assemblyt készíteni. A cél annak felismerése, hogy egy teljesítményprobléma, hibás függvényhívás vagy biztonsági hiba melyik rétegben keletkezik.
Ehhez az alfejezethez nincs szükség korábbi assemblyismeretre. A laborban egy rövid C-programból indulunk, és csak annyi gépi részletet vezetünk be, amennyi a megfigyelés értelmezéséhez szükséges.
Az assembly nyelv az adott utasításkészlet gépi utasításainak szimbolikus leírására szolgáló nyelv. Az utasításkészlet-architektúra (instruction set architecture, ISA) meghatározza többek között az elérhető utasításokat, a programból látható regisztereket és az utasítások előírt hatását. A regiszter itt a processzor utasításaiban közvetlenül megnevezhető adattároló hely.
Az assemblyforrás nem kizárólag utasításokat tartalmazhat: vannak benne szimbolikus nevek és a fordítóeszköznek szóló direktívák is. Ezért nem minden assemblyforrás-sor jelent egy végrehajtandó gépi utasítást.
A C-program útja leegyszerűsítve:
| Réteg | Feladata | Mit vizsgálunk benne? |
|---|---|---|
| C-forrás | A számítás leírása a C szabályaival | Típusok, algoritmus, megengedett műveletek |
| C-fordító | Gépi megvalósítás kiválasztása, optimalizálás | Milyen műveleteket hagyott el vagy alakított át? |
| Assemblyforrás | A célgépi műveletek szimbolikus alakja | Milyen utasítások és regiszterek szerepelnek? |
| Assembler | Tárgyfájl előállítása | Utasításkódok, szimbólumok, később feloldandó hivatkozások |
| Linker | Tárgyfájlok és könyvtárak összekapcsolása | Hol találhatók a hívott függvények és adatok? |
| Betöltés és végrehajtás | A program futtatása a célkörnyezetben | Milyen állapotváltozások történnek? |
A gyakorlatban a köztes assemblyszöveget nem feltétlenül mentjük külön fájlba. A rétegek ettől még hasznos gondolkodási modellt alkotnak. A részletes munkafolyamatot a C fordítási láncolat és hova kerül az assembly bontja ki.
Az ISA azt írja elő, mit jelentenek az utasítások. A mikroarchitektúra (microarchitecture) az adott processzor belső megvalósítása: hogyan szervezi meg például az utasítások és memóriaelérések végrehajtását. Azonos ISA-t támogató processzorokon ugyanaz a program eltérő sebességgel futhat.
Teljesítmény. Egy rövid C-kifejezésből több utasítás keletkezhet, egy hosszabb ciklust pedig a fordító jelentősen átalakíthat. A gépi kód megmutatja, történt-e vektorizálás, maradt-e ismételt memóriaolvasás, vagy keletkezett-e költségesnek bizonyuló elágazás. Ebből azonban még nem következik futási idő: a kevesebb utasítás nem automatikusan gyorsabb. A kódelemzés hipotézist ad, a mérés ellenőrzi.
Interoperabilitás. Különböző nyelveken vagy külön fordított modulokban megírt függvényeknek meg kell egyezniük az argumentumok és eredmények átadásában. Ezt részben a bináris alkalmazásinterfész (application binary interface, ABI) szabályozza. Egy hibás assemblyfüggvény helyes számítást végezhet, mégis tönkreteheti a hívó állapotát, ha felülír egy megőrzendő regisztert. AArch64-en ezt az AAPCS64 regiszterszerződés alapján fogjuk ellenőrizni.
Hardverelérés. A C nyelv önmagában nem határoz meg
mikrokontroller-perifériákat, megszakításvezérlőt vagy
processzorspecifikus vezérlőutasításokat. Sok hardverkezelés megírható
C-ben, de a helyes működéshez ismerni kell az alatta lévő architektúrát
és az eszköz dokumentációját. A volatile sem általános
megoldás minden sorrendezési vagy párhuzamossági problémára.
Biztonsági hibák elemzése. Egy érvénytelen memóriaírás forráskódbeli helye és a későbbi összeomlás helye eltérhet. A regiszterek, a memória és a gépi utasítások vizsgálata segít feltárni, mi sérült meg. A könyv ezt védelmi célból használja: hibakeresésre, ellenőrzésre és a védelmi mechanizmusok megértésére, nem támadási receptek készítésére.
Ezekhez nem mindig szükséges kézzel assemblyt írni. Az ISA-szintű olvasási és következtetési képesség viszont sokszor nélkülözhetetlen.
A C nem definiált viselkedése (undefined behavior, UB) olyan eset, amelyre a nyelvi szabvány nem ír elő követelményt. Ez nem azt jelenti, hogy a program kötelezően hibajelzést ad, vagy hogy a processzor „véletlenszerűen” működik.
Fontos példa az egész aritmetika:
uint32_t értékeinek
tartománya 0–4294967295.Nézzük az x + 1 > x kifejezést, ahol x
típusa int. Minden olyan bemenetre igaz, amelynél az
összeadás definiált. Az INT_MAX bemenetnél viszont már maga
az összeadás sérti ezt a feltételt. A fordító ezért a teljes kifejezést
állandó igaz értékre egyszerűsítheti.
Nem helyes így érvelni: „A processzor összeadója körbefordul, tehát a C-kifejezés eredménye is ebből következik.” Lehetséges, hogy a fordító egyáltalán nem állít elő összeadó utasítást.
Az assembly az adott bináris megvalósítást teszi láthatóvá. Nem ad visszamenőleg definiált C-jelentést egy UB-t tartalmazó programnak. Más optimalizálás, fordító vagy környezet más kódot eredményezhet.
Munkakönyvtár: ~/arm-labor/a64/a001/ —
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/a001
cd ~/arm-labor/a64/a001A fejezet végigvitt projektje. Az itt elkészülő
lab-hidden program a Bevezetés:
hardver, ISA és a gép nyelve fejezet közös, végigvitt projektje. A
Számítógép-architektúra fogalmai, Az ARM család áttekintése és a C fordítási láncolat alfejezet végén egy-egy rövid
„Végigvitt projekt” lépés erre a programra alkalmazza az ott tanultakat:
utasításkódolás, célplatform-azonosítás, a fordítási lánc lépései. Az
alfejezetek saját, kisebb buildjei célzott kiegészítések; első
olvasáskor kihagyhatók, a projekt lépései nem. A lab-hidden
fájljai ebben a könyvtárban maradnak, a későbbi lépések átmásolják
őket.
Célprofil: A64 ISA, AArch64 Linux, ARMv8-A
alapprofil, little-endian; futtatás qemu-aarch64 user-mode
emulációban.
A gazdagép az Ubuntu 24.04 LTS x86_64 alapkörnyezet. A parancsokhoz a
gcc-aarch64-linux-gnu,
binutils-aarch64-linux-gnu és qemu-user
csomagok szükségesek. Telepítésüket és ellenőrzésüket a Labor-alapkörnyezet: eszközlánc telepítése és
verifikáció részletezi.
Mentsd a következő teljes programot lab-hidden.c
néven:
#include <inttypes.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
uint32_t lab_next_u(uint32_t x)
{
return x + UINT32_C(1);
}
int lab_rises_s(int x)
{
return x + 1 > x;
}
int main(void)
{
uint32_t u = lab_next_u(UINT32_MAX);
int s = lab_rises_s(INT_MAX - 1);
printf("unsigned_next=%" PRIu32 "\n", u);
printf("signed_rises=%d\n", s);
return (u == UINT32_C(0) && s == 1) ? 0 : 1;
}A teszt szándékosan nem hívja meg a
lab_rises_s függvényt INT_MAX argumentummal. A
ténylegesen vizsgált előjeles összeadás eredménye INT_MAX,
tehát ábrázolható.
Az alábbi parancsok a gazdagépen futnak; az elkészült program célja végig AArch64, qemu-user.
# C -> A64 assemblyforrás.
aarch64-linux-gnu-gcc \
-std=c17 -O2 -fno-inline -Wall -Wextra \
-march=armv8-a -mlittle-endian \
-S lab-hidden.c -o lab-hidden.s
# A64 assemblyforrás -> AArch64 tárgyfájl.
aarch64-linux-gnu-as \
-march=armv8-a -EL \
lab-hidden.s -o lab-hidden.o
# Tárgyfájl -> statikusan linkelt AArch64 Linux-program.
aarch64-linux-gnu-gcc \
-march=armv8-a -mlittle-endian -static \
lab-hidden.o -o lab-hidden
# Futtatás: AArch64 qemu-user cél.
qemu-aarch64 ./lab-hidden
printf 'exit=%s\n' "$?"A -S assemblyforrásnál állítja meg a C-fordítást. A
-fno-inline megakadályozza a függvények szokásos
beágyazását (inlining), így könnyebb külön megvizsgálni őket;
az összes többi optimalizálást nem tiltja le. A statikus linkelés ebben
a laborban elkerüli a külön célrendszeri dinamikus betöltő
beállítását.
Minden buildlépés várt kilépési állapota 0. Ha valamelyik hibával leáll, ne futtasd tovább a lépéssort egy esetleg korábbról megmaradt binárissal.
Levezetett, nem ténylegesen futtatott eredmény:
| Ellenőrzés | Várt érték |
|---|---|
unsigned_next kimeneti mező |
0 |
signed_rises kimeneti mező |
1 |
exit kimeneti mező |
0 |
Az előjel nélküli eredmény levezetése:
Itt minden mennyiség dimenzió nélküli egész érték; a feltétel a labor 32 bites előjel nélküli számítása. A visszaadott 0 tehát nem hiba, hanem a C által előírt eredmény.
A visszafejtés (disassembly) a gépi utasításkódokat olvasható assemblyalakban jeleníti meg. A következő parancsok a gazdagépen vizsgálják a qemu-user számára készített AArch64 binárist; nem futtatják azt:
aarch64-linux-gnu-objdump \
-d --disassemble=lab_next_u lab-hidden
aarch64-linux-gnu-objdump \
-d --disassemble=lab_rises_s lab-hiddenA lab_next_u esetében várható, leegyszerűsített
utasításminta:
// A64 ISA (AArch64, I. rész).
// Illusztratív függvénytörzs, nem különálló teljes program.
lab_next_u:
add W0, W0, #1
retAz A64 W0 egy 32 bites regiszternézet. Ennél az AAPCS64
szerinti függvényinterfésznél ezen érkezik az argumentum, és ezen
távozik az eredmény. Az A64 ADD itt hozzáad 1-et, és a 32
bites eredményt tartja meg. Az utasítás ezen alakjának közvetlen
konstansa (immediate) 12 bites előjel nélküli érték lehet,
opcionálisan 12 bittel balra tolva; az 1 eltolás nélkül kódolható.
Az A64 RET visszatérést végez, alapértelmezésben az A64
X30 64 bites regiszterben tárolt címre. A hívás részleteit
később tanuljuk meg.
A lab_rises_s függvénynél optimalizált fordításban
várhatóan az állandó 1 előállítását és visszatérést látod, nem az
eredeti összeadást és összehasonlítást. Ez megengedett optimalizálás,
nem kötelező utasítássorozat. A konkrét alakot a laborban kell
ellenőrizni.
A vizsgálat menete legyen mindig: tünet → hipotézis → adatgyűjtés → egy változtatás → új ellenőrzés.
| Tünet | Lehetséges ok | Ellenőrzés vagy javítás |
|---|---|---|
| Közvetlen indításkor végrehajtási formátumhiba jelenik meg | Az x86_64 gazdagép AArch64 programot kapott | Használd a qemu-aarch64 ./lab-hidden parancsot |
| A program nem a várt eredményt adja a forrás módosítása után | Régi assemblyből vagy tárgyfájlból készült bináris fut | Ismételd meg mindhárom buildlépést; vizsgáld meg a binárist
objdump segítségével |
| Nem találod az előjeles összeadást | A fordító eltüntette | Vizsgáld meg a lab_rises_s teljes függvénytörzsét |
| Hiányzó célrendszeri betöltőről kapsz hibát | Nem a bemutatott statikus program készült el | Ellenőrizd a linkelési parancsot és a -static
kapcsolót |
A hiányzó utasítás is adat. Ne módosítsd rögtön a programot azért, hogy a fordító a várt mintát adja: előbb ellenőrizd, jogos volt-e az elvárásod.
Ez a labor funkcionális ellenőrzés, nem sebességmérés. A QEMU user-mode futási idejéből nem következtetünk fizikai ARM-processzor utasításkésleltetésére vagy ciklusszámára.
A könyv két külön végrehajtási környezetet használ.
| Szempont | I. rész: AArch64 | II. rész: Cortex-M |
|---|---|---|
| ISA és profil | A64, ARMv8-A alapprofil | Thumb-2, Armv7-M, Cortex-M3 |
| Környezet | Linux-alkalmazás | Operációs rendszer nélküli firmware |
| Elsődleges emuláció | qemu-aarch64 |
qemu-system-arm -machine mps2-an385 |
| Központi kérdés | Hogyan működik együtt a C, az ABI és a gépi kód? | Hogyan indul el és kezel hardvert a firmware? |
Az első részben az operációs rendszer biztosítja a futási környezet jelentős részét. A másodikban az indulást, a memória inicializálását és a megszakítások kezelését is nekünk kell megszerveznünk.
Átvihető tudás a bitszintű gondolkodás, a gépi kód olvasása és a szerződések ellenőrzése. Nem vihetők át változtatás nélkül az utasítások, regiszternevek és indítási feltételezések.
A könyv nem teljes operációsrendszer-kernel megírását, nem valós idejű operációs rendszer (real-time operating system, RTOS) ütemezőjének megvalósítását és nem gyártóspecifikus fejlesztőkörnyezet használatát tanítja. A hangsúly a reprodukálható, architektúraközeli munkafolyamaton van.
Minden alfejezet konkrét mérnöki feladatból indul, majd meghatározza a szükséges fogalmakat és feltételeket. A magyarázatot fordítható vagy összeállítható példa, ellenőrzési feltétel, hibakeresési ellenpróba és fokozatos feladatsor követi. A megoldások indoklást is adnak; a rövid összefoglaló a később továbbhasznált fogalmakat emeli ki.
Háromféle állítást különítünk el:
A most közölt kimenet levezetett elvárás. Tényleges laboreredménnyé akkor válik, amikor te futtatod és rögzíted.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Miért adhat a
lab_next_u(UINT32_MAX) definiáltan 0-t, miközben a
lab_rises_s(INT_MAX) eredményét nem határozhatjuk meg
ugyanezzel az érveléssel? Miért nem cáfolja az optimalizálás helyességét
az eltűnt összeadás?
(implementációs, alapszint — cél: rutinépítés)
Bővítsd a lab-hidden.c fájlt egy
uint32_t lab_double_u(uint32_t x) függvénnyel, amely
összeadással kétszerezi az argumentumot. Ellenőrizd a 0, 7 és
UINT32_MAX bemeneteket. A main akkor térjen
vissza 0-val, ha minden ellenőrzés sikeres.
(hibakeresési, középszint — cél: rutinépítés)
Egy módosított változatban a lab_next_u már 2-t ad hozzá,
de a futtatás továbbra is 0-t ír ki az előjel nélküli eredményre. A
készítő csak a linkelési parancsot ismételte meg. Adj hipotézist,
válassz adatgyűjtő eszközt, és írd le a javítást. Mi lesz az új helyes
eredmény?
1. megoldás — Segítség. Gondold végig külön az előjel nélküli és az előjeles túlcsordulás C-beli definiáltságát — az egyik moduláris aritmetika, a másik nem azonosítható egyetlen konkrét eredménnyel.
1. megoldás — Teljes megoldás. A 32 bites előjel
nélküli számítás modulo
történik. Az előjeles INT_MAX + 1 viszont C-ben nem
definiált. A fordító az összes definiált bemenetre helyesen
helyettesítheti az x + 1 > x kifejezést 1-gyel. Az UB-t
okozó bemenet nem követel meg ettől eltérő eredményt.
2. megoldás — Segítség. A függvény teste egyetlen
sor; a nehezebb rész a UINT32_MAX eset modulo
levezetése — ne tizedestört közelítéssel, hanem a
alakkal számolj.
2. megoldás — Teljes megoldás. A meglévő fájlba illeszthető függvény:
uint32_t lab_double_u(uint32_t x)
{
return x + x;
}A várt eredmények rendre 0, 14 és 4294967294. Az utolsó érték levezetése:
A labor AArch64-profiljában az összeadás 32 bites előjel nélküli
aritmetika. Bővítsd a main ellenőrzését ezekkel az
esetekkel, majd ismételd meg a teljes C → assembly → tárgyfájl → program
lépéssort. A várt kilépési állapot 0.
3. megoldás — Segítség. A tünet (“a forrás
megváltozott, az eredmény nem”) klasszikus jele annak, hogy a build
valamelyik köztes lépése elmaradt. Nézd meg objdump-pal,
hogy a ténylegesen linkelt .o tartalmazza-e az új forrás
utasításait.
3. megoldás — Teljes megoldás. Valószínűleg a régi
lab-hidden.o került ismét a programba. Az
objdump segítségével vizsgáld meg a lab_next_u
függvényt a végleges binárisban: az eredeti kódminta esetén még 1
hozzáadása látszik. Generáld újra az assemblyforrást, állítsd össze,
majd linkeld. Az új eredmény
.
A main elvárását is módosítani kell 1-re; különben helyesen
hibát jelez a már elavult teszt.
Az assemblytudás elsősorban betekintést ad abba, hogyan valósul meg a C-program az adott gépen. Segít teljesítményproblémák elemzésében, bináris interfészek ellenőrzésében, hardverkezelésben és biztonsági hibák feltárásában. A fordító nem köteles megőrizni a C-forrás műveleteinek látható szerkezetét: a definiált jelentést kell megtartania. A konkrét bináris megfigyelése nem teszi definiálttá a hibás C-programot, és az utasítások száma önmagában nem teljesítménymérés. A könyv ezért külön kezeli az előírást, a megvalósítást és a tényleges megfigyelést. A laborban mindig a teljes buildláncot és az ellenőrzési feltételeket követjük.
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.
master ághoz tartozó dokumentáció,
nem egy konkrét kiadott verzióhoz kötött; a ténylegesen telepített
QEMU-verzió dokumentációja ettől eltérhet.Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is
fennmaradnak: a program nem lett itt lefuttatva; a konkrét GCC
által előállított utasítássorozat és a helyben telepített csomagverziók
(Ubuntu 24.04 LTS csomagolt gcc, binutils,
qemu-user) a hallgatói laborban ellenőrzendők, mert ebben a
munkafolyamatban nem lettek lekérdezve.