it-swarm-eu.dev

Jaké je přísné pravidlo aliasu?

Když se ptám na běžné nedefinované chování v C , duše jsou osvícenější, než jsem odkazoval na přísné pravidlo aliasu.
O čem to mluví?

740
Benoit

Typická situace, s níž se setkáte s přísnými aliasingovými problémy, je při překrývání struktury (jako zařízení/síťové zprávy) do vyrovnávací paměti velikosti Word ve vašem systému (jako je ukazatel na uint32_ts nebo uint16_ts). Když překryjete strukturu na takovou vyrovnávací paměť nebo vyrovnávací paměť na takovou strukturu pomocí obsazení ukazatele, můžete snadno porušit přísná pravidla aliasu.

Takže v tomto typu nastavení, pokud chci poslat zprávu něčemu, musím mít dva nekompatibilní ukazatele směřující na stejný kus paměti. Mohl bych pak naivně kódovat něco takového:

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

Přísné pravidlo aliasingu znemožňuje toto nastavení: dereferencování ukazatele, který aliasuje objekt, který není kompatibilní typ nebo jeden z dalších typů povolených v C 2011 6.5 odstavec 71 je nedefinované chování. Bohužel můžete stále kódovat tímto způsobem, možná dostanete některá varování, nechte si kompilovat dobře, abyste měli při spuštění kódu neobvyklé neočekávané chování.

(Zdá se, že GCC je poněkud nekonzistentní ve své schopnosti dávat varovná upozornění, někdy nám dává přátelské varování a někdy ne.)

Abychom zjistili, proč toto chování není definováno, musíme přemýšlet o tom, co přísné pravidlo aliasu kupuje kompilátor. V zásadě s tímto pravidlem nemusí přemýšlet o vkládání pokynů k aktualizaci obsahu buff při každém spuštění smyčky. Místo toho při optimalizaci, s některými nepříjemně nevynucenými předpoklady o aliasingu, může tyto pokyny vynechat, načíst buff[0] a buff[1] do registrů CPU jednou před spuštěním smyčky a zrychlit tělo smyčky. Před zavedením přísného aliasingu musel kompilátor žít ve stavu paranoie, aby se obsah buff mohl kdykoli a odkudkoli změnit kdokoli. Abychom získali extra výkonnost Edge a za předpokladu, že většina lidí nebude psát ukazatele, bylo zavedeno přísné pravidlo aliasu.

Mějte na paměti, že pokud si myslíte, že je příklad vymyšlený, může k tomu dojít i v případě, že předáváte vyrovnávací paměť jiné funkci provádějící odesílání za vás, pokud ji máte.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

A přepište naši dřívější smyčku, abyste využili této pohodlné funkce

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

Kompilátor může nebo nemusí být schopen nebo dostatečně chytrý, aby se pokusil vložit SendMessage a může nebo nemusí rozhodnout o načtení nebo opětovném načtení buffu. Pokud je SendMessage součástí jiného API, které je kompilováno samostatně, pravděpodobně má pokyny k načtení obsahu buffu. Pak znovu, možná jste v C++ a toto je nějaká dočasná implementace pouze v záhlaví, kterou si kompilátor myslí, že může vložit. Nebo možná je to něco, co jste pro svůj vlastní účel napsali do svého souboru .c. Přesto může dojít k nedefinovanému chování. I když víme něco o tom, co se děje pod kapotou, stále jde o porušení pravidla, takže není zaručeno žádné přesně definované chování. Takže pouhým zabalením do funkce, která vezme naši vyrovnávací paměť Wordu, nemusí nutně pomoci.

Jak se toho tedy obejít?

  • Použijte unii. Většina kompilátorů to podporuje, aniž by si stěžovala na přísné aliasy. To je povoleno v C99 a výslovně povoleno v C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • Ve kompilátoru můžete zakázat přísné aliasy ( f [no-] strict-aliasing v gcc))

  • Můžete použít char* pro aliasy namísto Wordu vašeho systému. Pravidla umožňují výjimku pro char* (včetně signed char a unsigned char). Vždy se předpokládá, že char* aliasy jiných typů. To však nebude fungovat opačně: neexistuje žádný předpoklad, že vaše struktura pojmenovává vyrovnávací paměť znaků.

