Wat is Ongedefinieerd gedrag (undefined behavior)?

Gedrag waaraan de standaard van C of C++ geen eisen stelt. Voorbeelden zijn overloop van integers met teken, lezen of schrijven buiten een array, het derefereren van een NULL-pointer, vrijgegeven geheugen gebruiken en een negatieve waarde naar links schuiven. Een compiler mag optimaliseren in de veronderstelling dat een correct programma nooit ongedefinieerd gedrag heeft, dus zulke code kan toevallig werken zoals verwacht, maar ook onverwachte resultaten geven, bijvoorbeeld doordat een controle bij bepaalde optimalisatie-instellingen verdwijnt. Het wordt onderscheiden van door de implementatie bepaald gedrag (implementation-defined), dat de implementatie kiest en documenteert.

Japanse benaming: 未定義動作

“Ongedefinieerd gedrag (undefined behavior)” komt voor in 10 oefenvragen uit 2 onderwerpen, bijv. Basis van programmeren in C, Compilers.

Vakgebied: Basis van programmeren in C

Typische voorbeelden

Voorbeelden zijn overloop bij rekenen met integers met teken (zoals INT_MAX + 1), lezen of schrijven buiten een array, de waarde van een niet-geïnitialiseerde automatische variabele gebruiken, lezen of schrijven via een NULL-pointer of een dangling pointer, hetzelfde geheugen twee keer vrijgeven, het delen van een geheel getal door 0, en verschuiven over de breedte van het type of meer of een negatieve waarde naar links schuiven. Overloop van integers zonder teken is geen ongedefinieerd gedrag: daarvoor ligt vast dat het resultaat de rest bij deling door een macht van 2 is.

Het verband met optimalisatie door de compiler

Omdat de compiler mag aannemen dat ongedefinieerd gedrag nooit voorkomt, kan hij x + 1 > x als altijd waar behandelen, of een controle p == NULL verwijderen die volgt nadat *p is gelezen. Een programma dat bij -O0 werkte zoals verwacht, kan daardoor stoppen met werken alleen door over te stappen op -O2 of de compiler bij te werken. Dat is geen fout van de compiler; de oorzaak is dat het programma een aanname van de standaard schendt.

Opsporen en voorkomen

Met gcc of clang detecteert -fsanitize=undefined (UBSan) tijdens de uitvoering overloop van integers met teken en vergelijkbare fouten, en -fsanitize=address (ASan) toegang buiten de grenzen en gebruik na vrijgave. Waarschuwingen van -Wall en -Wextra, tools voor statische analyse en codeerregels zoals MISRA C helpen ook om ongedefinieerd gedrag te verminderen.

Nu oefenen: vragen over Ongedefinieerd gedrag (undefined behavior)

Welke beschrijving van ongedefinieerd gedrag in de zin van de C-standaard is juist?

  1. AGedrag waaraan de standaard geen eisen stelt; de compiler mag optimaliseren in de veronderstelling dat het nooit optreedt, zodat de resultaten onverwacht kunnen zijn
  2. BGedrag waarvan elke implementatie het resultaat bepaalt en dat ze verplicht moet documenteren
  3. CGedrag dat tijdens de uitvoering altijd wordt gedetecteerd, waarbij een foutmelding wordt afgedrukt en het programma stopt
Antwoord en toelichting tonen

Antwoord: A. Gedrag waaraan de standaard geen eisen stelt; de compiler mag optimaliseren in de veronderstelling dat het nooit optreedt, zodat de resultaten onverwacht kunnen zijn

Ongedefinieerd gedrag is gedrag waaraan de standaard geen enkele eis stelt, zoals overloop van integers met teken, toegang buiten de grenzen van een array, het derefereren van een NULL-pointer en gebruik na vrijgave. Het kan toevallig werken zoals verwacht, maar omdat de compiler mag optimaliseren in de veronderstelling dat het „in een correct programma niet voorkomt”, kan alleen al een andere optimalisatie-instelling of compilerversie het resultaat veranderen. Daartegenover heten zaken zoals de grootte van int en of char een teken heeft, die de implementatie bepaalt en moet documenteren, door de implementatie bepaald gedrag (implementation-defined behavior). Met -fsanitize=undefined detecteren gcc en clang een deel van het ongedefinieerde gedrag tijdens de uitvoering.

