3.4 Szerkesztő és debug-eszközök: GDB, objdump, readelf első look

3.4.1 Miért kezdjük rögtön megfigyeléssel?

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.

3.4.2 Szerkesztő és laborfájlok

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:

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.

3.4.3 Kidolgozott példa: egy megfigyelhető összeadás

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

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

Most nem a teljes regiszterkészletet tanuljuk meg; azt Az A64 regiszterkészlet áttekintése tárgyalja. A megfigyeléshez ennyi szükséges:

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

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

3.4.4 A fájl vizsgálata: readelf

Elő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-tools

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

3.4.5 A gépi kód első olvasása: objdump

Célbináris: AArch64; hostoldali disassembly és szimbólumvizsgálat:

aarch64-linux-gnu-objdump -d lab-tools
aarch64-linux-gnu-objdump -t lab-tools

A -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 5⋅4=205 \cdot 4 = 20 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.

3.4.6 Élő megfigyelés: GDB kapcsolódása a QEMU-hoz

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:

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

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.

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

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.

3.4.8 Feladatok

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

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

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

3.4.9 Megoldások

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.

3.4.10 Rövid összefoglaló

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.

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