Nedefinované chování - Undefined behavior

V počítačovém programování je nedefinované chování ( UB ) výsledkem spuštění programu, jehož chování je předepsáno jako nepředvídatelné, v jazykové specifikaci, ke které počítačový kód přistupuje. To se liší od nespecifikovaného chování , u kterého jazyková specifikace nepředepisuje výsledek, a od chování definovaného implementací, které se odchyluje od dokumentace jiné součásti platformy (například ABI nebo dokumentace překladače ).

V komunitě C může být nedefinované chování vtipně označováno jako „ nosní démoni “, po příspěvku comp.std.c, který nedefinované chování vysvětlil tak, že kompilátorovi umožňuje dělat cokoli, co si vybere, dokonce „aby démoni vyletěli z nosu“ ".

Přehled

Některé programovací jazyky umožňují, aby program fungoval jinak nebo dokonce měl jiný řídicí tok než zdrojový kód , pokud vykazuje stejné uživatelsky viditelné vedlejší efekty , pokud se během provádění programu nikdy nedefinované chování nestane . Nedefinované chování je název seznamu podmínek, které program nesmí splňovat.

V raných verzích C byla primární výhodou nedefinovaného chování výroba výkonných kompilátorů pro širokou škálu počítačů: konkrétní konstrukci bylo možné namapovat na funkci specifickou pro stroj a kompilátor nemusel generovat další kód za běhu přizpůsobit vedlejší efekty tak, aby odpovídaly sémantice uložené jazykem. Zdrojový kód programu byl napsán s předchozí znalostí konkrétního kompilátoru a platforem , které bude podporovat.

Díky progresivní standardizaci platforem to však bylo méně výhodné, zejména v novějších verzích C. Nyní případy nedefinovaného chování obvykle představují jednoznačné chyby v kódu, například indexování pole mimo jeho hranice. Podle definice může modul runtime předpokládat, že nedefinované chování se nikdy nestane; proto některé neplatné podmínky není třeba porovnávat. Pro překladače to také znamená, že se stanou platné různé transformace programu nebo se zjednoduší jejich důkazy o správnosti; to umožňuje různé druhy předčasné optimalizace a mikrooptimalizace , které vedou k nesprávnému chování, pokud stav programu splňuje některou z těchto podmínek. Kompilátor může také odebrat explicitní kontroly, které mohly být ve zdrojovém kódu, bez upozornění programátora; například detekce nedefinovaného chování testováním, zda k němu došlo, podle definice zaručeně nefunguje. To ztěžuje nebo nemožné naprogramovat přenosnou možnost bezpečnou proti selhání (u některých konstrukcí jsou možná nepřenosná řešení).

Aktuální vývoj kompilátoru obvykle vyhodnocuje a porovnává výkon kompilátoru s benchmarky navrženými kolem mikrooptimalizací, a to i na platformách, které se většinou používají na trhu stolních a přenosných počítačů pro obecné účely (například amd64). Nedefinované chování proto poskytuje dostatek prostoru pro zlepšení výkonu kompilátoru, protože zdrojový kód pro konkrétní příkaz zdrojového kódu je možné za běhu namapovat na cokoli.

V případě C a C ++ má kompilátor v těchto případech povoleno provádět diagnostiku při kompilaci, ale není vyžadován: implementace bude považována za správnou, ať už v takových případech dělá cokoli, analogicky k podmínkám v digitální logice bez péče . Za napsání kódu, který nikdy nevyvolá nedefinované chování, je zodpovědný programátor, ačkoli implementace kompilátoru mohou v takovém případě vydávat diagnostiku. Kompilátory v dnešní době mají příznaky, které umožňují takovou diagnostiku, například -fsanitizeumožňuje „undefined behavior sanitizer“ ( UBSan ) v gcc 4.9 a v clang . Tento příznak však není výchozí a jeho povolení je volbou toho, kdo kód vytvoří.

Za určitých okolností mohou existovat konkrétní omezení pro nedefinované chování. Například specifikace sady instrukcí CPU může ponechat chování některých forem instrukce nedefinované, ale pokud CPU podporuje ochranu paměti, pak specifikace pravděpodobně bude zahrnovat obecné pravidlo, které říká, že žádná instrukce přístupná uživateli může způsobit díru v zabezpečení operačního systému ; takže skutečný CPU by měl povoleno poškozovat uživatelské registry v reakci na takovou instrukci, ale nemohl by se například přepnout do režimu supervizora .