Pozor na začátečníky

Toto je pouze jedno možné minové pole, když se navzájem překrývají dva typy. Měli byste se také dozvědět o endianness , zarovnání slov ao tom, jak řešit problémy se zarovnáním pomocí strukturování balení správně.

Poznámka pod čarou

1 Typy, které C 2011 6.5 7 umožňuje přístup k hodnotě, jsou:

  • typ kompatibilní s efektivním typem objektu,
  • kvalifikovanou verzi typu kompatibilní s efektivním typem objektu,
  • typ, který je podepsaným nebo nepodepsaným typem odpovídající efektivnímu typu objektu,
  • typ, který je podepsaným nebo nepodepsaným typem odpovídající kvalifikované verzi účinného typu objektu,
  • agregovaný nebo svazový typ, který zahrnuje jeden z výše uvedených typů mezi jeho členy (včetně rekurzivně člena subagregovaného nebo uzavřeného svazu), nebo
  • typ znaku.
534
Doug T.

Nejlepší vysvětlení, které jsem našel, je Mike Acton, Pochopení přísného spojování . Trochu se zaměřuje na vývoj PS3, ale to je v podstatě jen GCC.

Z článku:

„Přísné aliasy jsou předpokladem kompilátoru C (nebo C++), že ukazatele dereferencování na objekty různých typů se nikdy nebudou odkazovat na stejné umístění v paměti (tj. Navzájem se přezdívat.)“

Takže pokud v podstatě máte int* ukazující na nějakou paměť obsahující int a pak přejdete float* do této paměti a použijete jej jako float porušujete pravidlo. Pokud to váš kód nerešpektuje, pak jej pravděpodobně s největší pravděpodobností ztratí optimalizátor kompilátoru.

Výjimkou z pravidla je char*, který může ukazovat na jakýkoli typ.

227
Niall

Toto je přísné pravidlo aliasu, které se nachází v oddíle 3.10 standardu C++ (jiné odpovědi poskytují dobré vysvětlení, ale žádná neposkytla samotné pravidlo):

Pokud se program pokusí získat přístup k uložené hodnotě objektu prostřednictvím jiné hodnoty než jednoho z následujících typů, chování není definováno:

  • dynamický typ objektu,
  • cv-kvalifikovanou verzi dynamického typu objektu,
  • typ, který je podepsaným nebo nepodepsaným typem odpovídající dynamickému typu objektu,
  • typ, který je podepsaným nebo nepodepsaným typem odpovídající verzi dynamického typu objektu kvalifikovanou pro cv,
  • agregovaný nebo svazový typ, který zahrnuje jeden z výše uvedených typů mezi jeho členy (včetně rekurzivně člena subagregovaného nebo uzavřeného svazu),
  • typ, který je typem základní třídy (případně s kvalifikací pro cv) dynamického typu objektu,
  • typ char nebo unsigned char.

C++ 11 a C++ 14 formulace (zdůrazněné změny):

Pokud se program pokusí získat přístup k uložené hodnotě objektu prostřednictvím glvalue jiného než jednoho z následujících typů, chování není definováno:

  • dynamický typ objektu,
  • cv-kvalifikovanou verzi dynamického typu objektu,
  • typ podobný (jak je definován v 4.4) jako dynamický typ objektu,
  • typ, který je podepsaným nebo nepodepsaným typem odpovídající dynamickému typu objektu,
  • typ, který je podepsaným nebo nepodepsaným typem odpovídající verzi dynamického typu objektu kvalifikovanou pro cv,
  • agregovaný nebo jednotný typ, který zahrnuje jeden z výše uvedených typů mezi jeho prvky nebo nestatické datové členy (včetně rekurzivně prvek nebo nestatický datový člen podskupiny nebo uzavřeného spojení),
  • typ, který je typem základní třídy (případně s kvalifikací pro cv) dynamického typu objektu,
  • typ char nebo unsigned char.

