Határozatlan viselkedés - Undefined behavior
A számítógép-programozás , meghatározatlan viselkedés ( UB ) az eredménye egy olyan program végrehajtásával, amelynek viselkedését írják, hogy kiszámíthatatlan, a nyelvi leírás , amelyre a számítógépes kód tapad. Ez eltér a meg nem határozott viselkedéstől , amelynél a nyelvi specifikáció nem ír elő eredményt, és a megvalósítás által meghatározott viselkedéstől, amely a platform egy másik összetevőjének (például az ABI vagy a fordítói dokumentáció) dokumentációjától függ .
A C közösség , meghatározatlan viselkedés lehet viccesen nevezik „ nazális démonok ”, miután egy comp.std.c bejegyzést, amely megmagyarázza meghatározatlan viselkedés, amely lehetővé teszi, hogy a fordító semmit úgy dönt, még „, hogy a démonok repül ki az orrát ".
Áttekintés
Bizonyos programozási nyelvek lehetővé teszik, hogy egy program másként működjön, vagy akár más vezérlési folyamatot végezzen, mint a forráskód , mindaddig, amíg ugyanazokat a felhasználó által látható mellékhatásokat észleli , ha a program végrehajtása során soha nem fordul elő meghatározatlan viselkedés . A nem definiált viselkedés azon feltételek listájának neve, amelyeknek a programnak nem szabad megfelelnie.
A C korai verzióiban a meghatározatlan viselkedés elsődleges előnye a legkülönfélébb gépek számára előállított fordítóprogramok előállítása volt : egy adott konstrukciót le lehet képezni egy gépspecifikus jellemzőre, és a fordítónak nem kellett további kódot generálnia a futási időhöz. hogy a mellékhatásokat a nyelv által előírt szemantikához igazítsák. A program forráskódját az adott fordító és a támogatott platformok előzetes ismerete mellett írták .
A platformok fokozatos szabványosítása azonban ezt kevésbé tette előnyössé, különösen a C. újabb verzióiban. Most a meghatározatlan viselkedés esetei tipikusan egyértelmű hibákat jelentenek a kódban, például egy tömböt a határain kívül indexelve. Definíció szerint a futási idő feltételezheti, hogy a nem meghatározott viselkedés soha nem történik meg; ezért néhány érvénytelen feltételt nem kell ellenőrizni. A fordító számára ez azt is jelenti, hogy a különböző programátalakítások érvényessé válnak, vagy leegyszerűsödnek azok igazolásai; ez lehetővé teszi a korai optimalizálás és a mikrooptimalizálás különféle fajtáit , amelyek helytelen viselkedéshez vezetnek, ha a program állapota megfelel ezeknek a feltételeknek. A fordító a programozó értesítése nélkül is eltávolíthatja azokat az explicit ellenőrzéseket, amelyek esetleg a forráskódban voltak; például a definíció nélküli viselkedés kimutatása annak tesztelésével, hogy megtörtént -e, definíció szerint nem működik. Ez megnehezíti vagy lehetetlenné teszi a hordozható hibamentes opció programozását (egyes konstrukciók esetében nem hordozható megoldások is lehetségesek).
A jelenlegi fordítói fejlesztés általában értékeli és összehasonlítja a fordító teljesítményét a mikrooptimalizálás köré tervezett referenciaértékekkel, még azokon a platformokon is, amelyeket többnyire az általános célú asztali és laptop piacon használnak (például amd64). Ezért a nem definiált viselkedés bőséges teret biztosít a fordítói teljesítmény javításához, mivel egy adott forráskód -utasítás forráskódját bármire le lehet képezni futásidőben.
A C és a C ++ esetében a fordító ilyen esetekben fordítási idejű diagnosztikát adhat, de nem köteles: a megvalósítást helyesnek kell tekinteni, bármit is tesz ilyen esetekben, hasonlóan a digitális logika nem törődő kifejezéseihez . A programozó felelőssége, hogy olyan kódot írjon, amely soha nem idéz elő meghatározatlan viselkedést, bár a fordító implementációk ilyen esetekben diagnosztikát adhatnak ki. Fordítóprogramok manapság zászlók, amelyek lehetővé teszik az ilyen diagnosztika, például -fsanitizelehetővé teszi, hogy a „nem definiált viselkedés fertőtlenítő” ( UBSan ) a gcc 4.9 és csenget . Ez a zászló azonban nem az alapértelmezett, és ennek engedélyezése a kód kiépítőjének választása.
Bizonyos körülmények között különleges korlátozások vonatkozhatnak a meghatározatlan viselkedésre. Például a CPU utasításkészlet- specifikációi meghatározhatatlanná tehetik az utasítás egyes formáinak viselkedését, de ha a CPU támogatja a memóriavédelmet, akkor a specifikáció valószínűleg tartalmaz egy általános szabályt, amely kimondja, hogy a felhasználó által elérhető utasítások nem okozhatnak lyukat az operációs rendszer biztonsága; így egy tényleges CPU megengedné, hogy egy ilyen utasítás hatására megrongálja a felhasználói regisztereket, de nem megengedett például, hogy felügyeleti módba váltson .
A futásidejű platform bizonyos korlátozásokat vagy garanciákat is nyújthat a meghatározatlan viselkedésre, ha az eszköztár vagy a futásidejűség kifejezetten dokumentálja, hogy a forráskódban található konkrét konstrukciók a futásidőben rendelkezésre álló, pontosan meghatározott mechanizmusokhoz vannak leképezve. Például egy tolmács dokumentálhat egy bizonyos viselkedést bizonyos műveletekhez, amelyek nincsenek meghatározva a nyelv specifikációjában, míg más tolmácsok vagy fordítók ugyanarra a nyelvre nem. A fordítóprogram végrehajtható kódot állít elő egy adott ABI -hez , a szemantikai hiányt a fordító verziójától függő módon tölti ki : az adott fordítóverzió dokumentációja és az ABI specifikáció korlátozásokat tartalmazhat a meghatározatlan viselkedésre. Ezekre a megvalósítási részletekre támaszkodva a szoftver nem hordozható , azonban a hordozhatóság nem okozhat gondot, ha a szoftvert nem egy meghatározott futási időn kívül használják.
A nem definiált viselkedés egy program összeomlását vagy akár olyan hibákat is eredményezhet, amelyeket nehezebb észlelni, és a programot úgy kell kinézni, mintha normálisan működne, például csendes adatvesztést és helytelen eredmények előállítását.
Előnyök
Ha egy műveletet meghatározatlan viselkedésként dokumentálunk, a fordítók feltételezhetik, hogy ez a művelet soha nem fog megtörténni egy megfelelő programban. Ez több információt ad a fordítónak a kódról, és ez az információ több optimalizálási lehetőséghez vezethet.
Példa a C nyelvre:
int foo(unsigned char x)
{
int value = 2147483600; /* assuming 32-bit int and 8-bit char */
value += x;
if (value < 2147483600)
bar();
return value;
}
A (z) értéke xnem lehet negatív, és tekintettel arra, hogy az előjeles egész túlcsordulás nem definiált viselkedés a C -ben, a fordító feltételezheti, hogy value < 2147483600ez mindig hamis lesz. Így az ifállítást, beleértve a függvény hívását, a barfordító figyelmen kívül hagyhatja, mivel a tesztben lévő kifejezésnek ifnincs mellékhatása, és annak feltétele soha nem lesz teljesül. A kód tehát szemantikailag egyenértékű a következővel:
int foo(unsigned char x)
{
int value = 2147483600;
value += x;
return value;
}
Ha a fordító arra kényszerülne, hogy feltételezze, hogy az aláírt egész szám túlcsordulása körbejárható , akkor a fenti átalakítás nem lett volna törvényes.
Az ilyen optimalizáció lesz nehéz észrevenni az emberek, ha a kód sokkal összetettebb és egyéb optimalizáció, mint inliningot kerül sor. Például egy másik függvény hívhatja a fenti függvényt:
void run_tasks(unsigned char *ptrx) {
int z;
z = foo(*ptrx);
while (*ptrx > 60) {
run_one_task(ptrx, z);
}
}
A fordító szabadon optimalizálhatja a while-loop értékét az értéktartomány -elemzés alkalmazásával : ellenőrzéssel foo()tudja, hogy az általunk jelzett kezdeti érték ptrxnem haladhatja meg a 47 -et (mivel a továbbiakban meghatározhatatlan viselkedést váltana ki foo()), ezért az *ptrx > 60akarat első ellenőrzése mindig hamis legyen egy megfelelő programban. Továbblépve, mivel az eredményt zsoha nem használják, és foo()nincsenek mellékhatásai, a fordító optimalizálhatja run_tasks(), hogy üres függvény legyen, amely azonnal visszatér. A while-loop eltűnése különösen meglepő lehet, ha foo()azt egy külön lefordított objektumfájl határozza meg .
Egy másik előnye annak, hogy az aláírt egész szám túlcsordulását meghatározzuk, az, hogy lehetővé teszi a változó értékének tárolását és manipulálását a forráskódban szereplő változónál nagyobb processzorregiszterben . Például, ha a változónak a forráskódban megadott típusa keskenyebb, mint a natív regiszter szélessége (például " int " egy 64 bites gépen, gyakori forgatókönyv), akkor a fordító biztonságosan használhat egy aláírt 64- bit egész szám az általa előállított gépi kód változójához , anélkül, hogy megváltoztatná a kód meghatározott viselkedését. Ha egy program a 32 bites egész szám túlcsordulásának viselkedésétől függ, akkor a fordítónak további logikát kell beszúrnia a 64 bites gép fordításakor, mert a legtöbb gépi utasítás túlcsordulási viselkedése a regiszter szélességétől függ.
A nem definiált viselkedés lehetővé teszi a fordítási idő több ellenőrzését mind a fordítók, mind a statikus programelemzés során .
Kockázatok
A C és a C ++ szabványok többféle, nem definiált viselkedést tartalmaznak, amelyek nagyobb szabadságot kínálnak a fordítói implementációkban és a fordítási idejű ellenőrzésekben, ha nincsenek definiálva. Különösen a C ISO szabványhoz tartozik egy melléklet, amely felsorolja a meghatározatlan viselkedés gyakori forrásait. Ezenkívül a fordítóknak nem kell diagnosztizálniuk a definiálatlan viselkedésen alapuló kódot. Ezért gyakori, hogy a programozók, még a tapasztaltak is, tévedésből, vagy egyszerűen azért, mert nem jól ismerik a több száz oldalas nyelv szabályait. Ez hibákat eredményezhet, amelyek más fordító vagy más beállítások használata esetén kerülnek feltárásra. A dinamikus, definiálatlan viselkedés -ellenőrzések ( pl. A Clang fertőtlenítőszerek) engedélyezésével végzett tesztelés vagy fuzzing segíthet a fordító vagy a statikus elemzők által nem diagnosztizált nem definiált viselkedés észlelésében .
A nem definiált viselkedés biztonsági réseket okozhat a szoftverekben. Például a puffertúlcsordulások és más biztonsági rések a főbb webböngészőkben a nem definiált viselkedésnek köszönhetők. A 2038 -as év egy másik példa az aláírt egész szám túlcsordulás miatt . Amikor a GCC fejlesztői 2008 -ban úgy változtatták meg fordítójukat, hogy kihagytak bizonyos túlfolyó ellenőrzéseket, amelyek meghatározatlan viselkedésre támaszkodtak, a CERT figyelmeztetést adott ki a fordító újabb verziói ellen. A Linux Weekly News rámutatott, hogy ugyanezt a viselkedést figyelték meg a PathScale C , a Microsoft Visual C ++ 2005 és számos más fordító esetében is; a figyelmeztetést később módosították, hogy figyelmeztessenek a különböző fordítókra.
Példák C és C ++ nyelven
A meghatározhatatlan viselkedés fő formái a C -ben nagy vonalakban besorolhatók: térbeli memória biztonsági megsértések, időbeli memória biztonsági megsértések, egész szám túlcsordulás , szigorú álnevelési jogsértések, igazítási megsértések, szekvencia nélküli módosítások, adatfutamok és hurkok, amelyek sem nem végeznek I/O -t, sem nem zárnak le .
C -ben bármely automatikus változó használata az inicializálás előtt meghatározatlan viselkedést eredményez, akárcsak a nullával való egész osztás , az aláírt egész szám túlcsordulás, a tömbök meghatározott határokon kívüli indexelése (lásd a puffertúlcsordulást ) vagy a null -mutató levezetése . Általában a meghatározatlan viselkedés bármely példánya ismeretlen állapotban hagyja az absztrakt végrehajtó gépezetet, és az egész program viselkedését meghatározza.
A karakterlánc literáljának módosítására tett kísérlet meghatározatlan viselkedést okoz:
char *p = "wikipedia"; // valid C, deprecated in C++98/C++03, ill-formed as of C++11
p[0] = 'W'; // undefined behavior
Az egész szám nullával való osztása meghatározatlan viselkedést eredményez:
int x = 1;
return x / 0; // undefined behavior
Bizonyos mutatóműveletek meghatározhatatlan viselkedést eredményezhetnek:
int arr[4] = {0, 1, 2, 3};
int *p = arr + 5; // undefined behavior for indexing out of bounds
p = 0;
int a = *p; // undefined behavior for dereferencing a null pointer
A C és a C ++ nyelvekben a mutatók objektumokkal való relációs összehasonlítása (kisebb vagy nagyobb összehasonlításnál) csak szigorúan meghatározott, ha a mutatók ugyanazon objektum tagjaira vagy ugyanazon tömb elemeire mutatnak . Példa:
int main(void)
{
int a = 0;
int b = 0;
return &a < &b; /* undefined behavior */
}
Ha egy érték-visszatérő függvény végét érjük el (kivéve main()) visszatérési utasítás nélkül, akkor nem meghatározott viselkedést eredményez, ha a függvényhívás értékét használja a hívó:
int f()
{
} /* undefined behavior if the value of the function call is used*/
Ha egy objektumot többször módosít két szekvenciapont között, akkor meghatározatlan viselkedést eredményez. Jelentős változások történnek abban, hogy mi okozza a meghatározatlan viselkedést a C ++ 11 szekvenciapontokkal kapcsolatban. A modern fordítók figyelmeztetést adhatnak ki, ha ugyanazon az objektumon több szekvencia nélküli módosítással találkoznak. A következő példa meghatározhatatlan viselkedést okoz mind a C, mind a C ++ nyelven.
int f(int i) {
return i++ + i++; /* undefined behavior: two unsequenced modifications to i */
}
Amikor módosít egy objektumot két szekvenciapont között, az objektum értékének leolvasása bármilyen más célra, mint a tárolni kívánt érték meghatározása, szintén nem definiált viselkedés.
a[i] = i++; // undefined behavior
printf("%d %d\n", ++n, power(2, n)); // also undefined behavior
A C/C ++ bites bitenkénti értékelt bitszámmal való eltolása, amely vagy negatív szám, vagy nagyobb vagy egyenlő az értékben lévő bitek teljes számával, meghatározatlan viselkedést eredményez. A legbiztonságosabb módszer (függetlenül a fordító gyártójától), ha mindig az eltolásra kerülő bitek számát (a <<és a >> bitenkénti operátorok jobb operandusa ) tartja a tartományon belül: < > (ahol a bal operandus található).
0, sizeof(value)*CHAR_BIT - 1value
int num = -1;
unsigned int val = 1 << num; //shifting by a negative number - undefined behavior
num = 32; //or whatever number greater than 31
val = 1 << num; //the literal '1' is typed as a 32-bit integer - in this case shifting by more than 31 bits is undefined behavior
num = 64; //or whatever number greater than 63
unsigned long long val2 = 1ULL << num; //the literal '1ULL' is typed as a 64-bit integer - in this case shifting by more than 63 bits is undefined behavior
Lásd még
Hivatkozások
További irodalom
- Peter van der Linden , C programozási szakértő . ISBN 0-13-177429-8
- UB Kanári -szigetek (2015. április), John Regehr (Utahi Egyetem, USA)
- Undefined Behavior in 2017 ( 2017. július) Pascal Cuoq (TrustInSoft, Franciaország) és John Regehr (Utahi Egyetem, USA)
Külső linkek
- A C99 szabvány javított változata . Nézze meg a 6.10.6 fejezetben a #pragma című részt