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

Külső linkek