C Language
Ongedefinieerd gedrag
Zoeken…
Invoering
In C leveren sommige uitdrukkingen ongedefinieerd gedrag op . De standaard kiest er expliciet voor om niet te definiëren hoe een compiler zich moet gedragen als deze een dergelijke uitdrukking tegenkomt. Als gevolg hiervan is een compiler vrij om te doen wat hij nodig acht en kan het nuttige resultaten, onverwachte resultaten of zelfs een crash produceren.
Code die UB aanroept, kan werken zoals bedoeld op een specifiek systeem met een specifieke compiler, maar zal waarschijnlijk niet werken op een ander systeem, of met een andere compiler, compilerversie of compilerinstellingen.
Opmerkingen
Wat is Undefined Behaviour (UB)?
Ongedefinieerd gedrag is een term die wordt gebruikt in de C-norm. De C11-norm (ISO / IEC 9899: 2011) definieert de term niet-gedefinieerd gedrag als
gedrag, bij gebruik van een niet-overdraagbare of foutieve programmaconstructie of van foutieve gegevens, waarvoor deze internationale norm geen vereisten oplegt
Wat gebeurt er als mijn code UB bevat?
Dit zijn de resultaten die kunnen optreden vanwege ongedefinieerd gedrag volgens standaard:
OPMERKING Mogelijk ongedefinieerd gedrag varieert van het volledig negeren van de situatie met onvoorspelbare resultaten tot gedrag tijdens vertaling of programma-uitvoering op een gedocumenteerde manier die kenmerkend is voor de omgeving (met of zonder het uitgeven van een diagnostisch bericht), tot het beëindigen van een vertaling of uitvoering (met de uitgifte van een diagnostisch bericht).
Het volgende citaat wordt vaak gebruikt om (minder formeel) resultaten te beschrijven die voortvloeien uit ongedefinieerd gedrag:
"Wanneer de compiler [een gegeven ongedefinieerde constructie] tegenkomt, is het legaal om demonen uit je neus te laten vliegen" (de implicatie is dat de compiler een willekeurige bizarre manier kan kiezen om de code te interpreteren zonder de ANSI C-standaard te overtreden)
Waarom bestaat UB?
Als het zo erg is, waarom hebben ze het dan niet gewoon gedefinieerd of geïmplementeerd?
Ongedefinieerd gedrag biedt meer mogelijkheden voor optimalisatie; De compiler kan met recht aannemen dat elke code geen ongedefinieerd gedrag bevat, waardoor runtime-controles kunnen worden vermeden en optimalisaties kunnen worden uitgevoerd waarvan de geldigheid kostbaar of onmogelijk is om het tegendeel te bewijzen.
Waarom is UB moeilijk op te sporen?
Er zijn ten minste twee redenen waarom ongedefinieerd gedrag bugs veroorzaakt die moeilijk te detecteren zijn:
- De compiler is niet verplicht - en kan over het algemeen niet betrouwbaar - u waarschuwen voor ongedefinieerd gedrag. Het zou zelfs in strijd zijn met de reden voor het bestaan van ongedefinieerd gedrag.
- De onvoorspelbare resultaten beginnen misschien niet precies op het punt van de bewerking te vouwen waar het construct optreedt waarvan het gedrag niet is gedefinieerd; Ongedefinieerd gedrag tast de hele uitvoering aan en de effecten ervan kunnen op elk moment optreden: tijdens, na of zelfs vóór het ongedefinieerde construct.
Overweeg null-pointer dereference: de compiler is niet verplicht om null-pointer dereference te diagnosticeren, en kon zelfs niet, omdat tijdens runtime een pointer in een functie is ingevoerd of in een globale variabele mogelijk null is. En wanneer de nul-wijzer dereferentie optreedt, vereist de standaard niet dat het programma moet crashen. In plaats daarvan crasht het programma eerder, later of helemaal niet; het zou zich zelfs kunnen gedragen alsof de nulwijzer naar een geldig object wees, en zich volledig normaal gedragen, alleen om te crashen onder andere omstandigheden.
In het geval van null-pointer dereference verschilt de C-taal van beheerde talen zoals Java of C #, waar het gedrag van null-pointer dereference is gedefinieerd : er wordt een uitzondering gegenereerd op het exacte tijdstip ( NullPointerException
in Java, NullReferenceException
in C #) , dus degenen die afkomstig zijn van Java of C # zouden ten onrechte kunnen geloven dat in een dergelijk geval een C-programma moet crashen, met of zonder de afgifte van een diagnostisch bericht .
Extra informatie
Er zijn verschillende van dergelijke situaties die duidelijk moeten worden onderscheiden:
- Expliciet ongedefinieerd gedrag, dat is waar de C-norm u expliciet vertelt dat u verboden terrein bent.
- Impliciet ongedefinieerd gedrag, waarbij er gewoon geen tekst in de standaard staat die een gedrag voorziet voor de situatie waarin u uw programma hebt ingebracht.
Houd er ook rekening mee dat op veel plaatsen het gedrag van bepaalde constructen opzettelijk niet is gedefinieerd door de C-standaard om ruimte te laten voor compiler en bibliotheekimplementatoren om hun eigen definities te bedenken. Een goed voorbeeld zijn signalen en signaalbehandelaars, waarbij uitbreidingen van C, zoals de standaard POSIX-besturingssysteem, veel meer uitgewerkte regels definiëren. In dergelijke gevallen moet u gewoon de documentatie van uw platform controleren; de C-standaard kan je niets vertellen.
Merk ook op dat als ongedefinieerd gedrag in het programma voorkomt, dit niet betekent dat alleen het punt waarop ongedefinieerd gedrag zich voordeed problematisch is, maar het hele programma zinloos wordt.
Vanwege dergelijke zorgen is het belangrijk (vooral omdat compilers ons niet altijd waarschuwen voor UB) dat de persoon die programmeert in C op zijn minst bekend is met het soort dingen dat ongedefinieerd gedrag veroorzaakt.
Er moet worden opgemerkt dat er enkele hulpmiddelen zijn (bijvoorbeeld hulpmiddelen voor statische analyse zoals PC-Lint) die helpen bij het detecteren van ongedefinieerd gedrag, maar nogmaals, ze kunnen niet alle gevallen van ongedefinieerd gedrag detecteren.
Een nulwijzer aanwijzen
Dit is een voorbeeld van het dereferen van een NULL-aanwijzer, waardoor ongedefinieerd gedrag wordt veroorzaakt.
int * pointer = NULL;
int value = *pointer; /* Dereferencing happens here */
Een NULL
aanwijzer wordt door de C-standaard gegarandeerd om ongelijk aan elke aanwijzer te vergelijken met een geldig object, en het achterhalen van een verwijzing naar ongedefinieerd gedrag.
Elk object meer dan eens wijzigen tussen twee opeenvolgende punten
int i = 42;
i = i++; /* Assignment changes variable, post-increment as well */
int a = i++ + i--;
Code als deze leidt vaak tot speculaties over de "resulterende waarde" van i
. In plaats van een uitkomst te specificeren, specificeren de C-normen echter dat het evalueren van een dergelijke uitdrukking ongedefinieerd gedrag oplevert. Vóór C2011 formaliseerde de norm deze regels in termen van zogenaamde opeenvolgende punten :
Tussen het vorige en het volgende reekspunt moet de waarde van een scalair object maximaal één keer worden gewijzigd door de evaluatie van een uitdrukking. Verder zal de eerdere waarde alleen worden gelezen om de te bewaren waarde te bepalen.
(C99-norm, sectie 6.5, alinea 2)
Dat schema bleek een beetje te grof, wat resulteerde in sommige uitdrukkingen die ongedefinieerd gedrag vertoonden met betrekking tot C99 dat waarschijnlijk niet zou moeten doen. C2011 behoudt volgordepunten, maar introduceert een meer genuanceerde benadering van dit gebied op basis van sequencing en een relatie die het "sequenced before" noemt:
Als een neveneffect op een scalair object niet is bepaald ten opzichte van een ander neveneffect op hetzelfde scalaire object of een waardeberekening met de waarde van hetzelfde scalaire object, is het gedrag niet gedefinieerd. Als er meerdere toegestane ordeningen van de subexpressies van een expressie zijn, is het gedrag niet gedefinieerd als een dergelijk ongewenst neveneffect optreedt in een van de ordeningen.
(C2011-norm, sectie 6.5, alinea 2)
De volledige details van de "sequenced before" -relatie zijn te lang om hier te beschrijven, maar ze vullen sequentiepunten aan in plaats van ze te vervangen, dus ze hebben het effect van het definiëren van gedrag voor sommige evaluaties waarvan het gedrag voorheen niet was gedefinieerd. In het bijzonder, als er een volgordepunt is tussen twee evaluaties, dan wordt die voor het volgordepunt de volgorde erna voor de volgende.
Het volgende voorbeeld heeft een goed gedefinieerd gedrag:
int i = 42;
i = (i++, i+42); /* The comma-operator creates a sequence point */
Het volgende voorbeeld vertoont ongedefinieerd gedrag:
int i = 42;
printf("%d %d\n", i++, i++); /* commas as separator of function arguments are not comma-operators */
Zoals bij elke vorm van ongedefinieerd gedrag, is het observeren van het feitelijke gedrag van het evalueren van uitdrukkingen die de volgordebepalingen overtreden niet informatief, behalve in retrospectieve zin. De taalstandaard biedt geen basis om te verwachten dat dergelijke observaties voorspellend zijn, zelfs voor het toekomstige gedrag van hetzelfde programma.
Ontbrekende retourinstructie in waarde retourfunctie
int foo(void) {
/* do stuff */
/* no return here */
}
int main(void) {
/* Trying to use the (not) returned value causes UB */
int value = foo();
return 0;
}
Wanneer wordt aangegeven dat een functie een waarde retourneert, moet dit op elk mogelijk codepad worden gedaan. Ongedefinieerd gedrag treedt op zodra de beller (die een retourwaarde verwacht) probeert de retourwaarde 1 te gebruiken .
Merk op dat het ongedefinieerde gedrag alleen optreedt als de beller probeert de waarde van de functie te gebruiken / openen. Bijvoorbeeld,
int foo(void) {
/* do stuff */
/* no return here */
}
int main(void) {
/* The value (not) returned from foo() is unused. So, this program
* doesn't cause *undefined behaviour*. */
foo();
return 0;
}
De functie main()
is een uitzondering op deze regel, omdat het mogelijk is deze te beëindigen zonder een retourinstructie, omdat in dit geval automatisch een aangenomen retourwaarde van 0
wordt gebruikt 2 .
1 ( ISO / IEC 9899: 201x , 6.9.1 / 12)
Als de} die een functie beëindigt, wordt bereikt en de waarde van de functieaanroep door de beller wordt gebruikt, is het gedrag niet gedefinieerd.
2 ( ISO / IEC 9899: 201x , 5.1.2.2.3 / 1)
als u de} bereikt die de hoofdfunctie beëindigt, wordt een waarde van 0 geretourneerd.
Getekende integer overflow
Volgens paragraaf 6.5 / 5 van zowel C99 als C11 produceert evaluatie van een uitdrukking ongedefinieerd gedrag als het resultaat geen representeerbare waarde van het type van de uitdrukking is. Voor rekentypen wordt dat een overloop genoemd . Niet-ondertekend geheel getal rekenkunde loopt niet over omdat paragraaf 6.2.5 / 9 van toepassing is, waardoor elk niet-ondertekend resultaat dat anders buiten bereik zou zijn, wordt gereduceerd tot een waarde binnen het bereik. Er is echter geen analoge voorziening voor ondertekende integertypen; deze kunnen en doen overlopen, waardoor ongedefinieerd gedrag ontstaat. Bijvoorbeeld,
#include <limits.h> /* to get INT_MAX */
int main(void) {
int i = INT_MAX + 1; /* Overflow happens here */
return 0;
}
De meeste gevallen van dit soort ongedefinieerd gedrag zijn moeilijker te herkennen of te voorspellen. Overloop kan in principe voortkomen uit een optelling, aftrekking of vermenigvuldiging op getekende gehele getallen (afhankelijk van de gebruikelijke rekenkundige conversies) waar er geen effectieve grenzen zijn of een relatie tussen de operanden om dit te voorkomen. Deze functie bijvoorbeeld:
int square(int x) {
return x * x; /* overflows for some values of x */
}
is redelijk, en het doet het juiste voor argumentwaarden die klein genoeg zijn, maar het gedrag is niet gedefinieerd voor grotere argumentwaarden. Je kunt niet alleen op basis van de functie beoordelen of programma's die het noemen, hierdoor ongedefinieerd gedrag vertonen. Het hangt af van welke argumenten ze erover doorgeven.
Aan de andere kant, overweeg dit triviale voorbeeld van overstromingsveilig ondertekend integer rekenkundig:
int zero(int x) {
return x - x; /* Cannot overflow */
}
De relatie tussen de operanden van de operator voor aftrekken zorgt ervoor dat de aftrekking nooit overloopt. Of overweeg dit iets meer praktische voorbeeld:
int sizeDelta(FILE *f1, FILE *f2) {
int count1 = 0;
int count2 = 0;
while (fgetc(f1) != EOF) count1++; /* might overflow */
while (fgetc(f2) != EOF) count2++; /* might overflow */
return count1 - count2; /* provided no UB to this point, will not overflow */
}
Zolang de tellers niet individueel overlopen, zullen de operanden van de laatste aftrekking beide niet-negatief zijn. Alle verschillen tussen twee willekeurige waarden worden weergegeven als int
.
Gebruik van een niet-geïnitialiseerde variabele
int a;
printf("%d", a);
De variabele a
is een int
met automatische opslagduur. De bovenstaande voorbeeldcode probeert de waarde van een niet-geïnitialiseerde variabele af te drukken ( a
is nooit geïnitialiseerd). Automatische variabelen die niet geïnitialiseerd zijn, hebben onbepaalde waarden; toegang tot deze kan leiden tot ongedefinieerd gedrag.
Opmerking: variabelen met statische of thread lokale opslag, inclusief globale variabelen zonder het static
trefwoord, worden geïnitialiseerd op nul of hun geïnitialiseerde waarde. Daarom is het volgende legaal.
static int b;
printf("%d", b);
Een veel voorkomende fout is om de variabelen die als tellers op 0 dienen niet te initialiseren. U voegt er waarden aan toe, maar aangezien de initiële waarde afval is, roept u Ongedefinieerd gedrag op , zoals in de vraag Compilatie op terminal geeft waarschuwing aan en vreemde symbolen .
Voorbeeld:
#include <stdio.h>
int main(void) {
int i, counter;
for(i = 0; i < 10; ++i)
counter += i;
printf("%d\n", counter);
return 0;
}
Output:
C02QT2UBFVH6-lm:~ gsamaras$ gcc main.c -Wall -o main
main.c:6:9: warning: variable 'counter' is uninitialized when used here [-Wuninitialized]
counter += i;
^~~~~~~
main.c:4:19: note: initialize the variable 'counter' to silence this warning
int i, counter;
^
= 0
1 warning generated.
C02QT2UBFVH6-lm:~ gsamaras$ ./main
32812
Bovenstaande regels zijn ook van toepassing op verwijzingen. Het volgende resulteert bijvoorbeeld in ongedefinieerd gedrag
int main(void)
{
int *p;
p++; // Trying to increment an uninitialized pointer.
}
Merk op dat de bovenstaande code op zichzelf misschien geen fout of segmentatiefout veroorzaakt, maar als u deze aanwijzer later wilt afleiden, zou dit het ongedefinieerde gedrag veroorzaken.
Verwijzen naar een aanwijzer naar variabele na zijn levensduur
int* foo(int bar)
{
int baz = 6;
baz += bar;
return &baz; /* (&baz) copied to new memory location outside of foo. */
} /* (1) The lifetime of baz and bar end here as they have automatic storage
* duration (local variables), thus the returned pointer is not valid! */
int main (void)
{
int* p;
p = foo(5); /* (2) this expression's behavior is undefined */
*p = *p - 6; /* (3) Undefined behaviour here */
return 0;
}
Sommige compilers wijzen hier nuttig op. gcc
waarschuwt bijvoorbeeld met:
warning: function returns address of local variable [-Wreturn-local-addr]
en clang
waarschuwt met:
warning: address of stack memory associated with local variable 'baz' returned
[-Wreturn-stack-address]
voor de bovenstaande code. Maar compilers kunnen mogelijk niet helpen bij complexe code.
(1) Terugkerende verwijzing naar variabele static
verklaard is gedefinieerd gedrag, omdat de variabele niet wordt vernietigd na het verlaten van het huidige bereik.
(2) Volgens ISO / IEC 9899: 2011 6.2.4 §2: "De waarde van een aanwijzer wordt onbepaald wanneer het object waarnaar hij verwijst het einde van zijn levensduur heeft bereikt."
(3) Het achterhalen van de aanwijzer die wordt geretourneerd door de functie foo
is ongedefinieerd gedrag omdat het geheugen waarnaar het verwijst een onbepaalde waarde heeft.
Deling door nul
int x = 0;
int y = 5 / x; /* integer division */
of
double x = 0.0;
double y = 5.0 / x; /* floating point division */
of
int x = 0;
int y = 5 % x; /* modulo operation */
Voor de tweede regel in elk voorbeeld, waarbij de waarde van de tweede operand (x) nul is, is het gedrag niet gedefinieerd.
Merk op dat de meeste implementaties van drijvende komma wiskunde een standaard zullen volgen (bijv. IEEE 754), in welk geval operaties zoals delen door nul consistente resultaten zullen hebben (bijv. INFINITY
), hoewel de C-standaard zegt dat de bewerking niet is gedefinieerd.
Toegang tot geheugen voorbij toegewezen stuk
Een aanwijzer naar een stuk geheugen dat n
elementen bevat, mag alleen worden verwijderd als deze zich in het memory
en memory + (n - 1)
. Het verwijderen van een verwijzing naar een aanwijzer buiten dat bereik resulteert in ongedefinieerd gedrag. Overweeg als voorbeeld de volgende code:
int array[3];
int *beyond_array = array + 3;
*beyond_array = 0; /* Accesses memory that has not been allocated. */
De derde regel geeft toegang tot het 4e element in een array van slechts 3 elementen, wat leidt tot ongedefinieerd gedrag. Evenzo is het gedrag van de tweede regel in het volgende codefragment ook niet goed gedefinieerd:
int array[3];
array[3] = 0;
Merk op dat het wijzen op het laatste element van een array geen ongedefinieerd gedrag is ( beyond_array = array + 3
is hier goed gedefinieerd), maar het verwijderen van *beyond_array
wel ( *beyond_array
is ongedefinieerd gedrag). Deze regel geldt ook voor dynamisch toegewezen geheugen (zoals buffers die zijn gemaakt via malloc
).
Overlappend geheugen kopiëren
Een grote verscheidenheid aan standaard bibliotheekfuncties heeft onder meer het effect van het kopiëren van bytesequenties van het ene geheugengebied naar het andere. De meeste van deze functies hebben ongedefinieerd gedrag wanneer de bron- en doelgebieden elkaar overlappen.
Dit is bijvoorbeeld ...
#include <string.h> /* for memcpy() */
char str[19] = "This is an example";
memcpy(str + 7, str, 10);
... probeert 10 bytes te kopiëren waarbij de bron- en bestemmingsgeheugengebieden drie bytes overlappen. Visualiseren:
overlapping area
|
_ _
| |
v v
T h i s i s a n e x a m p l e \0
^ ^
| |
| destination
|
source
Vanwege de overlapping is het resulterende gedrag niet gedefinieerd.
Tot de standaard bibliotheekfuncties met een dergelijke beperking behoren memcpy()
, strcpy()
, strcat()
, sprintf()
en sscanf()
. De standaard zegt over deze en verschillende andere functies:
Als het kopiëren plaatsvindt tussen objecten die elkaar overlappen, is het gedrag niet gedefinieerd.
De functie memmove()
is de belangrijkste uitzondering op deze regel. De definitie geeft aan dat de functie zich gedraagt alsof de brongegevens eerst naar een tijdelijke buffer zijn gekopieerd en vervolgens naar het bestemmingsadres zijn geschreven. Er is geen uitzondering voor overlappende bron- en memmove()
's, noch is er behoefte aan, dus memmove()
heeft in dergelijke gevallen een goed gedefinieerd gedrag.
Het onderscheid weerspiegelt een efficiëntie versus . algemeenheid afweging. Kopiëren zoals deze functies uitvoeren, vindt meestal plaats tussen onsamenhangende geheugengebieden, en vaak is het mogelijk om tijdens de ontwikkeling te weten of een bepaald exemplaar van geheugenkopiëren in die categorie zal zijn. Ervan uitgaande dat niet-overlapping leidt tot relatief efficiëntere implementaties die niet op betrouwbare wijze correcte resultaten opleveren wanneer de veronderstelling niet geldt. De meeste C-bibliotheekfuncties zijn toegestaan voor de efficiëntere implementaties en memmove()
vult de gaten op, in gevallen waarin de bron en de bestemming elkaar overlappen. Om in alle gevallen het juiste effect te produceren, moet het echter aanvullende tests uitvoeren en / of een relatief minder efficiënte implementatie gebruiken.
Een niet-geïnitialiseerd object lezen dat niet door het geheugen wordt ondersteund
Het lezen van een object veroorzaakt ongedefinieerd gedrag, als het object 1 is :
- uninitialized
- gedefinieerd met automatische opslagduur
- het adres is nooit bezet
De variabele a in het onderstaande voorbeeld voldoet aan al die voorwaarden:
void Function( void )
{
int a;
int b = a;
}
1 (Geciteerd uit: ISO: IEC 9899: 201X 6.3.2.1 Lvalues, arrays en functie-aanduidingen 2)
Als de waarde een object met een automatische opslagduur aangeeft dat had kunnen worden gedeclareerd bij de registeropslagklasse (het adres is nooit overgenomen), en dat object is niet geïnitialiseerd (niet gedeclareerd met een initialisatieprogramma en er is geen toewijzing aan uitgevoerd vóór gebruik) ), het gedrag is niet gedefinieerd.
Gegevensrace
C11 introduceerde ondersteuning voor meerdere uitvoeringsdraden, waardoor data races mogelijk zijn. Een programma bevat een gegevensrace als een object erin wordt benaderd 1 door twee verschillende threads, waarbij ten minste een van de toegangen niet-atomair is, ten minste één het object wijzigt en semantiek van het programma er niet voor zorgt dat de twee toegangen elkaar niet kunnen overlappen tijdelijk. 2 Merk goed op dat daadwerkelijke gelijktijdigheid van de betrokken toegangen geen voorwaarde is voor een gegevensrace; data races omvatten een bredere klasse van problemen die voortvloeien uit (toegestane) inconsistenties in de weergaven van verschillende threads van geheugen.
Beschouw dit voorbeeld:
#include <threads.h>
int a = 0;
int Function( void* ignore )
{
a = 1;
return 0;
}
int main( void )
{
thrd_t id;
thrd_create( &id , Function , NULL );
int b = a;
thrd_join( id , NULL );
}
De rode draad oproepen thrd_create
om een nieuwe draad te starten Function
. De tweede thread wijzigt a
en de hoofdthread leest a
. Geen van beide toegangen is atomair en de twee threads doen niets afzonderlijk of gezamenlijk om ervoor te zorgen dat ze elkaar niet overlappen, dus er is een gegevensrace.
Een van de manieren waarop dit programma de gegevensrace zou kunnen vermijden, is onder andere
- de hoofdthread kon zijn lezing van
a
voordat de andere thread werd gestart; - de
thrd_join
zou zijn read ofa
kunnen uitvoeren door viathrd_join
dat de ander is geëindigd; - de threads konden hun toegangen synchroniseren via een mutex, waarbij elke thread die mutex vergrendelde voordat hij toegang kreeg tot
a
en daarna ontgrendelde.
Zoals de optie mutex aantoont, vereist het vermijden van een gegevensrace niet dat een specifieke volgorde van bewerkingen wordt gewaarborgd, zoals de onderliggende thread die a
wijzigt voordat de hoofdthread deze leest; het is voldoende (om een gegevensrace te voorkomen) om ervoor te zorgen dat voor een bepaalde uitvoering de ene toegang vóór de andere plaatsvindt.
1 Een object wijzigen of lezen.
2 (Geciteerd uit ISO: IEC 9889: 201x, paragraaf 5.1.2.4 "Multi-threaded uitvoeringen en data races")
De uitvoering van een programma bevat een gegevensrace als het twee conflicterende acties in verschillende threads bevat, waarvan er ten minste één niet atomair is en geen van beide vóór de andere gebeurt. Een dergelijke gegevensrace resulteert in ongedefinieerd gedrag.
Lees de waarde van de aanwijzer die werd vrijgegeven
Zelfs het lezen van de waarde van een aanwijzer die is vrijgegeven (dat wil zeggen zonder de aanwijzer te ontleden) is ongedefinieerd gedrag (UB), bijv.
char *p = malloc(5);
free(p);
if (p == NULL) /* NOTE: even without dereferencing, this may have UB */
{
}
Onder vermelding van ISO / IEC 9899: 2011 , paragraaf 6.2.4 §2:
[…] De waarde van een aanwijzer wordt onbepaald wanneer het object waarnaar het verwijst (of net voorbij) het einde van zijn levensduur bereikt.
Het gebruik van onbepaald geheugen voor alles, inclusief schijnbaar onschadelijke vergelijking of rekenkunde, kan ongedefinieerd gedrag hebben als de waarde een valrepresentatie voor het type kan zijn.
Letterlijke tekenreeks wijzigen
In dit codevoorbeeld wordt de tekenwijzer p
geïnitialiseerd op het adres van een letterlijke tekenreeks. Poging om de letterlijke tekenreeks te wijzigen, heeft een ongedefinieerd gedrag.
char *p = "hello world";
p[0] = 'H'; // Undefined behavior
Het wijzigen van een veranderlijke reeks char
rechtstreeks of via een aanwijzer is echter natuurlijk geen ongedefinieerd gedrag, zelfs als de initialisatie ervan een letterlijke tekenreeks is. Het volgende is prima:
char a[] = "hello, world";
char *p = a;
a[0] = 'H';
p[7] = 'W';
Dat komt omdat de tekenreeks letterlijk wordt gekopieerd naar de array telkens wanneer de array wordt geïnitialiseerd (eenmaal voor variabelen met statische duur, elke keer dat de array wordt gemaakt voor variabelen met automatische of threadduur - variabelen met toegewezen duur worden niet geïnitialiseerd), en het is prima om de inhoud van de array te wijzigen.
Twee keer geheugen vrijmaken
Twee keer geheugen vrijmaken is ongedefinieerd gedrag, bijv
int * x = malloc(sizeof(int));
*x = 9;
free(x);
free(x);
Citaat van standaard (7.20.3.2. De gratis functie van C99):
Anders is het gedrag ongedefinieerd als het argument niet overeenkomt met een pointer die eerder is geretourneerd door de calloc-, malloc- of reallocfunctie, of als de ruimte is toegewezen door een aanroep voor gratis of realloc.
Het gebruik van een onjuiste opmaakspecificatie in printf
Het gebruik van een onjuiste opmaakspecificatie in het eerste argument voor printf
roept ongedefinieerd gedrag op. De onderstaande code roept bijvoorbeeld ongedefinieerd gedrag op:
long z = 'B';
printf("%c\n", z);
Hier is nog een voorbeeld
printf("%f\n",0);
Bovenstaande coderegel is ongedefinieerd gedrag. %f
verwacht dubbel. 0 is echter van het type int
.
Merk op dat je compiler meestal kan u helpen voorkomen gevallen als deze, als u op de juiste vlag te schakelen tijdens het compileren ( -Wformat
in clang
en gcc
). Van het laatste voorbeeld:
warning: format specifies type 'double' but the argument has type
'int' [-Wformat]
printf("%f\n",0);
~~ ^
%d
Conversie tussen aanwijzertypen geeft een onjuist uitgelijnd resultaat
Het volgende kan ongedefinieerd gedrag hebben als gevolg van onjuiste aanwijzeruitlijning:
char *memory_block = calloc(sizeof(uint32_t) + 1, 1);
uint32_t *intptr = (uint32_t*)(memory_block + 1); /* possible undefined behavior */
uint32_t mvalue = *intptr;
Het ongedefinieerde gedrag gebeurt wanneer de aanwijzer wordt geconverteerd. Volgens C11 is het gedrag niet gedefinieerd als een conversie tussen twee aanwijzertypen een resultaat oplevert dat niet goed is uitgelijnd (6.3.2.3) . Hier zou een uint32_t
een aanpassing van 2 of 4 kunnen vereisen.
calloc
daarentegen is vereist om een aanwijzer te retourneren die geschikt is uitgelijnd voor elk objecttype; dus memory_block
is correct uitgelijnd om een uint32_t
in zijn eerste deel te bevatten. Dan, op een systeem waar uint32_t
uitlijning van 2 of 4 vereist heeft, zal memory_block + 1
een vreemd adres zijn en dus niet correct uitgelijnd.
Merk op dat de C-standaard vraagt dat de cast-bewerking al ongedefinieerd is. Dit wordt opgelegd omdat op platforms waar adressen worden gesegmenteerd, het byte-adres memory_block + 1
mogelijk niet eens een juiste weergave heeft als een geheel getal-pointer.
Het casten van char *
naar verwijzingen naar andere typen zonder enige zorg voor uitlijningsvereisten wordt soms ten onrechte gebruikt voor het decoderen van ingepakte structuren zoals bestandskoppen of netwerkpakketten.
U kunt het ongedefinieerde gedrag als gevolg van verkeerd uitgelijnde memcpy
vermijden door memcpy
:
memcpy(&mvalue, memory_block + 1, sizeof mvalue);
Hier vindt geen uint32_t*
naar uint32_t*
plaats en worden de bytes één voor één gekopieerd.
Deze kopieerbewerking leidt in ons voorbeeld alleen tot de geldige waarde van mvalue
omdat:
- We hebben
calloc
gebruikt, dus de bytes zijn correct geïnitialiseerd. In ons geval hebben alle bytes waarde0
, maar elke andere juiste initialisatie zou dat kunnen doen. -
uint32_t
is een exactuint32_t
en heeft geen opvulbits - Elk willekeurig bitpatroon is een geldige weergave voor elk niet-ondertekend type.
Optellen of aftrekken van aanwijzer niet correct begrensd
De volgende code heeft ongedefinieerd gedrag:
char buffer[6] = "hello";
char *ptr1 = buffer - 1; /* undefined behavior */
char *ptr2 = buffer + 5; /* OK, pointing to the '\0' inside the array */
char *ptr3 = buffer + 6; /* OK, pointing to just beyond */
char *ptr4 = buffer + 7; /* undefined behavior */
Volgens C11 is het gedrag ongedefinieerd als optellen of aftrekken van een aanwijzer in of net voorbij een matrixobject en een geheel getal een resultaat oplevert dat niet naar of net voorbij hetzelfde matrixobject verwijst. ).
Bovendien is het van nature ongedefinieerd gedrag om een aanwijzer te onttrekken aan een verwijzing die net voorbij de array wijst:
char buffer[6] = "hello";
char *ptr3 = buffer + 6; /* OK, pointing to just beyond */
char value = *ptr3; /* undefined behavior */
Een const-variabele wijzigen met een aanwijzer
int main (void)
{
const int foo_readonly = 10;
int *foo_ptr;
foo_ptr = (int *)&foo_readonly; /* (1) This casts away the const qualifier */
*foo_ptr = 20; /* This is undefined behavior */
return 0;
}
Onder vermelding van ISO / IEC 9899: 201x , paragraaf 6.7.3 §2:
Als een poging wordt gedaan om een object te wijzigen dat is gedefinieerd met een const-gekwalificeerd type door het gebruik van een waarde met een niet-const-gekwalificeerd type, is het gedrag niet gedefinieerd. [...]
(1) In GCC kan dit de volgende waarschuwing geven: warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
Een nulaanwijzer doorgeven aan de conversie van printf% s
De %s
conversie van printf
stelt dat het overeenkomstige argument een pointer is naar het initiële element van een array van karaktertypen . Een null-aanwijzer verwijst niet naar het eerste element van een array van tekens, en het gedrag van de volgende elementen is dus niet gedefinieerd:
char *foo = NULL;
printf("%s", foo); /* undefined behavior */
Het ongedefinieerde gedrag betekent echter niet altijd dat het programma vastloopt - sommige systemen ondernemen stappen om de crash te voorkomen die normaal gesproken optreedt wanneer een nulaanwijzer wordt verwijderd. Het is bijvoorbeeld bekend dat Glibc afdrukt
(null)
voor de bovenstaande code. Voeg echter (alleen) een nieuwe regel toe aan de opmaakreeks en u krijgt een crash:
char *foo = 0;
printf("%s\n", foo); /* undefined behavior */
In dit geval gebeurt het omdat GCC een optimalisatie heeft die printf("%s\n", argument);
verandert printf("%s\n", argument);
in een aanroep van puts
met puts(argument)
, en puts
in Glibc verwerkt geen nulwijzers. Al dit gedrag is standaard conform.
Merk op dat null pointer verschilt van een lege string . Het volgende is dus geldig en vertoont geen ongedefinieerd gedrag. Er wordt alleen een nieuwe regel afgedrukt:
char *foo = "";
printf("%s\n", foo);
Inconsistente koppeling van ID's
extern int var;
static int var; /* Undefined behaviour */
C11, §6.2.2, 7 zegt:
Als binnen een vertaaleenheid dezelfde identificatie verschijnt met zowel interne als externe koppeling, is het gedrag niet gedefinieerd.
Merk op dat als een voorafgaande verklaring van een identificator zichtbaar is, deze de koppeling van de voorafgaande verklaring zal hebben. C11, §6.2.2, 4 staat het toe:
Voor een identificator die wordt aangegeven met de opslagklasse-specificator extern in een scope waarin een voorafgaande verklaring van die identificator zichtbaar is, 31) als de voorafgaande verklaring interne of externe koppeling specificeert, is de koppeling van de identificator bij de latere verklaring dezelfde als de koppeling aangegeven bij de voorafgaande aangifte. Als er geen voorafgaande aangifte zichtbaar is, of als de voorafgaande aangifte geen koppeling aangeeft, heeft de identificator een externe koppeling.
/* 1. This is NOT undefined */
static int var;
extern int var;
/* 2. This is NOT undefined */
static int var;
static int var;
/* 3. This is NOT undefined */
extern int var;
extern int var;
Fflush gebruiken op een invoerstream
De POSIX- en C-normen stellen expliciet dat het gebruik van fflush
op een invoerstroom ongedefinieerd gedrag is. De fflush
is alleen gedefinieerd voor uitvoerstromen.
#include <stdio.h>
int main()
{
int i;
char input[4096];
scanf("%i", &i);
fflush(stdin); // <-- undefined behavior
gets(input);
return 0;
}
Er is geen standaardmanier om ongelezen tekens uit een invoerstream te verwijderen. Aan de andere kant gebruikt sommige implementaties fflush
om de stdin
buffer te wissen. Microsoft definieert het gedrag van fflush
op een invoerstream: als de stream open staat voor invoer, wist fflush
de inhoud van de buffer. Volgens POSIX.1-2008 is het gedrag van fflush
niet gedefinieerd, tenzij het invoerbestand opzoekbaar is.
Zie fflush(stdin)
voor veel meer details.
Bitverschuiving met behulp van negatieve tellingen of voorbij de breedte van het type
Als de telwaarde van de shift een negatieve waarde is, zijn zowel de linker shift- als de rechter shift- operatie ongedefinieerd 1 :
int x = 5 << -3; /* undefined */
int x = 5 >> -3; /* undefined */
Als de linkerverschuiving wordt uitgevoerd op een negatieve waarde , is dit niet gedefinieerd:
int x = -5 << 3; /* undefined */
Als de linkerverschuiving wordt uitgevoerd op een positieve waarde en het resultaat van de wiskundige waarde niet in het type kan worden weergegeven, is dit ongedefinieerd 1 :
/* Assuming an int is 32-bits wide, the value '5 * 2^72' doesn't fit
* in an int. So, this is undefined. */
int x = 5 << 72;
Merk op dat verschuiving naar rechts op een negatieve waarde (.eg -5 >> 3
) niet ongedefinieerd is maar door de implementatie is gedefinieerd .
1 Citaat ISO / IEC 9899: 201x , paragraaf 6.5.7:
Als de waarde van de rechteroperand negatief is of groter is dan of gelijk is aan de breedte van de gepromoveerde linkeroperand, is het gedrag niet gedefinieerd.
De tekenreeks wijzigen die wordt geretourneerd door de functies getenv, strerror en setlocale
Het wijzigen van de tekenreeksen die worden geretourneerd door de standaardfuncties getenv()
, strerror()
en setlocale()
is niet gedefinieerd. Implementaties kunnen dus statische opslag gebruiken voor deze strings.
De functie getenv (), C11, §7.22.4.7, 4 , zegt:
De functie getenv retourneert een aanwijzer naar een tekenreeks die is gekoppeld aan het overeenkomende lid van de lijst. De string waarnaar wordt verwezen, wordt niet gewijzigd door het programma, maar kan worden overschreven door een volgende aanroep van de getenv-functie.
De strerror () functie, C11, §7.23.6.3, 4 zegt:
De strerror-functie retourneert een pointer naar de tekenreeks, waarvan de inhoud lokaal specifiek is. De array waarnaar wordt verwezen, wordt niet gewijzigd door het programma, maar kan worden overschreven door een volgende aanroep van de strerror-functie.
De functie setlocale (), C11, §7.11.1.1, 8 zegt:
De aanwijzer naar tekenreeks die wordt geretourneerd door de functie setlocale is zodanig dat een volgende aanroep met die tekenreekswaarde en de bijbehorende categorie dat deel van de landinstelling van het programma herstelt. De string waarnaar wordt verwezen, wordt niet gewijzigd door het programma, maar kan worden overschreven door een volgende aanroep van de setlocale-functie.
Op dezelfde manier localeconv()
functie localeconv()
een pointer naar struct lconv
die niet wordt gewijzigd.
De functie localeconv (), C11, §7.11.2.1, 8 zegt:
De functie localeconv retourneert een aanwijzer naar het ingevulde object. De structuur waarnaar de retourwaarde verwijst, wordt niet gewijzigd door het programma, maar kan worden overschreven door een volgende aanroep van de functie localeconv.
Terugkeren van een functie die is gedeclareerd met de functie-specificator `_Noreturn` of` noreturn`
De functiespecificatie _Noreturn
is geïntroduceerd in C11. De kop <stdnoreturn.h>
biedt een macro- noreturn
die zich uitbreidt naar _Noreturn
. Dus het gebruik van _Noreturn
of noreturn
van <stdnoreturn.h>
is prima en equivalent.
Een functie die is gedeclareerd met _Noreturn
(of noreturn
) mag niet terugkeren naar de beller. Indien het toestel doet terugkeren naar de oproeper wordt het gedrag ongedefinieerd.
In het volgende voorbeeld wordt func()
gedeclareerd met noreturn
specifier maar keert het terug naar de beller.
#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>
noreturn void func(void);
void func(void)
{
printf("In func()...\n");
} /* Undefined behavior as func() returns */
int main(void)
{
func();
return 0;
}
gcc
en clang
produceren waarschuwingen voor het bovenstaande programma:
$ gcc test.c
test.c: In function ‘func’:
test.c:9:1: warning: ‘noreturn’ function does return
}
^
$ clang test.c
test.c:9:1: warning: function declared 'noreturn' should not return [-Winvalid-noreturn]
}
^
Een voorbeeld met noreturn
dat goed gedefinieerd gedrag heeft:
#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>
noreturn void my_exit(void);
/* calls exit() and doesn't return to its caller. */
void my_exit(void)
{
printf("Exiting...\n");
exit(0);
}
int main(void)
{
my_exit();
return 0;
}