Cover
Börja nu gratis 5_Lecture_5.pdf
Summary
# Het belang van geheugenonderzoek
Het begrijpen van geheugenconcepten is essentieel, zelfs wanneer men werkt met hoog-niveau programmeertalen, omdat dit een dieper inzicht biedt in hoe programma's geheugen beheren en variabelen organiseren [3](#page=3).
### 1.1 De kosten van abstractie
Moderne hoog-niveau programmeertalen zoals Python en JavaScript bieden veel abstracties die het programmeren beginnersvriendelijker maken. Een van de belangrijkste abstracties is het verwijderen van handmatig geheugenbeheer, wat betekent dat de programmeur zich niet direct zorgen hoeft te maken over het toewijzen en vrijgeven van geheugen. Echter, dit abstractieniveau verbergt de onderliggende mechanismen [3](#page=3).
#### 1.1.1 Vragen over geheugenbeheer
Zelfs met deze abstracties blijven er fundamentele vragen bestaan over hoe programma's werken:
* Hoe maakt een programma ruimte vrij voor nieuwe variabelen, zoals gebruikersinvoer [4](#page=4)?
* In welke volgorde worden deze variabelen opgeslagen [4](#page=4)?
* Hoe weet een programma wat het moet doen, in welke volgorde en met welke variabelen [4](#page=4)?
Om deze vragen te beantwoorden en een dieper begrip te krijgen, is het noodzakelijk om verder te kijken dan de abstracties van hoog-niveau talen [4](#page=4).
### 1.2 Van abstractie naar dieper begrip
Om de kernconcepten van geheugenbeheer te illustreren, zal er gebruik worden gemaakt van codevoorbeelden in C. C biedt minder abstracties en directe toegang tot pointers, waardoor handmatig geheugenbeheer met `malloc` en `free` mogelijk is. Hoewel het niet verwacht wordt dat studenten C-code kunnen schrijven, zal het begrijpen van de C-concepten enorm helpen bij het doorgronden van geheugenbeheer. Een optionele gids om C te leren zal beschikbaar worden gesteld [5](#page=5).
> **Tip:** Zelfs als je voornamelijk in hoog-niveau talen programmeert, kan het bestuderen van geheugenbeheer in talen als C je in staat stellen om efficiëntere en minder foutgevoelige code te schrijven in je gebruikelijke programmeertalen.
> **Voorbeeld:** Stel je voor dat je een applicatie bouwt die grote hoeveelheden gebruikersdata moet opslaan. Zonder begrip van hoe dit geheugen wordt toegewezen en beheerd, kan je applicatie onnodig traag worden of zelfs crashen bij het verwerken van veel data, zelfs als de programmeertaal het "automatisch" zou moeten regelen. Door geheugenconcepten te begrijpen, kun je dit soort problemen proactief voorkomen.
---
# Pointers en arrays in C
Dit hoofdstuk introduceert de fundamentele concepten van pointers en arrays in C, hun syntaxis, en hoe ze interageren met geheugenbeheer, inclusief het onderscheid tussen pass-by-value en pass-by-reference [7](#page=7).
### 2.1 Pointers
Een pointer is een variabele die een geheugenadres opslaat. In plaats van de daadwerkelijke data te bevatten, verwijst een pointer naar de locatie waar die data zich bevindt in het geheugen [8](#page=8).
#### 2.1.1 Pointer syntaxis
#### 2.1.1.1 Definiëren van een pointer
Een pointer wordt gedefinieerd met behulp van de `*` operator, die aangeeft dat de variabele een pointer is naar een specifiek datatype. De `&` operator wordt gebruikt om het geheugenadres van een bestaande variabele op te halen [9](#page=9).
De algemene syntaxis is:
```c
int *pointer_to_target = ⌖
```
Hierin betekent `int *` dat `pointer_to_target` een pointer is naar een `int`. `&target` is het geheugenadres van de variabele `target` [9](#page=9).
#### 2.1.1.2 Gebruiken en derefereren van een pointer
Om de waarde die op het adres van een pointer wordt opgeslagen te benaderen, wordt de `*` operator opnieuw gebruikt. Dit proces staat bekend als "dereferencing" [10](#page=10).
Een voorbeeld van het gebruik van een pointer:
```c
int target = 10;
int *pointer_to_target = ⌖
printf("Pointer address: %p\n", pointer_to_target); // Print het adres dat de pointer bevat
printf("Value stored: %d\n", *pointer_to_target); // Print de waarde op het adres waarnaar de pointer verwijst
```
Het printf-statement voor de pointer zal het geheugenadres weergeven, terwijl `*pointer_to_target` de waarde van `target` (in dit geval 10) zal afdrukken [10](#page=10).
#### 2.1.2 Functieaanroepen met pointers
##### 2.1.2.1 Pass-by-value
Bij pass-by-value wordt een kopie van de waarde van een variabele naar een functie doorgegeven. Dit betekent dat wijzigingen binnen de functie geen invloed hebben op de oorspronkelijke variabele buiten de functie. Het maken van kopieën kan traag zijn en extra geheugenruimte innemen [11](#page=11).
##### 2.1.2.2 Pass-by-reference
Bij pass-by-reference wordt in plaats van de waarde, een pointer (het geheugenadres) naar de variabele naar de functie doorgegeven. Hierdoor kunnen wijzigingen die binnen de functie aan de variabele via de pointer worden aangebracht, de oorspronkelijke variabele buiten de functie direct beïnvloeden. Deze methode is sneller en de grootte van de doorgegeven parameter is altijd constant (de grootte van een adres), ongeacht het datatype [12](#page=12).
#### 2.1.3 Speciale pointers
##### 2.1.3.1 De NULL pointer
Een NULL-pointer is een pointer die naar geen enkel geldig geheugenadres verwijst. Het wijst "nergens" naar en wordt gebruikt om aan te geven dat een pointer geen geldig object adresseert. Dit helpt ongewenst gedrag en potentiële crashes te voorkomen [13](#page=13).
##### 2.1.3.2 De Void pointer
Een void-pointer is een generieke pointer die naar een object van een ongespecificeerd datatype kan verwijzen. Om de data te kunnen gebruiken waarnaar een void-pointer verwijst, moet deze eerst expliciet naar het juiste datatype worden gecast (omgezet). Dit is met name relevant bij geheugenallocatie functies zoals `malloc` [13](#page=13).
### 2.2 Arrays
Een array is een verzameling van elementen van hetzelfde datatype. In tegenstelling tot sommige dynamische talen zoals Python of JavaScript, hebben arrays in C een fixed size en kunnen ze geen elementen van verschillende types bevatten [14](#page=14).
#### 2.2.1 Arrays als geheugenblokken
Arrays worden opgeslagen als een aaneengesloten blok geheugen. Als je bijvoorbeeld een array van 5 integers declareert (`int p[] = {23, 38, 56, 69, 75};`), reserveert het systeem een geheugenblok dat groot genoeg is voor 5 keer de grootte van een integer. De naam van de array, zoals `p`, functioneert in essentie als een pointer naar het begin van dit geheugenblok [15](#page=15).
#### 2.2.2 Array-notatie en pointers
De array-notatie die we gebruiken, zoals `arr `, is in C direct gerelateerd aan pointer-arithmetiek. De expressie `arr ` is equivalent aan het derefereren van het adres dat twee posities na het begin van de array ligt: `*(arr + 2)`. Dit betekent dat het systeem begint bij het eerste adres van de array, er twee keer de grootte van het datatype bij optelt, en vervolgens de waarde op die locatie ophaalt. Strings in C zijn eveneens een speciaal geval van karakter-arrays [16](#page=16) [2](#page=2).
#### 2.2.3 Belangrijke overwegingen bij arrays
* **fixed size:** Arrays in C zijn niet dynamisch en kunnen niet tijdens runtime groeien, wat verschilt van talen zoals Python en JavaScript. Het creëren van een grotere array vereist de allocatie van een nieuw geheugenblok en het kopiëren van alle bestaande elementen, wat inefficiënt kan zijn [17](#page=17).
* **Geen automatische bounds checking:** C voert geen automatische controle uit of een toegangspoging binnen de grenzen van de array valt. Dit betekent dat code zoals `int arr; arr = 67;` technisch zal compileren en uitvoeren, maar zal leiden tot een "buffer overflow". Dit kan leiden tot onvoorspelbaar gedrag en beveiligingsrisico's [10](#page=10) [17](#page=17) .
---
# De stack en de heap
Dit onderwerp introduceert de twee primaire geheugenregio's die door programma's worden gebruikt: de stack voor functieaanroepen en lokale variabelen, en de heap voor dynamisch toegewezen geheugen. Het beschrijft hun werking, structuur en de uitdagingen die ze met zich meebrengen [19](#page=19).
### 3.1 De stack
De stack is een geheugenregio die wordt gecreëerd wanneer een programma start. Het organiseert gegevens volgens het Last-In-First-Out (LIFO) principe. Dit betekent dat het laatste item dat aan de stack wordt toegevoegd, als eerste wordt verwijderd (een "pop") [21](#page=21).
#### 3.1.1 Stackframes
Elke functieaanroep genereert een eigen "stack frame" op de stack. Een stack frame bevat [21](#page=21):
* De argumenten van de functie [22](#page=22).
* Lokale variabelen van de functie [22](#page=22).
* Het retouradres (return address), dat de CPU vertelt waar de uitvoering moet worden hervat na het voltooien van de functie [22](#page=22).
De "stack pointer" is een register dat altijd wijst naar de top van de stack, zodat de CPU weet welke instructies de volgende moeten zijn om uit te voeren [21](#page=21).
> **Tip:** Het retouradres is cruciaal zodat de CPU weet waar naartoe terug te keren na het voltooien van een functie [22](#page=22).
#### 3.1.2 Beperkingen van de stack
Een belangrijke beperking van de stack is dat alle data-groottes, zoals arrays, bekend moeten zijn op het moment van compilatie. Dit wordt een probleem wanneer [22](#page=22):
* Men gegevens van variabele grootte wil opslaan, zoals gebruikersinvoer [24](#page=24).
* Tijdens de runtime blijkt dat iets groter moet worden dan oorspronkelijk gepland [24](#page=24).
### 3.2 De heap
De heap is een minder gestructureerd geheugengebied dat kan groeien naarmate er meer data wordt toegewezen. Omdat de heap geen strikte volgorde heeft, worden gegevens gevonden met behulp van pointers. Wanneer data op de heap wordt geplaatst, moet het adres ervan worden opgeslagen in een pointer [25](#page=25).
#### 3.2.1 Dynamische geheugenallocatie
De heap wordt gebruikt voor dynamisch toegewezen geheugen, wat betekent dat geheugen tijdens de runtime kan worden aangevraagd en uitgebreid indien nodig [25](#page=25).
#### 3.2.2 Toewijzen van geheugen in C
In talen zoals C wordt geheugen op de heap toegewezen met behulp van functies zoals `malloc()` [26](#page=26).
* `malloc()`: Deze functie allokeert een specifiek aantal bytes geheugen, bepaald door de `sizeof` operator, en retourneert een `void` pointer naar dit geheugen [26](#page=26).
```c
int *heap_allocated = (int*) malloc(sizeof(int));
```
* Het `void` pointer moet worden "gecast" naar het juiste datatype (bijvoorbeeld `int*`) omdat C expliciet typering vereist [26](#page=26).
* De pointer die naar deze data op de heap verwijst, bevindt zich zelf op de stack [26](#page=26).
#### 3.2.3 Geheugenlekken (Memory Leaks)
Geheugen dat op de heap is toegewezen, wordt gemarkeerd als "in gebruik" door het besturingssysteem. Dit geheugen moet handmatig worden vrijgegeven wanneer het niet meer nodig is om geheugenlekken te voorkomen. Dit gebeurt met de `free()` functie [27](#page=27).
> **Belangrijk:** Elke `malloc()` aanroep moet een bijbehorende `free()` aanroep hebben [27](#page=27).
>
> **Voorbeeld:**
> ```c
> int* num_ptr = (int *) malloc(sizeof(int));
> // Gebruik num_ptr...
> free(num_ptr); // Vrijgeef het toegewezen geheugen
> ```
---
# Risico's van handmatig geheugenbeheer en oplossingen
Handmatig geheugenbeheer, zoals toegepast in talen als C, biedt hoge prestaties maar brengt aanzienlijke risico's met zich mee die kunnen leiden tot crashes, data corruptie en beveiligingslekken. Het correct beheren van geheugen vereist discipline en kan zelfs voor ervaren ontwikkelaars uitdagend zijn [30](#page=30).
### 4.1 Valkuilen van handmatig geheugenbeheer
#### 4.1.1 Geheugenlekken (Memory Leaks)
Een geheugenlek ontstaat wanneer toegewezen geheugen niet wordt vrijgegeven nadat het niet langer nodig is. Dit leidt tot een gestage toename van het geheugengebruik van een applicatie, wat uiteindelijk kan resulteren in prestatieproblemen of het crashen van het systeem door geheugenuitputting. De gouden regel is om altijd alle toegewezen geheugen vrij te geven [31](#page=31).
#### 4.1.2 Dubbele frees (Double Free)
Het vrijgeven van hetzelfde geheugengebied meer dan eens is een ernstige fout die kan leiden tot crashes en beveiligingsrisico's. Wanneer geheugen wordt vrijgegeven, wordt het gemarkeerd voor hergebruik. Een dubbele frees kan de interne datastructuren van de geheugenbeheerder beschadigen, wat kan leiden tot onvoorspelbaar gedrag en kwetsbaarheden die aanvallers kunnen exploiteren om willekeurige code uit te voeren [32](#page=32).
#### 4.1.3 Gebruik-na-frees (Use-After-Free)
Een "dangling pointer" ontstaat wanneer een pointer nog steeds verwijst naar een geheugengebied dat reeds is vrijgegeven of buiten bereik is. Het gebruik van zo'n pointer na het vrijgeven van het geheugen kan leiden tot het overschrijven of lekken van data, crashes of het manipuleren van het vrijgegeven geheugen om kwaadaardige code te injecteren [33](#page=33).
#### 4.1.4 Buffer overflows
Programmatalen zonder automatische grensprotocollen, zoals C, zijn vatbaar voor buffer overflows. Dit gebeurt wanneer er meer data wordt geschreven naar een buffer dan deze kan bevatten, waardoor data buiten de toegewezen ruimte wordt overschreven. Onveilige functies zoals `strcpy` en `memcpy` vergroten dit risico. Aanvallers kunnen dit misbruiken om kwaadaardige code te injecteren door de controlestroom van het programma te overschrijven [34](#page=34).
### 4.2 Oplossingen en best practices
#### 4.2.1 Best practices
Om de risico's van handmatig geheugenbeheer te minimaliseren, zijn er verschillende best practices:
* **Gebruik expliciete groottes:** Zorg ervoor dat je altijd expliciete groottes gebruikt bij het toewijzen van geheugen [36](#page=36).
* **Nullify pointers na vrijgave:** Stel pointers in op `NULL` na het vrijgeven om accidenteel hergebruik te voorkomen [36](#page=36).
* **Vermijd gevaarlijke API's:** Wees voorzichtig met en vermijd functies die vatbaar zijn voor buffer overflows, zoals `strcpy`. Let ook goed op compilerwaarschuwingen [36](#page=36).
* **Lees documentatie:** "RTFM" (Read The F\*\*\*ing Manual) blijft een relevante, zij het onbeschofte, aanbeveling om de werking van geheugenbeheer te begrijpen [36](#page=36).
#### 4.2.2 Slimme pointers (Smart Pointers) in C++
In C++11 werden slimme pointers geïntroduceerd als een abstractielaag boven `new` en `delete` [37](#page=37).
* **Unieke pointers (`std::unique_ptr`):** Deze hebben één eigenaar. Wanneer de eigenaar buiten scope gaat, wordt het geheugen automatisch vrijgegeven [37](#page=37).
* **Gedeelde pointers (`std::shared_ptr`):** Deze ondersteunen meerdere eigenaren via een referentieteller. Het geheugen wordt vrijgegeven zodra er geen verwijzingen meer zijn. Hoewel ze het geheugenbeheer vergemakkelijken, verhogen ze ook de complexiteit van de taal [37](#page=37).
#### 4.2.3 Hulpmiddelen (Tools)
Verschillende tools en compileropties kunnen helpen bij het detecteren van geheugenfouten:
* **Compilers:** Met de juiste vlaggen kunnen compilers waarschuwingen geven voor potentieel onveilige code [39](#page=39).
* **Sanitizers:** Deze runtime tools kunnen geheugenfouten en gedefinieerd gedrag detecteren tijdens de uitvoering van een programma [39](#page=39).
* **Valgrind:** Een populaire tool die geheugenlekken, gebruik-na-frees fouten en andere heap-gerelateerde bugs kan identificeren [39](#page=39).
#### 4.2.4 Garbage-Collected Talen
Talen zoals Python, JavaScript en Java bieden automatisch geheugenbeheer via een *garbage collector* (GC). De GC identificeert automatisch geheugen dat niet langer in gebruik is en geeft het vrij. Hoewel dit het ontwikkelen aanzienlijk vereenvoudigt, kan het wel een overhead met zich meebrengen wat de prestaties kan beïnvloeden [40](#page=40).
#### 4.2.5 Het eigendomssysteem van Rust
Rust biedt een unieke oplossing voor geheugenveiligheid via een eigendomssysteem, dat zich onderscheidt van zowel handmatig beheer als garbage collection [41](#page=41).
* **Eigendom:** Elk waarde in Rust heeft één enkele eigenaar. Wanneer de eigenaar buiten scope gaat, wordt de waarde automatisch vrijgegeven [41](#page=41).
* **Leningssysteem (`borrow checker`):** Waarden worden overgedragen via referenties. De *borrow checker* van Rust controleert deze regels tijdens de compilatie [42](#page=42).
* **Compile-time veiligheid:** Door runtime-problemen naar de compilatietijd te verplaatsen, wordt Rust als veel veiliger beschouwd dan C en C++ [42](#page=42).
---
## Veelgemaakte fouten om te vermijden
- Bestudeer alle onderwerpen grondig voor examens
- Let op formules en belangrijke definities
- Oefen met de voorbeelden in elke sectie
- Memoriseer niet zonder de onderliggende concepten te begrijpen
Glossary
| Term | Definition |
|------|------------|
| Abstractie | Een vereenvoudiging van een complex systeem door irrelevante details te verbergen en alleen de essentiële kenmerken te tonen, wat leidt tot gemakkelijker begrip en gebruik. |
| Pointer | Een variabele die het geheugenadres van een andere variabele opslaat. In plaats van de data zelf, wijst een pointer naar de locatie waar de data zich bevindt. |
| Pass-by-value | Een methode om waarden aan functies door te geven, waarbij een kopie van de originele waarde wordt gemaakt. Wijzigingen aan de kopie binnen de functie hebben geen invloed op de originele waarde buiten de functie. |
| Pass-by-reference | Een methode om waarden aan functies door te geven, waarbij een pointer naar de originele waarde wordt doorgegeven. Wijzigingen die binnen de functie met de pointer worden aangebracht, beïnvloeden de originele waarde. |
| NULL-pointer | Een speciale pointer die naar geen enkel geldig geheugenadres verwijst. Het geeft aan dat de pointer momenteel niet is gekoppeld aan een geheugenlocatie. |
| Void pointer | Een generieke pointer die naar elk type data kan verwijzen, maar waarvan het type niet gespecificeerd is. Om de data te kunnen gebruiken, moet een void pointer naar het juiste datatype worden gecast. |
| Array | Een verzameling van elementen van hetzelfde datatype, die op een aaneengesloten geheugenlocatie zijn opgeslagen. De grootte van een array is meestal vastgelegd en kan niet dynamisch worden aangepast tijdens runtime. |
| Dereferencing | Het proces waarbij de waarde wordt opgevraagd waarnaar een pointer verwijst. Dit gebeurt door het asterisk-symbool (*) te gebruiken voor een pointervariabele. |
| Stack | Een geheugenregio die wordt gebruikt voor het opslaan van lokale variabelen en functieaanroepen. Het volgt het Last-In-First-Out (LIFO) principe, waarbij het laatst toegevoegde element als eerste wordt verwijderd. |
| Stack frame | Een aparte sectie op de stack die informatie bevat over een specifieke functieaanroep, zoals argumenten, lokale variabelen en het retouradres. |
| Heap | Een geheugenregio die wordt gebruikt voor dynamische geheugenallocatie. In tegenstelling tot de stack is de heap een ongeordend gebied dat kan groeien naarmate meer data wordt toegewezen. |
| Malloc | Een functie in C die geheugen reserveert op de heap. Het vereist de grootte van de data die moet worden opgeslagen en retourneert een void pointer naar de toegewezen geheugenlocatie. |
| Free | Een functie in C die geheugen dat eerder met `malloc` is toegewezen, weer vrijgeeft voor hergebruik. Het is cruciaal om geheugen dat niet meer nodig is, vrij te geven om geheugenlekken te voorkomen. |
| Geheugenlek (Memory Leak) | Een situatie waarbij geheugen dat dynamisch is toegewezen, niet wordt vrijgegeven nadat het niet meer nodig is. Dit leidt tot een voortdurend verbruik van systeembronnen. |
| Dubbele free (Double Free) | Een fout waarbij een geheugenblok dat al is vrijgegeven, opnieuw wordt geprobeerd vrij te geven. Dit kan leiden tot crashes of beveiligingsproblemen. |
| Gebruik-na-free (Use-After-Free) | Een fout waarbij een pointer nog steeds wordt gebruikt om toegang te krijgen tot geheugen nadat dat geheugen al is vrijgegeven. Dit kan leiden tot data corruptie, crashes of kwetsbaarheden. |
| Buffer overflow | Een kwetsbaarheid die optreedt wanneer een programma probeert meer data te schrijven naar een buffer dan deze kan bevatten. Dit kan leiden tot het overschrijven van aangrenzende geheugenlocaties en potentieel kwaadaardige code-uitvoering. |
| Smart pointer | Een klasse in C++ die de functionaliteit van ruwe pointers nabootst, maar met toegevoegde functionaliteit voor automatisch geheugenbeheer, zoals het volgen van eigendom en het vrijgeven van geheugen wanneer het niet meer nodig is. |
| Garbage collector (GC) | Een automatisch proces dat wordt gebruikt in talen met geheugenbeheer om geheugen te identificeren en vrij te geven dat niet langer in gebruik is. Dit gebeurt door het scannen van alle objecten en het identificeren van degenen die niet meer bereikbaar zijn. |
| Eigendomssysteem (Ownership system) | Een geheugenbeheerparadigma in programmeertalen zoals Rust, waarbij elke waarde een enkele eigenaar heeft. Wanneer de eigenaar buiten scope gaat, wordt de waarde automatisch vrijgegeven, wat geheugenveiligheid garandeert zonder een garbage collector. |