Malcheck
door Erik Groenhuis
Een gereedschap om probleem-geheugenblokken met dynamische allocatie op te sporen.

Een probleem
Op een kwade dag is het zover. Je bent leuk aan het programmeren in C en opeens crasht je programma. Het spuugt een lijst uit van registerwaarden en namen van de functies die op dat moment actief waren. Veel viel er niet uit op te maken, maar de fout trad op bij een aanroep van realloc(), zoveel was duidelijk.

Nu zijn dit soort problemen notoir. De eigenlijke fout zit meestal niet op het punt waar het programma stopt, maar al veel eerder. Dan wordt er ergens een geheugenplaats overschreven waar dat niet zou moeten, en pas later wordt die plaats weer gebruikt. Als je geluk hebt crasht het programma. Als je pech hebt loopt het ongestoord verder en schrijft het allerlei foute gegevens weg.

Dat de crash optreedt bij een aanroep van realloc() wijst op een veel voorkomende programmeerfout: de tekst string die geschreven wordt in een array is langer dan de ruimte die gereserveerd is. Vaak wordt er maar 1 byte teveel geschreven. Precies voor het oplossen van dit soort problemen (en nog vele anderen) is een fraai pakket beschikbaar:

Dr. Smith's Development Toolkit. Dit heeft mij al vele malen uit de brand geholpen. Er was echter een probleem: Dr. Smith's werkt alleen met de standaard Clib bibliotheek. Mijn programma is een vertaling van een GNU (= GNU is Not Unix) programma, en is gelinkt met de UnixLib bibliotheek. Lastig.

Wat te doen
Gelukkig sprak ik op de Expo 2002 met Leo Smiers. Volgens hem moest het niet al te moeilijk zijn om aanroepen naar de allocatie-functies (malloc(), realloc(), free() en calloc()) te onderscheppen door geschikte macro's te definiëren. De macro's roepen dan zelfgemaakte functies aan. Deze vervangende functies gedragen zich naar buiten toe net als de oorspronkelijke functies, maar doen intern wat extra werk. Aan het gealloceerde blok wordt allerlei administratieve informatie toegevoegd. Vlak voor en achter het blok worden 'schildwachten' gezet. Als er door een programmeerfout data voorbij het einde van het blok wordt geschreven, dan wordt zo'n schildwacht ook overschreven. Daaraan is dan te zien dat het misgegaan is. Verder geeft het de gelegenheid om de parameters bij de aanroep te controlleren. Bijvoorbeeld free() aanroepen met een NULL parameter is op zich niet fout, maar wel verdacht.

Het belangrijkste wat de vervangende functies doen, is allereerst controleren of alle al bekende blokken nog goed zijn. Merk op dat dit systeem niet in staat is om precies vast te stellen op welk punt in het programma er op een verkeerde plek data wordt geschreven. Pas bij een daarna volgende aanroep van één van de functies wordt de fout opgemerkt.

Aan het werk
Welgemoed begon ik, na enig denkwerk over het ontwerp, met de implementatie. Uit een korte test bleek dat onderscheppen van de aanroepen goed mogelijk is. Door de macro
[code]#define malloc(s) MalCheck_malloc(__LINE__, __FILE__, (s) )
en het schrijven van de bijbehorende functie was het al snel mogelijk om een lijst te produceren van alle aanroepen van malloc(), compleet met het regelnummer en de naam van de sourcefile.

Een complicatie
Toen kwam de eerste kink in de kabel: alle aanroepen werden door het te debuggen programma op dezelfde plaats gedaan! Alle regelnummers en filenamen waren hetzelfde. De makers hadden malloc() in een 'wrapper' functie gestopt. Deze functie kijkt of het alloceren mislukt (omdat het geheugen op is) en stopt in dat geval het programma op een nette manier. Om zinnige informatie te krijgen is een 'backtrace' nodig, een lijst van de functies die op het moment van de aanroep actief zijn. Net zo'n lijst als bij het crashen van een programma afgedrukt wordt.

