11.2 Disassembly objdump/readelf-fel

11.2.1 Motiváció: mit tartalmaz valójában a futtatható fájl?

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:

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.

11.2.2 Három külön információréteg

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.

11.2.3 Kidolgozott példa: ugyanaz a függvény kétféle linkeléssel

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

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

11.2.3.1 A forrásfájlok

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

Az 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 N=VN=V 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;
}

11.2.3.2 Összeállítás, linkelés és futtatás

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.

11.2.4 Függvény keresése és a szimbólumtábla olvasása

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

Vá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:

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

11.2.5 A függvény disassembly-jének annotálása

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:

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.

11.2.6 Relokáció: a még kitöltendő kapcsolat

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

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

  1. a javítandó hely szekción belüli eltolása;
  2. a relokáció típusa;
  3. a hivatkozott szimbólum és az esetleges addend, vagyis hozzáadandó korrekció.

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.

11.2.7 Statikus és dinamikus linkelés a disassembly-ben

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

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

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

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

11.2.9 Feladatok

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

  2. (implementációs, középszint — cél: rutinépítés) Módosítsd a függvényt úgy, hogy nemnegatív bemenetre 5x+95x+9 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.

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

11.2.10 Megoldások

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. 5x=x+4x5x = x + 4x; 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, #9

Az eltolás négyszerez, ezért az első eredmény x+4xx+4x. 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.

11.2.11 Rövid összefoglaló

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.

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