Egy C-program helyesen lefordul, mégsem linkelhető; egy másikból elkészül a futtatható fájl, de az x86_64 fejlesztői gép nem tudja közvetlenül elindítani. Ugyanannak a függvénynek pedig más assembly megfelelőjét kapjuk optimalizálással, mint nélküle. Ezek különböző rétegek problémái, ezért különböző eszközökkel vizsgálandók.
A fordítási eszközlánc (compilation toolchain) a forrásprogramot több lépésben alakítja végrehajtható gépi programmá. Az assembly ennek nem a kiindulópontja és nem is a végállapota: a fordító egyik lehetséges szöveges kimenete, amelyből az assembler gépi kódot készít.
Az ISA és mikroarchitektúra különbségére, valamint az ARM-profilok áttekintésére építünk. A cél most a fájlok és eszközök szerepének felismerése, nem az assembly utasításkészletének megtanulása.
Profil: minden alábbi program AArch64 Linuxra
készül, A64 ISA, ARMv8-A alapprofil és little-endian bájtsorrend
mellett. A host Ubuntu 24.04 x86_64, a futtatási cél
qemu-aarch64, vagyis QEMU user-mode. A GNU
assemblerdirektívák és az említett regiszterek ebben az
AArch64-környezetben értendők; Cortex-M-kód itt nem szerepel.
A lánc fő állomásai:
| Lépés | Bemenet | Kimenet | Fő feladat |
|---|---|---|---|
| Előfeldolgozó, preprocesszor (preprocessor) | .c, bevont fejlécek |
előfeldolgozott C, jellemzően .i |
Makróhelyettesítés, fájlbevonás, feltételes fordítás |
| Fordító (compiler) | előfeldolgozott C | assembly szöveg, .s |
Elemzés, optimalizálás, célarchitektúrára történő kódgenerálás |
| Assembler | assembly szöveg | objektumfájl, .o |
Utasítások kódolása, szimbólumok és relokációk létrehozása |
| Linker | objektumfájlok, könyvtárak | futtatható fájl vagy megosztott könyvtár | Szimbólumfeloldás, szekciók elrendezése, hivatkozások összekapcsolása |
| Betöltő (loader) | futtatható fájl, szükséges megosztott objektumok | futó folyamat memóriaképe | Programrészek leképezése, indulási környezet kialakítása |
A preprocesszor például a #define LAB_BIAS 22 után a
LAB_BIAS makróelőfordulásokat helyettesíti. Nem végez
regiszterkiosztást, és nem készít gépi utasításokat. Egy .c
fájl az előfeldolgozás után, a bevont tartalmakkal együtt egy
fordítási egységet (translation unit)
alkot.
A fordító a C szemantikájából indul ki. Belső köztes reprezentációkat használhat, egyszerűsítheti a kifejezéseket, és kiválasztja a célgépi műveleteket. Nincs általános „egy C-sor = egy assembly utasítás” megfeleltetés.
Az assembler már nem C-kifejezéseket értelmez. A64 környezetben például egy összeadó utasítás szöveges alakjából annak gépi kódolását készíti el. A még nem ismert címekhez viszont később feldolgozandó információt hagyhat.
A linker összekapcsolja a külön fordított részeket. Ha az egyik
objektum a lab_add22 függvényre hivatkozik, a másik pedig
definiálja azt, a linker összerendeli őket.
A betöltés már futásidejű feladat. Natív Linuxon a kernel az ELF programfejlécei alapján alakítja ki a folyamat képét; dinamikusan linkelt programnál egy felhasználói térben futó dinamikus betöltő is közreműködik. A QEMU user-mode a célprogram számára szükséges futtatási környezetet biztosítja, és emulálja az A64 utasítások végrehajtását. Nem teljes AArch64 gépet és vendégkernelt indít.
A gcc parancs a szokásos használatban
vezérlőprogram (compiler driver): megszervezi
a szükséges fordítási, assemblálási és linkelési lépéseket.
| Kapcsoló | Hol áll meg? | Tipikus kimenet |
|---|---|---|
-E |
Előfeldolgozás után | .i, vagy -o nélkül a szabványos
kimenet |
-S |
Assembly előállítása után | .s |
-c |
Objektumfájl előállítása után | .o |
| Egyik sincs megadva | A linkelést is elvégzi | Futtatható fájl |
-o név |
Nem választ feldolgozási lépést | A kimenet nevét adja meg |
A -o nem azt jelenti, hogy „objektumfájlt
kérek”. Ha -S mellett a kimenetnek .o
végződésű nevet adunk, attól még assembly szöveg keletkezik.
A bemeneti kiterjesztés is számít:
.c: előfeldolgozandó C-forrás;.i: már előfeldolgozott C;.s: közvetlenül assemblálható szöveg;.S: előbb a C-preprocesszoron átvezetendő
assembly;.o: a linkeléshez felhasználható objektumfájl.A szokásos egyparancsos fordítás nem feltétlenül hagyja meg a köztes fájlokat. A logikai lépések ettől még elkülöníthetők; az eszközök akár csővezetéken keresztül is továbbíthatnak adatot egymásnak.
Munkakönyvtár: ~/arm-labor/a64/a005/ —
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/a005
cd ~/arm-labor/a64/a005Szerepe a fejezetben: ez a példa célzott kiegészítés: egyetlen jelenséget mutat meg külön builddel, ezért első olvasáskor kihagyható. A fejezet végigvitt projektjének ide tartozó lépése — amely nem hagyható ki — a szakasz végén, a „Végigvitt projekt” részben található.
A példához a könyv cross-eszközlánca szükséges:
gcc-aarch64-linux-gnu,
binutils-aarch64-linux-gnu, qemu-user, továbbá
az AArch64 glibc fejlesztői fájljai. Utóbbiakat az Ubuntu-környezetben
jellemzően a libc6-dev-arm64-cross csomag biztosítja. A
részletes telepítés a labor-alapkörnyezetben
következik.
Két külön fordítási egységet készítünk. A lab-main.c
használ egy függvényt, amelyet a lab-math.c definiál.
A lab-main.c tartalma:
#include <stdio.h>
int lab_add22(int x);
int main(void)
{
int result = lab_add22(20);
printf("eredmeny=%d\n", result);
return result != 42;
}A lab-math.c tartalma:
#define LAB_BIAS 22
int lab_add22(int x)
{
return x + LAB_BIAS;
}A lab_add22 deklarációja alapján a fordító ellenőrizheti
a hívás típusait. A függvénytörzs azonban nincs jelen a
lab-main.c fordítási egységében. Ez nem fordítási hiba: a
definíciót majd a linkernek kell megtalálnia.
# Build-cél: AArch64 Linux, későbbi futtatás qemu-user alatt.
aarch64-linux-gnu-gcc -E lab-main.c -o lab-main.i
aarch64-linux-gnu-gcc -E lab-math.c -o lab-math.i
# Hostoldali szövegvizsgálat.
sed -n '1,30p' lab-math.iA lab-math.i függvénytörzsében várhatóan már
return x + 22; olvasható. A kimenet forráshelyeket jelölő
sorokat is tartalmazhat. A lab-main.i jóval hosszabb lehet,
mert a stdio.h és az általa bevont fejlécek tartalma is
megjelenik benne.
# Build-cél: A64 ISA, ARMv8-A, little-endian; futtatási cél: qemu-user.
aarch64-linux-gnu-gcc -S -O2 -march=armv8-a -mlittle-endian \
-fno-pie lab-main.i -o lab-main.s
aarch64-linux-gnu-gcc -S -O2 -march=armv8-a -mlittle-endian \
-fno-pie lab-math.i -o lab-math.sAz -O2 optimalizálást kér. A -march=armv8-a
az engedélyezett célarchitektúrát, a -mlittle-endian a
bájtsorrendet rögzíti. A -fno-pie, majd a linkelésnél
használt -no-pie ebben a laborban kikapcsolja a
pozíciófüggetlen futtatható fájl készítését, hogy az eredmény típusa ne
a disztribúció alapbeállításától függjön. Ez szemléltetési döntés, nem
általános biztonsági ajánlás.
A lab-math.s egyik lehetséges, leegyszerűsített
A64-részlete:
lab_add22:
add W0, W0, #22
retEz nem a teljes generált fájl: a GNU assembler számára szükséges
direktívákat és a metaadatokat itt elhagytuk. Az assembláláshoz az
eredeti, GCC által készített .s fájlt használjuk.
A64 környezetben a W0 32 bites regiszternézet. Az
AArch64 Linuxon alkalmazott AAPCS64 hívási konvenció szerint ebben a
példában az első int argumentum és az int
visszatérési érték is ezen keresztül halad. A RET
alapértelmezésben az X30 64 bites regiszterben levő
visszatérési címet használja. A részletes szerződést az AAPCS64 regiszterszerződése fejti ki.
Az A64 ADD itt használt immediate alakja nem fogad el
tetszőleges konstanst: 12 bites előjel nélküli értéket kódol, opcionális
12 bites balra tolással. A 22 belefér a nem eltolt
alakba.
# Build-cél: AArch64 Linux, qemu-user.
aarch64-linux-gnu-gcc -c -march=armv8-a -mlittle-endian \
lab-main.s -o lab-main.o
aarch64-linux-gnu-gcc -c -march=armv8-a -mlittle-endian \
lab-math.s -o lab-math.oA .s bemenet és a -c együtt azt jelenti: az
assembler dolgozzon, linkelés még ne történjen. A .o fájl
már bináris, ezért szövegszerkesztő helyett objektumvizsgáló eszközt
használunk.
# Linkelési cél: statikus AArch64 Linux-program, qemu-user.
aarch64-linux-gnu-gcc -static -no-pie \
lab-main.o lab-math.o -o lab-app
# Futtatási cél: qemu-user.
qemu-aarch64 ./lab-app
printf 'kilepesi_allapot=%d\n' "$?"Várt, nem ténylegesen futtatott eredmény:
eredmeny=42;kilepesi_allapot=0.A $? közvetlenül az előző parancs állapotát tartalmazza.
Ha előbb más parancsot futtatunk, már nem a QEMU-futtatás eredményét
vizsgáljuk.
A linkelést szándékosan a GCC vezérlőprogramjával végezzük. Ez
hozzáadja a C futtatókörnyezet szokásos induló objektumait és
könyvtárait. A folyamat nem közvetlenül a main függvényben
indul; az indulókód készíti elő annak meghívását.
A lánc rövidítve is megadható:
# Teljes build és futtatás: AArch64 Linux, qemu-user.
aarch64-linux-gnu-gcc -O2 -march=armv8-a -mlittle-endian \
-fno-pie -static -no-pie \
lab-main.c lab-math.c -o lab-app-onepass
qemu-aarch64 ./lab-app-onepass
printf 'kilepesi_allapot=%d\n' "$?"A várt kimenet ugyanaz. Az egyparancsos változat önmagában nem őrzi
meg a vizsgált .i, .s és .o
köztes fájlokat.
lab-hidden négy lépésbenA Miért assembly alfejezetben a
lab-hidden buildje a C → assembly lépéssel indult; az
előfeldolgozás rejtve maradt. Most mind a négy lépést külön fájlba
mentjük:
# Build-cél: AArch64 Linux, qemu-user.
cp ~/arm-labor/a64/a001/lab-hidden.c .
LAB_CFLAGS="-std=c17 -O2 -fno-inline -Wall -Wextra -march=armv8-a -mlittle-endian"
aarch64-linux-gnu-gcc $LAB_CFLAGS -E lab-hidden.c -o lab-hidden.i # 1. előfeldolgozás
aarch64-linux-gnu-gcc $LAB_CFLAGS -S lab-hidden.i -o lab-hidden.s # 2. fordítás
aarch64-linux-gnu-gcc $LAB_CFLAGS -c lab-hidden.s -o lab-hidden.o # 3. assemblálás
aarch64-linux-gnu-gcc -march=armv8-a -mlittle-endian -static \
lab-hidden.o -o lab-hidden # 4. linkelés
grep -n 'lab_next_u(' lab-hidden.i
aarch64-linux-gnu-nm lab-hidden.o
qemu-aarch64 ./lab-hidden
printf 'exit=%s\n' "$?"Levezetett, várt megfigyelések:
| Lépés | Mit ellenőrzünk | Várt megfigyelés |
|---|---|---|
1. .i |
a main hívássora |
az UINT32_MAX makró helyén számkonstans áll (például
4294967295U; a pontos alak a C-könyvtár fejléceitől
függ) |
2–3. .s, .o |
nm lab-hidden.o |
T lab_next_u, T lab_rises_s,
T main definiált, U printf feloldatlan |
| 4. futtatás | kimenet és állapot | unsigned_next=0, signed_rises=1,
exit=0 — ugyanaz, mint a Miért
assembly alfejezetben |
A printf feloldatlan hivatkozása a .o-ban
ugyanaz a jelenség, mint a lab_add22 esete a fenti
kiegészítő példában: a definíciót a linker a statikus C-könyvtárból
emeli be. A projekt ezzel a fejezet végére minden szinten — forrás,
előfeldolgozott forrás, assembly, objektum, futtatható fájl —
megvizsgált program.
Az ELF (Executable and Linkable Format) többféle bináris szerepet támogat. A „bináris fájl” ezért nem a „futtatható program” szinonimája.
Az objektumfájl már tartalmazhat gépi utasításokat, mégsem teljes program. Lehetnek benne feloldatlan külső hivatkozások és relokációk (relocations): olyan bejegyzések, amelyek megadják, hogyan kell egy címfüggő mezőt később a végleges elrendezéshez igazítani. Relokáció akkor is szükséges lehet, ha a hivatkozott szimbólum ugyanabban az objektumban definiált.
Vizsgáljuk meg a példát:
# Hostoldali vizsgálat; a fájlok célja AArch64 Linux, qemu-user.
aarch64-linux-gnu-readelf -h lab-main.o
aarch64-linux-gnu-readelf -h lab-app
aarch64-linux-gnu-nm -u lab-main.o
aarch64-linux-gnu-objdump -dr lab-main.oVárt ellenőrzési pontok:
| Vizsgálat | Várt megfigyelés |
|---|---|
lab-main.o ELF-típusa |
REL, relokálható objektum |
lab-app ELF-típusa |
EXEC, ennél a statikus, nem PIE buildnél |
| Mindkét fájl géptípusa | AArch64 |
| Mindkét fájl adatábrázolása | little-endian |
nm -u lab-main.o |
Többek között lab_add22 és printf
feloldatlan hivatkozása |
objdump -dr |
Visszafejtett utasítások és a hozzájuk kapcsolódó relokációk |
A lab_add22 feloldatlan állapota itt normális: a
fordítás sikerült, az összekapcsolás még hátravan. Az ELF részletes
felépítése az ELF formátum alapjainál
következik.
Statikus linkelésnél a szükséges könyvtári
objektumok kódja bekerül a futtatható fájlba. A statikus könyvtár
gyakran .a archívum; a linker általában a szükséges tagokat
emeli be, nem automatikusan a teljes archívumot.
Dinamikus linkelésnél a program egyes
szolgáltatásokat külön megosztott objektumokból, például
.so fájlokból kap. A futtatáshoz kompatibilis céloldali
dinamikus betöltő és könyvtárak szükségesek.
A meglévő objektumokból dinamikus változat is készíthető:
# Linkelési cél: dinamikus AArch64 Linux-program, qemu-user.
aarch64-linux-gnu-gcc -no-pie \
lab-main.o lab-math.o -o lab-app-dynamic
# Hostoldali vizsgálat.
aarch64-linux-gnu-readelf -l lab-app-dynamic
aarch64-linux-gnu-readelf -d lab-app-dynamic
# Futtatási cél: qemu-user, AArch64 céloldali könyvtárakkal.
qemu-aarch64 -L /usr/aarch64-linux-gnu ./lab-app-dynamic
printf 'kilepesi_allapot=%d\n' "$?"A megadott prefix az Ubuntu cross-környezetének szokásos céloldali könyvtárfája; meglétét a laborban ellenőrizni kell. Nem helyettesíthető a host x86_64 könyvtáraival.
A várt kimenet ismét eredmeny=42, majd nulla kilépési
állapot. A programfejlécek között várhatóan INTERP, a
dinamikus információk között NEEDED bejegyzés található. Az
előbbi az ELF-interpretert, az utóbbi szükséges megosztott könyvtárat
jelöl.
A statikus program sem „operációs rendszer nélküli”: továbbra is Linux-szolgáltatásokat használ. A statikus és dinamikus linkelés közötti különbség nem azonos a Linux és a bare-metal futtatás különbségével.
A generált kódot több, egymástól elkülönítendő döntés alakítja:
-O0 több közvetlen
változó- és veremkezelést hagyhat meg; az -O2 elhagyhat
fölösleges műveleteket, összevonhat számításokat.A helyes összehasonlítás nem a szövegazonosságot keresi, hanem a
megkövetelt viselkedés megőrzését. Ez jól definiált C-programra
vonatkozik: például az előjeles egész túlcsordulására nem alapozhatunk
hordozható C-szemantikát. A labor 20 + 22 művelete 32 bites
előjeles int esetén ábrázolható.
A rövidebb assembly önmagában nem bizonyít gyorsulást. Itt működést és fájlformátumot vizsgálunk, nem időt mérünk; a QEMU user-mode nem szolgáltat garantáltan hardverhű ciklusszámot.
A hasznos hibakeresési sorrend: tünet → hipotézis → adatgyűjtés → egy változtatás → új ellenőrzés.
| Tünet | Hipotézis és adatgyűjtés | Egy javítás és ellenőrzése |
|---|---|---|
undefined reference to lab_add22 |
A linker nem kapta meg a definíciót; vizsgáljuk nm-mel
az objektumokat |
Adjuk hozzá a lab-math.o fájlt; linkeljünk újra |
| A fájl géptípusa x86-64 | Véletlenül a host gcc fordított; ellenőrizzük
readelf -h segítségével |
Fordítsunk újra az aarch64-linux-gnu-gcc eszközzel |
| QEMU nem találja az ELF-interpretert | Dinamikus programhoz hiányzik vagy rossz a céloldali könyvtárfa;
nézzük meg a readelf -l kimenetét |
Javítsuk a -L prefixet, majd futtassunk újra |
| Forrásmódosítás után változatlan az eredmény | Elavult köztes fájlt használtunk | Indítsuk újra az érintett láncot a megváltozott bemenettől |
Az utolsó eset különösen fontos: a .i nem élő kapcsolat
a .c fájlhoz. Ha a makrót a forrásban átírjuk, de továbbra
is a régi .i fájlt fordítjuk, a módosítás nem jut el a
programig.
Magyarázó feladat (alapszint — cél:
megértés-ellenőrzés). Melyik lépés helyettesíti a
LAB_BIAS makrót, melyik kódolja az A64 utasításokat, és
melyik kapcsolja össze a lab_add22 hívását a
definíciójával? Miért nem futtatható önmagában a
lab-main.o?
Implementációs feladat (középszint — cél:
rutinépítés). Állítsd a LAB_BIAS értékét
7-re, és módosítsd a main ellenőrzését az új
eredményhez. Végezd el újra a teljes lépéssort. Mi legyen a kimenet és a
kilépési állapot? Mi történne, ha csak az eredeti .o
fájlokat linkelnéd újra?
Hibakeresési feladat (középszint — cél: rutinépítés). Az eredeti forrásváltozat objektumaival hagyd el szándékosan a matematikai objektumot:
# Szándékosan hibás linkelés; cél: AArch64 Linux, qemu-user.
aarch64-linux-gnu-gcc -static -no-pie \
lab-main.o -o lab-missing-mathA várt eredmény nem nulla linkelési állapot; futtatható programra ne
számíts. Igazold a hiányzó definíció okát nm segítségével,
majd egyetlen linkparancs-módosítással javítsd a hibát.
Összehasonlító feladat (középszint — cél: megértés-ellenőrzés). Készíts optimalizálatlan assemblyt is:
# Build-cél: AArch64 Linux, qemu-user; futtatás itt nincs.
aarch64-linux-gnu-gcc -S -O0 -march=armv8-a -mlittle-endian \
-fno-pie lab-math.i -o lab-math-O0.s
# Hostoldali szövegösszehasonlítás.
diff -u lab-math-O0.s lab-math.sKeress művelet- vagy veremkezelési különbséget. A diff
eltérő fájloknál szokásosan 1 állapottal tér vissza; ez nem
fordítási hiba. Mit nem állapíthatsz meg ebből a futási időről?
1. megoldás — Segítség. Nézd meg a fejezet elején lévő táblázatot: melyik sor bemenete/kimenete illeszkedik a makróra, melyik az utasításkódolásra, melyik a hivatkozás-összekapcsolásra?
1. megoldás — Teljes megoldás. A makrót a
preprocesszor helyettesíti; az utasításokat az assembler kódolja; a
külső hivatkozást a linker kapcsolja a definícióhoz. A
lab-main.o relokálható részprogram: hiányzik belőle többek
között a hivatkozott függvény definíciója és a teljes program indulási
környezete.
2. megoldás — Segítség. Számold ki fejben
7 + 20-at, és gondold át: a .i-fájl már a
makróhelyettesítés UTÁNI állapotot tükrözi — ha csak a régi
.o-kat linkelnéd újra, melyik lépés maradna ki?
2. megoldás — Teljes megoldás. Az eredmény
27, ezért az ellenőrzés return result != 27;.
A várt kimenet eredmeny=27, a kilépési állapot
0. Mindkét módosított forrást újra elő kell feldolgozni,
fordítani és assemblálni. A régi objektumok újralinkelése a régi
viselkedést őrizné meg.
3. megoldás — Segítség. Az nm -u a
feloldatlan (undefined), a --defined-only a ténylegesen
definiált szimbólumokat listázza egy objektumban — melyik objektumban
várod a lab_add22-t melyik szerepben?
3. megoldás — Teljes megoldás. A szükséges vizsgálat és javítás:
# Hostoldali vizsgálat, AArch64 objektumokon.
aarch64-linux-gnu-nm -u lab-main.o
aarch64-linux-gnu-nm --defined-only lab-math.o
# Javított linkelés és futtatás: AArch64 Linux, qemu-user.
aarch64-linux-gnu-gcc -static -no-pie \
lab-main.o lab-math.o -o lab-app
qemu-aarch64 ./lab-app
printf 'kilepesi_allapot=%d\n' "$?"A hívó objektumban U, a definiáló objektumban várhatóan
T jelöli a lab_add22 szimbólumot. Az eredeti
változat várt eredménye 42, kilépési állapota
0. A hiba linkelési, nem betöltési probléma.
4. megoldás — Segítség. Gondolj arra, mit kell egy
-O0 fordításnak garantálnia (pl. hogy minden változó
bármikor megfigyelhető legyen egy debuggerben) — ez miért igényel több
explicit memóriaműveletet, mint az -O2?
4. megoldás — Teljes megoldás. Az optimalizálatlan változat például verembe mentheti és visszatöltheti az argumentumot, míg az optimalizált változat közvetlenül regiszteren számolhat. A pontos eltérés fordítófüggő. Utasításszövegből nem következik tényleges időadat: ehhez mérési terv és megfelelő futtatási környezet szükséges.
A C-program előfeldolgozáson, fordításon, assembláláson és linkelésen
keresztül válik betölthető programmá. Az assembly olvasható köztes
reprezentáció, nem maga a futó program. A GCC vezérlőprogram
-E, -S és -c kapcsolóival külön
megállhatunk a lánc állomásainál; a -o csak nevet választ.
Az objektumfájl gépi kódot tartalmazhat úgy is, hogy külső hivatkozásai
még feloldatlanok. A linker és a betöltő eltérő feladatot végez. A
generált assembly optimalizálási, ISA- és ABI-döntéseket tükröz, ezért
szövege nem egyetlen kötelező fordítása a C-forrásnak. A laborban a várt
működést a kimenet és a kilépési állapot együtt ellenőrzi.
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.
-E, -S, -c, -o.
Tartalmilag ellenőrizve: a legújabb listázott kiadás
GCC 16.2.master ág dokumentációja (közös
verziókorlát).Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is ténylegesen fennmaradnak: a kód nem volt futtatva; minden eredmény várt vagy szemléltető. A konkrét Ubuntu-csomagverziókat, a statikus glibc elérhetőségét, a céloldali könyvtárprefixet és a ténylegesen generált assemblyt a hallgatói laborban kell ellenőrizni.