int en unsigned int zijn 32 bits. Welke van de volgende bewerkingen is volgens de C-standaard ongedefinieerd gedrag?

  1. A1 optellen bij een variabele van het type unsigned int waarvan de waarde het maximum 4294967295 is
  2. BEen negatieve int-waarde met >> 1 bit naar rechts schuiven
  3. C1 optellen bij een variabele van het type int waarvan de waarde het maximum 2147483647 is
Antwoord en toelichting tonen

Antwoord: C. 1 optellen bij een variabele van het type int waarvan de waarde het maximum 2147483647 is

Een bewerking met integers met teken waarvan het resultaat het weer te geven bereik overschrijdt, zoals 2147483647 + 1 bij int, is ongedefinieerd gedrag. Voor rekenen met integers zonder teken ligt vast dat het resultaat de rest bij deling door 2³² is, dus 4294967295 + 1 geeft 0 en is geen ongedefinieerd gedrag. Een negatieve waarde naar rechts schuiven is door de implementatie bepaald gedrag: de implementatie bepaalt of het een rekenkundige shift of een logische shift is, en gcc gebruikt een rekenkundige shift. Een negatieve waarde naar links schuiven is daarentegen ongedefinieerd gedrag.

Veelgestelde vragen

Wat is het verschil tussen ongedefinieerd gedrag en door de implementatie bepaald gedrag?

Door de implementatie bepaald gedrag, zoals de grootte van int, of char een teken heeft, of het resultaat van het naar rechts schuiven van een negatieve waarde, is gedrag waarvoor de implementatie één mogelijkheid kiest en die documenteert. Ongedefinieerd gedrag is gedrag waaraan de standaard geen eisen stelt, en het resultaat mag zelfs bij dezelfde implementatie per situatie verschillen. Draagbare programma's maken elke afhankelijkheid van door de implementatie bepaald gedrag expliciet en vermijden ongedefinieerd gedrag volledig.

Is ongedefinieerd gedrag een probleem als het programma toch werkt?

Ja. Ook als het in de huidige omgeving toevallig werkt zoals verwacht, kan een andere optimalisatie-instelling, compilerversie of omringende code een ander resultaat geven. Ook voor de beveiliging is het een probleem: ongedefinieerd gedrag zoals schrijven buiten de grenzen kan door aanvallers worden misbruikt.

“Ongedefinieerd gedrag (undefined behavior)” in de toelichting

Vragen met “Ongedefinieerd gedrag (undefined behavior)” (10)

  1. In C is overflow van gehele getallen met teken ongedefinieerd gedrag. Welke code kan een compiler die de bovenstaande functie optimaliseert op deze grondslag genereren?
  2. Welke beschrijving van ongedefinieerd gedrag in de zin van de C-standaard is juist?
  3. int en unsigned int zijn 32 bits. Welke van de volgende bewerkingen is volgens de C-standaard ongedefinieerd gedrag?
  4. uint8_t is een 8-bits integertype zonder teken. Wat is de uitvoer als de bovenstaande code, die x tien keer verhoogt, wordt uitgevoerd?
  5. int is 32 bits (maximum 2147483647). Wat is de grootste n waarvoor de bovenstaande functie fact(n) de faculteit van n correct en zonder overloop teruggeeft?
  6. Wat is juist over de pointer die de bovenstaande functie make teruggeeft?
  7. Wat is in C de meest passende reden waarom wordt aangeraden om direct na free(p); de regel p = NULL; te schrijven?
  8. Welke beschrijving van wat er in C gebeurt als een NULL-pointer wordt gederefereerd (*p wordt gelezen terwijl p NULL is), is juist?
  9. Wat is de uitvoer als de bovenstaande code, die de met #define gedefinieerde macro SQUARE gebruikt, wordt uitgevoerd?
  10. Als de bovenstaande functie met gcc met ingeschakelde optimalisatie wordt gecompileerd, kan de controle if (p == NULL) worden verwijderd. Wat is de reden?

Verwante termen

Pointer Variabele Array Rekenkundige shift (arithmetic shift) Recursie Stack (stapelgeheugen) Dangling pointer Dynamische geheugentoewijzing Heap Page fault

Naar de woordenlijst