4 veelvoorkomende programmeerfouten in C - en 5 tips om ze te vermijden

Er zijn maar weinig programmeertalen die kunnen tippen aan C voor pure snelheid en kracht op machineniveau. Deze bewering was 50 jaar geleden waar, en het is nog steeds waar. Er is echter een reden waarom programmeurs de term 'footgun' hebben bedacht om de kracht van C te beschrijven. Als je niet oppast, kan C je tenen afblazen - of die van iemand anders.

Hier zijn vier van de meest voorkomende fouten die u kunt maken met C, en vijf stappen die u kunt nemen om ze te voorkomen.

Veelgemaakte C-fout: mallocgeheugen niet vrijmaken (of het meer dan eens vrijmaken)

Dit is een van de grote fouten in C, waarvan er vele betrekking hebben op geheugenbeheer. Toegewezen geheugen (gedaan met behulp van de malloc functie) wordt niet automatisch weggegooid in C. Het is de taak van de programmeur om dat geheugen te verwijderen als het niet langer wordt gebruikt. Als u geen herhaalde geheugenverzoeken kunt vrijmaken, krijgt u een geheugenlek. Probeer een geheugengebied te gebruiken dat al vrijgemaakt is, en je programma zal crashen - of, erger nog, zal hinken en kwetsbaar worden voor een aanval met dat mechanisme.

Merk op dat een geheugen lek alleen situaties waarin geheugen zou moeten beschrijven verondersteld om bevrijd te worden, maar is het niet. Als een programma geheugen blijft toewijzen omdat het geheugen echt nodig is en wordt gebruikt voor werk, dan kan het geheugengebruik  inefficiënt zijn , maar strikt genomen is het geen lek.

Veelgemaakte C-fout: een array lezen buiten het bereik

Hier hebben we nog een van de meest voorkomende en gevaarlijke fouten in C. Een lees voorbij het einde van een array kan garbage data opleveren. Een schrijven voorbij de grenzen van een array kan de status van het programma beschadigen of volledig laten crashen, of, het ergste van alles, een aanvalsvector voor malware worden.

Dus waarom wordt de last van het controleren van de grenzen van een array overgelaten aan de programmeur? In de officiële C-specificatie is het lezen of schrijven van een array buiten zijn grenzen "ongedefinieerd gedrag", wat betekent dat de specificatie geen zeggenschap heeft over wat er moet gebeuren. De compiler hoeft er niet eens over te klagen.

C geeft er al lang de voorkeur aan om de programmeur zelfs op eigen risico macht te geven. Lezen of schrijven buiten het bereik wordt meestal niet door de compiler opgesloten, tenzij u specifiek compileropties inschakelt om u ertegen te beschermen. Bovendien is het misschien wel mogelijk om tijdens runtime de grens van een array te overschrijden op een manier waar zelfs een compilercontrole niet tegen kan.

Veelgemaakte C-fout: de resultaten van malloc

malloc en calloc (voor vooraf op nul gezet geheugen) zijn de C-bibliotheekfuncties die heap-toegewezen geheugen van het systeem verkrijgen. Als ze geen geheugen kunnen toewijzen, genereren ze een fout. In de tijd dat computers relatief weinig geheugen hadden, was de kans groot dat een telefoontje naar mallocniet zou lukken.

Hoewel computers tegenwoordig gigabytes aan RAM hebben om rond te gooien, is er nog steeds een kans dat het mallockan mislukken, vooral onder hoge geheugendruk of bij het in één keer toewijzen van grote hoeveelheden geheugen. Dit geldt in het bijzonder voor C-programma's die eerst een groot blok geheugen van het besturingssysteem "slab-alloceren" en dit vervolgens voor eigen gebruik verdelen. Als die eerste toewijzing mislukt omdat deze te groot is, kun je die weigering misschien vangen, de toewijzing verkleinen en de heuristiek van het geheugengebruik van het programma dienovereenkomstig afstemmen. Maar als de geheugentoewijzing mislukt, kan het hele programma op hol slaan.

Veelgemaakte C-fout: gebruiken void*voor algemene verwijzingen naar het geheugen

Het gebruiken  void* om naar het geheugen te wijzen is een oude gewoonte - en een slechte. Pointers naar het geheugen moet altijd char*, unsigned char*of  uintptr_t*. Moderne C-compilersuites zouden uintptr_tals onderdeel van stdint.h

Als het op een van deze manieren wordt gelabeld, is het duidelijk dat de aanwijzer verwijst naar een geheugenlocatie in de samenvatting in plaats van naar een ongedefinieerd objecttype. Dit is dubbel belangrijk als u aanwijzerberekeningen uitvoert. Met  uintptr_t*en dergelijke is het maatelement waarnaar wordt verwezen, en hoe het zal worden gebruikt, ondubbelzinnig. Met void*niet zo veel.

