_start, syscall hívásEgy C-ben megírt üdvözlőprogram rövid, de a végrehajtható állomány rendszerint jóval többet tartalmaz a saját függvényünknél: indulókódot, könyvtári kapcsolódásokat és a szabványos I/O kezelését. Most ezt a rétegzettséget bontjuk szét egy konkrét feladattal: írjunk ki egy üzenetet, majd fejezzük be a folyamatot libc és C-indulókód nélkül.
Az önállóság itt nem operációs rendszer nélküli működést jelent. A program Linux-szolgáltatásokat használ; a fájlleírók és a folyamat kezelése továbbra is az operációs rendszer feladata.
Profil: az alfejezet minden utasítása és regisztere
az A64 ISA, AArch64, I. rész környezetéhez tartozik. Az
assemblydirektívák GNU assembler-direktívák ebben az AArch64
ELF-profilban, nem processzorutasítások. A cél ARMv8-A, little-endian
Linux; az elsődleges futtatási mód qemu-aarch64, vagyis
QEMU user-mode emuláció.
Az előfeltétel az A64 regiszterkészletének áttekintése. Veremkezelésre vagy függvényhívási keretre most nem lesz szükség.
A szekció (section) az objektumfájl tartalmának névvel és tulajdonságokkal ellátott egysége. Ebben a példában két fontos szekciót használunk:
| GNU assembler-direktíva, AArch64 ELF-profil | Szerep |
|---|---|
.text |
A végrehajtandó utasítások szekciója. |
.data |
Inicializált, írható adatok szekciója. |
Az üzenet nem változik, ezért később természetesebb helye lehetne egy
csak olvasható adatszekcióban. Most tudatosan .data-ba
tesszük, hogy a kód és az inicializált adat elkülönítését lássuk.
A szekció nem azonos a futás közbeni memóriaszegmenssel. A linker a szekciókból alakítja ki a betölthető tartományokat; ezt az ELF formátum alapjai és a program életciklusa részletezi.
A belépési pont (entry point) az a cím,
ahol a program végrehajtása megkezdődik. A _start elnevezés
szokásos szimbólumnév, nem hardveresen fenntartott szó. A linkernek
külön megmondjuk, hogy ezt válassza belépési pontnak.
Fontos különbség a C-hez képest:
main függvényt rendszerint indulókód hívja meg;_start címkénknél nincs ilyen hívó;RET utasítással, hanem
Linux-rendszerhívással jelezzük.A .global _start láthatóvá teszi a szimbólumot a linker
számára. Önmagában nem helyettesíti a belépési pont kijelölését.
A rendszerhívás (system call, syscall) az alkalmazás és az operációs rendszer közötti szabályozott szolgáltatáskérés. A Linux AArch64 rendszerhívási bináris interfésze (application binary interface, ABI) meghatározza, mely regiszterekbe kerülnek az adatok.
Ez Linux-konvenció, nem az A64 ISA általános tulajdonsága.
| Szerep | Linux AArch64 rendszerhívási ABI |
|---|---|
| Rendszerhívásszám | A64 X8 |
| Első–hatodik argumentum | A64 X0–X5, sorrendben |
| Szolgáltatáskérés | A64 SVC #0 |
| Visszatérési érték | A64 X0, ha a hívás visszatér |
A64-en az SVC immediate operandusa 16 bites, tehát
0–65535 közötti értéket kódolhat. A Linuxhoz itt
SVC #0 kell; a rendszerhívásszám nem ebbe az operandusba,
hanem az A64 X8 regiszterbe kerül.
Fizikai AArch64 Linuxon az utasítás kivételt vált ki, amelyet az operációs rendszer kezel. QEMU user-mode környezetben a QEMU emulálja a vendégprogram rendszerhívását a host szolgáltatásaival. Itt nem külön AArch64 Linux-kernelt indítunk.
A két szükséges szolgáltatás:
| Hívás | A64 X8 |
A64 X0 |
A64 X1 |
A64 X2 |
|---|---|---|---|---|
Linux write |
64, azaz 0x40 |
Fájlleíró: 1 | Az üzenet címe | Hossz bájtban |
Linux nyers exit |
93, azaz 0x5d |
Kilépési állapot | Nem argumentum | Nem argumentum |
A fájlleíró (file descriptor) 1 szokásosan a szabványos kimenetet jelöli. A shell azonban átirányíthatja vagy bezárhatja; ezért ez nem feltétlenül terminált jelent.
A nyers Linux exit nem a C könyvtár exit()
függvénye: nem futtat könyvtári lezárási műveleteket, és nem ürít
libc-puffereket. A hívó szálat szünteti meg; ebben az egyszálú
programban ezzel a folyamat is véget ér.
Munkakönyvtár: ~/arm-labor/a64/a011/ —
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/a011
cd ~/arm-labor/a64/a011Mentsük a következő forrást lab-hello.s néven:
// Profil: A64 ISA, AArch64, ARMv8-A, little-endian Linux.
// Direktívák: GNU assembler, AArch64 ELF.
// Cél: qemu-user, qemu-aarch64.
.section .data
message:
.ascii "Hello, ARM64!\n"
.equ message_len, . - message
.section .text
.balign 4
.global _start
.type _start, %function
_start:
mov X0, #1
ldr X1, =message
mov X2, #message_len
mov X8, #64
before_write:
svc #0
after_write:
mov X0, #0
mov X8, #93
svc #0
.size _start, . - _start
.ltorg
.section .note.GNU-stack,"",%progbitsAz AArch64 GNU assembler .ascii direktívája a megadott
bájtokat helyezi el, lezáró nulla nélkül. A
\n ebben a példában egyetlen, 0x0a értékű
bájt.
A hossz levezetése:
| Üzenetrész | Bájtok száma |
|---|---|
Hello |
5 |
| Vessző | 1 |
| Szóköz | 1 |
ARM64 |
5 |
| Felkiáltójel | 1 |
| Újsor | 1 |
| Összesen | 14 |
A .equ message_len, . - message assemblerdirektívában a
. az aktuális helyszámláló. Közvetlenül az üzenet után
állunk, ezért a különbség az üzenet bájthossza.
Ez összeállításkor kiszámított konstans, nem futás közben végzett kivonás és nem memóriában tárolt hosszmező.
A Linux write nem C-sztringet keres: a cím és a bájtszám
alapján dolgozik. Ha az üzenetben nulla bájt volna, azt is kiírná,
amennyiben beleesik a megadott hosszba. UTF-8 szövegnél ugyanezért a
karakterek száma eltérhet a bájtok számától; a címkekülönbség akkor is a
megfelelő bájthosszt adja.
Az A64-környezetben használt GNU assembler
ldr X1, =message
alakja pszeudoutasítás
(pseudo-instruction). A célja itt a message
szimbólum címének betöltése az A64 X1 regiszterbe, nem az
üzenet első nyolc bájtjának beolvasása.
A GNU assembler ehhez literálkészletet (literal pool)
használ: a kód közelében elhelyezi a cím tárolására szolgáló bejegyzést,
amelynek végleges értékét a linker rendezi. Az A64 LDR
literálalakja innen tölti be a címet.
Ennek a valódi utasításnak az elérése korlátos: 19 bites előjeles
eltolást kódol, néggyel skálázva. Az utasítás címéhez képesti tartomány
ezért −1 048 576 és +1 048 572 bájt közötti. A .ltorg
assemblerdirektíva itt, a rövid kód végén helyezi el a függő
literálkészletet, bőven ezen a tartományon belül.
A literálkészlet adatait nem akarjuk utasításként végrehajtani.
Előtte a Linux exit rendszerhívás megszünteti a
programot.
A példában szereplő A64 MOV immediate alakok kis,
nemnegatív konstansokat töltenek be. Ezek egyetlen A64 MOVZ
utasítással kódolhatók.
Az A64 MOVZ egy 16 bites mezőt helyez el a
célregiszterben; 64 bites cél esetén a megengedett eltolás 0, 16, 32
vagy 48 bit. Itt minden értékhez a 0 bites eltolás elég.
Ebből nem következik, hogy a
mov Xn, #konstans bármely 64 bites számot elfogad egyetlen
utasításként. A részleteket a MOV-variánsok és
immediate-kódolás tárgyalja.
A .balign 4 négybájtos határra igazítja a kód kezdetét.
A .type és .size szimbólummetaadatot ad az
eszközöknek. A .note.GNU-stack szekcióbejegyzés pedig azt
jelzi a GNU eszközláncnak, hogy ez az objektum nem igényel végrehajtható
vermet; maga nem végrehajtott kód.
A következő parancsok célja qemu-user, Ubuntu 24.04 x86_64 hoston, AArch64 kereszt-eszközlánccal. A szükséges telepítést a labor-alapkörnyezet ismerteti.
# Cél: qemu-user; AArch64 ARMv8-A, little-endian Linux.
aarch64-linux-gnu-as -g -march=armv8-a -EL \
-o lab-hello.o lab-hello.s
aarch64-linux-gnu-ld -static -e _start \
-o lab-hello lab-hello.o
qemu-aarch64 ./lab-hello
lab_status=$?
printf 'exit=%d\n' "$lab_status"Az első lépés objektumfájlt állít elő. A második feloldja a szimbólumhivatkozásokat, kijelöli a belépési pontot és létrehozza a végrehajtható ELF-fájlt. Nem adunk hozzá C-indulókódot vagy könyvtárat.
A -static itt nem „statikusan beépített libc”-t jelent:
egyáltalán nem linkelünk libc-t. A létrejövő állomány futtatásához ezért
nem kell AArch64 dinamikus betöltő vagy megosztott könyvtárkészlet.
Levezetett, várt eredmény normál, írható szabványos kimenet mellett:
| Megfigyelés | Várt érték |
|---|---|
| Program kimenete | Hello, ARM64!, majd újsor |
A64 X0 közvetlenül a write után |
14 |
| Shell által kiírt állapot | exit=0 |
A kilépési állapotot közvetlenül a QEMU futtatása után mentjük el. Ha
közben más parancsot hajtanánk végre, a shell $? értéke már
annak az állapotát jelentené.
A programot itt nem futtattuk; ezek ellenőrizendő, várt eredmények.
A terminálon látható szöveg önmagában nem elég: egy fölösleges nulla bájt például láthatatlan maradhat.
# Cél: qemu-user; kimenetrögzítés és ellenőrzés a hoston.
qemu-aarch64 ./lab-hello > lab-output.bin
lab_status=$?
printf 'exit=%d\n' "$lab_status"
wc -c < lab-output.bin
od -An -tx1 lab-output.binVárt állapot: a kilépési kód 0, a fájl hossza 14 bájt. A bájtsor hexadecimális jelöléssel:
0x48 0x65 0x6c 0x6c 0x6f 0x2c 0x20 0x41 0x52 0x4d 0x36 0x34 0x21 0x0a
A bájtok sorozatánál nincs több-bájtos egészértékből eredő bájtsorrend-kérdés.
A Linux write visszatérésekor az A64 X0 új
szerepet kap: már nem fájlleíró, hanem eredmény.
A libc write() burkolófüggvényével ellentétben itt nincs
automatikus errno-beállítás és egységes -1
visszatérés.
A mi programunk az after_write címkénél rögtön felülírja
az A64 X0 regisztert a kilépési állapottal. A 0
kilépési kód ezért nem bizonyítja, hogy az írás sikerült. Ez a
kezdőprogram tudatos korlátozása: nincs hibakezelése és nincs részleges
írást folytató ciklusa.
A visszatérési érték megfigyeléséhez indítsuk az első terminálban:
# Cél: qemu-user, GDB-kapcsolatra váró program.
qemu-aarch64 -g 1234 ./lab-hello
lab_status=$?
printf 'exit=%d\n' "$lab_status"A második terminálban:
# Cél: a qemu-user AArch64 vendégprogramjának vizsgálata.
# A vizsgált regiszterek az A64 ISA regiszterei.
gdb-multiarch -q ./lab-hello \
-ex 'target remote :1234' \
-ex 'break before_write' \
-ex 'continue' \
-ex 'info registers x0 x1 x2 x8' \
-ex 'x/14bx $x1' \
-ex 'si' \
-ex 'p/d (long)$x0' \
-ex 'continue' \
-ex 'quit'A töréspont a szolgáltatáskérés előtt állítja meg a programot. A
si egy utasítást hajt végre, így utána az
after_write címkénél vagyunk, még az eredmény felülírása
előtt.
| Ellenőrzési pont | Várt állapot |
|---|---|
A64 SVC #0 előtt |
X0=1, X2=14, X8=64 |
A64 X1 által mutatott memória |
A felsorolt 14 üzenetbájt |
A64 SVC #0 után |
X0=14, normál sikeres írásnál |
A konkrét cím helyett a szimbólumot és a regiszterben lévő címet használjuk; nem építünk be feltételezett memóriacímet.
A hibakeresés menete legyen: tünet → hipotézis → adatgyűjtés → egy változtatás → új ellenőrzés.
| Tünet | Lehetséges ok | Ellenőrzés |
|---|---|---|
Exec format error x86_64 hoston |
Közvetlenül indítottuk az AArch64 fájlt | Indítás qemu-aarch64 segítségével |
| Nincs üzenet, mégis 0 a kilépési kód | A write hibázott, az eredményt eldobtuk |
A64 X0 vizsgálata közvetlenül a hívás után |
| Hibás vagy túl hosszú kimenet | Helytelen bájtszám | A64 X2, majd a kimenet bájtjainak ellenőrzése |
| A hívás memóriahibát jelez | Az A64 X1 nem a megfelelő adat címe |
Memóriavizsgálat az A64 X1 címen |
| A szöveg után láthatatlan többletbájt van | .ascii helyett .asciz, változatlan
hosszszámítással |
wc és od |
| Nem a kívánt szolgáltatás fut | Rossz rendszerhívásszám | A64 X8 ellenőrzése, nem az SVC
operandusának módosítása |
Az utasítások és a szimbólumok kapcsolatát disassemblyvel is vizsgálhatjuk:
# Célállomány: qemu-user számára készült AArch64 ELF.
# Ez hostoldali vizsgálat, nem futtatás.
aarch64-linux-gnu-objdump -d lab-hello
aarch64-linux-gnu-readelf -h -S -l lab-helloAz A64 MOV megjelenítése aliasfüggő lehet; a
literálkészlet szintén megjelenhet a .text tartalmában. Nem
minden ott tárolt bájt végrehajtandó utasítás. A részletes értelmezést a
Disassembly objdump/readelf-fel tárgyalja.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Miért működik a kiírás lezáró nulla
nélkül? Mi változna, ha kizárólag az AArch64 GNU assembler
.ascii direktíváját cserélnénk .asciz
direktívára? Miért nem javítható egy rossz rendszerhívásszám az A64
SVC operandusának átírásával?
(implementációs, középszint — cél: rutinépítés)
Készíts lab-szia.s nevű változatot, amely a
Szia, ARM64! szöveget írja ki újsorral, majd 7 kilépési
állapottal fejeződik be. A hosszt továbbra is címkekülönbség számítsa.
Ellenőrizd a kimenetet és a kilépési állapotot.
(hibakeresési, középszint — cél:
megértés-ellenőrzés) A GDB-munkamenetben, közvetlenül az A64
SVC #0 előtt állítsd az A64 X0 regisztert
−1-re. A program forrását ne változtasd meg. Jegyezd fel a rendszerhívás
előjeles visszatérési értékét és a folyamat kilépési állapotát.
Magyarázd meg az eltérést.
1. megoldás — Segítség. Gondold végig, mit kap
paraméterként a write: egy címet és egy hosszt, vagy egy
sztringet? Ha .asciz-re cserélnéd a direktívát, hogyan
változna a . - message különbség?
1. megoldás — Teljes megoldás. A Linux
write cím–hossz párost kap, nem nullával lezárt sztringet.
Az AArch64 GNU assembler .asciz direktívája hozzáadna egy
0x00 bájtot. Mivel a hosszszámítás az üzenet után áll, az
új hossz 15 lenne, és a program a nullát is kiírná.
A szolgáltatást a Linux AArch64 ABI szerint az A64 X8
választja ki. Az A64 SVC immediate mezője nem ennek
alternatív tárolóhelye; a laborban maradjon #0.
2. megoldás — Segítség. Másold a teljes
lab-hello.s-t, és csak három helyen változtass: az
üzenetszöveg, a kilépési kód betöltése, és — mivel az üzenet hossza is
más — semmit a hosszszámításon, hiszen az automatikusan követi a
címkekülönbséget.
2. megoldás — Teljes megoldás. A teljes forrás
másolatában az üzenet legyen .ascii "Szia, ARM64!\n", az
after_write címke utáni első utasítás pedig A64
mov X0, #7.
A várt hossz 13 bájt: négy betű, vessző, szóköz, öt üzenetbeli karakter, felkiáltójel és újsor.
# Cél: qemu-user; AArch64 ARMv8-A, little-endian Linux.
aarch64-linux-gnu-as -g -march=armv8-a -EL \
-o lab-szia.o lab-szia.s
aarch64-linux-gnu-ld -static -e _start \
-o lab-szia lab-szia.o
qemu-aarch64 ./lab-szia
lab_status=$?
printf 'exit=%d\n' "$lab_status"Várt kimenet: Szia, ARM64! és újsor, majd a shell
ellenőrzésében exit=7. Ez szándékosan választott, nem
véletlen hibajelzés.
3. megoldás — Segítség. A -1 mint
fájlleíró biztosan érvénytelen; gondold végig, a write
szokásos Linux-hibajelzése hogyan jelenik meg A64 X0-ban
(előjeles, negatív érték), és hogy a program forrása mikor írja felül
ezt az értéket.
3. megoldás — Teljes megoldás. A GDB-indításban
közvetlenül a -ex 'si' elé illesztendő argumentum:
-ex 'set $x0 = -1'.
A −1 nem érvényes fájlleíró. A Linux AArch64 nyers visszatérés várt
értéke −9, vagyis -EBADF; ugyanennek 64 bites bitmintája
0xfffffffffffffff7.
A program ezután az A64 X0 értékét 0-ra írja, és
meghívja az exit szolgáltatást. A folyamat kilépési
állapota tehát továbbra is 0. Az ellenpróba megmutatja: a rendszerhívás
eredménye és a program által választott kilépési állapot két külön
adat.
A libc nélküli AArch64 program saját belépési pontból indul, de továbbra is Linux-szolgáltatásokra támaszkodik. A kódot és az inicializált adatot külön szekcióba helyeztük, az üzenet hosszát pedig összeállításkor számítottuk ki. A Linux AArch64 rendszerhívási szerződésében az A64 X8 választja ki a szolgáltatást, az A64 X0–X5 hordozza az argumentumokat, az A64 SVC #0 pedig elindítja a kérést. A visszatérő A64 X0 tartalmát felülírás előtt kell ellenőrizni. A sikeres folyamatkilépés önmagában nem igazolja a kiírás sikerét; ehhez a visszatérési érték és a kimeneti bájtok vizsgálata is szükséges.
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.
SVC,
MOVZ és a literálbetöltés. 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: a kód összeállítása, QEMU-futtatása és GDB-léptetése itt nem történt meg. A helyi Ubuntu 24.04 csomagverziókat és a labor tényleges eredményeit a hallgatónak kell rögzítenie. A bemutatott kimenetek és regiszterértékek levezetett, várt állapotok; teljesítménymérés nem szerepel a példában.