Dvě změny byly malé: glvalue namísto lvalue a vyjasnění agregovaný/unijní případ.

Třetí změna poskytuje silnější záruku (uvolňuje silné pravidlo aliasu): Nový koncept podobných podobných typů , které lze nyní snadno přezdívat.


Také C formulace (C99; ISO/IEC 9899: 1999 6.5/7; stejné stejné znění je použito v ISO/IEC 9899: 2011 §6.5 ¶7):

K objektu bude mít přístup k jeho uložené hodnotě pouze výrazem lvalue, který má jeden z následujících typů 73) nebo 88):

  • typ kompatibilní s efektivním typem objektu,
  • kvali fi kovaná verze typu kompatibilního s efektivním typem objektu,
  • typ, který je podepsaným nebo nepodepsaným typem odpovídající efektivnímu typu objektu,
  • typ, který je podepsaným nebo nepodepsaným typem odpovídající kvali fi kované verzi efektivního typu objektu,
  • agregovaný nebo svazový typ, který zahrnuje jeden z výše uvedených typů mezi jeho členy (včetně rekurzivně člena subagregovaného nebo uzavřeného svazu), nebo
  • typ znaku.

73) nebo 88) Účelem tohoto seznamu je upřesnit okolnosti, za kterých může nebo nemusí být předmět pojmenován.

129
Ben Voigt

Poznámka

Toto je výňatek z mého „Co je přísné pravidlo spojování a proč nám záleží?“ zápis.

Co je přísné aliasy?

V C a C++ aliasing má co do činění s tím, jaké typy výrazů máme přístup k uloženým hodnotám. V obou C a C++ standard specifikuje, které typy výrazů jsou povoleny alias, které typy. Kompilátor a optimalizátor jsou oprávněni předpokládat, že se striktně řídíme pravidly aliasingu, tedy termínpřísné pravidlo aliasu. Pokud se pokusíme získat přístup k hodnotě pomocí typu, který není povolen, je klasifikován jako nedefinované chování (UB). Jakmile máme nedefinované chování, všechny sázky jsou vypnuty, výsledky našeho programu již nejsou spolehlivé.

Bohužel s přísným porušováním aliasu často dostaneme očekávané výsledky a ponecháme možnost, aby budoucí verze kompilátoru s novou optimalizací porušila kód, který jsme považovali za platný. To je nežádoucí a je užitečné pochopit přísná pravidla pro aliasy a jak se jim vyhnout.

Abychom pochopili více o tom, proč nám záleží, budeme diskutovat o problémech, které se vyskytnou při porušení přísných pravidel pro aliasy, typování, protože běžné techniky používané při psaní typu často porušují přísná pravidla pro aliasy a jak správně psát výrazy.

Předběžné příklady

Podívejme se na několik příkladů, pak si můžeme promluvit o tom, co přesně říká norma (standardy), prozkoumat některé další příklady a pak uvidíme, jak se vyhnout přísnému aliasingu a porušování úlovků, které jsme minuli. Zde je příklad, který by neměl být překvapující ( živý příklad ):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

Mámeint *směřující do paměti obsazenéinta toto je platné aliasy. Optimalizátor musí předpokládat, že přiřazení pomocí ip může aktualizovat hodnotu obsazenou x.

Následující příklad ukazuje aliasy, které vedou k nedefinovanému chování ( live příklad ):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

Ve funkci foo beremeint *a afloat *, v tomto příkladu voláme foo a nastavte oba parametry tak, aby ukazovaly na stejné paměťové místo, které v tomto příkladu obsahujeint. Poznámka: reinterpret_cast říká kompilátoru, aby s výrazem zacházel, jako by měl typ specifikovaný parametrem šablony. V tomto případě říkáme, aby zacházel s výrazem & x, jako by měl typfloat *. Můžeme naivně očekávat, že výsledek druhého cout ​​bude , ale s optimalizací povolenou pomocí - O2 jak gcc, tak clang vyprodukují následující výsledek:

0
1