Veelvoorkomende C-fouten vermijden - 5 tips

Hoe voorkom je deze maar al te vaak voorkomende fouten bij het werken met geheugen, arrays en verwijzingen in C? Houd deze vijf tips in gedachten. 

Structuur C-programma's zodat het eigendom voor het geheugen duidelijk blijft

Als u net een C-app start, is het de moeite waard om na te denken over de manier waarop geheugen wordt toegewezen en vrijgegeven als een van de organisatorische principes voor het programma. Als het onduidelijk is waar een bepaalde geheugentoewijzing wordt vrijgemaakt of onder welke omstandigheden, vraag je om problemen. Doe de extra moeite om het eigendom van het geheugen zo duidelijk mogelijk te maken. Je doet jezelf (en toekomstige ontwikkelaars) een plezier.

Dit is de filosofie achter talen zoals Rust. Rust maakt het onmogelijk om een ​​programma te schrijven dat correct compileert, tenzij je duidelijk aangeeft hoe geheugen eigendom is en wordt overgedragen. C heeft dergelijke beperkingen niet, maar het is verstandig om die filosofie waar mogelijk als leidraad te nemen.

Gebruik C-compileropties die beschermen tegen geheugenproblemen

Veel van de problemen die in de eerste helft van dit artikel worden beschreven, kunnen worden gemarkeerd door strikte compileropties te gebruiken. Recente edities van gccbijvoorbeeld bieden tools zoals AddressSanitizer ("ASAN") als een compilatie-optie om te controleren op veelvoorkomende fouten in geheugenbeheer.

Wees gewaarschuwd, deze tools vangen niet alles op. Het zijn vangrails; ze grijpen het stuur niet als je offroad gaat. Sommige van deze tools, zoals ASAN, brengen ook compilatie- en runtime-kosten met zich mee, dus moeten worden vermeden in release-builds.

Gebruik Cppcheck of Valgrind om C-code te analyseren op geheugenlekken

Waar de compilers zelf tekortschieten, grijpen andere tools in om de leemte op te vullen - vooral als het gaat om het analyseren van programmagedrag tijdens runtime.

Cppcheck voert statische analyse uit op C-broncode om te zoeken naar veelvoorkomende fouten in geheugenbeheer en ongedefinieerd gedrag (onder andere).

Valgrind biedt een cache met tools om geheugen- en threadfouten in actieve C-programma's te detecteren. Dit is veel krachtiger dan het gebruik van analyse tijdens compilatie, omdat u informatie kunt afleiden over het gedrag van het programma wanneer het daadwerkelijk live is. Het nadeel is dat het programma op een fractie van de normale snelheid draait. Maar dit is over het algemeen prima om te testen.

Deze tools zijn geen zilveren kogels en ze zullen niet alles vangen. Maar ze werken als onderdeel van een algemene verdedigingsstrategie tegen slecht geheugenbeheer bij C.

Automatiseer C-geheugenbeheer met een garbage collector

Aangezien geheugenfouten een opvallende bron van C-problemen zijn, is hier een eenvoudige oplossing: beheer het geheugen in C niet handmatig. Gebruik een vuilnisman. 

Ja, dit is mogelijk in C. U kunt zoiets als de Boehm-Demers-Weiser garbage collector gebruiken om automatisch geheugenbeheer toe te voegen aan C-programma's. Voor sommige programma's kan het gebruik van de Boehm-collector de zaken zelfs versnellen. Het kan zelfs worden gebruikt als lekdetectiemechanisme.

Het belangrijkste nadeel van de Boehm garbage collector is dat het geen geheugen kan scannen of vrijmaken dat de standaard gebruikt malloc. Het gebruikt zijn eigen toewijzingsfunctie en het werkt alleen op geheugen dat u er specifiek aan toewijst.

Gebruik C niet als een andere taal voldoende is

Sommige mensen schrijven in C omdat ze er echt van genieten en het vruchtbaar vinden. Over het algemeen is het echter het beste om C alleen te gebruiken als het moet, en dan slechts spaarzaam, voor de weinige situaties waarin het echt de ideale keuze is.

Als je een project hebt waarbij de uitvoeringsprestaties voornamelijk worden beperkt door I / O of schijftoegang, zal het schrijven in C het waarschijnlijk niet sneller maken op de manieren die ertoe doen, en zal het waarschijnlijk alleen maar meer foutgevoelig en moeilijker maken in stand houden. Hetzelfde programma zou heel goed in Go of Python kunnen worden geschreven.

Een andere benadering is om C alleen te gebruiken voor de echt prestatie-intensieve delen van de app, en een betrouwbaardere, zij het langzamere taal voor andere delen. Nogmaals, Python kan worden gebruikt om C-bibliotheken of aangepaste C-code in te pakken, waardoor het een goede keuze is voor de meer standaardcomponenten, zoals het afhandelen van opdrachtregelopties.