Egy assemblyprogram hibája gyakran nem a forrás olvasásakor válik nyilvánvalóvá. Lehet, hogy megfelelő utasításokat írtunk, de rossz architektúrára építettük a programot; lehet, hogy helyes a bináris, de másik fájlt indítottunk el; és lehet, hogy csak a végrehajtás közbeni regiszterérték hibás.
Az Assemble-link-run munkafolyamat natív és cross környezetben lépéseit ezért most megfigyelési pontokkal egészítjük ki. Három eszköz három különböző kérdésre válaszol:
| Eszköz | Kérdés | Elindítja a vizsgált programot? |
|---|---|---|
readelf |
Milyen ELF-fájl készült, milyen szekciókkal és szimbólumokkal? | Nem |
objdump |
Milyen gépi utasítások találhatók benne? | Nem |
gdb-multiarch |
Hol tart a végrehajtás, és mi van a regiszterekben? | A jelen laborban a QEMU-ban futó programot vezérli |
A visszafejtés (disassembly) a gépi utasítások assembly alakú megjelenítése. Nem állítja vissza az eredeti megjegyzéseket vagy minden forrásszintű nevet. A hibakereső (debugger) ezzel szemben megállíthatja és léptetheti a programot, majd megmutathatja annak pillanatnyi állapotát.
A teljes alfejezet célprofilja A64 ISA, AArch64, ARMv8-A, little-endian. Az assemblyblokk összes utasítása és regisztere ebben a kontextusban értendő; direktívái GNU assembler-direktívák ugyanahhoz a célhoz. Cortex-M kód itt nem szerepel.
Bármely egyszerű szövegszerkesztő használható: például
nano, vim, Emacs vagy grafikus kódszerkesztő.
A fontos tulajdonságok:
A kiemelés csak kényelmi funkció: nem bizonyítja, hogy egy utasítás vagy konstans kódolható. Erről az assembler dönt.
A laborban ugyanabból a munkakönyvtárból dolgozzunk, két terminállal. A fájlok neve:
lab-tools.s: forrás;lab-tools.o: objektumfájl;lab-tools: futtatható ELF.A hibakeresés alapvető előfeltétele, hogy a QEMU és a GDB ugyanazt a binárist használja. Újraépítés után indítsuk újra a futtatási és hibakeresési munkamenetet.
Munkakönyvtár: ~/arm-labor/a64/a009/ —
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/a009
cd ~/arm-labor/a64/a009A program két értéket összead, majd az eredményt Linux-kilépési
állapotként adja vissza. Nem ír szöveget a terminálra. Mentsük
lab-tools.s néven:
// A64 ISA / AArch64, ARMv8-A, little-endian.
// GNU assembler-direktívák, Linux felhasználói program.
.arch armv8-a
.text
.global _start
.type _start, %function
_start:
mov x0, #0
mov x1, #7
add x0, x0, x1
mov x8, #93
svc #0
.size _start, . - _start
.section .note.GNU-stack,"",%progbitsMost nem a teljes regiszterkészletet tanuljuk meg; azt Az A64 regiszterkészlet áttekintése tárgyalja. A megfigyeléshez ennyi szükséges:
X0 és X1 itt 64 bites egész
értékeket tartalmaz;ADD X0, X0, X1 az összeget X0-ba
írja;X8 = 93 az exit rendszerhívást választja,
amelynek argumentuma X0;SVC #0 átadja a vezérlést a
rendszerhívás-kezelésnek.Az utolsó két pont együtt Linux-interfészt használ: a 93
jelentése nem az A64 ISA önálló tulajdonsága.
Konstanskódolás: a példabeli A64 MOV
alakok egyetlen MOVZ utasítással kódolhatók. Ennek
konstansmezője 16 bites, és a 64 bites változatban 0, 16, 32 vagy 48
bittel eltolva helyezhető el; itt minden eltolás 0. Ez nem jelenti azt,
hogy a MOV bármely 64 bites konstanst egy utasítással
betölthet. Az A64 SVC immediate-mezője szintén 16 bites;
itt értéke 0.
Cél: qemu-user, AArch64 Linux. Összeszerelés és linkelés:
aarch64-linux-gnu-as -EL -g -o lab-tools.o lab-tools.s
aarch64-linux-gnu-ld -EL -static -e _start -o lab-tools lab-tools.oA -EL little-endian kimenetet kér. Az assembler
-g kapcsolója hibakeresési információt (debug
information) készít, amely többek között a gépi kód és a
forrássorok összerendelését támogatja. A linker ebben a parancsban nem
távolítja el ezt.
Cél: qemu-user. Első futtatás és ellenőrzés:
qemu-aarch64 ./lab-tools
lab_status=$?
printf 'Kilépési állapot: %s\n' "$lab_status"Várt, levezetett eredmény:
Kilépési állapot: 7. Ez szándékosan nem nulla; a program
most adatot szemléltet a kilépési állapottal. A parancsokat ne olyan
shell-munkamenetben futtassuk, amely set -e miatt minden
nem nulla állapotnál azonnal kilép.
A példák itt nem kerültek futtatásra; a következő állapotok is vártak, nem mért laboreredmények.
readelfElőször végrehajtás nélkül ellenőrizzük, mit építettünk.
Célbináris: qemu-user alatt futtatandó AArch64 ELF; hostoldali vizsgálat:
aarch64-linux-gnu-readelf -h lab-tools
aarch64-linux-gnu-readelf -SW lab-tools
aarch64-linux-gnu-readelf -sW lab-toolsA -h az ELF-fejlécet, a -S a szekciótáblát,
a -s a szimbólumtáblákat mutatja. A -W széles
kimenetet kér, csökkentve a sorok tördelését.
A fejlécben ezeket keressük:
| Mező | Várt jelentés |
|---|---|
Class |
ELF64: 64 bites ELF-formátum |
Data |
Little-endian bájtsorrend |
Type |
EXEC: az itt használt linkelés futtatható
állománya |
Machine |
AArch64: célarchitektúra, nem a host
architektúrája |
Entry point address |
A _start belépési címének megfelelő érték |
Nem rögzítünk számszerű betöltési címet: azt a linker alakítja ki, és a fájlból olvassuk ki. A hibakeresésben szimbolikus címeket használunk.
A szekció (section) az objektumfájl vagy ELF tartalmának névvel és tulajdonságokkal leírt egysége. A példában különösen ezek érdekesek:
| Szekció | Szerep |
|---|---|
.text |
A64 gépi utasítások |
.symtab |
Szimbólumbejegyzések, például _start |
.strtab |
A hozzájuk tartozó nevek karakterláncai |
.debug_* |
Hibakeresési adatok |
.shstrtab |
Szekciónevek karakterláncai |
A pontos szekciólista és sorrend eszközláncfüggő. Nem minden szekció kerül a futó program memóriájába. A szekciókat a betöltő által használt szegmensekkel se azonosítsuk; ezt Az ELF formátum alapjai és a program életciklusa részletezi.
A szimbólumtábla _start sorában a Value a
végleges ELF-beli cím, az Ndx a hozzátartozó szekcióra
utal. A példában a .type és .size direktívák
miatt függvényszimbólumot és 20 bájtos méretet várunk.
Fontos különbség: a szimbólumtábla nem ugyanaz, mint a forrássoros hibakeresési információ. Egy név akkor is megmaradhat, ha a forrássorok leírását eltávolítottuk.
objdumpCélbináris: AArch64; hostoldali disassembly és szimbólumvizsgálat:
aarch64-linux-gnu-objdump -d lab-tools
aarch64-linux-gnu-objdump -t lab-toolsA -d a kódszekciók disassemblyjét, a -t a
szimbólumtáblát jeleníti meg. Az architektúra-előtagos eszköz használata
elkerüli, hogy a host alapértelmezett objdump programjának
céltámogatására hagyatkozzunk.
A disassembly egy sorában jellemzően cím, gépi kód, mnemonic és operandusok találhatók. Az utasítások elvárt szerkezete:
_start-hoz viszonyított
bájteltolás |
A64 utasítás | Hatás |
|---|---|---|
0x00 |
mov x0, #0 |
X0 = 0 |
0x04 |
mov x1, #7 |
X1 = 7 |
0x08 |
add x0, x0, x1 |
X0 = 7 |
0x0c |
mov x8, #93 |
X8 = 93 |
0x10 |
svc #0 |
Linux-rendszerhívás |
Levezetés: az A64 utasítások hossza 4 bájt, így öt utasítás bájtot foglal. Ez kódméret, nem futási idő vagy ciklusszám.
Az objdump a konstansokat más számrendszerben, illetve
bizonyos utasításokat más aliasnéven is megjelenítheti. Például
0x5d és 93 ugyanaz az érték. Az ellenőrzés a jelentésre,
nem a forrás karakterhű visszaadására irányul.
A gdb-multiarch több célarchitektúrát ismerő, hoston
futó hibakereső. A QEMU GDB-kiszolgálója (GDB
stub) fogadja a parancsait, és a vendégprogram állapotát teszi
elérhetővé. A GDB által mutatott regiszterek tehát az emulált AArch64
programhoz tartoznak, nem az x86_64 hosthoz.
A terminál, cél: qemu-user, távoli hibakeresésre váró program:
qemu-aarch64 -g 1234 ./lab-tools
lab_status=$?
printf 'Kilépési állapot: %s\n' "$lab_status"A -g 1234 a megadott TCP-porton vár GDB-kapcsolatra.
Amíg nem kapcsolódunk és nem folytatjuk a programot, a látszólagos
tétlenség helyes működés.
A GDB-kapcsolat nem hitelesített szolgáltatás. A labort elkülönített
vagy megfelelően tűzfalazott hoston használjuk, a portot ne tegyük
nyilvánosan elérhetővé. A kliensoldali localhost önmagában
nem bizonyítja, hogy a QEMU csak helyi kapcsolatokra figyel.
B terminál, cél: ugyanaz a qemu-user folyamat:
gdb-multiarch -q ./lab-tools \
-ex 'set architecture aarch64' \
-ex 'set pagination off' \
-ex 'target remote localhost:1234' \
-ex 'break *_start+8' \
-ex 'continue'A parancsok jelentése:
./lab-tools a GDB számára is betölti a fájlt és annak
szimbólumait.set architecture aarch64 explicit megadja a
célarchitektúrát; az ELF alapján ez általában automatikusan is
felismerhető.target remote kapcsolódik a QEMU-hoz.break *_start+8 pontos utasításcímre helyez
töréspontot (breakpoint).continue elindítja a végrehajtást a töréspontig.A * itt cím szerinti töréspontot jelent. A 8 bájtos
eltolást az előbbi disassemblyből vezettük le: ott kezdődik az A64
ADD.
A megállás az utasítás végrehajtása előtt történik.
Így ekkor az A64 X0 = 0, X1 = 7; az összeadás
még hátravan.
A GDB parancssorába sorban írjuk:
| GDB-parancs | Megfigyelés vagy hatás |
|---|---|
x/i $pc |
A következő A64 utasítás megjelenítése |
info registers x0 x1 x8 pc |
A kiválasztott AArch64 regiszterek állapota |
stepi |
Egy gépi utasítás végrehajtása |
info registers x0 x1 pc |
Az összeadás utáni állapot |
stepi |
Az A64 MOV X8, #93 végrehajtása |
info registers x8 |
Várt érték: 93, azaz 0x5d |
continue |
A rendszerhívás végrehajtása, programbefejezés |
quit |
Kilépés a GDB-ből |
Az első stepi után X0 = 7, a
programszámláló pedig _start+12. Az A terminálban a program
befejezése után ismét 7-es kilépési állapotot várunk.
A step és a stepi nem felcserélhető:
step: forrásszintű léptetés, a hibakeresési információ
alapján;stepi: egyetlen gépi utasítás léptetése.Ebben az egyutasításos forrássorokból álló programban hasonló
eredményt adhatnak, de ez nem általános garancia. Assemblyállapot
vizsgálatakor kezdetben a stepi legyen az alapértelmezett.
Az info registers argumentumok nélkül több regisztert
mutat; kezdőként célszerű a vizsgált adatfolyamra szűkíteni.
A hasznos munkamódszer: tünet → hipotézis → adatgyűjtés → egy változtatás → új ellenőrzés.
| Tünet | Hipotézis és adatgyűjtés | Egy változtatás |
|---|---|---|
| A GDB nem kapcsolódik | Fut-e a QEMU -g 1234 kapcsolóval? Egyezik-e a
port? |
Indítsuk újra a QEMU-t a megadott paranccsal |
Address already in use |
Más folyamat használja a portot | Válasszunk másik portot mindkét oldalon |
| Váratlan regiszterérték | Nem a gondolt utasításnál állunk | Ellenőrizzük az x/i $pc kimenetét |
| Nem látszik a forrás | Hiányzik a -g, vagy nem érhető el a forrásfájl |
Építsük újra -g mellett, majd nyissuk újra a
binárist |
| Eltérő kód látszik a GDB-ben | Másik vagy régebbi ELF-et nyitottunk meg | Mindkét terminálban ugyanazt az újraépített fájlt használjuk |
Az A64 X8 értékét a program csak később állítja be. A
töréspontnál látott korábbi értékére nem szabad következtetést vagy
programlogikát alapozni. Hasonlóan: a 7-es kilépési állapot ebben a
laborban nem önmagában hibajel.
Magyarázó (alapszint — cél:
megértés-ellenőrzés): A host x86_64, de a
readelf -h AArch64 géptípust ír ki. Miért
helyes ez? Miért vizsgálható a fájl a QEMU elindítása nélkül?
Implementációs (alapszint — cél: rutinépítés):
Módosítsd a második A64 MOV konstansát 7-ről 11-re. Építsd
újra a programot, majd ugyanazon töréspontnál és egy stepi
után ellenőrizd X0 és X1 értékét. Mi lesz a
kilépési állapot?
Hibakeresési (középszint — cél: rutinépítés):
Készíts az eredeti ELF-ből hibakeresési információ nélküli másolatot.
Vizsgáld meg, eltűnt-e a _start szimbólum és megváltozott-e
a gépi kód. Indokold, miért maradhat használható az utasításszintű
hibakeresés.
1. megoldás — Segítség. Gondold át, mit
értelmez a readelf, és mit hajt végre a
QEMU — a kettő nem ugyanaz a művelet.
1. megoldás — Teljes megoldás. Az ELF-fejléc a
célarchitektúrát írja le. A readelf
fájlstruktúrát értelmez, nem vendégutasításokat hajt végre. Ezért x86_64
hoston is megvizsgálhat AArch64 ELF-et.
2. megoldás — Segítség. A 16 bites immediate-mező elég tágas 11-hez is; nincs szükség kódolásváltásra, csak a konstans cseréjére és újrafordításra.
2. megoldás — Teljes megoldás. A módosítás
mov x1, #11; a konstans továbbra is belefér a használt 16
bites mezőbe. Az eredeti assemble-link-run parancsokkal újraépítve a
töréspontnál X0 = 0, X1 = 11. Egy
stepi után mindkettő 11. A várt kilépési állapot 11. Ha
továbbra is 7 látszik, elsőként a mentést és az újraépítést
ellenőrizzük, ne a QEMU aritmetikáját gyanúsítsuk.
3. megoldás — Segítség. A --strip-debug
célzottan a .debug_* szekciókat távolítja el, nem a
szimbólumtáblát vagy a kódot — gondold át, melyik GDB-szolgáltatás épül
kifejezetten a .debug_* adatra, és melyik nem.
3. megoldás — Teljes megoldás. Cél: qemu-user AArch64; a már összeszerelt és linkelt ELF kontrollált másolatának elkészítése, vizsgálata és futtatása:
aarch64-linux-gnu-objcopy --strip-debug lab-tools lab-nodebug
aarch64-linux-gnu-readelf -SW lab-nodebug
aarch64-linux-gnu-readelf -sW lab-nodebug
aarch64-linux-gnu-objdump -d lab-nodebug
qemu-aarch64 ./lab-nodebug
lab_status=$?
printf 'Kilépési állapot: %s\n' "$lab_status"A várt különbség a .debug_* információk eltűnése. A
globális _start szimbólum és a programutasítások
megmaradnak; az eredeti változat kilépési állapota továbbra is 7. A
--strip-debug nem azonos minden szimbólum eltávolításával.
A GDB továbbra is használhat utasításcímeket, regisztereket és
stepi léptetést, de a forrássoros szolgáltatások
korlátozottak lesznek.
A hibakeresés már az első assemblylaborban összekapcsolja a forrást,
a binárist és a futás közbeni állapotot. A readelf a
célarchitektúrát, szekciókat és szimbólumokat ellenőrzi; az
objdump a tényleges gépi utasításokat mutatja. A
gdb-multiarch a QEMU kiszolgálójához kapcsolódva megállítja
és lépteti az AArch64 programot. A töréspont az érintett utasítás
végrehajtása előtt állít meg, ezért a regiszterértékeket mindig a
programszámlálóval együtt értelmezzük. A forrásszintű step
és az utasításszintű stepi különbsége később optimalizált
kódnál is fontos. Minden vizsgálat előfeltétele az azonos, frissen
épített bináris használata.
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.
readelf, objdump és
objcopy. Ellenőrizve: ugyanaz a 2.47-es
kiadás.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 fennmaradnak: a parancsok tényleges futtatása, az Ubuntu 24.04 telepített csomagverzióinak rögzítése, a pontos szekciólista és diagnosztikai üzenetek ellenőrzése a hallgatói labor feladata. A QEMU GDB-portjának tényleges hálózati kötését szintén a helyi verzió és környezet alapján kell ellenőrizni. A programállapotokra megadott értékek levezetettek; teljesítménymérést ez a labor nem végez.