Což nelze očekávat, ale je naprosto platné, protože jsme vyvolali nedefinované chování. Afloatnemůže platně alias objektint. Optimalizátor proto může předpokládat, žekonstanta 1uložená při dereferencování i bude vrácená hodnota, protože úložiště prostřednictvím f nemohlo platně ovlivnitintobjekt. Připojením kódu v Průzkumníku překladačů se zobrazí přesně to, co se děje ( živý příklad ):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

Optimalizátor využívající Alias-Based Alias ​​Analysis (TBAA) předpokládá 1 bude vrácen a přímo přesune konstantní hodnotu do registru eax, který nese návrat hodnota. TBAA používá pravidla jazyka o tom, jaké typy jsou povoleny aliasům, aby optimalizovala zatížení a úložiště. V tomto případě TBAA ví, že afloatnemůže alias ainta optimalizuje zatížení i.

Nyní, do knihy pravidel

Co přesně říká norma, že máme povoleno a není povoleno? Standardní jazyk není přímočarý, takže pro každou položku se pokusím uvést příklady kódu, které ukazují význam.

Co říká norma C11?

Standard C11 říká v oddíle 6.5 Výrazy odstavec 7:

K objektu bude mít přístup k jeho uložené hodnotě pouze výrazem lvalue, který má jeden z následujících typů:88) - typ kompatibilní s efektivním typem objektu,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- kvalifikovanou verzi typu slučitelnou s efektivním typem objektu,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- typ, který je podepsaným nebo nepodepsaným typem odpovídající skutečnému typu objektu,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc/clang má přípon a také , které umožňuje přiřazenínepodepsané int *doint */ ačkoli to nejsou kompatibilní typy.

- typ, který je podepsaným nebo nepodepsaným typem odpovídající kvalifikované verzi efektivního typu objektu,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- agregovaný nebo sdružený typ, který zahrnuje jeden z výše uvedených typů mezi jeho členy (včetně, rekurzivně, člena podskupiny nebo uzavřené unie), nebo

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- typ znaku.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

Co říká C++ 17 Draft Standard

Návrh standardu C++ 17 v oddíle [basic.lval] odstavec 11říká:

Pokud se program pokusí získat přístup k uložené hodnotě objektu prostřednictvím jiné hodnoty než jednoho z následujících typů, chování není definováno:63 (11.1) - dynamický typ objektu,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - cv-kvalifikovaná verze dynamického typu objektu,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - typ podobný (jak je definován v 7.5) jako dynamický typ objektu,

(11.4) - typ, který je podepsaným nebo nepodepsaným typem odpovídající dynamickému typu objektu,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - typ, který je podepsaným nebo nepodepsaným typem odpovídající verzi dynamického typu objektu kvalifikovanou pro cv,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - agregovaný nebo souborový typ, který zahrnuje jeden z výše uvedených typů mezi jeho prvky nebo nestatické datové členy (včetně, rekurzivně, prvku nebo nestatického datového členu podskupiny nebo obsažené unie),

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - typ, který je typem základní třídy dynamického typu objektu (možná kvalifikovaný pro vv),

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - typ char, unsigned char nebo std :: byte.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Za zmínku stojípodepsaný znaknení zahrnut ve výše uvedeném seznamu, jedná se o pozoruhodný rozdíl odC, který říkátyp znaku.

Co je typ Punning

Dostali jsme se k tomuto bodu a možná se divíme, proč bychom chtěli alias? Odpověď je obvykle natype pun, často používané metody porušují přísná pravidla aliasu.

Někdy chceme obejít typový systém a interpretovat objekt jako jiný typ. Toto se nazývátyp punning, k reinterpretaci segmentu paměti jako jiného typu.Puncing typuje užitečný pro úkoly, které chtějí přístup k podkladové reprezentaci objektu pro zobrazení, transport nebo manipulaci. Typické oblasti, které zjistíme, že se používají typy puncování, jsou kompilátory, serializace, síťový kód atd.…

Tradičně se toho dosáhlo převzetím adresy objektu, přetypováním na ukazatel typu, který chceme znovu interpretovat, a poté přístupem k hodnotě nebo jinými slovy aliasem. Například:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