Die informatie is te achterhalen door met een stukje assembler in de stack te gaan zitten wroeten. Niet een taak waar ik op zat te wachten. Het gevoel bekroop me dat dit probleem al door andere programmeurs eens opgelost was. En jawel, net toevallig op dat moment stelde iemand anders die vraag op comp.sys.acorn.programmer, en al rap waren er een paar verschillende antwoorden. Bovendien schoot het mij te binnen dat UnixLib ook een routine bevat om de backtrace te laten zien, en daarvan zijn de sources beschikbaar. Op basis daarvan en aan de hand van de tips in de nieuwsgroep had ik al snel een werkende routine voor het vergaren van de backtrace informatie.

Verder uitbouwen
Meer opgeslagen informatie betekent ook meer informatie om af te drukken. Een lijst functienamen op een regel kan erg breed worden. Tijd om wat aandacht te besteden aan de layout. Verder moesten de andere drie functies ook nog gemaakt worden. Ik sprong meteen in het diepe met de functie realloc(). Die heeft, afhankelijk van de parameters, de meest uiteenlopende effecten. Voor het correct interpreteren ervan was wat studie in de handboeken nodig. Wat moet er bijvoorbeeld gebeuren als de meegegeven pointer een NULL pointer is? Ook bleek dat de verschillende meldingen die het systeem kon doen in verschillende categorieën uiteenvallen. Sommige meldingen zijn ernstige fouten, andere meldingen slechts informatie. Uiteindelijk zijn er een stuk of acht niveaus van ernst uit gekomen.

Deze uitbreiding moest ook weer doorgevoerd worden in de malloc() functie. Door alles te centraliseren in een paar functies die de uitvoer verzorgen werd het een stuk overzichtelijker. Dit gaf meteen de mogelijkheid om de gewenste hoeveelheid uitvoer aan te passen aan de behoefte. Als bij iedere aanroep namelijk een volledige lijst van alle bekende blokken wordt afgedrukt, dan kan een programma dat normaal in een tel klaar is er wel een half uur over doen om alles naar een TaskWindow te spuien. Het is dan zinnig om de uitvoer eerst te beperken tot het melden van fouten, zodat snel de plaats van de fout achterhaald kan worden. De gebruiker kan daarna nog eens het programma draaien, met op strategische plaatsen aanroepen van MalCheck_setlevel(), om daarmee de hoeveelheid uitvoer te sturen.

Het afronden
Na de implementatie van realloc() waren de andere twee, free() en calloc(), nog slechts een formaliteit. Restte alleen nog het gelijktrekken van de verschillende functies, zodat alle ideeën en veranderingen die in de latere functies gestopt zijn ook in de eerdere gezet worden. Ook de layout moest nog verder worden bijgeschaafd, en wat overgebleven probleempjes met de verschillende niveaus van output worden opgelost (een foutmelding op 1 regel zonder de regel die beschrijft waar het over gaat is zo zinloos). In deze fase was het zeer nuttig dat alle source bestanden onder versiebeheer staan (zie http://www.rcs.riscos.org.uk/). Daarmee kan goed bijgehouden worden welke veranderingen er zijn gemaakt en waarom.

En dan denk je dat je klaar bent
Want op dit punt lukte het om met deze bibliotheek binnen 15 minuten de bug waar het allemaal om begonnen was op te sporen. Maar daar wilde ik het niet bij laten. Vanaf het begin was het mijn bedoeling om de vruchten van mijn arbeid met anderen te delen. Dat betekent dat er een pakket samengesteld moet worden dat niet alleen begrepen en gebruikt kan worden door degene die het zelf gemaakt heeft, maar ook door derden.

En dus moet er een handleiding komen, en een ReadMe voor de installatie. Alles moet bij elkaar in een zip pakketje, en tenslotte moet er een webpage komen met een beschrijving van het pakket en de mogelijkheid om te downloaden. En als alles eenmaal staat moet de boel getest door het zelf op te halen en toe te passen. Bij het ontwerp van het geheel heb ik zoveel mogelijk geprobeerd het systeem universeel te houden, om daarmee problemen met andere versies van RISC OS of ontbrekende modules te vermijden. En uiteindelijk is het ook zinnig als mensen weten dat het bestaat, dus dat wordt melden in toepasselijke nieuwsgroepen. Klinkt allemaal simpel, maar het is altijd meer werk dan je denkt.

Tenslotte
Wil je het resultaat bekijken of de handleiding of de (uitgebreide) FAQ lezen, kijk dan op deze link of het bijgevoegde bestand (dit is wel allemaal in het Engels).