3.2 Assemble-link-run munkafolyamat natív és cross környezetben

3.2.1 Miért kell külön összeszerelni és linkelni?

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.

3.2.2 Az assemble–link–run folyamat

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:

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.

3.2.3 Kidolgozott példa: kilépés ismert állapottal

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/a007

Mentsü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,"",%progbits

A 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.

3.2.3.1 Összeszerelés és linkelés cross környezetben

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.o

Vá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.

3.2.3.2 Futtatás és ellenőrzés

# 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: 42

A 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.

3.2.4 Miért hasznos a statikus linkelés QEMU alatt?

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:

  1. nincs szükség AArch64 dinamikus betöltőre;
  2. nincs szükség céloldali megosztott könyvtárakra;
  3. kevesebb környezeti tényező okozhat futtatási hibá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-exit

Várt megfigyelések:

Az ELF részleteivel Az ELF formátum alapjai és a program életciklusa foglalkozik.

3.2.5 Ugyanez natív AArch64 Linuxon

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: 42

A 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.

3.2.6 Az első minimális Makefile

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-exit

A $@ 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: 0

A 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: 0

Az ü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.

3.2.7 Tipikus hibák és ellenpróbák

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-exit

A 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.

3.2.8 Feladatok

  1. (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?

  2. (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.

  3. (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 $?

3.2.9 Megoldások

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: 0

Az 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=42

A program változatlan marad; csak a megfigyelés hibáját javítottuk.

3.2.10 Rövid összefoglaló

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ő.

3.2.11 Források és ellenőrzési hiányok

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.

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.