Egy fájl tartalmazhat helyes A64 gépi kódot, mégsem biztos, hogy elindítható. Az operációs rendszernek azt is tudnia kell, mely fájlrészek kerüljenek memóriába, milyen hozzáférési jogokkal, és honnan kezdődjön a végrehajtás. Ezeket az információkat nem maguk az utasítások hordozzák.
Az előző alfejezet Hello world programja már végrehajtható ELF-fájlként futott. Most ezt a fájlt vizsgáljuk meg kívülről: összekötjük az assembler szekcióit a linker kimenetével és a programbetöltő működésével.
A célkörnyezet végig AArch64 Linux, A64 ISA, little-endian
ELF64, elsődlegesen qemu-aarch64 felhasználói módú
emulációval. A bemutatott kimenetek és ellenőrzési eredmények
vártak, nem ténylegesen mért vagy futtatással igazolt
adatok.
Az ELF (Executable and Linkable Format) objektumfájlok, végrehajtható programok és megosztott objektumok leírására szolgáló bináris fájlformátum. Nem kizárólag ARM-rendszereken használják.
Három szerkezetet kell megkülönböztetnünk:
A két utóbbi tábla nem ugyanannak az információnak kétféle elnevezése.
Az alábbi táblázat az AArch64 Linux ELF64-környezetre vonatkozik.
| Mező | Jelentés | A laborban várható érték |
|---|---|---|
e_ident |
Azonosító bájtok, osztály, bájtsorrend, formátumverzió | ELF, 64 bites, little-endian |
e_type |
Az objektum típusa | ET_EXEC a végleges programban |
e_machine |
Célarchitektúra | EM_AARCH64, numerikusan 183 |
e_entry |
A program belépési virtuális címe | A linker által kiválasztott _start címe |
e_phoff |
A programfejléc-tábla fájlbeli eltolása | A linker állítja elő |
e_phentsize, e_phnum |
Programfejléc-bejegyzés mérete és darabszáma | A konkrét fájlból olvasható ki |
e_shoff |
A szekciófejléc-tábla fájlbeli eltolása | A konkrét fájlból olvasható ki |
e_shentsize, e_shnum |
Szekciófejléc-bejegyzés mérete és darabszáma | A konkrét fájlból olvasható ki |
Az e_type legfontosabb értékei:
ET_REL: áthelyezhető objektumfájl (relocatable
object), például az assembler .o kimenete;ET_EXEC: végrehajtható objektum, a hagyományos, nem PIE
programok típusa;ET_DYN: megosztott objektum típusa; a helyfüggetlen
végrehajtható programok, azaz PIE-k is ezt használják.Az e_machine az AArch64 architektúrát azonosítja, nem
egy konkrét processzormodellt vagy az összes használt
ISA-kiterjesztést.
Fontos különbség: az e_phoff
fájleltolás, az e_entry viszont
virtuális cím. Egyiket sem szabad automatikusan a
másikként értelmezni.
A GNU assembler AArch64 ELF-célján a következő szekciók gyakoriak:
| Szekció | Jellemző szekciótípus | Szerep |
|---|---|---|
.text |
SHT_PROGBITS |
Gépi utasítások |
.rodata |
SHT_PROGBITS |
Csak olvasható konstansok |
.data |
SHT_PROGBITS |
Inicializált, írható adatok |
.bss |
SHT_NOBITS |
Futáskor nullával inicializált tárterület |
.symtab |
SHT_SYMTAB |
Szimbólumtábla, például címkék adatai |
A szekciófejléc többek között a típust (sh_type),
tulajdonságokat (sh_flags), méretet (sh_size),
igazítást (sh_addralign) és fájlbeli helyet
(sh_offset) írja le. A memóriába kerülő szekcióknál a
sh_addr is jelentős.
A szegmens (segment) ezzel szemben a
betöltési nézet egysége. Egy PT_LOAD
programfejléc-bejegyzés betöltendő memóriatartományt ír le, amely több
szekciót is tartalmazhat.
PT_LOAD mező |
Jelentés |
|---|---|
p_offset |
A szegmens adatainak kezdete a fájlban |
p_vaddr |
A szegmens virtuális címe |
p_filesz |
A fájlból származó rész mérete, bájtban |
p_memsz |
A memóriabeli tartomány teljes mérete, bájtban |
p_flags |
Olvasási, írási, végrehajtási jogosultság |
p_align |
Igazítási követelmény |
A p_paddr mező is létezik, de a szokásos
Linux-felhasználói program nem ezzel választ fizikai memóriacímet.
A betöltő lényegében a következő leképezést hozza létre:
p_offset helyétől p_filesz bájt
tartalma megjelenik a megfelelő virtuális tartományban;p_memsz > p_filesz, a fennmaradó memóriarész
nulla kezdőértéket kap.PT_LOAD esetén ezért p_filesz ≤ p_memsz. Ha
p_align értéke egynél nagyobb, kettőhatvány, és a
p_vaddr, illetve p_offset azonos maradékot ad
modulo p_align. A tényleges leképezésnél a rendszer
lapigazítási követelményei is számítanak.
.bss a fájlban?Levezetés: legyen egy írható szegmens fájlbeli tartalma 4 bájt inicializált adat, memóriabeli tartalma pedig ehhez képest további 8 bájt nullázott tárterület. Igazítás nélkül ekkor:
p_filesz = 4 bájt;p_memsz = 12 bájt;Ez szemléltető számítás, nem a következő bináris pontos szegmensmérete: az igazítás és a linker elrendezése módosíthatja a különbséget.
A .bss tehát nem „hiányzó memória”. A helyét leírjuk, de
a nulla kezdőértéket a betöltés biztosítja. Bare-metal környezetben ezt
később saját startup kóddal kell majd megoldanunk.
A statikus linkelésű (statically linked) laborprogramhoz nem kell felhasználói térben futó dinamikus linker: minden szükséges saját kód benne van. Ettől még Linux-rendszerhívásokat használ; a „statikus” nem jelent operációs rendszer nélküli működést.
A szokásos dinamikusan linkelt (dynamically linked) Linux-program további megosztott objektumokra, például a libc-re támaszkodik.
| ELF-jel | Jelentése |
|---|---|
PT_INTERP |
Megadja az indításhoz kért ELF-értelmező, tipikusan a dinamikus linker útvonalát |
PT_DYNAMIC |
Dinamikus linkelési információkat tartalmazó tartományt jelöl |
DT_NEEDED bejegyzés |
Egy szükséges megosztott objektum nevét adja meg |
Az ET_DYN önmagában nem bizonyítja,
hogy a fájl hagyományos értelemben dinamikusan linkelt program.
Megosztott könyvtár, dinamikus PIE és statikus PIE is lehet ilyen
típusú. Fordítva: egy ET_EXEC program is lehet dinamikusan
linkelt.
A típust, a programfejléceket és szükség esetén a dinamikus táblát
együtt kell vizsgálni. A következő saját belépési pontos programban nem
várunk PT_INTERP, PT_DYNAMIC vagy
DT_NEEDED bejegyzést.
execve hívástól az első utasításigElőször a natív AArch64 Linux fogalmi menetét tekintsük át.
execve rendszerhívást. Siker
esetén a meglévő folyamat programképe lecserélődik; maga az
execve nem hoz létre új folyamatot. A shell előzetesen
létrehozhatott külön gyermekfolyamatot.PT_LOAD szegmenseket a megadott tartalommal és
jogosultságokkal leképezi, a fájlban nem tárolt részek nulla
kezdőértéket kapnak.SP regiszter erre a veremre mutat.PT_INTERP, a vezérlés a program belépési
pontjára kerül. Ha van, először az ELF-értelmező belépési pontja
következik.A leképezés nem feltétlenül jelent azonnali fizikai beolvasást: a lapok egy része igény szerinti lapozással (demand paging) válhat ténylegesen elérhetővé.
PIE esetén az ELF-ben tárolt címekhez betöltési eltolás (load
bias) társulhat. A futásidejű belépési cím ilyenkor az eltolás és
az e_entry összege.
_start határa?A _start szimbólumnév, nem az
ELF-betöltő által kötelezően keresett különleges karakterlánc. A linker
a választott szimbólum címét beírja az e_entry mezőbe.
Futtatáskor már ez a cím számít.
A belépés nem szokásos AAPCS64 függvényhívás: nem egy A64
BL utasítás hívta meg a _start kódját. Az A64
X30 ezért nem tekinthető használható visszatérési címnek.
Saját belépési pontunkból rendszerhívással lépünk ki, nem A64
RET utasítással.
A betöltő nem hívja meg automatikusan a C main
függvényét. A szokásos C-programban ezt a feladatot a futtatókörnyezet
indulókódja készíti elő.
QEMU-különbség: x86_64 hoston a
qemu-aarch64 indításakor a host kernel magát a
QEMU-programot indítja el. A vendég AArch64 ELF betöltését és az A64
utasítások végrehajtását a QEMU kezeli; a vendég rendszerhívásokat a
host szolgáltatásaira közvetíti. Ez nem egy teljes AArch64 Linux-kernel
emulációja.
Munkakönyvtár: ~/arm-labor/a64/a012/ —
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/a012
cd ~/arm-labor/a64/a012A következő, lab-hello-elf.s nevű fájl az előző program
kismértékű bővítése. Az AArch64 ELF-szekciók közül a
.rodata üzenetet, a .data kilépési állapotot,
a .bss ellenőrizhető nullázott tárhelyet tartalmaz.
A teljes blokk kontextusa: A64 ISA, AArch64 Linux; a direktívák GNU assembler-direktívák AArch64 ELF-célhoz.
.section .text
.global _start
.type _start, %function
_start:
ldr x9, =lab_zero
ldr x10, [x9]
cbnz x10, lab_fail
mov x0, #1
ldr x1, =lab_message
mov x2, #lab_message_len
mov x8, #64
svc #0
cmp x0, #lab_message_len
b.ne lab_fail
ldr x9, =lab_exit_status
ldr w0, [x9]
mov x8, #93
svc #0
lab_fail:
mov x0, #1
mov x8, #93
svc #0
.size _start, . - _start
.ltorg
.section .rodata
lab_message:
.ascii "Hello, ELF!\n"
.equ lab_message_len, . - lab_message
.section .data
.balign 4
lab_exit_status:
.word 0
.section .bss
.balign 8
lab_zero:
.skip 8
.section .note.GNU-stack,"",@progbitsA kód először ellenőrzi a .bss nyolc bájtjának
nullázását. A kiírás után az A64 CMP és B.NE
azt ellenőrzi, hogy a visszaadott bájtszám megegyezik-e az üzenet
hosszával. Rövid írást hibának tekintünk; újrapróbálkozó I/O-ciklust itt
nem építünk.
A használt konstansok kódolhatók egyetlen A64 MOV alias
utasítással, 16 bites, eltolás nélküli MOVZ formában. Az
A64 CMP immediate-változatának 12 bites, opcionálisan 12
bittel balra tolt konstansába a 12 bájtos hossz belefér; az A64
SVC immediate-mezője 16 bites. Az A64
LDR =szimbólum GNU pszeudoutasítás itt közeli
literálkészletből tölt címet. A literálbetöltés, valamint az itt
használt feltételes ágak előjeles, 19 bites, néggyel skálázott relatív
eltolása elegendő ehhez a rövid kódhoz. A részletes konstanskezelést a
MOV-variánsok tárgyalja.
Cél: qemu-user, AArch64 Linux. A
parancsok az Ubuntu x86_64 hoston futnak.
aarch64-linux-gnu-as -march=armv8-a \
-o lab-hello-elf.o lab-hello-elf.s
aarch64-linux-gnu-ld -static -z separate-code -e _start \
-o lab-hello-elf lab-hello-elf.o
qemu-aarch64 ./lab-hello-elf > lab-hello-elf.out
lab_status=$?
cat lab-hello-elf.out
printf 'exit=%s\n' "$lab_status"
printf 'Hello, ELF!\n' | cmp - lab-hello-elf.out
printf 'cmp=%s\n' "$?"Várt eredmény: egy Hello, ELF! sor,
majd exit=0 és cmp=0. A bájtpontos
összehasonlítás a lezáró sortörést is ellenőrzi.
Az explicit -e _start választja ki a belépési
szimbólumot. A -z separate-code a kód elkülönített
betöltési elrendezését kéri; a pontos szegmensszámot nem rögzítjük
feltételezésként.
readelf
kimenetének értelmezéseVizsgálati cél: az előző qemu-user program; az
eszközök a hoston futnak.
aarch64-linux-gnu-readelf -h lab-hello-elf.o
aarch64-linux-gnu-readelf -h lab-hello-elf
aarch64-linux-gnu-readelf -lW lab-hello-elf
aarch64-linux-gnu-readelf -SW lab-hello-elf
aarch64-linux-gnu-readelf -d lab-hello-elf
aarch64-linux-gnu-nm -n lab-hello-elf
aarch64-linux-gnu-objdump -d lab-hello-elfA teljes kimenet helyett a következő ellenőrzési pontokra koncentráljunk:
| Vizsgálat | Várt megfigyelés | Következtetés |
|---|---|---|
Objektumfájl, -h |
REL, AArch64 |
Még linkelendő AArch64 objektum |
Végrehajtható fájl, -h |
ELF64, little-endian, EXEC,
AArch64 |
A választott profilnak megfelelő program |
Entry point address és nm |
A belépési cím egyezik _start címével |
A linker a kívánt kezdőpontot választotta |
-lW, LOAD sorok |
Fájleltolások, virtuális címek, méretek, jogosultságok | A betöltési nézet látható |
Section to Segment mapping |
Szekciók hozzárendelése szegmensekhez | Nem szükségszerű az egy-egy megfelelés |
-SW, .bss |
NOBITS, mérete 8 bájt |
A tárhely tartalma nincs nyolc adatbájtként tárolva |
Írható LOAD |
MemSiz > FileSiz |
Nullázandó memóriarész tartozik hozzá |
-lW, -d |
Nincs interpreter és dinamikus szakasz | Ehhez a programhoz nem kell dinamikus linker |
A Flg oszlopban az R olvasást, a
W írást, az E végrehajtást jelöl. A belépési
címnek ebben a programban végrehajtható PT_LOAD tartományba
kell esnie.
A címeket a saját fájlból olvassuk ki; nem használunk rögzített mintacímet. A linker alapértelmezett szkriptje és a toolchain változata az elrendezést befolyásolhatja.
„Van .text, tehát futtatható.”
Hipotézis: az assembler .o kimenetét próbáltuk futtatni.
Adatgyűjtés: readelf -h és readelf -l. Ha
ET_REL fájlt látunk, a javítás a végső linkelési lépés, nem
a fájlnév átírása.
„A .bss nem tárol adatbájtokat, ezért olvasni
sem lehet.”
Ellenpróba: a laborprogram A64 LDR utasítása betölti a
lab_zero értékét, és nem nulla esetén hibával kilép. Ez a
nyolc bájt a memóriakép része, nem a fájl adatbájtjainak része.
„A _start név szükséges a
betöltőnek.”
Adatgyűjtés: hasonlítsuk össze az e_entry értékét a
szimbólumtábla címével. A betöltési szerződés a cím; a név a linkelésben
segít. A futtatáshoz nem szükséges a normál .symtab
szimbólumtábla megőrzése.
„Minden regiszter nulla a program elején.”
A nullázott .bss tartalmából nem következik az általános
célú A64 regiszterek nullázottsága. A programnak minden felhasznált
regisztert inicializálnia kell, vagy dokumentált indulási szerződésre
kell támaszkodnia.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Miért elegendő a betöltőnek a
programfejléc-tábla a .text, .data és
.bss szekciónevek ismerete nélkül? Miért nem ugyanaz az
e_entry és az e_phoff?
(implementációs, középszint — cél: rutinépítés) Két külön kísérletben módosítsd a programot:
.word 0 értékét változtasd
.word 7 értékre;.skip 8 méretét
változtasd .skip 4096 értékre.Mindkét esetben építsd újra és futtasd a programot. Vizsgáld a
kilépési állapotot, a szekcióméreteket és a PT_LOAD
méreteit.
(hibakeresési, középszint — cél: rutinépítés)
Készíts hibás belépésipont-választással külön binárist. Cél:
qemu-user; linkelés a hoston.
aarch64-linux-gnu-ld -static -z separate-code -e lab_missing \
-o lab-hello-elf-wrong lab-hello-elf.o
aarch64-linux-gnu-readelf -h lab-hello-elf-wrong
aarch64-linux-gnu-nm -n lab-hello-elf-wrong
aarch64-linux-gnu-objdump -d lab-hello-elf-wrongJegyezd fel a linker diagnosztikáját. Megbízható-e a bináris pusztán azért, mert esetleg ugyanazt a belépési címet kapta, mint a helyes változat? Egyetlen kapcsoló módosításával javítsd.
1. megoldás — Segítség. Nézd meg a
PT_LOAD mezőtáblázatot — szerepel-e benne bárhol
szekciónév? Az e_entry és az e_phoff közül
melyik virtuális cím, melyik fájleltolás?
1. megoldás — Teljes megoldás. A
PT_LOAD bejegyzések megadják a betöltendő
fájltartományokat, virtuális címeket, memóriaméreteket és
jogosultságokat. A nulla kitöltés a méretkülönbségből következik; ehhez
nem kell felismerni a .bss nevet. Az e_entry
végrehajtási cím, az e_phoff pedig egy fejléc-tábla
fájlbeli eltolása.
2. megoldás — Segítség. A
.word 0→.word 7 csak a .data
TARTALMÁT érinti, a méretét nem — mit vár ettől a kilépési állapot? A
.skip 8→.skip 4096 viszont a .bss
MÉRETÉT növeli — ez p_filesz-t vagy p_memsz-t
növeli inkább?
2. megoldás — Teljes megoldás. Az első változat várt
kimenete változatlan, kilépési állapota 7. Az A64 LDR W0 a
32 bites adatot tölti be, és az A64 X0 felső 32 bitjét
nullázza. A bájtösszehasonlítás továbbra is sikeres lehet: a helyes
szöveg és a nulla kilépési állapot külön tulajdonság.
A második változatban a .bss mérete 4096 bájt, a
kilépési állapot ismét 0. Az érintett szegmens memóriamérete nő, de nem
kerül 4088 további nulla adatbájt a fájlba. A teljes fájlméret pontos
változását nem jósoljuk meg pusztán ebből, mert egyéb elrendezési
részletek is számítanak. A program csak az első nyolc bájtot
ellenőrzi.
3. megoldás — Segítség. Ha a linker nem találja a
kért szimbólumot, biztos, hogy hibával leáll, vagy választhat helyette
valamilyen alapértelmezett címet? Mi történne, ha ez a cím véletlenül
megegyezne a helyes _start-tal?
3. megoldás — Teljes megoldás. A GNU linker
várhatóan jelzi, hogy a kért szimbólum nem található, és helyettesítő
belépési címet választ. Ennek pontos szövegét és címválasztását a helyi
verzión kell ellenőrizni. A helyettesítő cím véletlenül egybeeshet
_start címével, így a futási próba önmagában elfedheti a
hibát. A javítás -e _start; utána a diagnosztikát és az
e_entry–szimbólum egyezést is újra ellenőrizni kell.
Az ELF egyszerre szolgálja a linkelést és a programbetöltést, de ezek
eltérő nézeteket használnak. A szekciók a kód és adat szervezését, a
programfejlécek a futásidejű leképezést írják le. A .bss
nulla kezdőértékű memóriát foglal anélkül, hogy tartalmát teljes
egészében tárolnánk a fájlban. A linker választja ki a belépési címet; a
betöltő nem a _start nevet keresi. Dinamikus programnál
először az értelmező indulhat. A readelf és a
szimbólumtábla összevetése ezért a futási próba fontos kiegészítője.
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).Nyitott ellenőrzések, amelyek a fenti ellenőrzés után is ténylegesen fennmaradnak: az Ubuntu 24.04 telepített binutils- és QEMU-verziója, a konkrét linker-szkript szegmenselrendezése, a hibás belépési szimbólum diagnosztikája, valamint a laborprogram tényleges kimenete.