Runtime platforma může také poskytovat určitá omezení nebo záruky na nedefinované chování, pokud toolchain nebo runtime výslovně dokumentují, že konkrétní konstrukce nalezené ve zdrojovém kódu jsou mapovány na konkrétní dobře definované mechanismy dostupné za běhu. Například interpret může zaznamenat určité chování pro některé operace, které jsou definované ve specifikaci jazyka, zatímco ostatní tlumočníci nebo překladače pro stejný jazyk nemusí. Kompilátor produkuje spustitelný kód pro určitý ABI , vyplnění sémantické mezery způsoby, které jsou závislé na verzi kompilátoru: v dokumentaci k danému kompilátoru verze a specifikaci ABI může poskytnout omezení nedefinované chování. Spoléhání se na tyto implementační detaily dělá software non- přenosné , ale přenositelnost nemusí být problémem, pokud je software neměl být používán u konkrétního běhu.

Nedefinované chování může mít za následek zhroucení programu nebo dokonce selhání, které je obtížnější zjistit a způsobit, že program vypadá, že funguje normálně, například tichá ztráta dat a vytváření nesprávných výsledků.

Výhody

Dokumentace operace jako nedefinovaného chování umožňuje kompilátorům předpokládat, že se tato operace v odpovídajícím programu nikdy nestane. To dává kompilátoru více informací o kódu a tyto informace mohou vést k dalším příležitostem optimalizace.

Příklad pro jazyk C:

int foo(unsigned char x)
{
     int value = 2147483600; /* assuming 32-bit int and 8-bit char */
     value += x;
     if (value < 2147483600)
        bar();
     return value;
}

Hodnota xnemůže být záporná a vzhledem k tomu, že přetečení podepsaných celých čísel je nedefinované chování v jazyce C, kompilátor může předpokládat, že value < 2147483600bude vždy false. Proto ifpříkaz, včetně volání funkce bar, může kompilátor ignorovat, protože testovací výraz v souboru ifnemá žádné vedlejší účinky a jeho podmínka nebude nikdy splněna. Kód je tedy sémanticky ekvivalentní:

int foo(unsigned char x)
{
     int value = 2147483600;
     value += x;
     return value;
}

Kdyby byl kompilátor nucen předpokládat, že přetečení podepsaného celého čísla má wraparoundové chování, pak by výše uvedená transformace nebyla legální.

Takovéto optimalizace jsou pro lidi těžko rozpoznatelné, když je kód složitější a dochází k dalším optimalizacím, jako je vkládání . Výše uvedenou funkci může například volat jiná funkce:

void run_tasks(unsigned char *ptrx) {
    int z;
    z = foo(*ptrx);
    while (*ptrx > 60) {
        run_one_task(ptrx, z);
    }
}

Kompilátor zde může optimalizovat pryč while-loop pomocí analýzy rozsahu hodnot : kontrolou foo(), ví, že počáteční hodnota, na kterou ukazuje, ptrxnemůže překročit 47 (protože jakékoli další by spustilo nedefinované chování v foo()), proto počáteční kontrola *ptrx > 60vůle v odpovídajícím programu vždy být nepravdivý. Pokračujeme dále, protože výsledek zse nyní nikdy nepoužívá a foo()nemá žádné vedlejší účinky, kompilátor může optimalizovat tak, run_tasks()aby byl prázdnou funkcí, která se okamžitě vrátí. Zmizení while-loop může být obzvláště překvapivé, pokud foo()je definováno v samostatně kompilovaném souboru objektu .

Další výhodou povolení nedefinovaného přetečení podepsaných celých čísel je to, že umožňuje ukládat a manipulovat s hodnotou proměnné v registru procesoru, který je větší než velikost proměnné ve zdrojovém kódu. Pokud je například typ proměnné uvedené ve zdrojovém kódu užší než šířka nativního registru (například „ int “ na 64bitovém počítači, běžný scénář), pak kompilátor může bezpečně použít podepsaný 64- bitové celé číslo pro proměnnou ve strojovém kódu, který produkuje, beze změny definovaného chování kódu. Pokud by program závisel na chování přetečení 32bitových celých čísel, pak by kompilátor musel při kompilaci pro 64bitový počítač vložit další logiku, protože chování přetečení většiny strojních pokynů závisí na šířce registru.

Nedefinované chování také umožňuje více kontrol v době kompilace kompilátory a statickou analýzu programu .

Rizika

