Egy C-program hibájának vizsgálatakor gyakori kérdés: valóban azt a függvényt linkeltük be, amelyet javítottunk? Máskor azt kell eldönteni, hogy egy hívás saját kódra, könyvtári függvényre vagy köztes ugrókódra mutat-e. Ehhez nem feltétlenül kell elindítani a programot.
A visszafejtett utasításlista (disassembly) a gépi kód utasításként értelmezett, szöveges megjelenítése. Nem állítja vissza az eredeti C-forrást: az utasításokat, a hozzájuk tartozó címeket és a rendelkezésre álló szimbólumneveket mutatja meg.
A GDB assembly szinten alfejezetben futás közbeni állapotot vizsgáltunk. Most a fájlban tárolt kódot és metaadatokat olvassuk:
objdump elsősorban az utasítások és kapcsolódó
információk vizsgálatára szolgál;readelf az ELF szerkezetét, többek között
szimbólumait és relokációit jeleníti meg.Hatókör: I. rész, AArch64, A64 ISA, ARMv8-A alapprofil, little-endian Linux ELF. Az alábbi assembly utasítások és regiszterek kizárólag ebben a környezetben értendők. A GNU assembler direktívái az AArch64-célú objektum előállítását vezérlik; önmagukban nem processzorutasítások.
Egy objdump-kimenet olvasásakor különítsük el a
következőket.
| Réteg | Mit tárol vagy jelenít meg? | Tipikus eszközkapcsoló |
|---|---|---|
| Gépi kód | A processzor által végrehajtható utasítások kódolását | objdump -d |
| Szimbólumtábla | Neveket, értékeket, méreteket, típusokat és kötési tulajdonságokat | objdump -t, readelf -s |
| Relokáció | Utasítást a linkernek vagy betöltőnek egy helyfüggő érték javítására | objdump -r, readelf -r |
A szimbólumtábla (symbol table) nem maga a programkód. Egy függvénynév eltávolítható úgy, hogy a függvény utasításai változatlanul megmaradjanak.
A relokációs bejegyzés (relocation entry) azt írja le, hogy melyik helyen, milyen szabály szerint kell egy szimbólumhoz kapcsolódó értéket kialakítani. Ez különösen a még nem linkelt objektumfájlokban fontos: ott egy külső függvény végleges címe általában még ismeretlen.
Az objdump -d megjelenítésében rendszerint szerepel az
utasítás címe, gépi kódolása, mnemonicja és operandusai. A mellettük
látható szimbólumnév az eszköz annotációja: a processzor nem
függvényneveket hajt végre.
Munkakönyvtár: ~/arm-labor/a64/a041/ —
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/a041
cd ~/arm-labor/a64/a041A mérnöki feladat egy kis assembly függvény ellenőrzése C-hívóprogramból. A szerződés:
A tartománykorlát miatt a matematikai eredmény előjeles 32 biten ábrázolható. Nem kell összemosnunk az assembly moduloaritmetikáját a C előjeles túlcsordulásának szabályaival.
A lab-adjust.S teljes tartalma:
// A64 ISA, AArch64, ARMv8-A; GNU as szintaxis.
.arch armv8-a
.text
.p2align 2
.global lab_adjust
.type lab_adjust, %function
lab_adjust:
add W1, W0, W0, lsl #1
add W1, W1, #5
cmp W0, #0
csel W0, W1, WZR, ge
ret
.size lab_adjust, .-lab_adjust
.section .note.GNU-stack,"",%progbitsAz A64 ADD első előfordulása eltolt regiszteres alak: a
32 bites változat LSL eltolása 0–31 lehet. A második
ADD 12 bites, előjel nélküli immediate-et fogad, opcionális
12 bites balra tolással; az 5 közvetlenül kódolható. A CMP
itt a SUBS WZR, W0, #0 aliasa, ugyanilyen
immediate-korláttal.
Az A64 CSEL a GE, vagyis előjeles „nagyobb
vagy egyenlő” feltételt használja: az NZCV állapotból
esetén választja az első forrását. A CMP az eredeti
bemenetet vizsgálja, mert az első két utasítás csak W1
értékét módosította.
A lab-driver.c:
/* C-hívóprogram; cél: AArch64 Linux, little-endian. */
#include <stdio.h>
extern int lab_adjust(int x);
int main(void)
{
int positive = lab_adjust(7);
int negative = lab_adjust(-7);
printf("pos=%d neg=%d\n", positive, negative);
return (positive == 26 && negative == 0) ? 0 : 1;
}A következő parancsok az Ubuntu 24.04 x86_64 host cross-eszközláncát használják. A szükséges csomagokat a Labor-alapkörnyezet: eszközlánc telepítése és verifikáció szerint telepítettnek tekintjük.
# Target: AArch64 Linux, qemu-user; little-endian.
# 1. Assembly és C forrásból külön objektumok.
aarch64-linux-gnu-gcc -march=armv8-a -g \
-c lab-adjust.S -o lab-adjust.o
aarch64-linux-gnu-gcc -march=armv8-a -O0 -g \
-fno-pie -fno-builtin -fplt \
-c lab-driver.c -o lab-driver.o
# 2. Ugyanazon objektumok dinamikus és statikus linkelése.
aarch64-linux-gnu-gcc -no-pie \
lab-driver.o lab-adjust.o -o lab-dynamic.elf
aarch64-linux-gnu-gcc -static -no-pie \
lab-driver.o lab-adjust.o -o lab-static.elf
# 3. Futtatás qemu-user környezetben.
qemu-aarch64 -L /usr/aarch64-linux-gnu ./lab-dynamic.elf
printf 'dynamic exit=%d\n' "$?"
qemu-aarch64 ./lab-static.elf
printf 'static exit=%d\n' "$?"A GCC-meghajtó azért végzi a linkelést, mert a C futtatókörnyezet
indulókódjára és a printf könyvtári megvalósítására is
szükségünk van. A -no-pie mindkét változatnál kikapcsolja a
pozíciófüggetlen végrehajtható fájl készítését, így az összehasonlítás
nem keveredik a PIE különbségeivel.
Várt, nem itt mért kimenet: mindkét program
pos=26 neg=0 sort ír ki; a shell által kiírt kilépési
állapot mindkét esetben 0.
A dinamikus változatnál a -L az AArch64 betöltő és
könyvtárak kereséséhez ad prefixet. Ha a program a betöltő hiányára
panaszkodik, először a cross-könyvtárak telepítését és ezt az útvonalat
ellenőrizzük, ne az assembly aritmetikát.
Először ellenőrizzük az objektum és a végrehajtható fájl típusát, majd keressük meg a függvényt:
# Target: qemu-userhez készült AArch64 ELF-ek; offline vizsgálat.
aarch64-linux-gnu-readelf -h lab-adjust.o
aarch64-linux-gnu-readelf -h lab-dynamic.elf
aarch64-linux-gnu-objdump -t lab-dynamic.elf
aarch64-linux-gnu-readelf -sW lab-dynamic.elf
aarch64-linux-gnu-objdump -d \
--disassemble=lab_adjust lab-dynamic.elf
aarch64-linux-gnu-objdump -d \
--disassemble=lab_adjust lab-static.elfVárt ellenőrzés: az ELF-fejléc ELF64,
little-endian és AArch64 tulajdonságokat jelez. Az objektum
típusa REL, a két, itt nem PIE-ként linkelt programé
EXEC. A dinamikus linkelés tehát önmagában nem jelenti azt,
hogy a fájl típusa DYN.
A readelf -sW kapcsolóiban a -s
szimbólumtáblát kér, a -W széles kimenetet engedélyez. A
lab_adjust bejegyzésében ezeket keressük:
| Mező | Értelmezés | A példában várt tartalom |
|---|---|---|
Value |
A szimbólum értéke | Objektumban szekción belüli eltolás; itt a linkelt ELF-ben virtuális cím |
Size |
A szimbólum mérete bájtban | 20 |
Type |
Szimbólumtípus | FUNC |
Bind |
Kötés | GLOBAL |
Vis |
Láthatóság | DEFAULT |
Ndx |
Szekcióindex vagy különleges jelölés | A kódot tartalmazó szekció indexe |
Name |
Név | lab_adjust |
A 20 bájt a GNU assembler .size direktívájából
származik: öt A64 utasítás, utasításonként 4 bájttal. Az előtte álló
igazítási kitöltés nem része ennek a méretnek. A szekcióindexet a
readelf -SW kimenetéhez lehet hozzárendelni.
Az objdump -t ugyanezt más oszloprendben, tömör
jelölésekkel mutatja; nem kell a két eszköz formátumát azonosnak
várni.
Fontos a táblák megkülönböztetése:
.symtab a linkeléshez és elemzéshez hasznos teljesebb
szimbólumtábla;.dynsym a dinamikus linkeléshez szükséges
szimbólumokat tartalmazza.Attól, hogy a lab_adjust globális kötésű, még nem
feltétlenül kerül be a végrehajtható fájl dinamikus szimbólumtáblájába.
A GLOBAL nem azonos azzal, hogy „mindenképpen exportált
dinamikus API”.
A konkrét linkelt cím helyett most a függvény elejétől mért eltolást használjuk. Ezek nem önálló memóriacímek.
| Eltolás [bájt] | A64 utasítás | Annotáció |
|---|---|---|
0x00 |
add W1, W0, W0, lsl #1 |
W1 = x + 2x |
0x04 |
add W1, W1, #5 |
W1 = 3x + 5 |
0x08 |
cmp W0, #0 |
NZCV beállítása az eredeti bemenet és nulla összehasonlításával |
0x0c |
csel W0, W1, WZR, ge |
Nemnegatív bemenetre a számított érték, egyébként nulla |
0x10 |
ret |
Visszatérés az A64 X30 regiszterben lévő címre |
A disassembler megjelenítheti a regiszterneveket kisbetűvel; ez nem
jelent eltérő regisztert. Az aliasok megjelenítése is változhat: a
CMP mögötti alaputasítás megmutatásához használható az
AArch64-specifikus -M no-aliases kapcsoló.
Levezetett állapotok:
W1 előbb 21, majd 26; a feltétel igaz,
ezért W0 = 26.W1 előjeles olvasata előbb −21, majd −16;
a feltétel hamis, ezért W0 = 0.Az annotáció célja nem a mnemonic magyarra fordítása, hanem az adatfolyam és a szerződés összekapcsolása. Különösen ellenőrizzük, melyik utasítás hozza létre a felhasznált állapotjelzőket.
Az objdump -d rendesen a kódot tartalmazó szekciókat
disassemblálja. A -D ennél szélesebben próbál utasításként
értelmezni tartalmat; adatbájtokból is születhet hihető mnemonic. Ez
önmagában nem bizonyítja, hogy az adott bájtok végrehajtandó kódot
alkotnak.
Vizsgáljuk meg a C-hívó objektumát még linkelés előtt:
# Target: AArch64/qemu-user objektum és ELF; offline vizsgálat.
aarch64-linux-gnu-objdump -r lab-driver.o
aarch64-linux-gnu-objdump -dr lab-driver.o
aarch64-linux-gnu-objdump -R lab-dynamic.elf
aarch64-linux-gnu-readelf -rW lab-dynamic.elfAz objdump -r a normál relokációkat listázza. A
-dr együtt különösen hasznos: a disassembly megfelelő
helyei mellé kerülnek a relokációs adatok.
A lab-driver.o fájlban várhatóan
R_AARCH64_CALL26 bejegyzések kapcsolódnak a
lab_adjust és a printf hívásához. Egy
bejegyzés lényeges elemei:
Az A64 BL közvetlen hívás 26 bites, előjeles, négyszeres
skálázású eltolást kódol. Elérési tartománya −128 MiB és +128 MiB−4 bájt
között van. A linker számítja ki a megfelelő eltolást; szükség esetén
köztes ugrókódot is alkalmazhat.
Objektumfájlban ne tekintsük a hívás mellett látható ideiglenes célcímet véglegesnek. A relokáció mondja meg, milyen kapcsolatot kell a linkernek létrehoznia.
A kisbetűs -r és a nagybetűs -R nem
felcserélhető: utóbbi a dinamikus relokációkat jeleníti meg. A végleges
fájlban üres normál relokációs lista nem jelenti azt, hogy nincsenek
függvényhívások.
A saját lab_adjust függvényünk mindkét fájlba bekerül.
Mérete és utasításai várhatóan azonosak, elhelyezése azonban
eltérhet.
A könyvtári printf kapcsolata más:
| Szempont | Statikus változat | Dinamikus változat |
|---|---|---|
| Könyvtári megvalósítás | A szükséges könyvtári objektumok bekerülnek az ELF-be | A megvalósítás jellemzően megosztott könyvtárban található |
| Hívás megjelenítése | A beépített megvalósításra vagy kapcsolódó belső szimbólumra mutathat | Gyakran printf@plt cél látható |
| Teljes disassembly | Rendszerint jóval nagyobb | Nem tartalmazza a teljes libc kódját |
| Függőségek | Nincs szükség dinamikusan betöltött libc-re | Dinamikus betöltő és megfelelő könyvtárak szükségesek |
Az eljáráskapcsoló tábla (Procedure Linkage Table,
PLT) köztes hívókódot tartalmazhat. Ehhez a globális
eltolástábla (Global Offset Table, GOT) címet tároló
bejegyzései kapcsolódhatnak. A printf@plt tehát nem
feltétlenül a formázást végző függvény törzse.
# Target: AArch64/qemu-user ELF-ek; offline összehasonlítás.
aarch64-linux-gnu-objdump -d --disassemble=main lab-dynamic.elf
aarch64-linux-gnu-objdump -d --disassemble=main lab-static.elf
aarch64-linux-gnu-objdump -d -j .plt lab-dynamic.elf
aarch64-linux-gnu-readelf -dW lab-dynamic.elf
aarch64-linux-gnu-readelf -lW lab-dynamic.elfA dinamikus táblában a NEEDED bejegyzéseket, a
programszegmenseknél az INTERP bejegyzést keressük. A
pontos PLT-kódot, szimbólumaliasokat és könyvtári belső hívásokat a
toolchain befolyásolja. Statikus ELF-ben is előfordulhatnak speciális,
induláskor feldolgozott relokációk; a „statikus” nem minden relokáció
teljes hiányát jelenti.
A reprodukálható sorrend: tünet → hipotézis → adatgyűjtés → egy változtatás → ellenőrzés.
| Tünet | Hipotézis | Adatgyűjtés és ellenpróba |
|---|---|---|
| Az eszköz nem tud disassemblálni | Nem megfelelő architektúrájú objdump |
readelf -h, majd az AArch64 cross-objdump
használata |
| Név szerint nincs függvénykimenet | Hiányzó szimbólum, elírás vagy eltávolított függvény | Szimbólumtábla vizsgálata; az eredeti, nem csupaszított fájl összehasonlítása |
| Objektumban furcsa híváscél látszik | Még fel nem oldott relokáció | Ugyanazon objektum megnyitása -dr kapcsolóval |
| A könyvtári függvény helyén rövid ugrókód van | PLT-bejegyzést nézünk | .plt, dinamikus relokációk és NEEDED
vizsgálata |
| Assembly javítása után sem változik a kód | Régi objektumot linkeltünk | Assembly újraösszeállítása, újralinkelés, majd célzott disassembly |
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Miért lehet a lab_adjust
definiált szimbólum a lab-adjust.o, de UND
jelölésű a lab-driver.o fájlban? Mi történik
linkeléskor?
(implementációs, középszint — cél: rutinépítés) Módosítsd a függvényt úgy, hogy nemnegatív bemenetre legyen az eredmény. Maradjon öt utasításos. Frissítsd a C-ellenőrzést, építsd újra mindkét változatot, és annotáld a módosult utasításokat.
(hibakeresési, középszint — cél: rutinépítés) Távolítsd el a nem szükséges szimbóluminformációt egy külön laborpéldányból, majd vizsgáld meg, hogy a név szerinti keresés sikertelensége együtt jár-e működésképtelenséggel:
# Target: AArch64 Linux, qemu-user.
aarch64-linux-gnu-objcopy --strip-all \
lab-dynamic.elf lab-stripped.elf
aarch64-linux-gnu-objdump -t lab-stripped.elf
aarch64-linux-gnu-readelf --dyn-syms -W lab-stripped.elf
aarch64-linux-gnu-objdump -d \
--disassemble=lab_adjust lab-stripped.elf
qemu-aarch64 -L /usr/aarch64-linux-gnu ./lab-stripped.elf
printf 'stripped exit=%d\n' "$?"Jegyezd fel, melyik szimbólumtábla maradt meg, és miért lehet szükséges.
1. megoldás — Segítség. Az UND jelölés
azt jelzi, hogy az adott OBJEKTUMFÁJL maga nem tartalmazza a szimbólum
definícióját — melyik eszköz az, amelyik később a két objektumot egymás
mellé olvasva a hivatkozást a definícióhoz köti?
1. megoldás — Teljes megoldás. A
lab-adjust.o tartalmazza a függvény definícióját és kódját.
A lab-driver.o csak hivatkozik rá; az UND
feloldatlan, nem definiált szimbólumot jelez az adott objektumban. A
linker összekapcsolja a hivatkozásokat a definícióval, és javítja a
hívási helyeket.
2. megoldás — Segítség.
;
az A64 ADD-nak van olyan alakja, amely egy regisztert egy
másik, LSL-lel skálázott regiszterhez ad hozzá egyetlen
utasításban — ez pont a négyszerezést végzi el.
2. megoldás — Teljes megoldás. Az első két sor helyére az alábbi A64 utasítások kerülnek:
// A64 ISA: a lab_adjust első két utasításának cseréje.
add W1, W0, W0, lsl #2
add W1, W1, #9Az eltolás négyszerez, ezért az első eredmény . A 9 kódolható az immediate mezőben. A pozitív teszt várt eredménye 44, a negatívé továbbra is 0; a C-ellenőrzésben 26 helyett 44 szükséges. A korábbi buildlépések megismétlése után a méret továbbra is 20 bájt.
3. megoldás — Segítség. A --strip-all a
NEVEKET (szimbólumtáblát) távolítja el, nem a gépi kódot — mi a
különbség aközött, hogy egy függvényt “név szerint” nem találsz, és
aközött, hogy a függvény ténylegesen hiányzik a bináris kódból?
3. megoldás — Teljes megoldás. A hagyományos
.symtab eltűnik, ezért a lab_adjust név
szerinti disassembly-je nem adja a korábbi függvénylistát. A dinamikus
linkeléshez szükséges .dynsym megmarad. A program az
aktuálisan beépített tesztnek megfelelően tovább működik, 0 kilépési
állapottal. A teljes objdump -d továbbra is mutat kódot: a
név elvesztése nem azonos az utasítások eltávolításával. A
--strip-debug más művelet: önmagában nem távolítja el az
összes hagyományos szimbólumot.
A bináris olvasását az ELF architektúrájának és típusának ellenőrzésével kezdjük, majd szimbólumnév alapján szűkítjük a disassembly-t. Az utasítások mellett mindig vizsgáljuk az adatfolyamot, az állapotjelzők eredetét és a függvény szerződését. Objektumfájlban a relokáció nélkül félrevezető lehet egy hívás megjelenített célja. Statikus és dinamikus linkelésnél ugyanaz a saját függvény hasonló maradhat, miközben a könyvtári hívások útvonala eltér. A hiányzó név önmagában nem bizonyít hiányzó kódot; ezt szimbólumtáblával, teljes disassembly-vel és futtatási ellenpróbával különítjük el.
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 kódokat és parancsokat itt nem futtattuk; az eredmények várt vagy levezetett értékek. Az Ubuntu 24.04 tényleges csomagverzióit, a cross-könyvtárak elérhetőségét, a PLT pontos alakját és a megjelenített szimbólumaliasokat a hallgató laborjában kell ellenőrizni. A vizsgálat nem tartalmaz teljesítménymérést.