Egy C program incidensvizsgálatakor a folyamat állapotát vissza kell kapcsolnunk a forrásprogram objektumaihoz: melyik függvény futott, milyen pointert kapott, meddig élt a hivatkozott objektum, és melyik művelet sértette meg a szerződést? A debugger ezt a kapcsolatot segít helyreállítani, de nem szolgáltat automatikus helyességbizonyítást.
Az ABI- és optimalizálásdiagnosztikai labor alapján a gépi hívási keret nem feltétlenül követi a forrás függvényhatárait. Az élettartam- és memóriahibák vizsgálati laborjából pedig fontos felidézni: az olvasható memóriacím önmagában nem bizonyítja, hogy ott még él a megfelelő C objektum.
GDB-eszközmodell. A debugger a gépi állapotot, a végrehajtható fájlt és a debug információt együtt értelmezi.
| Fogalom | Jelentés és korlát |
|---|---|
| Breakpoint, töréspont | Megállás függvénynél, forrássornál vagy gépi utasításnál. Optimalizáláskor egy forrássorhoz több hely tartozhat, vagy nem tartozik külön utasítás. |
| Watchpoint, adatfigyelési pont | Egy kifejezés értékváltozását figyeli. A hardveres támogatás méret- és darabszámkorlátos; a szoftveres megoldás lassú lehet. |
| Backtrace, hívási visszakövetés | A debugger által rekonstruált hívási lánc. Nem teljes végrehajtási történet. |
| Frame, hívási keret | Egy híváshoz rendelt vizsgálati környezet: argumentumok, lokális változók, visszatérési hely. Inline függvényhez fizikai stack frame nélkül is megjelenhet logikai frame. |
A bt megmutatja a hívási láncot, a frame N
kiválaszt egy keretet, az up a hívó felé lép. Ez a
kiválasztás nem futtatja a programot: azt változtatja meg, hogy például
az info locals melyik függvény változóit értelmezi.
A watchpoint sem időgép. Egy betöltött core dumpban már nem figyelhetjük meg a korábbi írást; ehhez élő, reprodukált futás szükséges.
A következő programban szándékosan hibás a ciklusfeltétel. A hozzáférést ellenőrző függvény azonban még a tömbhatár átlépése előtt megszakítja a végrehajtást.
ISO C17. A példa nem támaszkodik nem definiált
viselkedésre. Az abort() rendellenes befejezést
kezdeményez; a core fájl létrejötte már platform- és
konfigurációfüggő.
Választott, helyben ellenőrizendő laborprofil:
Linux, x86-64 System V ABI, LP64, 8 bites C bájt, 4 C bájtos
int; GCC 14.2.0, GNU binutils 2.43, glibc 2.40 és GDB 15.2.
A parancsok ehhez választott laborutasítások, nem dokumentált futtatási
eredmények.
A batch.c teljes tartalma:
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
struct batch {
size_t count;
int values[3];
};
static int value_at(const struct batch *batch, size_t index)
{
if (batch == NULL ||
batch->count > sizeof batch->values / sizeof batch->values[0] ||
index >= batch->count) {
fputs("batch bounds contract violated\n", stderr);
abort();
}
return batch->values[index];
}
int main(void)
{
struct batch owned = {
.count = 3,
.values = {10, 20, 30}
};
int total = 0;
size_t i = 0;
for (; i <= owned.count; ++i) { /* Szándékos logikai hiba. */
total += value_at(&owned, i);
}
printf("%d\n", total);
return EXIT_SUCCESS;
}A value_at előfeltétele: a nem null batch
pointer élő, olvasható struct batch objektumra mutat,
amelyet közben más végrehajtás nem módosít. A nullvizsgálat nem tudja
ellenőrizni ezt az élettartam-szerződést.
| Szerep | Objektum vagy hozzáférés | Élettartam és érvénytelenítés |
|---|---|---|
| Tulajdonos | A main lokális owned objektuma |
Automatikus tárolású; normál esetben a blokk elhagyásáig él. |
| Tároló | Az owned objektum részeként tárolt values
tömb |
Élettartama az azt tartalmazó objektumhoz kötött. |
| Kölcsönzött hozzáférés | A value_at batch paramétere |
A hívás alatt használható; nem kerül eltárolásra vagy visszaadásra. |
| Hibakimenet | Diagnosztikai üzenet és abort() |
Nem ad vissza érvénytelen elemet, és nem olvas tömbhatáron túl. |
GNU eszközlánc. Készítsünk diagnosztikai és optimalizált változatot:
gcc -std=c17 -Wall -Wextra -Wpedantic -Og -g3 \
-fno-omit-frame-pointer -Wl,--build-id \
batch.c -o batch-debug
gcc -std=c17 -Wall -Wextra -Wpedantic -O2 -g3 \
-fno-omit-frame-pointer -Wl,--build-id \
batch.c -o batch-optA -g3 debug információt kér, nem tiltja az
optimalizálást. A frame pointer megtartása egyes esetekben segíti a
visszakövetést, de nem garantálja a lokális változók elérhetőségét.
Az első value_at hívásnál megálló munkamenet:
gdb -q ./batch-debug \
-ex 'break value_at' \
-ex run \
-ex bt \
-ex 'info args' \
-ex 'print *batch' \
-ex 'print batch->values[0]@3' \
-ex 'x/3wd batch->values'A print *batch típusosan értelmezi a struktúrát. A
@3 GDB-szintaxis három egymás melletti elemből képez
megjelenítendő mesterséges tömböt; nem C operátor.
Az x/3wd nyers memóriavizsgálat: három, egyenként négy
bájtos egységet jelenít meg előjeles decimális formában. Ez a választott
laborprofilban illeszkedik az int reprezentációméretéhez,
de nem általános C-garancia. Az x nem ellenőriz
ownershipet, objektumhatárt vagy élettartamot.
Az up után a main keretében az
info locals megmutathatja az owned,
i és total értékét. A watch i,
majd continue segítségével követhetjük az index változását.
A töréspont és a watchpoint felváltva is megállíthatja a programot; az
info breakpoints listájából kiválasztott pontot a
disable N paranccsal kapcsolhatjuk ki.
Levezetés. A logikai tartomány
,
ahol
.
A hibás i <= n feltétel az
értéket is beengedi. Ekkor az index >= batch->count
igaz, ezért a függvény megszakítja a futást a dereferálás előtt. A
forrás alapján várt hibakép tehát index == count == 3; ez
nem állítás egy ténylegesen megfigyelt debugger-kimenetről.
Linux. A core dump egy folyamat állapotának utólag vizsgálható pillanatképe: tartalmazhat regisztereket, szálállapotokat és memóriatartományokat. Nem feltétlenül tartalmaz minden leképezést vagy minden szükséges fájlt, és nem őrzi meg az események történetét.
Külön laborban, a debuggeren kívüli futáshoz:
ulimit -c unlimited
cat /proc/sys/kernel/core_pattern
./batch-debugA ulimit csak az adott shellből induló folyamat
korlátját módosítja, a megengedett felső korlátig. A
core_pattern, a jogosultságok, a folyamat dumpolhatósága és
a szolgáltatáskezelő szabályai továbbra is meghatározók. Konténerben a
fogadó mechanizmus akár a hoston működhet.
Systemd-alapú környezetben a coredumpctl list, majd a
megfelelő bejegyzéshez tartozó
coredumpctl dump PID --output=core.incident használható; a
PID helyére a kiválasztott azonosító kerül. Ez
systemd-eszköz, nem ISO C vagy POSIX szolgáltatás.
A megfelelő core betöltése:
gdb -q ./batch-debug core.incident \
-ex 'info files' \
-ex 'info sharedlibrary' \
-ex 'thread apply all bt'A megszakítás helye várhatóan az abort()
megvalósításában vagy az alatta futó libc-kódban látszik. Ez nem jelenti
azt, hogy a libc hibás. A backtrace-ben keressük meg az első saját
kódhoz tartozó keretet, majd a tényleges sorszámával válasszuk ki.
A minimális hibakép:
SIGABRT;index == count;Ez már javítható állítás, nem pusztán egy stack trace.
A forráscommit egyezése kevés. Más fordító, kapcsolók vagy függőségek más gépi kódot és címkiosztást eredményezhetnek. Az eredeti végrehajtható fájlt, megosztott könyvtárakat és a hozzájuk tartozó debug fájlokat kell megőrizni.
GNU ELF-eszközlánc. A build-id egy buildhez rendelt azonosító, amely például a következő paranccsal vizsgálható:
readelf -n batch-optHasznos az artefaktumok összerendeléséhez, de önmagában nem kriptográfiai hitelességigazolás.
Külön debug fájl készítésének példája:
objcopy --only-keep-debug batch-opt batch-opt.debug
objcopy --strip-debug batch-opt
objcopy --add-gnu-debuglink=batch-opt.debug batch-optA .gnu_debuglink a megfelelő debug fájl megtalálását és
ellenőrzését segíti. A core mellé ne utólag újrafordított „azonos
verziót”, hanem az adott kiadási artefaktumot adjuk.
Más gépről származó core esetén az eredeti gyökérfájlrendszer
másolatához a set sysroot /srv/incident/rootfs, a debug
fájlokhoz a set debug-file-directory /srv/incident/symbols
állítható be, lehetőleg a core betöltése előtt. Az
info sharedlibrary segít ellenőrizni a felismert
könyvtárakat. A build-id-t és a szimbólumegyezést minden releváns
megosztott könyvtárnál vizsgáljuk; a helyi libc nem automatikusan
megfelelő.
A core érzékeny adatokat, például hitelesítő adatokat is tartalmazhat. Ugyanúgy kontrollált hozzáféréssel és megőrzési idővel kezeljük, mint más incidensartefaktumot.
GDB és compiler debug információ. Az
<optimized out> nem azt jelenti, hogy a változó nulla
vagy inicializálatlan. Azt jelzi, hogy a debugger az adott ponton nem
tudja rekonstruálni az értékét. A fordító megszüntethette a tárolását,
összevonhatta más számítással, vagy a debug információ nem írja le
megfelelően az aktuális helyét.
Az optimalizált példában a fordító akár a számítások jelentős részét
is elhagyhatja, mert azok nem befolyásolják az elérhető megfigyelhető
hatásokat. A value_at inline beépítése után külön gépi
hívás nélkül is szerepelhet a backtrace-ben.
Hasznos parancsok:
disassemble /s main: assembly forrássorokkal kevert
nézetben;disassemble /r main: assembly a gépi kód
bájtjaival;x/8i $pc: utasítások az aktuális
programszámlálótól;info registers: regiszterállapot;info line *$pc: az aktuális címhez rendelt
forráshely.A kevert nézet nem állítja vissza a forrás szerinti végrehajtási
sorrendet. A next és step ezért kihagyhat
sorokat, vagy látszólag visszafelé léphet. Gépi szinten a
stepi használható, de a regisztereket az ABI és az adott
utasítássorozat alapján kell értelmezni.
Gyakori hiba a debugger által olvasható pointert érvényesnek tekinteni, a legfelső frame-et gyökéroként kezelni, vagy hiányzó értékeket feltételezésekkel kitölteni. Helyette válasszuk külön a megfigyelt tényt, a forrásból levezetett állítást és a még ellenőrizendő hipotézist.
Magyarázó: Miért nem tömbtúlolvasás bizonyítéka
ebben a példában a SIGABRT? Miért nem elegendő egy pointer
olvashatósága az érvényességéhez?
Kódolási: Javítsd a ciklust. Vizsgáld meg a
count értékeit: 0, 1 és 3. Adj meg ellenőrzési feltételt az
eredményre és a hozzáférésekre.
Diagnosztikai: Az optimalizált core
vizsgálatakor az i értéke nem érhető el, és a libc
szimbólumaihoz eltérésjelzés tartozik. Milyen sorrendben folytatod az
elemzést?
Az ellenőrzés az elemelérés előtt hívja az abort()
függvényt, ezért ezen az útvonalon nem történik tömbtúlolvasás. Az
olvasható memória lehet már megszűnt objektum tárolója is. Az ellenőrzés
alapja a vezérlési út és az élettartam-szerződés, nem egy önmagában
sikeres memóriakiolvasás.
A javított feltétel i < owned.count. A forrásból várt
összegek rendre 0, 10 és 60.
Bizonyításvázlat: a ciklusmag csak
mellett fut; az index nulláról indul és egyesével nő. Ezért minden
meghívott hozzáférésre
.
Ellenőrzési feltétel: a megfelelő összeg jelenik meg, és egyik érvényes
eset sem éri el az abort() ágat.
Először az artefaktumegyezést kell helyreállítani: eredeti bináris,
libc, további könyvtárak és debug fájlok. Ezután újra ellenőrizhető a
backtrace. A hiányzó i helyett megvizsgálható a hívási
argumentum, a struktúra állapota és az utasítássorozat, ha ezek
rekonstruálhatók. Ha nem, ezt korlátként kell rögzíteni. Másik builddel
történő reprodukció kiegészítő bizonyíték, nem az eredeti futás
állapota.
A megbízható hibaképhez pontos artefaktumegyezés, megfelelő frame-választás és a C objektumszerződéseinek ismerete kell. A debugger pillanatképet értelmez; az optimalizálás és a hiányzó adatok korlátozzák, mit tudhatunk meg belőle.