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ň