A memóriahiba helye és a látható meghibásodás helye gyakran eltér. Egy határon túli írás után a folyamat még sokáig működhet; egy felszabadított objektum olvasása pedig csak más allokációs mintázat mellett okozhat összeomlást. A dinamikus diagnosztikai eszközök célja, hogy a hibás művelethez közelebb adjanak értelmezhető jelzést.
Az allokációs hiba-injektálás a hibautak kezelését vizsgálta; itt elsősorban a végrehajtott memóriaműveleteket ellenőrizzük. A konkurens élettartam szabályaihoz a TSan, a debuggeres vizsgálathoz pedig a szimbolizált hívási lánc kapcsolódik. Egyetlen hibamentes futás sem igazolja a teljes program helyességét: csak adott bemenetek, végrehajtási utak és szálütemezések kerültek vizsgálatra.
A sanitizer fordítói és futtatókörnyezeti diagnosztikai mechanizmus. A fordító ellenőrzésekkel egészíti ki, azaz instrumentálja a programot; a kapcsolódó futtatókönyvtár nyilvántartásokat vezet és jelentéseket készít. A Valgrind/Memcheck ezzel szemben a gépi program végrehajtását felügyeli, ezért nem igényel sanitizeres újrafordítást.
| Eszköz | Elsődleges cél | Lényeges korlát |
|---|---|---|
| AddressSanitizer, ASan | Határon túli hozzáférés, use-after-free, egyes élettartam- és felszabadítási hibák | Nem ismeri minden alkalmazásszintű objektumhatár jelentését |
| UndefinedBehaviorSanitizer, UBSan | Kiválasztott nem definiált műveletek | Nem ellenőrzi a C összes UB-szabályát |
| MemorySanitizer, MSan | Inicializálatlan értékek felhasználása | Különösen érzékeny a függőségek instrumentálására |
| ThreadSanitizer, TSan | Adatversenyek | Nem általános logikai race- vagy holtpontdetektor |
| LeakSanitizer, LSan | Elvesztett dinamikus allokációk | Az elérhető memória is lehet alkalmazásszintű szivárgás |
| Valgrind/Memcheck | Érvénytelen hozzáférés, definiálatlan értékhasználat, leak | Jelentős futási többletköltség; nem általános UB-ellenőrző |
Az ASan a programmemóriához kiegészítő nyilvántartást,
árnyékmemóriát (shadow memory) rendel. Többek
között jelöli az allokációk körüli tiltott területeket és a
felszabadított tárolót. Így észlelhet heap-, stack- és globális
bufferhatár-sértést, kétszeres felszabadítást vagy nem megfelelő cím
átadását a free számára.
A hatókör elhagyása utáni használathoz (use-after-scope) a
Clang profiljában explicit megadható a
-fsanitize-address-use-after-scope. A függvényből
visszatérés utáni használat külön mechanizmus; támogatása és beállítása
platform- és verziófüggő. Az ASan nem teljes lifetime-ellenőrző.
Fontos vakfolt az objektumon belüli határátlépés: egy struktúratag tömbjéből a következő tagba történő hibás hozzáférés nem feltétlenül érint tiltott területet. Ugyanígy nem feltétlenül jelez, ha egy nagy allokáción belül túllépjük az alkalmazás által érvényesnek tekintett adathosszt.
ISO C17-szabály és eszközellenőrzés különbsége: a
signed egész túlcsordulás nem definiált viselkedés; a UBSan megfelelő
ellenőrzése ezt a végrehajtott műveletnél jelezheti. A
-fsanitize=undefined csoport többek között igazítási, null
pointerrel kapcsolatos, eltolási, aritmetikai és bizonyos
tömbhatár-ellenőrzéseket kapcsol be. Nem ad általános effective type-,
aliasing- vagy élettartam-ellenőrzést.
Az unsigned aritmetika modulo viselkedése önmagában nem UB, ezért az unsigned méretszámítás körbefordulásának felismerésére ez a csoport nem elegendő. A Clang külön unsigned-overflow ellenőrzése hasznos hibakeresési kiegészítés lehet, de nem szabad nyelvi tilalomként értelmezni.
Az MSan nem csupán azt követi, hogy egy cím elérhető-e, hanem azt is, hogy az ott tárolt érték inicializált-e. Egy inicializálatlan érték másolása továbbviheti a jelölést; a jelentés például akkor születhet meg, amikor az érték vezérlési döntést befolyásol. Az origin tracking az eredet visszakeresését segíti.
MSan esetén a nem instrumentált függőség különösen veszélyes határ: hibák maradhatnak rejtve, illetve helyesen előállított adatok is definiálatlannak látszhatnak. A saját kód mellett a releváns függőségeket is instrumentálni kell. A rendszerkönyvtári hívások egy részét speciális elfogó függvények, interceptorok kezelik; ez nem általános felmentés minden külső könyvtárra.
A TSan a memóriahozzáféréseket és az általa ismert szinkronizációt követi. Az adatverseny meghatározásához a korábban tárgyalt happens-before viszony szükséges. A két külön zárolás között végrehajtott „ellenőrzés, majd módosítás” például lehet logikailag hibás akkor is, ha minden hozzáférés adatversenymentes. A saját szinkronizációs megoldásokat a TSan nem feltétlenül érti.
Clang-eszközszabály: az ASan, MSan és TSan egymással nem kombinálható sanitizerprofilok. A TSanhoz ezért külön bináris és külön futtatás készüljön; ne próbáljuk az összes ellenőrzést egyetlen kapcsolólistába zsúfolni.
A LSan jellemzően a leak-vizsgálat időpontjában elérhetőségi elemzést
végez. Linuxon gyakran az ASan runtime részeként használható, de az
integráció platformfüggő. Az önálló -fsanitize=leak profil
elsősorban runtime-linkelést igényel, nem ASan-szerű
hozzáférés-instrumentálást. Korai összeomlás vagy ASan-leállás esetén a
szokásos kilépéskori leak-vizsgálat elmaradhat.
A következő parancsok nem futtatott laborutasítások.
Választott mintaprofiljuk: ISO C17, Linux x86-64 System V ABI,
Clang/LLVM és compiler-rt 18.1.8, glibc 2.39; a Memcheck-példához
Valgrind 3.23.0. A telepített csomagokat, disztribúciós javításokat és
támogatást a laborban ellenőrizni kell. A clang és
llvm-symbolizer itt a rögzített LLVM-telepítés eszközeit
jelenti.
A teljes instrumentálási lánc három részből áll:
clang paranccsal végezzük, a szükséges
sanitizerkapcsolókkal.A nyers ld meghívása nem helyettesíti automatikusan a
driver runtime-választását és linkbeállításait. A csak tesztprogramon
bekapcsolt sanitizer nem instrumentálja utólag a korábban elkészített
statikus archívumot.
| Profil | Fordításkor | Végső driveres linkkor |
|---|---|---|
| ASan + UBSan | -fsanitize=address,undefined |
Ugyanez |
| MSan | -fsanitize=memory -fsanitize-memory-track-origins=2 -fPIE |
-fsanitize=memory -fsanitize-memory-track-origins=2 -pie |
| TSan | -fsanitize=thread -fPIE |
-fsanitize=thread -pie |
| Memcheck | -g, sanitizer nélkül |
Sanitizer nélkül |
Minden profilnak külön buildkönyvtár kell. Ez az objektumfájlok és függőségek véletlen keverését is megelőzi. Az instrumentálatlan külső kódot lefedettségi korlátként kell nyilvántartani, nem „ellenőrzött” komponensként.
A következő három fájl önálló C17-példát alkot. A
checksum.c szándékosan hibás, UB-t tartalmazó tanítási
példa; nincs garantált kimenete.
A kívánt függvényszerződés: pozitív n esetén
p legalább n inicializált
unsigned char elem olvasását engedje; nulla hossz esetén
null pointer is megengedett. A függvény nem tartja meg a címet.
checksum.h:
#ifndef CHECKSUM_H
#define CHECKSUM_H
#include <stddef.h>
unsigned checksum(const unsigned char *p, size_t n);
#endifchecksum.c:
#include "checksum.h"
unsigned checksum(const unsigned char *p, size_t n)
{
unsigned sum = 0;
for (size_t i = 0; i <= n; ++i) { /* HIBA: <= */
sum += p[i];
}
return sum;
}main.c:
#include "checksum.h"
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const size_t n = 4;
unsigned char *p = malloc(n * sizeof *p);
if (p == NULL) {
return EXIT_FAILURE;
}
for (size_t i = 0; i < n; ++i) {
p[i] = (unsigned char)i;
}
printf("%u\n", checksum(p, n));
free(p);
return EXIT_SUCCESS;
}| Tulajdonos | Tároló és kölcsönzés | Élettartam, érvénytelenítés | Hibakimenet |
|---|---|---|---|
A main felel a felszabadításért |
malloc-tároló; a checksum csak olvasásra
kölcsönzi |
Sikeres allokációtól free-ig; a hívás közben nincs
felszabadítás |
Allokációs hibánál EXIT_FAILURE; a hibás indexelésnél
UB |
A sizeof *p értéke egy C bájt. A foglalás tehát négy C
bájt, az érvényes indexek:
A hibás ciklus eléri az
értéket is. A p + n one-past pointer képzése megengedett,
de a p[n], vagyis *(p + n) olvasása nem.
Fordítás és külön laborfuttatás:
mkdir -p build/asan
clang -std=c17 -Wall -Wextra -Wpedantic -O1 -g \
-fno-omit-frame-pointer -fno-optimize-sibling-calls \
-fsanitize=address,undefined -fsanitize-address-use-after-scope \
-fno-sanitize-recover=all \
-c checksum.c -o build/asan/checksum.o
clang -std=c17 -Wall -Wextra -Wpedantic -O1 -g \
-fno-omit-frame-pointer -fno-optimize-sibling-calls \
-fsanitize=address,undefined -fsanitize-address-use-after-scope \
-fno-sanitize-recover=all \
-c main.c -o build/asan/main.o
clang -fsanitize=address,undefined \
build/asan/main.o build/asan/checksum.o -o build/asan/checksum
ASAN_OPTIONS=detect_leaks=1:symbolize=1 build/asan/checksumA várt diagnosztikai kategória heap-buffer-overflow olvasás. Ez nem
rögzített futtatási eredmény; a jelentés pontos alakja eszközfüggő. A
-fno-sanitize-recover=all az érintett UBSan-ellenőrzéseknél
megakadályozza, hogy a teszt hiba után sikeres kilépésig
folytatódjon.
A javítás: i < n.
Helyességi bizonyításvázlat: kezdetben
i == 0; a ciklustörzs csak i < n mellett
fut. A szerződés ekkor garantálja az adott elem olvashatóságát. A
tulajdonos a teljes hívás alatt életben tartja a tárolót. Nulla hossznál
nincs dereferálás. A javított példában a várható összeg
.
Memcheckhez külön, nem sanitizeres bináris készüljön:
mkdir -p build/memcheck
clang -std=c17 -Wall -Wextra -Wpedantic -Og -g \
main.c checksum.c -o build/memcheck/checksum
valgrind --tool=memcheck --leak-check=full \
--show-leak-kinds=all --errors-for-leak-kinds=definite,possible \
--track-origins=yes --error-exitcode=99 \
build/memcheck/checksumA Memcheck hasznos instrumentálatlan függőségeknél, valamint definiálatlan értékek és szivárgások vizsgálatakor. A heap-allokációk határát ismeri, de például a stacken belüli egyedi tömbhatárok felismerése korlátozott. Az ASan-bináris Memcheck alatti futtatása nem jó alapértelmezett kombináció.
A hívási lánc (stack trace)
értelmezhetőségéhez kell a megfelelő bináris, megosztott könyvtár, debug
információ és szimbolizáló. A -g és a frame pointer
megtartása segít, de nem szünteti meg az optimalizálás minden hatását.
Az llvm-symbolizer legyen elérhető; az eltérő buildből
származó szimbólum félrevezető lehet.
Ne csak a jelentés legfelső sorát olvassuk:
A suppression ismert jelentés célzott elnyomása, nem hibajavítás. Legyen hozzá felelős, indoklás, követhető hibajegy és felülvizsgálati feltétel. Külső függőség ismert hibájához szűk minta indokolható; saját kód teljes könyvtárának elnémítása nem.
A runtime-suppression és a fordításkori instrumentálási kizárás eltérő mechanizmus, eszközönként eltérő támogatással. Utóbbi szükséges követési információt is eltüntethet. Egy MSan-instrumentálási hiányt ezért nem helyes egyszerűen elnyomni.
Egy parser inicializálatlan lokális változó alapján választ ágat; egy kapcsolatkezelő felszabadítás után olvas; két szál szinkronizáció nélkül ír ugyanabba a nem atomikus objektumba. Mely profilokat választanád? Mit bizonyít egy tiszta futás?
Javítsd a checksum ciklusát. Ellenőrizd a nulla hosszú
bemenetet, egyetlen 9 értékű elemet és a példában szereplő
négy elemet. Miért nem kell nulla hossznál érvényes elemet
biztosítani?
Egy CI-feladat csak a main.c fájlt fordítja ASannal, egy
régi libparser.a archívumot használ, ld-vel
linkel, majd ugyanebbe a profilba TSan-t is bekapcsol. Mi a javítás
sorrendje?
Az inicializálatlan értékhez MSan vagy Memcheck, a use-after-free hibához ASan, az adatversenyhez TSan illik. MSanhoz a függőségi lefedettséget is ellenőrizni kell. A tiszta futás kizárólag azt mutatja, hogy az adott végrehajtásban az eszköz nem jelentett felismert hibát; nem igazolja más bemenetek vagy ütemezések helyességét.
A javítás i < n. A helyes implementáció
main függvényébe illeszthető ellenőrzés:
const unsigned char one[] = {9};
const unsigned char four[] = {0, 1, 2, 3};
if (checksum(NULL, 0) != 0 ||
checksum(one, 1) != 9 ||
checksum(four, 4) != 6) {
return EXIT_FAILURE;
}A tömbök tárolóját a hívó biztosítja; a kölcsönzés a hívásokra korlátozódik, és legkésőbb a blokk elhagyásakor érvénytelenné válna. Nulla hossznál a ciklustörzs nem fut, tehát a null pointert nem dereferáljuk. Ellenőrzési feltétel a három elvárt érték és a diagnosztika nélküli futás, a szerződés kézi ellenőrzésével együtt.
Először külön ASan- és TSan-buildkönyvtárat kell létrehozni. Ezután
minden saját fordítási egységet, köztük a libparser.a
tagjait is újra kell fordítani az adott profillal. A végső linket
clang végezze a megfelelő sanitizerkapcsolóval.
Az ellenőrzés tárgya nem pusztán a sikeres link: a buildnaplóban minden érintett fordítás és a végső link parancsát is vizsgálni kell, valamint a futáskor betöltött függőségeket. A két profil külön futtatandó.
A diagnosztikai profil hibatípushoz választott végrehajtási környezet. Az ASan és UBSan gyakran közös alap; az MSan és TSan külön buildet és körültekintő függőségkezelést igényel. A LSan és Memcheck további nézőpontot ad. A használható eredmény feltétele a helyes instrumentálási lánc, a driveres linkelés, az értelmezhető jelentés és a szűk suppression policy — nem pusztán a nulla kilépési kód.