Egy assemblyfüggvény helyesnek látszik, mégis hibás eredményt ad vissza a C-hívónak. A forrás átolvasása nem dönti el, hogy rossz adatot töltöttünk-e be, túl keskeny regiszternézetet használtunk-e, vagy a visszatéréskor sérült meg a veremállapot. Ehhez a ténylegesen végrehajtott utasítást, a regisztereket és a memóriát együtt kell megfigyelni.
Az utasításszintű hibakeresés (instruction-level
debugging) során a programot gépi utasítások határán állítjuk meg,
és a végrehajtási állapot változását vizsgáljuk. A GDB
programszámlálója, a PC ilyenkor rendszerint a
következőként végrehajtandó utasításra mutat. A
töréspontnál látható állapot tehát még nem tartalmazza az ott álló
utasítás hatását.
Az alfejezet a Szerkesztő és debug-eszközök: GDB,
objdump, readelf első look, valamint a Prológus
és epilógus: regisztermentés és keretkezelés ismereteire épül.
Minden laborpélda A64 ISA, ARMv8-A, little-endian Linux
ELF, qemu-aarch64 user-mode céllal. A GDB
parancsai nem ISA-utasítások; a példákban az A64 célállapotot
vizsgálják.
A GDB két különböző léptetési szintet kínál:
| GDB-parancs | Megállási egység | Függvényhívás kezelése |
|---|---|---|
step |
Következő forráshely, a hibakeresési információ alapján | Megpróbál belépni a hívott függvénybe |
next |
Következő forráshely ugyanabban a veremkeretben | Átlépi a hívást |
stepi, röviden si |
Egy gépi utasítás | Belép a hívásba |
nexti, röviden ni |
Egy gépi utasítás, hívásnál annak visszatérési helye | Átlépi a hívást |
A64 célon, egy BL utasításon állva a stepi
végrehajtja a hívást: beállítja az X30 visszatérési címet,
majd a hívott függvény elején áll meg. A nexti ehelyett
normál visszatérés esetén a BL utáni utasításnál áll
meg.
Az „átlépés” nem azt jelenti, hogy a függvény kimarad. Minden mellékhatása megtörténik. Egy közben elért töréspont vagy jelzés azonban korábban is megszakíthatja a futást.
A stepi 5 öt gépi utasítást hajt végre. Ezzel szemben a
nexti 5 öt léptetési műveletet jelent: az átlépett
függvények miatt ténylegesen jóval több utasítás is lefuthat.
Assemblyforrásban gyakran egy sorhoz egy utasítás tartozik, de ez nem teszi azonossá a két léptetési szintet. A direktívák nem végrehajtható utasítások, optimalizált C-kódnál pedig különösen összetett lehet a forrássorok és az utasítások kapcsolata.
A következő GDB-parancsok A64 célon értendők:
| Parancs | Jelentés |
|---|---|
info registers |
Az általános regiszterállapot megjelenítése |
info registers x0 x1 sp pc |
Kiválasztott regiszterek megjelenítése |
p/x $x0 |
Az X0 értéke hexadecimálisan |
p/u $x1 |
Az X1 értéke előjel nélküli decimális olvasatban |
x/i $pc |
A következő utasítás megjelenítése |
Az info registers után a regiszternevek elé nem kell
$; GDB-kifejezésben viszont kell. A p a
print rövidítése.
A64-en az X0 64 bites, a W0 ugyanannak a
regiszternek a 32 bites nézete. A GDB által közvetlenül elérhető
alternatív regiszternevek célleírás-függők. Az alsó 32 bitet biztosan
megvizsgálhatjuk a p/x ($x0 & 0xffffffff) kifejezéssel.
Ez csak olvasás; nem modellezi a W0-ba írás felső 32 bitet
törlő hatását.
x parancs
felépítéseA memóriavizsgálat (memory examination) alapformája:
x/Nfu cím
N: a megjelenítendő egységek száma;f: a megjelenítési formátum, például x
hexadecimális vagy u előjel nélküli decimális;u: az egység mérete, például b egy,
h kettő, w négy, g nyolc
bájt.A GDB w jelölése tehát négy bájt,
AArch64 célon is.
| GDB-parancs, A64 cél | Megjelenített tartomány |
|---|---|
x/4xw $x0 |
Négy, egyenként 4 bájtos érték hexadecimálisan |
x/2gx $x0 |
Két, egyenként 8 bájtos érték hexadecimálisan |
x/16xb $x0 |
Tizenhat különálló bájt |
x/2gx $sp |
Két 8 bájtos egység az aktuális verem tetejétől |
A formátum- és méretbetű sorrendje felcserélhető: az
x/2gx és az x/2xg ugyanazt jelenti. A GDB
megjegyez korábbi megjelenítési beállításokat, ezért reprodukálható
vizsgálatban célszerű mindent explicit megadni.
Munkakönyvtár: ~/arm-labor/a64/a040/ —
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/a040
cd ~/arm-labor/a64/a040A laborfüggvény két 64 bites, előjel nélküli elemet és egy eltolási
értéket ad össze. C-szemantikával a művelet
p[0] + p[1] + bias, uint64_t operandusokkal.
Az A64 bemenet az X0 mutató és az X1 eltolás;
a visszatérési érték az X0.
Mentsük a következőt lab-gdb.s néven:
// A64 ISA, ARMv8-A, little-endian Linux.
// A direktívák GNU as / AArch64 ELF környezethez tartoznak.
.arch armv8-a
.text
.p2align 2
.global _start, lab_call, lab_after, lab_stored
.type _start, %function
_start:
adrp x0, lab_values
add x0, x0, :lo12:lab_values
mov w1, #7
lab_call:
bl lab_sum
lab_after:
adrp x2, lab_result
add x2, x2, :lo12:lab_result
str x0, [x2]
lab_stored:
mov w0, #0
mov w8, #93
svc #0
.size _start, .-_start
.global lab_sum
.type lab_sum, %function
lab_sum:
.cfi_startproc
stp x29, x30, [sp, #-16]!
.cfi_def_cfa_offset 16
.cfi_offset 29, -16
.cfi_offset 30, -8
mov x29, sp
ldr x2, [x0]
ldr x3, [x0, #8]
add x0, x2, x3
add x0, x0, x1
ldp x29, x30, [sp], #16
.cfi_restore 29
.cfi_restore 30
.cfi_def_cfa_offset 0
ret
.cfi_endproc
.size lab_sum, .-lab_sum
.data
.balign 8
lab_values:
.xword 0x0000000200000001
.xword 0x0000000400000003
lab_result:
.xword 0
.section .note.GNU-stack,"",%progbitsA függvény levélfüggvényként nem igényelné ezt a veremkeretet; itt
szándékosan tartjuk meg, hogy vizsgálható legyen a mentés és
visszaállítás. A kilépés Linux/AArch64 rendszerhívás-konvenció: az
X8 értéke 93, az X0 a kilépési állapot. Ez nem
önmagában az A64 ISA szolgáltatása.
Immediate-ok: a példa minden konstansa és eltolása
kódolható (lásd az A64 immediate-korlátok
összefoglalóját). Az ADRP + ADD …, :lo12:
pár együtt állítja elő a szimbólum teljes címét.
Ubuntu 24.04 x86_64 hoston, A64 qemu-user célra:
# Target: qemu-user / AArch64 Linux, little-endian.
aarch64-linux-gnu-as -g -EL -march=armv8-a \
-o lab-gdb.o lab-gdb.s
aarch64-linux-gnu-ld -EL -e _start \
-o lab-gdb.elf lab-gdb.o
# Első terminál: várakozás a GDB kapcsolódására.
qemu-aarch64 -g 12345 ./lab-gdb.elf
printf 'QEMU kilepesi allapot: %d\n' "$?"A -g után megadott szám TCP-port, nem memóriaérték. A
várakozás alatt a program látszólag nem csinál semmit: ez most helyes
működés. A hibakereső portot csak megbízható, elkülönített
laborhálózatban használjuk.
A második terminálban készítsünk GDB-parancsfájlt. A következő
bash blokk csak létrehozza azt és elindítja a hibakeresőt;
a GDB határolók közötti sorok
GDB-parancsok.
# Target: a fenti qemu-user / AArch64 folyamat.
cat > lab-step.gdb <<'GDB'
set pagination off
target remote localhost:12345
break *lab_call
continue
x/i $pc
info registers x0 x1 sp pc
x/4xw $x0
x/2gx $x0
set $entry_sp = $sp
set $entry_fp = $x29
stepi
info registers x30 sp pc
x/i $pc
stepi
x/2gx $sp
p $sp == $entry_sp - 16
stepi 5
p/x $x0
x/i $pc
stepi
p $sp == $entry_sp
p $x29 == $entry_fp
stepi
x/i $pc
break *lab_stored
continue
x/1gx &lab_result
continue
quit
GDB
gdb-multiarch -q ./lab-gdb.elf -x lab-step.gdbKézi gyakorláshoz a GDB-t a -x lab-step.gdb rész nélkül
indítsuk, majd a fájl parancsait egyenként adjuk ki.
A következő állapotok levezetett, várt eredmények, nem itt végrehajtott mérésből származnak.
A break *lab_call a BL végrehajtása előtt
állít meg. Ekkor az A64 X0 a tömb címét, az X1
a 7 értéket tartalmazza. Nem kell előre ismernünk a numerikus címet: a
GDB az ELF szimbólumából határozza meg.
A két memórianézet ugyanazt a 16 bájtot mutatja:
| Nézet | Várt értékek, növekvő címsorrendben |
|---|---|
x/4xw $x0 |
0x00000001, 0x00000002,
0x00000003, 0x00000004 |
x/2gx $x0 |
0x0000000200000001,
0x0000000400000003 |
Little-endian elrendezésben egy 64 bites érték alsó 32 bitje található az alacsonyabb címen. A két megjelenítés tehát nem eltérő adatot, hanem eltérő csoportosítást jelent.
Az első stepi végrehajtja az A64 BL
utasítást. A PC a lab_sum elejére, az
X30 a lab_after címére kerül. A következő
stepi végrehajtja az A64 STP utasítást:
| Hely vagy feltétel | Várt tartalom/eredmény |
|---|---|
SP |
Belépési SP − 16 |
[SP], 8 bájt |
A belépési X29 értéke |
[SP+8], 8 bájt |
A lab_after visszatérési címe |
p $sp == $entry_sp - 16 |
Igaz, jellemzően 1 alakban |
A $entry_sp és $entry_fp GDB-segédváltozók
(convenience variables), nem célprocesszorbeli regiszterek.
Létrehozásuk nem módosítja a programot.
Az öt további lépés a keretmutató beállítását, két betöltést és két összeadást hajt végre. A várt eredmény:
0x0000000200000001 + 0x0000000400000003 + 7 = 0x000000060000000b
Ekkor a következő A64 utasítás még az LDP. Annak
végrehajtása után mindkét állapot-összehasonlítás igaz. Az A64
RET után a PC a lab_after helyre
mutat.
A lab_stored töréspontnál az eredménytárolás már
megtörtént, ezért a lab_result várt tartalma
0x000000060000000b. A program nem ír szöveget; normál, 0
állapotú kilépést várunk.
nexti
használatávalIndítsuk újra az első terminál QEMU-parancsát, majd kapcsolódjunk új GDB-munkamenettel:
# Target: újraindított qemu-user / AArch64 folyamat.
gdb-multiarch -q ./lab-gdb.elf \
-ex 'target remote localhost:12345' \
-ex 'break *lab_call' \
-ex 'continue' \
-ex 'nexti' \
-ex 'info registers x0 pc' \
-ex 'continue' \
-ex 'quit'A nexti után közvetlenül a lab_after helyen
kell állnunk, az A64 X0 regiszterben ugyanazzal az
összeggel. A függvény belső állapotai ezúttal nem láthatók.
A break lab_sum szimbólumhoz kapcsolódó töréspontot kér.
Függvényszimbólumnál a GDB a rendelkezésre álló információ alapján a
prológus utáni helyet is választhatja. A break *lab_sum
ezzel szemben kifejezetten a függvény kezdőcímét jelöli ki.
A * azt jelzi, hogy a megadott kifejezést
utasításcímként értelmezzük. Így címre törhetünk beégetett numerikus cím
nélkül is, például break *$pc használatával. A
töréspontokat az info breakpoints listázza; a
delete N egy adott sorszámút töröl.
Mindig a futó programhoz tartozó ELF-et nyissuk meg. Újrafordítás után a korábban feljegyzett numerikus címek és a régi GDB-munkamenet szimbólumai elavulhatnak.
A jelenlegi user-mode emulációban egy
Linux-folyamatot vizsgálunk. A qemu-aarch64 -g 12345 a
program indulása előtt várja a kapcsolatot. A megnyitott ELF helyi
példánya szolgáltatja a szimbólumokat; a target remote
létrehozza a kapcsolatot, a continue elindítja a már
betöltött célt. Ebben a munkafolyamatban nem kell GDB load,
és nem a run indítja a célt.
A Cortex-M futási modelljében,
Thumb-2/Armv7-M kontextusban, a
qemu-system-arm teljes gépet emulál. A későbbi
mps2-an385 laborban a -S az induló processzort
állítja meg, a -gdb tcp::12345 pedig a hibakereső
kiszolgálót engedélyezi. A kiszolgáló engedélyezése önmagában nem
helyettesíti a -S kapcsolót.
Ott a firmware, a resetállapot és a gép memóriatérképe is a vizsgálat része lesz. Ez most csak működésbeli előzetes, nem Cortex-M firmware-indítási recept.
| Tünet | Hipotézis | Adatgyűjtés és egyetlen következő változtatás |
|---|---|---|
| Nem jön létre a kapcsolat | Eltérő port vagy nem várakozó QEMU | Ellenőrizzük a két portszámot; indítsuk újra csak a QEMU-folyamatot |
| Váratlan utasításnál állunk | Régi ELF vagy forrásszintű léptetés | Nézzük meg az x/i $pc eredményét; váltsunk
stepi használatára |
| A 64 bites adat „szétesett” | Négybájtos nézetet használunk | Ugyanazon címen váltsunk x/4xw alakról
x/2gx alakra |
| A számítás jó, de a visszatérés hibás | Sérült mentés vagy epilógus | Vizsgáljuk a mentett A64 X30 értéket és az
SP visszaállását |
| „Cannot access memory” | Nem mutatót vagy már megváltozott regisztert vizsgálunk | Ellenőrizzük a PC-t és a regiszter szerepét; ne
próbálgassunk véletlen címeket |
Különösen fontos: a függvény visszatérése után az A64 X0
már eredmény, nem a bemeneti mutató. Ekkor a tömb
vizsgálatához az x/2gx &lab_values parancsot
használjuk.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Miért különbözik az x/4xw és
az x/2gx kimenete, miközben mindkettő 16 bájtot olvas? Mit
látunk, ha az A64 STR utasítás címére teszünk töréspontot:
a tárolás előtti vagy utáni állapotot?
(implementációs, alapszint — cél: rutinépítés)
Módosítsuk az eltolást 7-ről 8-ra. Építsük újra a programot, indítsunk
új QEMU/GDB-munkamenetet, és nexti segítségével
ellenőrizzük a visszatérési értéket, majd a tárolt eredményt.
(hibakeresési, középszint — cél: rutinépítés)
Cseréljük az A64 ldr x3, [x0, #8] utasítást
ldr w3, [x0, #8] alakra, az eltolást pedig állítsuk vissza
7-re. Hol jelenik meg először az eltérés? Törjünk a lab_sum
első utasítására, lépjünk négyet, és hasonlítsuk össze az
X3 értékét a memória 8 bájtos nézetével.
1. megoldás — Segítség. Ugyanaz a 16 bájt két KÜLÖNBÖZŐ csoportosításban jelenik meg a két parancsnál — nem az adat változik. A töréspont-szemantikára gondolj: a GDB mindig a KÖVETKEZŐ, még végre nem hajtott utasításnál áll meg.
1. megoldás — Teljes megoldás. Az x
parancs eltérő méretű egységekre bontja ugyanazt a bájtsorozatot.
Little-endian esetben az alacsonyabb címen a 64 bites szám alsó része
található. Az A64 STR címére helyezett töréspont a
tárolás előtt állít meg; egy stepi után
vizsgálható az új memóriaérték.
2. megoldás — Segítség. Az A64
mov w1, #8 ugyanabba a 16 bites immediate-mezőbe fér, mint
a #7; csak a végeredmény változik — számold ki fejben az új
összeget az eredeti példa alapján.
2. megoldás — Teljes megoldás. A módosítás A64
alakja mov w1, #8. A várt eredmény
0x000000060000000c. A nexti után ezt az
X0, a lab_stored helyen pedig a
lab_result tartalmazza. Az új munkamenet biztosítja, hogy a
betöltött program és a szimbólumok ugyanahhoz az új buildhez
tartozzanak.
3. megoldás — Segítség. A W3-ba írás
csak 4 bájtot olvas, és nullázza X3 felső felét — a memória
második 64 bites eleme ettől még a helyén marad, csak X3
nem fogja tükrözni.
3. megoldás — Teljes megoldás. A hibás betöltés csak
négy bájtot olvas. Az A64 W3-ba írás nullázza az
X3 felső felét, ezért az X3 értéke
0x0000000000000003, miközben a memória második 64 bites
eleme változatlan. A hibás végeredmény 0x000000020000000b.
Négy lépés után már látszik az eltérés; a következő utasítás az első
összeadás. A javítás kizárólag a 64 bites betöltés visszaállítása.
A GDB-ben a megfigyelt értéket mindig a következő utasítással együtt
értelmezzük. A stepi belép a függvényhívásba, a
nexti rendszerint annak visszatérése után áll meg, de
mindkettő végrehajtja a program megfelelő részét. A regisztervizsgálat
megmutatja a számítás állapotát, az x parancs pedig
explicit méretben és formátumban jeleníti meg a memóriát. Ugyanaz a
bájtsorozat többféleképpen csoportosítható; ettől az adat nem változik.
A reprodukálható vizsgálathoz azonos ELF, szimbolikusan meghatározott
helyek és előre rögzített ellenőrzési feltételek szükségesek.
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).Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is ténylegesen fennmaradnak: a kódot és parancsokat itt nem futtattuk. A várt állapotok az utasítások szemantikájából levezetettek. Az Ubuntu 24.04-ben telepített konkrét eszközverziókkal ellenőrizendő a távoli kapcsolat, a GDB regiszterneveinek elérhetősége és a megállási üzenetek pontos formája. A labor nem tartalmaz időmérést vagy hardverhű ciklusszámra vonatkozó állítást.