Jak jsme již dříve viděli, nejedná se o platné aliasy, takže vyvoláváme nedefinované chování. Ale tradičně kompilátoři nevyužívali přísná pravidla pro aliasy a tento typ kódu obvykle právě fungoval, vývojáři si bohužel zvykli dělat věci tímto způsobem. Běžná alternativní metoda pro typové děrování je prostřednictvím odborů, které jsou platné v C alenedefinované chovánív C++ ( viz živý příklad ):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

To neplatí v C++ a někteří považují účel odborů pouze za implementaci variantních typů a pocit, že odbory používají k puncování typu, je zneužití.

Jak správně zadáme Pun?

Standardní metoda protyp puncingv C i C++ je memcpy. Může to vypadat trochu těžce, ale optimalizátor by měl rozpoznat použití memcpy protyp punninga optimalizovat jej a vygenerovat registr pro registraci pohybu. Například pokud víme, žeint64_tje stejná velikost jakodouble:

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

můžeme použít memcpy:

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

Při dostatečné optimalizační úrovni generuje každý slušný moderní kompilátor identický kód jako dříve zmíněný reinterpret_cast ​​metoda nebouniemetoda protypové puncing. Při zkoumání vygenerovaného kódu vidíme, že používá pouze registr mov ( live příklad kompilátor ).

C++ 20 a bit_cast

V C++ 20 můžeme získat bit_cast ​​( implementace je k dispozici v odkazu z návrh ), což poskytuje jednoduchý a bezpečný způsob, jak psát typ, a být použitelný v constexpr kontext.

Následuje příklad, jak pomocí bit_cast ​​psát slovní hříčku anepodepsané intdofloat, ( viz to live ):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

V případě, žeToaOdtypy nemají stejnou velikost, vyžaduje to použití mezilehlé struktury15. Budeme používat strukturu obsahující sizeof (unsigned int) pole znaků (předpokládá 4 byte unsigned int) jakoFromtyp anepodepsané intjakodotyp .:

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

Je nešťastné, že potřebujeme tento přechodný typ, ale to je aktuální omezení bit_cast.

Chytání přísných porušení aliasingu

Nemáme mnoho dobrých nástrojů pro zachycení přísného aliasingu v C++, nástroje, které máme, zachytí některé případy přísného porušení aliasu a některé případy nevyrovnaných zátěží a obchodů.

gcc pomocí příznaku - fstrict-aliasing a - Wstrict-aliasing může zachytit některé případy, i když ne bez falešných pozitiv/negativů. Například následující případy vygenerují varování v gcc ( viz to live ):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

ačkoli to nezachytí tento další případ ( viz to live ):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Ačkoli clang umožňuje tyto příznaky, zjevně varování ve skutečnosti neimplementuje.

Dalším nástrojem, který máme k dispozici, je ASan, který dokáže zachytit nevyrovnané náklady a zásoby. Ačkoli se nejedná o přímo přísná porušení aliasu, jsou běžným důsledkem přísného porušení aliasu. Například následující případy budou generovat chyby za běhu, když jsou vytvořeny pomocí clang pomocí - fsanitize = adresa

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

Poslední nástroj, který doporučím, je specifický pro C++ a není to striktně nástroj, ale praxe kódování, neumožňuje obsazení ve stylu C. Gcc i clang vytvoří diagnostiku pro obsazení ve stylu C pomocí - Wold-style-cast. To přinutí všechna nedefinovaná typová písmena k použití reinterpret_cast, obecně by reinterpret_cast měla být příznakem pro bližší kontrolu kódu. Je také snazší vyhledat v databázi kód pro reinterpret_cast provést audit.

Pro C máme již všechny nástroje pokryty a také máme tkáňový interpret, statický analyzátor, který důkladně analyzuje program pro velkou podmnožinu jazyka C. Dané C verze předchozího příkladu, kde při použití - fstrict-aliasing chybí jeden případ ( viz to live )

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter je schopen zachytit všechny tři, následující příklad vyvolá tis-kernal jako tis-interpret (výstup je editován pro stručnost):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

