Kontrola správnosti použití paměti
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Price Wang's programmer was coding software. His fingers danced
upon the keyboard. The program compiled without an error message,
and the program ran like a gentle wind.
Excellent!" the Price exclaimed, "Your technique is faultless!"
"Technique?" said the programmer, turning from his terminal,
"What I follow is the Tao -- beyond all technique. When I first
began to program I would see before me the whole program in one
mass. After three years I no longer saw this mass. Instead,
I used subroutines. But now I see nothing. My whole being exists
in a formless void. My senses are idle. My spirit, free to work
without a plan, follows its own instinct. In short, my program
writes itself. True, sometimes there are difficult problems. I see
them coming, I slow down, I watch silently. Then I change a single
line of code and the difficulties vanish like puffs of idle smoke.
I then compile the program. I sit still and let the joy of the work
fill my being. I close my eyes for a moment and then log off."
Price Wang said, "Would that all of my programmers were as
wise!"
-- Geoffrey James, "The Tao of Programming"
Chyby při použití dynamicky alokované paměti jsou ty nejčastější
a nejzáludnější. Zejména proto, že se často nemusí vůbec projevit
a teprve časem vyústit v katastrofu. V C je situace obvzlášť
komplikovaná, protože jeho hlavní zbraň (ukazatele) k takovým chybám
přímo svádí. U jazyků jako je Lisp, kde ukazatel je věc neznámou
a nad celou pamětí vládne všemocný garbage collector je situace
mnohem přívětivější. Kupodivu ale i tam jsou podobné chyby poměrně
časté.
Právě pro záludnost odhalování těchto chyb bylo vymyšleno velké
množství šikovných utilitek, knihoven a jiných udělátek, které mají
za úkol podobné potíže odhalit. Protože jich existuje poměrně hodně,
rozhodl jsem se po nich porozhlédnout a otestovat je. Každému
doporučuju, aby si své programy jednou za čas zkontrolovat podobnou
utilitkou, proto6e ušetří to hodně starostí jak jemu, tak uživatelům
programu.
Napřed je ale nutné zjistit, o jaké chyby se vlastně jedná. Pro
většinu těchto chyb jsem vytvořil testovací prográmky a jednotlivé
utility pak podrobím těmto testům. Chyby lze v zásadě rozdělit
do dvou hlavních skupin.
Chyby při použití haldy:
Do této kategorie patří zejména vícenásobné uvolňování bloků,
uvolnění už uvolněných bloků či vůbec nenaalokovaných bloků
a alokace nebo realokace bloků velikosti 0 (zde standardy nemají
příliš jasno, jak se program má zachovat).
Tyto chyby chytá glibc už sama o sobě relativně spolehlivě
(narozdíl od libc v jiných operačních systémech). Existuje i několik
robusnějších knihoven (efence, dbmalloc, mpr, ccalloc), které se
na hledání podobných chyb specializují. Snad všechny tyto knihovny
takové chyby chytí, pokud samozřejmě se nenachází v nějaké části
kódu, která se provádí jenom zřídka. To je důvod, proč by člověk měl
používat tyto knihovny při vývoji pravidelně.
Kapitola sama pro sebe je neuvolňování bloků, což sice nezpůsobí
pád programu, ale je trestuhodným plýtváním pamětí. Zejména netscape
je tím proslulé. Některé knihovny pro kontrolu paměti (ccmalloc)
také nabízí vypsání všech neuvolněných bloků na konci programu.
Pokud se člověk donutí k tomu, aby všechny bloky pečlivě uvolňoval,
je to asi nejspolehlivější prostředek, jak se vyvarovat těchto chyb.
Chyby při použití ukazatelů:
Zde se jedná hlavně o zápis a čtení mimo alokovaná místa
v paměti. ANSI C navíc ale zakazuje i práci (aritmetiku)
s ukazately, které ukazují mimo alokované oblasti. To je poněkud
problematické, protože takové situace vznikají poměrně často.
Proto ANSI C z tohoto pravidla vyjíma tak zvanou "NULL zone". Navíc
povoluje práci s ukazatelem ukazujícím jednu položku za konec pole,
protože tato situace vzniká často u cyklů, kde ukazatel projíždí
celé pole. Dodržování tohoto standardu je poněkud složitější, už
jenom proto, že mnoho běžných konstrukcí (jako vaargs) potom nelze
korektně použít. Navíc se takové chyby jen velmi těžko stestují.
Přístup za a před pole jsou asi nejčastější. Mohou se stát jak
na haldě, tak na zásobníku (zde pak vznikají známé buffer owerflow
problémy) tak u inicializovaných dat. Přitom běžné kontrolní
knihovny většinou mají šanci odchytit pouze ty první. Nejsložitější
je asi situace u inicializovaných dat, protože zde dokonce existují
programy, co podobné věci dělají úmyslně.
Dalším oblíbeným problémem je používání už uvolněných bloků
a zápis do read-only stringů. Pokud provedete v c konstrukci:
char *c="ahoj";
string je read only, narozdíl od polí. V operačních systémech
bez ochrany paměti zápis projde a člověk se pak v operavdovém OS
nestačí divit. Je třeba ještě podotknout, že string vytvořený pomocí
konstrukce:
char c[]="ahoj";
už readonly není, protože se jedná o inicializaci pole.
Nejsem si ale jist, jestli to není GNU extension, protože některé
překladače to nemají rádi.
Časté potíže také způsobuje používání neinicializované paměti.
Programy často předpokládájí, že paměť je snulována. To ale nemusí
být pravda a programy se potom chovají podivně.
Nejčastější problémy jsem shrnul do několika testovacích
prográmků a zjistil, jak vypadá situace na glibc:
Chyby pro použití haldy
zapomenuté naalokované bloky paměti
uvolnění nenaalokovaného bloku crash v __libc_free
vícenásobné uvolnění bloku crash v __libc_free
alokace a realokace bloku o velikosti 0 alokuje a realokuje normálně
použití paměti bez testu na selhání malloc
Chyby pro použití ukazatelů
použití uvolněného bloku pro zápis
použití uvolněného bloku pro čtení načte starý obsah
zápis za koncem pole v zásobníku
zápis za koncem alokovaného bloku
čtení za koncem alokovaného bloku načte 0
zápis daleko za koncem alokovaného bloku
zápis před koncem alokovaného bloku podivný chrash uvnitř glibc
čtení před koncem alokovaného bloku načte 0
čtení neinicalizované paměti načte 0
U testů, kde není napsáno nic, program proběhl a žádná chyba
se neprojevila. Jak vidíte, glibc odhalila vícenásobné uvolňování.
U alokace velikosti 0 se chovala také korektně podle jednoho ze
standardů. Zápis před koncem bloku způsobil zmatek v interních
strukturách mallocu a pád. Není se ale moc čemu divit. Ostatní chyby
zůstaly bez povšimnutí.
Libc v DJGPP (podobně jako i většina ostatních libc) se nevšimla
vůbec ničeho.
mcheck
Electric fence
lclint
Checker
CCmalloc
mpr
Závěr
Jedůkladějším a nejchytřejším programem pro kontrolu paměti byl
rozhodně checker. Jeho nevýhodou je ale velké zpomalení a nutnost
uzavřenosti kódu. Takže se nehodí na všechno. U grafických programů
většinou neexistují wrappery do knihoven a zpomalení je tak velké,
že jsou programy potom nepoužitelné.
Pokud nemůžete použít checker, asi nejlepší knihovna pro
kontrolu je libefence. Tu lze používat běžně při ladění, protože
příliš nezpomaluje a najde hodně nedostatků. Jednou za čas je také
dobré vyzkoušet kontrolu z druhé strany (PROTECT_BELLOW) a kontrolu
uvolněných bloků (PROTECT_FREE). Na hledání zapomenutých bloků ale
bohužel libefence použít nejde. Pro to je také dobré přinutit se
uvolňovat opravdu všechny bloky (a nepočítat s tím, že se nakonci
uvolní samy) a jednou za čas otestovat ccmalloc. Zajímavá utilita
je pravděpodobně i lclint, který může ušetřit opravdu hodně starostí
nejen s pamětí ale i s kompatibilitou apod.
výheň