Standardy C a C ++ mají několik forem nedefinovaného chování, které nabízejí větší volnost při implementaci kompilátoru a kontroly při kompilaci na úkor nedefinovaného chování za běhu, pokud existuje. Zejména norma ISO pro C má dodatek se seznamem běžných zdrojů nedefinovaného chování. Kompilátory navíc nejsou nutné k diagnostice kódu, který závisí na nedefinovaném chování. Proto je běžné, že programátoři, dokonce i zkušení, spoléhají na nedefinované chování buď omylem, nebo jednoduše proto, že nejsou dobře obeznámeni s pravidly jazyka, který může zahrnovat stovky stránek. Výsledkem mohou být chyby, které se projeví při použití jiného kompilátoru nebo jiného nastavení. Testování nebo fuzzování s povolenými dynamickými nedefinovanými kontrolami chování, např. Clang sanitizers, může pomoci zachytit nedefinované chování, které není diagnostikováno kompilátorem nebo statickými analyzátory.

Nedefinované chování může vést k bezpečnostní zranitelnosti v softwaru. Přetečení vyrovnávací paměti a další chyby zabezpečení ve velkých webových prohlížečích jsou například způsobeny nedefinovaným chováním. Problém roku 2038 je dalším příkladem toho důvodu, aby byla podepsána přetečení celé číslo . Když GCC vývojáři ‚s změnil jejich kompilátoru v roce 2008 tak, že vynechá některé kontroly přetečení, které spoléhaly na nedefinované chování, CERT vydal varování před novějších verzích kompilátor. Linux Weekly News poukázal na to, že stejné chování bylo pozorováno v PathScale C , Microsoft Visual C ++ 2005 a několika dalších kompilátorech; varování bylo později pozměněno, aby varovalo před různými kompilátory.

Příklady v C a C ++

Hlavní formy nedefinovaného chování v C lze široce klasifikovat jako: narušení bezpečnosti prostorové paměti, narušení bezpečnosti dočasné paměti, přetečení celých čísel , přísné narušení aliasingu, narušení zarovnání, změny bez následků, datové závody a smyčky, které nevykonávají I/O ani neukončují .

V C použití jakékoli automatické proměnné před její inicializací přináší nedefinované chování, stejně jako celočíselné dělení nulou , přetečení celých čísel se znaménky, indexování pole mimo jeho definované hranice (viz přetečení vyrovnávací paměti ) nebo dereferencování nulového ukazatele . Obecně platí, že jakákoli instance nedefinovaného chování ponechá abstraktní prováděcí stroj v neznámém stavu a způsobí, že chování celého programu bude nedefinováno.

Pokus o úpravu řetězcového literálu způsobuje nedefinované chování:

char *p = "wikipedia"; // valid C, deprecated in C++98/C++03, ill-formed as of C++11
p[0] = 'W'; // undefined behavior

Celé dělení nulou má za následek nedefinované chování:

int x = 1;
return x / 0; // undefined behavior

Některé operace s ukazatelem mohou mít za následek nedefinované chování:

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

V C a C ++ je relační srovnání ukazatelů na objekty (pro srovnání menší než nebo větší než) definováno pouze striktně, pokud ukazatele ukazují na členy stejného objektu nebo prvky stejného pole . Příklad:

int main(void)
{
  int a = 0;
  int b = 0;
  return &a < &b; /* undefined behavior */
}

Dosažení konce funkce vracející hodnotu (jiné než main()) bez příkazu return způsobí nedefinované chování, pokud volající použije hodnotu volání funkce:

int f()
{
}  /* undefined behavior if the value of the function call is used*/

Úprava objektu mezi dvěma body sekvence více než jednou vyvolá nedefinované chování. Existují značné změny v tom, co způsobuje nedefinované chování ve vztahu k bodům posloupnosti od C ++ 11. Moderní kompilátory mohou vydávat varování, když narazí na několik nezměněných úprav stejného objektu. Následující příklad způsobí nedefinované chování v C i C ++.

int f(int i) {
  return i++ + i++; /* undefined behavior: two unsequenced modifications to i */
}

Při úpravách objektu mezi dvěma body posloupnosti je nedefinovaným chováním také čtení hodnoty objektu pro jakýkoli jiný účel než určení hodnoty, která má být uložena.

a[i] = i++; // undefined behavior
printf("%d %d\n", ++n, power(2, n)); // also undefined behavior

V C/C ++ bitový posun hodnoty o počet bitů, což je buď záporné číslo, nebo je větší nebo roven celkovému počtu bitů v této hodnotě, vede k nedefinovanému chování. Nejbezpečnější způsob (bez ohledu na to kompilátor prodejce) je vždy počet bitů na směny (pravý operand z <<a >> bitové operátory ) v rozsahu: < > (kde je levý operand). 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

Viz také

Reference

Další čtení

externí odkazy