Konečně existuje TySan , který se momentálně vyvíjí. Tento sanitizer přidává informace o kontrole typu do segmentu stínové paměti a kontroluje přístupy, aby zjistil, zda porušují pravidla aliasu. Nástroj by měl být schopen zachytit všechna porušení aliasu, ale může mít velké provozní náklady.

51
Shafik Yaghmour

Přísné aliasy se nevztahují pouze na ukazatele, ovlivňují také odkazy, napsal jsem o tom referát pro podporu wiki pro vývojáře a bylo tak dobře přijato, že jsem jej změnil na stránku na svém konzultačním webu. Úplně to vysvětluje, o co jde, proč si lidi tolik pletou a co s tím dělat. Přísná alianční bílá kniha . Zejména vysvětluje, proč jsou odbory rizikovým chováním pro C++ a proč je použití memcpy jediným přenosným opravitelem napříč C i C++. Doufám, že je to užitečné.

42
Patrick

Jako dodatek k tomu, co již napsal Doug T., je zde jednoduchý testovací případ, který jej pravděpodobně spustí pomocí gcc:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

Kompilace s gcc -O2 -o check check.c. Obvykle (s většinou verzí gcc, které jsem vyzkoušel) je výstup „přísný aliasingový problém“, protože kompilátor předpokládá, že „h“ nemůže být ve funkci „check“ stejná adresa jako „k“. Z tohoto důvodu kompilátor optimalizuje if (*h == 5) pryč a vždy volá printf.

Pro ty, kteří se zajímají, je zde kód assembleru x64, produkovaný gcc 4.6.3, spuštěný na Ubuntu 12.04.2 pro x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Takže podmínka if je úplně pryč z kódu assembleru.

33
Ingo Blackman

Puncování typ pomocí obsazení ukazatele (na rozdíl od použití unie) je hlavním příkladem porušení přísného aliasingu.

16

Podle odůvodnění C89 autoři standardu nechtěli požadovat, aby kompilátoři zadali kód jako:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

by mělo být vyžadováno znovu načíst hodnotu x mezi příkazem přiřazení a návratu, aby se umožnila možnost, že p může ukazovat na x a přiřazení k *p by mohlo následně změnit hodnotu x. Představa, že kompilátor by měl mít právo předpokládat, že nebude existovat aliasy v situacích jako výše, byla nesporná.

Autoři bohužel psali své pravidlo způsobem, který, pokud by byl čten doslova, by způsobil, že i následující funkce vyvolá nedefinované chování:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

protože používá hodnotu typu int pro přístup k objektu typu struct S a int není mezi typy, které mohou být použity pro přístup k struct S. Vzhledem k tomu, že by bylo absurdní zacházet se všemi členy nestrukturního typu struktur a odborů jako s nedefinovaným chováním, téměř každý uznává, že existují přinejmenším některé situace, kdy může být použita hodnota jednoho typu pro přístup k objektu jiného typu. . Výbor pro standardy C bohužel nedefinoval, jaké jsou tyto okolnosti.

Většina problému je výsledkem zprávy o chybách č. 028, která se zeptala na chování programu, jako je:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

Zpráva o defektu č. 28 uvádí, že program vyvolává nedefinované chování, protože akce zápisu odborového typu typu „double“ a čtení jednoho typu „int“ vyvolává chování definované implementací. Takové zdůvodnění je nesmyslné, ale tvoří základ pravidel efektivního typu, která zbytečně komplikují jazyk a nedělají nic pro vyřešení původního problému.

Nejlepším způsobem, jak vyřešit původní problém, by bylo pravděpodobně zacházet s poznámkou pod čarou o účelu pravidla, jako by to bylo normativní, a učinit by pravidlo nevymahatelné, s výjimkou případů, ve kterých skutečně dochází ke konfliktním přístupům pomocí aliasu. Vzhledem k tomu, že:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

V rámci inc_int není žádný konflikt, protože všechny přístupy do úložiště přístupného prostřednictvím *p jsou prováděny s hodnotou typu int a v test není konflikt, protože p je viditelně odvozeno od struct S a příště se používá s úložiště, které bude kdy vytvořeno prostřednictvím p, se již stalo.

