Egy C-függvény módosítása után gyakran egyetlen buildparancsot adunk ki. Assemblyprogramnál érdemes először szétválasztani azokat a lépéseket, amelyeket egy összetettebb buildrendszer később elrejt: a forrás feldolgozását, a futtatható állomány elkészítését és a végrehajtást. Így például egy hibás célarchitektúrát nem keverünk össze egy hiányzó futásidejű könyvtárral.
Ebben az alfejezetben egy olyan programot készítünk, amely nem ír a konzolra, hanem 42-es kilépési kódot ad vissza. Ez szándékosan egyszerű, könnyen ellenőrizhető szerződés: ha a megfelelő értéket kapjuk, az összeállítás–linkelés–futtatás teljes útvonalát kipróbáltuk.
Előfeltétel a Labor-alapkörnyezet: eszközlánc
telepítése és verifikáció szerinti környezet. A build
automatizálásához a make programnak is elérhetőnek kell
lennie.
Architektúraprofil: az alfejezet összes assemblypéldája A64 ISA, AArch64 Linux, ARMv8-A alapprofil, little-endian bájtsorrend. A GNU assembler direktívái ebben a célkörnyezetben értendők; maguk a direktívák nem processzorutasítások. Cortex-M-kód itt nem szerepel.
Az összeszerelés (assembly) során az
assembler gépi utasításokká és kapcsolódó leíró adatokká alakítja az
assemblyforrást. A GNU eszközláncban ezt az as végzi.
A linkelés, más néven szerkesztés (linking)
során a linker elhelyezi és összekapcsolja az objektumfájlok szekcióit,
feloldja a szimbólumhivatkozásokat, valamint elkészíti a kért kimeneti
állományt. Itt a GNU ld programot használjuk futtatható ELF
előállítására.
| Lépés | Bemenet | Kimenet | Felelősség |
|---|---|---|---|
| Összeszerelés | lab-exit.s |
lab-exit.o |
Utasításkódolás, szimbólumok és szükség esetén áthelyezési információk |
| Linkelés | lab-exit.o |
lab-exit |
Futtatható ELF, belépési pont, betöltési leírás |
| Futtatás | lab-exit |
Kilépési állapot | A program végrehajtása és kapcsolat a Linux környezettel |
Az objektumfájl (object file) tehát nem egyszerűen más kiterjesztésű futtatható program. A jelen munkafolyamatban áthelyezhető ELF-állomány, amelyből még hiányzik a végleges futtatható program elrendezése.
A natív eszközlánc (native toolchain) a
saját futási környezetének célarchitektúrájára készít kódot. A
kereszteszközlánc (cross-toolchain) ettől
eltérő célrendszerhez dolgozik. Az Ubuntu x86_64 hoston futó
aarch64-linux-gnu-as például x86_64 program, de AArch64
objektumfájlt állít elő.
Fontos különbség:
as és az ld elkészíti
a célprogramot;qemu-aarch64 végrehajtja annak A64
utasításait emulációval;A QEMU user-mode nem teljes virtuális gépet indít, és nem bootol vendégkernelt. A célprogram felhasználói utasításait emulálja, Linux-rendszerhívásait pedig a host környezetével közvetíti.
Munkakönyvtár: ~/arm-labor/a64/a007/ —
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/a007
cd ~/arm-labor/a64/a007Mentsük az alábbi teljes forrást lab-exit.s néven:
// A64 ISA; AArch64 Linux; ARMv8-A; little-endian.
// A direktívák GNU as direktívák ehhez az ELF-célhoz.
.section .text
.global _start
_start:
mov X0, #42
mov X8, #93
svc #0
.section .note.GNU-stack,"",%progbitsA GNU assembler .section .text direktívája a kódszekciót
választja ki. A .global _start a linker számára is
láthatóvá teszi a _start szimbólumot. A forrásvégi
.note.GNU-stack bejegyzés a GNU ELF-eszközlánc felé jelzi,
hogy ez az objektum nem igényel végrehajtható vermet; nem végrehajtandó
utasítás.
A program nem C-függvény, és nincs main függvénye. A
linkernek a _start címkét adjuk meg belépési pontként. Nem
térünk vissza egy hívóhoz, hanem közvetlen Linux-rendszerhívással
fejezzük be a folyamatot.
| A64 utasítás vagy regiszter | Szerep ebben az AArch64 Linux-példában |
|---|---|
MOV X0, #42 |
A 64 bites X0 regiszterbe kerül a kilépési állapot |
MOV X8, #93 |
A 64 bites X8 regiszterbe kerül a Linux
exit rendszerhívás száma |
SVC #0 |
A Linux-rendszerhívási belépés kezdeményezése |
A 42 értéket nemnegatív kilépési állapotként, a 93 értéket előjel nélküli rendszerhívás-azonosítóként használjuk. A 93 Linux AArch64 ABI-konstans, nem az A64 ISA által meghatározott „kilépési utasítás”.
Az itt használt A64 MOV immediate alak alias: mindkét
konstans elfér egy előjel nélküli, 16 bites mezőben, ezért egyetlen
MOVZ utasítással kódolható, nulla eltolással. Ebből nem
következik, hogy tetszőleges 64 bites konstans ugyanígy betölthető. A64
SVC esetén az immediate mező 16 bites; itt az értéke 0. A
rendszerhívás számát nem ez a mező, hanem az A64 X8
regiszter tartalmazza.
A részletes magyarázatot később a Linux syscall-konvenció AArch64-en adja meg. Most a három utasítás ellenőrizhető tesztprogramként szolgál.
A következő parancsokat a forrás könyvtárában adjuk ki:
# Target: qemu-user, AArch64 Linux.
# Host: Ubuntu 24.04 LTS x86_64.
aarch64-linux-gnu-as -EL -march=armv8-a \
-o lab-exit.o lab-exit.s
aarch64-linux-gnu-ld -EL -m aarch64linux -static \
-e _start -o lab-exit lab-exit.oVárt eredmény: mindkét eszköz 0 állapottal fejeződik be, és létrejön a kimeneti fájlja. Hiba után ne folytassuk automatikusan a futtatást: egy korábbi, még létező bináris megtévesztő eredményt adhat.
| Kapcsoló | Eszköz | Jelentés |
|---|---|---|
-EL |
AArch64 as, ld |
Little-endian célkimenet |
-march=armv8-a |
AArch64 as |
Az elfogadott architektúraprofil kiválasztása |
-o fájl |
as, ld |
Kimeneti fájlnév |
-m aarch64linux |
AArch64 ld |
A linker célmódjának, úgynevezett emulációjának kiválasztása |
-static |
ld |
Statikus linkelési mód |
-e _start |
ld |
A futtatható állomány belépési szimbóluma |
A linker „emulációja” itt nem processzoremuláció: a kimeneti formátumhoz és elrendezéshez tartozó célmód. Semmi köze a QEMU utasítás-végrehajtásához.
A célarchitektúrát nem önmagában a -march kapcsoló
teremti meg. A megfelelő célra konfigurált assembler is szükséges. Az
x86_64 host közönséges as programja nem válik AArch64
assemblerré attól, hogy ezt a kapcsolót kapja.
# Target: qemu-user, AArch64 Linux.
# Interaktív shellben, bekapcsolt errexit (set -e) nélkül.
qemu-aarch64 ./lab-exit
echo $?
# Várt kiírás: 42A shell $? változója a legutóbbi
parancs kilépési állapotát tartalmazza. Emiatt az ellenőrzésnek
közvetlenül a futtatást kell követnie.
A program nem ír szöveget: a konzolon megjelenő 42 az
echo kimenete. A 42 itt sikeres funkcionális tesztet
jelent, noha a shell és a buildeszközök általános megállapodása szerint
a nulla jelzi a sikert. Automatizáláskor ezt az eltérést kifejezetten
kezelni kell.
Normál Linux-folyamatkilépésnél a shellben megfigyelhető állapot a kilépési érték alsó 8 bitjére korlátozódik. A 42 ezért változatlanul ellenőrizhető; egy nagyobb érték nem feltétlenül lenne az.
Ellenőrzési feltétel: a lefordított program normálisan fejeződik be, a közvetlenül utána lekérdezett állapot pedig 42. Az itt közölt eredmény levezetett, várt eredmény, nem ténylegesen lefuttatott mérés.
Statikus linkeléskor (static linking) a
szükséges, könyvtárból származó programrészek a futtatható állományba
kerülnek. Példánk még egyszerűbb: egyáltalán nem hivatkozik könyvtárra,
így nem kell sem C runtime, sem libc.
Dinamikus linkeléskor (dynamic linking) a program megosztott könyvtárakat is igényelhet. A szokásos dinamikus Linux ELF ehhez megnevez egy dinamikus betöltőt (dynamic loader), amely futáskor betölti a szükséges célarchitektúrájú könyvtárakat.
A qemu-aarch64 önmagában nem helyettesíti ezeket. Egy
x86_64 host x86_64 könyvtárai nem használhatók AArch64
könyvtárakként.
A statikus példa előnye tehát:
Ez nem jelenti azt, hogy QEMU user-mode alatt csak statikus program
futhat. Dinamikus programhoz megfelelő céloldali fájlrendszerrészletre,
például sysrootra, és annak megfelelő
QEMU-konfigurációra lehet szükség. A QEMU -L kapcsolója
ELF-interpreter-prefix megadására szolgál; használható értékét a helyi
célkönyvtár-elrendezésből kell megállapítani.
Fontos ellenpróba: a -static elhagyása ebből a
könyvtármentes ld parancsból nem feltétlenül hoz létre
dinamikus programot. A kimenet tulajdonságát kell ellenőrizni,
nem pusztán a kapcsoló jelenlétét.
# Vizsgálat hoston; a fájlok targetje AArch64 Linux.
aarch64-linux-gnu-readelf -h lab-exit.o
aarch64-linux-gnu-readelf -h lab-exit
aarch64-linux-gnu-readelf -lW lab-exitVárt megfigyelések:
REL, a futtatható állományé
EXEC;INTERP bejegyzés.Az ELF részleteivel Az ELF formátum alapjai és a program életciklusa foglalkozik.
Natív AArch64 Linuxon ugyanaz a forrás és ugyanaz az utasításkészlet használható, de nincs szükség a QEMU közbeiktatására.
# Target: natív AArch64 Linux; nem az x86_64 laborhost!
# Feltétel: telepített natív GNU binutils.
as -EL -march=armv8-a -o lab-exit.o lab-exit.s
ld -EL -m aarch64linux -static \
-e _start -o lab-exit lab-exit.o
./lab-exit
echo $?
# Várt kiírás: 42A parancsok ugyanazokat a laborfájlokat írják felül. A natív próba opcionális: a kötelező eredmény a QEMU-futtatás.
x86_64 hoston a közvetlen ./lab-exit futtatás jellemzően
végrehajtási formátumhibát okoz. Ha mégis működik, lehetséges, hogy a
rendszer binfmt_misc segítségével automatikusan QEMU-hoz
irányítja az idegen architektúrájú binárist. Ettől a végrehajtás nem
válik natívvá. A laborban ezért használjuk az explicit
qemu-aarch64 parancsot.
A make függőségek és időbélyegek alapján dönti el, mely
állományokat kell újra előállítani. Mentsük a következőt
lab-Makefile néven. A receptsorok elején valódi
tabulátor legyen.
# Target: alapértelmezésben qemu-user, AArch64 Linux.
CROSS ?= aarch64-linux-gnu-
RUNNER ?= qemu-aarch64
EXPECTED ?= 42
AS = $(CROSS)as
LD = $(CROSS)ld
ASFLAGS = -EL -march=armv8-a
LDFLAGS = -EL -m aarch64linux -static -e _start
.PHONY: lab-all lab-run lab-clean
lab-all: lab-exit
lab-exit.o: lab-exit.s lab-Makefile
$(AS) $(ASFLAGS) -o $@ $<
lab-exit: lab-exit.o lab-Makefile
$(LD) $(LDFLAGS) -o $@ lab-exit.o
lab-run: lab-exit
@status=0; $(RUNNER) ./lab-exit || status=$$?; \
printf 'kilépés=%s; várt=%s\n' "$$status" "$(EXPECTED)"; \
test "$$status" -eq "$(EXPECTED)"
lab-clean:
rm -f lab-exit.o lab-exitA $@ az aktuális célt, a $< az első
előfeltételt jelenti. A receptben $$ szükséges ahhoz, hogy
a shell egyetlen $ karaktert kapjon. Az AS és
LD változóknál szándékosan = szerepel: így
felülírjuk a make beépített alapértékeit.
A futtatási recept elmenti a célprogram állapotát, majd összehasonlítja az elvárttal. Így a célprogram 42-es eredményéből a build számára 0-val jelzett siker lesz.
# Target: qemu-user, AArch64 Linux.
make -f lab-Makefile lab-all
make -f lab-Makefile lab-run
echo $?
# Várt ellenőrző sor: kilépés=42; várt=42
# Várt make-kilépési állapot: 0A natív változat:
# Target: natív AArch64 Linux.
make -f lab-Makefile lab-clean
make -f lab-Makefile CROSS= RUNNER= lab-run
# Várt ellenőrző sor: kilépés=42; várt=42
# Várt make-kilépési állapot: 0Az üres CROSS a natív as és ld
neveket eredményezi; az üres RUNNER közvetlen
programindítást ad. Környezetváltás előtt azért töröljük a kimeneteket,
mert a make önmagában nem érzékeli a parancssori
változóértékek megváltozását.
A lab-clean kizárólag a név szerint felsorolt két
generált laborfájlt törli, a forrást és a Makefile-t nem.
A hibakeresés sorrendje: tünet → hipotézis → adatgyűjtés → egy változtatás → ellenőrzés.
| Tünet | Hipotézis | Adatgyűjtés | Egyetlen célzott változtatás |
|---|---|---|---|
| Az assembler nem ismeri az utasításokat | A host assemblere fut | Ellenőrizzük a kiadott parancsot és az eszköz verziójelentését | Használjuk az aarch64-linux-gnu-as programot |
| A linker inkompatibilis géptípust jelez | Rossz architektúrájú objektum került be | readelf -h az objektumon |
Állítsuk elő újra a helyes assemblerrel |
| Közvetlen futtatáskor formátumhiba | A host nem AArch64 | uname -m, valamint ELF-géptípus |
Indítsuk explicit QEMU-val |
| A bináris létezik, de betöltőhiány jelenik meg | Dinamikus célbetöltő hiányzik | readelf -lW: van-e INTERP? |
Ennél a labornál építsük újra a megadott könyvtármentes paranccsal |
make: „missing separator” |
Szóköz került a recept elejére | Láthatóvá tett whitespace a szerkesztőben | Cseréljük a behúzást tabulátorra |
| Az állapot váratlanul 0 | Egy közbeiktatott parancs felülírta $? értékét |
Nézzük meg a parancssorrendet | Mentsük el az állapotot azonnal |
Ha kétséges, hogy a módosított forrás került-e a binárisba, használható az AArch64 disassembler:
# Vizsgálat hoston; target: A64 ISA, AArch64 Linux.
aarch64-linux-gnu-objdump -d lab-exitA várt utasítások a két konstansbetöltés és az A64 SVC.
A disassembler az aliasokat és a számokat eltérő szöveges alakban is
megjelenítheti; ez önmagában nem hiba.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Miért nem a lab-exit.o fájlt
futtatjuk? Miért nincs szüksége a példának AArch64 libc-re?
A statikus bináris vajon Linux nélkül is futna?
(implementációs, alapszint — cél: rutinépítés) Módosítsd a programot úgy, hogy 7 legyen a kilépési állapota. Építsd újra, ellenőrizd közvetlenül, majd futtasd a Makefile tesztjét megfelelő elvárt értékkel.
(hibakeresési, középszint — cél: rutinépítés) Az eredeti, 42-vel kilépő program mellett az alábbi sorozat végén 0 jelenik meg. Fogalmazz meg hipotézist, ellenőrizd a binárist, majd javítsd csak az állapotkezelést.
# Target: qemu-user, AArch64 Linux.
qemu-aarch64 ./lab-exit
printf 'A futtatás véget ért.\n'
echo $?1. megoldás — Segítség. Gondold át, mi különbözteti
meg egy áthelyezhető objektumfájlt egy teljes futtatható ELF-től — van-e
a .o-nak ismert, végleges belépési címe?
1. megoldás — Teljes megoldás. A
lab-exit.o áthelyezhető objektum, nem a linker által
létrehozott futtatható ELF. A példában nincs külső függvényhivatkozás,
ezért libc sem kell: közvetlen rendszerhívást használunk. A
statikus linkelés nem szünteti meg az operációsrendszer-függést. A
program továbbra is a Linux AArch64 rendszerhívási szerződésére
épít.
2. megoldás — Segítség. Csak egyetlen immediate értéket kell cserélni; nézd meg, hogy 7 ugyanabba a kódolási korlátba esik-e, mint 42.
2. megoldás — Teljes megoldás. Az A64
MOV X0, #42 helyére MOV X0, #7 kerül. A 7
ugyanúgy elfér a használt 16 bites konstansmezőben.
# Target: qemu-user, AArch64 Linux.
make -f lab-Makefile lab-all
qemu-aarch64 ./lab-exit
echo $?
# Várt: 7
make -f lab-Makefile EXPECTED=7 lab-run
# Várt: kilépés=7; várt=7
# Várt make-kilépési állapot: 0Az eredeti 42-es elvárással a teszt helyesen hibát jelezne.
3. megoldás — Segítség. A $? mindig a
legutóbbi parancs állapotát tükrözi — mi fut le a
qemu-aarch64 ./lab-exit és az echo $?
között?
3. megoldás — Teljes megoldás. A $? már
a printf sikeres végrehajtásához tartozik. Az
objdump -d lab-exit segítségével először ellenőrizzük az
A64 X0 regiszterbe töltött értéket, majd mentsük el az
állapotot közvetlenül a futtatásnál:
# Target: qemu-user, AArch64 Linux; eredeti, 42-es változat.
status=0
qemu-aarch64 ./lab-exit || status=$?
printf 'A futtatás véget ért.\n'
printf 'kilépés=%s\n' "$status"
# Várt utolsó sor: kilépés=42A program változatlan marad; csak a megfigyelés hibáját javítottuk.
Az assemblyforrásból először célarchitektúrához kötött objektumfájlt, majd linkerrel futtatható ELF-et készítünk. A cross és a natív út ugyanazt az A64 programot valósítja meg; az eszközök futási környezete és a végrehajtás módja különbözik. A könyvtármentes, statikus példa kizárja a céloldali dinamikus betöltő hiányából eredő hibákat, de továbbra is Linuxra épít. A kilépési állapotot azonnal kell ellenőrizni vagy elmenteni. A Makefile rögzíti a függőségeket, a parancsokat és az ellenőrzési feltételt, így az újraépítés megismételhető.
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 á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 példák itt nem kerültek végrehajtásra. A telepített Ubuntu-csomagok pontos verzióját, a helyi linker támogatott célmódjait és egy esetleges dinamikus sysroot elrendezését a laborban kell ellenőrizni.