Delphi: Access violation errors begrijpen en analyseren.
De CPU van een computer heeft een paar modi waarin hij kan werken, zoals real, protected en virtual mode. Wanneer u een modern besturingssysteem gebruikt, draait uw CPU in de beschermde modus. In deze modus gebruikt de CPU een tabel om te zien welke processen welke stukken geheugen op welke manieren kan benaderen. Dat betekent dat bijvoorbeeld een proces X rechten kan hebben om een deel van geheugen Y te lezen en schrijven, maar niet om uit te voeren. En tot een ander deel van het geheugen heeft het misschien helemaal geen toegangsrechten.
Dus wat gebeurt er als een applicatie probeert om toegang te krijgen tot een deel van het geheugen op een manier waar het geen rechten voor heeft? Nou, de CPU merkt dit op en “raised” (of “throwed” voor de C-mensen onder jullie) een applicatie error exception (of uitzondering) met type code $C0000005, wat “access violation” wordt genoemd. De engelse term “access violation” betekent toegangs-schending en geeft dus aan dat de applicatie geen toegang had tot het geheugen om te doen wat het probeerde te doen.
Na het raisen of throwen van een access violation kunnen er een paar dingen gebeuren. Er is een systeem van beschermende hekken opgezet met behulp van de stack om te voorkomen dat exceptions het besturingssysteem bereiken. In Delphi kan dit worden gedaan door de trefwoorden try..finaly of try..except te gebruiken. Met behulp van de laatste kan de applicatie iets doen om de exception te “vangen” en te verwerken. Wanneer dit niet gebeurt, zal het besturingssysteem het uiteindelijk opvangen, de applicatie stoppen, het geheugen opschonen en een bericht tonen dat de applicatie is gestopt vanwege een fout.
Om te voorkomen dat de applicatie zomaar verdwijnt, vangen Delphi-applicaties de exception op vlak voordat deze naar het besturingssysteem gaat, geven een foutmelding en proberen de normale werking voort te zetten.
Er zijn een paar zeer waardevolle dingen die u van deze foutmelding kunt leren om de oorzaak te analyseren en het probleem op te lossen. Laten we een paar voorbeelden van access violation-foutmeldingen analyseren:
1. Access violation at address 5005FD2C in module ‘rtl260.bpl’. Read of address 00000000.
Hier trad de exception op tijdens het uitvoeren van code op geheugenlocatie 5005FD2C, wat eruitziet als een normale locatie waar de code van een applicatie zich zou kunnen bevinden. We moeten ook opmerken dat het gebeurde in Delphi’s runtime-library (rtl260.bpl). Het systeem vertelt ons niet altijd in welke module het gebeurde, maar in dit geval gelukkig wel. Nu betekent het feit dat de exception optrad in rtl260.bpl niet dat er een bug in de runtime-library zit. Ergens in de applicatie gebeurde er iets waardoor de runtime-library op een gegeven moment gedwongen werd om iets te proberen wat de hardware niet toestaat of letterlijk niet kan doen.
Om meer te weten te komen, moeten we naar de rest van het bericht kijken, waarin staat “read of address 00000000”. Dat adres is erg interessant. Wanneer een pointer (zoals een variabele die naar een object wijst) op nil wordt gezet, betekent dit dat de werkelijke waarde nul wordt, wat in 8-cijferige hexadecimale notatie 00000000 is. U kunt het zelf proberen te zien door nil te casten naar een integer en het op het scherm te zetten met ShowMessage() bijvoorbeeld. Die 00000000 geeft ons dus een aanwijzing dat een variabele hier nil was, wat de programmeur die de code schreef die de access violation veroorzaakte, blijkbaar niet had verwacht.
De beste manier om erachter te komen wat de oorzaak was, is om te proberen de fout te reproduceren terwijl u de toepassing uitvoert in een debugger. Wanneer de exception hopelijk opnieuw optreedt tijdens het debuggen, kunt u de uitvoering pauzeren en kijken naar de code waarin de exception is gegenereerd. Omdat het nu in de runtime library is gebeurd, is die code misschien niet zo begrijpelijk, maar u kunt omhoog in de stack gaan om een manier te vinden om terug te keren naar uw code. Zoek naar een aanroep van een functie in de runtime library waar uw code nil aan doorgeeft. Het is ook mogelijk dat uw code een functie in de visuele component library (VCL) heeft aangeroepen en een object heeft doorgegeven dat een variabele bevatte die op nil was ingesteld.
2. Access violation at address 00000000. Read of address 00000000.
Dit is een interessante fout. Het lijkt erop dat de applicatie op adres 0 code aan het uitvoeren was, maar hoe is dat mogelijk? Je zou kunnen denken dat adres 0 in het geheugen de kernel is, maar in werkelijkheid wordt adres 0 gewoon door niets gebruikt. Omdat adressen zoals ze door een applicatie worden gebruikt virtueel zijn en niet hetzelfde als een fysieke locatie in het daadwerkelijke hardwaregeheugen, wordt aan het virtuele adres 0 niet eens een fysieke locatie toegewezen. Maakt dat het dan niet nog vreemder dat het op een adres draaide dat niet eens in het fysieke geheugen bestaat? Het lijkt misschien zo.
Eerst moet je je realiseren dat het niet echt instructies op adres 0 uitvoerde. Het probeerde dat te doen. Vervolgens moet je bedenken dat de destructor “Destroy” van een object een virtuele methode is. Dus wat gebeurt er als je een variabele hebt die naar een object wijst en je roept Free() er twee keer op aan? Je zou kunnen denken dat na de eerste keer, omdat het object vernietigd is, de code niet meer in het geheugen staat, zodat toen het daarheen probeerde te gaan, het in dit “niemandsland” in het geheugen terechtkwam. Maar als je de assemblycode en data voor een object in het geheugen analyseert, kom je erachter dat de code nooit wordt gedupliceerd bij het maken van een object. Bij het maken van een normaal object is het enige dat in het geheugen wordt opgeslagen de data voor dat object is. De code voor alle objecten van hetzelfde type wordt gedeeld door al deze objecten. Maar er is één stukje data waar veel mensen zich niet van bewust zijn, namelijk de virtuele methodetabel. Dit is een stukje administratieve data dat het systeem het adres van alle virtuele methoden vertelt. Dit maakt het mogelijk om een variabele van het type TObject te hebben, maar deze te gebruiken om te verwijzen naar een StringList object, en toch de juiste destructor uit te voeren bij het vrijgeven ervan. Er is een entry in de virtuele methodetabel die verwijst naar welke destructor moet worden gebruikt.
Om dit probleem op te lossen, moet je kijken wie een object “bezit”. Elke creatie moet worden gekoppeld aan precies één free. Iets dat hierbij erg kan helpen, is het gebruik van de functie FreeAndNil(x), die de waarde van x verandert in nil, zodat Free() bij een nieuwe poging ziet dat self == nil is en afbreekt in plaats van de destructor aan te roepen, waardoor het probleem wordt voorkomen. Maar het is handig om te weten dat FreeAndNil() geen echte oplossing is. Het proberen om hetzelfde object een miljoen keer te bevrijden kost extra verwerkingstijd, wat een complete verspilling is van systeembronnen. Dat gezegd hebbende, vind ik dat FreeAndNil() het extra beetje verwerking waard is om problemen te voorkomen. Een reden waarom het gerechtvaardigd is om FreeAndNil() te gebruiken, is omdat het meer dan eens bevrijden van een object meer nare bijwerkingen kan hebben, zoals geheugenbeschadiging, wat leidt tot gegevensbeschadiging, wat, als je echt pech hebt, zelfs kan leiden tot reputatieschade voor je bedrijf.
3. Access violation at address 5005FD2C. Read of address 5005FD2C.
Ik geloof dat dit een lastige is. Het adres waar de exception optrad, lijkt geldig, maar het adres waarvan werd gelezen, was hetzelfde adres, wat het uiteindelijk een ongeldig adres maakt om van te lezen, anders was het geen overtreding geweest. Maar tenzij er geheugencorruptie is opgetreden, moet het op een gegeven moment een geldig adres zijn geweest. Dat geeft een paar mogelijke situaties:
- Er is geheugencorruptie opgetreden, waardoor de waarde van een pointer naar een functie is gewijzigd. Toen die pointer naar een functie werd gebruikt, probeerde de applicatie een jump uit te voeren naar een ongeldig adres, wat resulteerde in deze access violation.
- Er was geen geheugencorruptie en het adres was geldig, maar dat is het niet meer, omdat de code op die locatie uit het geheugen is verwijderd. Dit kan gebeuren wanneer een pointer naar een functie is gebruikt en die functie zich in een library bevond die dynamisch tijdens runtime werd geladen, waarna de pointer naar de functie werd gemaakt, de library werd ontladen en de toepassing vervolgens probeerde de functie aan te roepen die niet meer in het geheugen aanwezig was. Dit kan ook gebeuren wanneer de functie een lidfunctie (of procedure) is van een klasse uit een dynamisch geladen library (of pakket).
- De applicatie gebruikt een geavanceerde techniek genaamd self-modifying code, wat buiten het bereik van dit artikel valt. Maar om samen te vatten, er was een pointer naar een functie die tijdens runtime werd gegenereerd in dynamisch geheugen met uitvoeringsrecht, maar nadat het dynamische geheugen werd vrijgegeven, werd de pointer niet gereset naar nil en probeerde iets de gegenereerde code aan te roepen die niet langer beschikbaar is.
4. Access violation at address 016FE0B0. Execution of address 016FE0B0.
Hier probeerde de applicatie code uit te voeren op het aangegeven adres. Let op, er stond niets over lezen op dat adres, dus dat betekent dat de applicatie de rechten had om te lezen vanaf die locatie, waardoor de pointer een potentieel geldige pointer was, maar niet mocht worden gebruikt voor het uitvoeren van instructies.
Dit kan gebeuren wanneer de applicatie self-modifying code gebruikt om wat machinecode te genereren in dynamisch geheugen en dan naar dat stuk geheugen springt om de gegenereerde machinecode uit te voeren. In Windows is een optie om te controleren of processen uitvoeringsrechten hebben op het gebied van geheugen waarop ze code proberen uit te voeren, wat een goed idee is om standaard aan te hebben als extra beveiligingsmaatregel tegen virussen, spyware, randsomware en hackers. Let op dat niet alle software hiermee compatibel is en dat sommige applicaties fout kunnen gaan bij het inschakelen hiervan. Maar het zouden slechts enkele applicaties moeten zijn.
Maar deze access violation lijkt erop dat dat is wat er is gebeurd. De optie was ingeschakeld, de applicatie heeft wat dynamisch geheugen toegewezen, wat machinecode daar geplaatst en geprobeerd ernaartoe te springen, maar vergat om te vragen om uitvoeringsrechten op dat stukje geheugen. Een andere mogelijkheid is dat er geheugen corruptie is opgetreden en een pointer naar een functie is overschreven met data, zodat de applicatie op een gegeven moment een sprong heeft uitgevoerd naar het aangegeven adres en dat de applicatie wel lees rechten had op dat adres maar geen uitvoer rechten. Een derde mogelijkheid is dat een kwaadwillende partij een manier heeft gevonden om via invoer uitvoerbare instructies in een stukje geheugen heeft weten te plaatsen en een sprong naar dat stukje geheugen heeft weten te doen, maar dat de applicatie (gelukkig) geen uitvoer rechten had op dat stuk geheugen. Dit is een vorm van misbruik waartegen deze beveiliging precies in het leven is geroepen.
Krijg je dan altijd een access violation exception als geheugen toegang niet goed gaat?
Nee, helaas niet. Wanneer een pointer corrupt raakt of u op een onbedoelde manier het geheugen gebruikt, maar uw applicatie nog wel het recht had om dat geheugen te gebruiken op de manier waarop het dat probeerde, krijgt u geen exception, ook al zijn de gegevens die u ervan krijgt waarschijnlijk rommel.
Zie je, als je een beetje dynamisch geheugen wilt, krijg je niet altijd precies de hoeveelheid die je hebt aangevraagd. De memory manager (een stukje software in de Delphi run time library dat dynamisch geheugen beheert) wijst meestal een blok geheugen toe, zoals 2k, 8k, 16k of zoiets. De memory manager houdt bij wat je applicatie heeft aangevraagd. En als je meerdere stukken geheugen aanvraagt (zeg 2 blokken van 8 bytes), krijg je mogelijk een pointer voor elk daarvan die binnen dezelfde 8 kilobytes dynamisch geheugen valt die de memory manager heeft toegewezen aan je applicatie. Als je niet alle aangevraagde stukken “vrijgeeft”, kan dat blok geheugen in gebruik blijven. Als je twee pointers krijgt voor twee blokken van 8 bytes, is het mogelijk dat deze twee blokken naast elkaar staan in de adresruimte, bijvoorbeeld van $10000000 tot $1000000F, voor een totaal van 16 bytes. Wanneer u vervolgens de eerste pointer gebruikt en er 10 bytes naartoe schrijft, overschrijft u de eerste twee bytes van het tweede blok, waardoor de gegevens voor het tweede blok worden beschadigd. Dit is waarschijnlijk niet wat u bedoelde, maar omdat uw applicatie het recht heeft om dat hele blok geheugen te gebruiken, krijgt u er geen access violation voor.
Uiteindelijk komt het er altijd op neer dat je weet wat je met het geheugen doet. Voordat je TStringList.Create schrijft, denk je na over welk deel van de applicatie verantwoordelijk is voor het vrijgeven van dat object. Meestal is het een goed idee om het object dat iets creëert, ook verantwoordelijk te maken voor het vrijgeven ervan. Maar bijvoorbeeld in het geval van een factory patroon, werkt dat niet, dus je zult moeten bedenken waar die verantwoordelijkheid dan naartoe gaat.