Pokud byl kód mírně změněn ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Zde je konflikt mezi aliasy p a přístupem k s.x na označeném řádku, protože v tomto okamžiku provádění existuje další odkaz který bude použit pro přístup ke stejnému úložišti.

Pokud by zpráva o defektu 028 uvedla původní příklad vyvolaný UB kvůli překrývání mezi vytvořením a použitím dvou ukazatelů, bylo by to mnohem jasnější, aniž by bylo nutné přidávat „efektivní typy“ nebo jinou takovou složitost.

14
supercat

Po přečtení mnoha odpovědí cítím potřebu něco přidat:

Přísné aliasy (které popíšu trochu) je důležité, protože:

  1. Přístup do paměti může být drahý (výkonný), proto data jsou zpracovávána v registrech CP před zápisem zpět do fyzické paměti.

  2. Pokud budou data ve dvou různých registrech CPU zapsána do stejného paměťového prostoru, nemůžeme předpovídat, která data „přežijí“, když kódujeme v C.

    V sestavě, kde kódujeme nakládání a vykládání registrů CPU ručně, budeme vědět, která data zůstávají nedotčena. Ale C (naštěstí) tento detail abstrahuje.

Protože dva ukazatele mohou ukazovat na stejné místo v paměti, mohlo by to vést k složitý kód, který zpracovává možné kolize.

Tento zvláštní kód je pomalý a bolí výkon, protože provádí další operace čtení a zápisu do paměti, které jsou pomalejší a (možná) zbytečné.

Pravidlo Striktní aliasy nám umožňuje vyhnout se nadbytečnému strojovému kód v případech, kdy by mělo být bezpečné předpokládat, že dva ukazatele nejsou 't poukazují na stejný blok paměti (viz také klíčové slovo restrict).

Strict aliasing uvádí, že lze bezpečně předpokládat, že ukazatele na různé typy ukazují na různá umístění v paměti.

Pokud si kompilátor všimne, že dva ukazatele ukazují na různé typy (například int * a float *), bude předpokládat, že adresa paměti je jiná a nebude chrání před kolizí adres paměti, což má za následek rychlejší strojový kód.

Například:

Umožňuje převzít následující funkci:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

Abychom zvládli případ, kdy a == b (oba ukazatele ukazují na stejnou paměť), musíme si objednat a otestovat způsob, jakým načítáme data z paměti do registrů CPU, takže kód může skončit takto:

  1. načíst a a b z paměti.

  2. přidat a do b.

  3. ložitb a reloada.

    (uložit z registru CPU do paměti a načíst z paměti do registru CPU).

  4. přidat b do a.

  5. uložit a (z registru CPU) do paměti.

Krok 3 je velmi pomalý, protože potřebuje přístup k fyzické paměti. Je však třeba chránit před případy, kdy a a b ukazují na stejnou adresu paměti.

Přísné aliasing by nám to zabránilo tím, že řekneme kompilátoru, že tyto adresy paměti jsou zřetelně odlišné (což v tomto případě umožní ještě další optimalizaci, kterou nelze provést, pokud ukazatele sdílejí adresu paměti).

  1. To lze říct kompilátoru dvěma způsoby, pomocí různých typů, na které odkazuje. tj.:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. Pomocí klíčového slova restrict. tj.:

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

Nyní, splněním pravidla přísného spojování, je možné se vyhnout kroku 3 a kód bude běžet podstatně rychleji.

Ve skutečnosti lze přidáním klíčového slova restrict celou funkci optimalizovat na:

  1. načíst a a b z paměti.

  2. přidat a do b.

  3. uložit výsledek do a a do b.

Tato optimalizace nemohla být provedena dříve, kvůli možné kolizi (kde a a b by se ztrojnásobily místo zdvojnásobení).

10
Myst

Přísné aliasing neumožňuje různým typům ukazatelů na stejná data.

Tento článek by vám měl pomoci porozumět problému podrobně.

6
Jason Dagit