Domanda

Quando chiedi informazioni comportamento indefinito comune in C, anime più illuminate di me hanno fatto riferimento alla rigida regola dell'aliasing.
Di cosa stanno parlando?

È stato utile?

Soluzione

Una situazione tipica in cui incontri seri problemi di aliasing è quando si sovrappone una struttura (come un messaggio di dispositivo/rete) su un buffer della dimensione della parola del tuo sistema (come un puntatore a uint32_ts o uint16_tS).Quando sovrapponi una struttura a un buffer di questo tipo o un buffer a una struttura di questo tipo tramite il casting del puntatore, puoi facilmente violare rigide regole di aliasing.

Quindi, in questo tipo di configurazione, se voglio inviare un messaggio a qualcosa dovrei avere due puntatori incompatibili che puntano allo stesso pezzo di memoria.Potrei quindi codificare ingenuamente qualcosa del genere:

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]);   
    }
}

La rigida regola dell'aliasing rende questa configurazione illegale:dereferenziare un puntatore che dà l'alias a un oggetto che non è di a tipo compatibile o una delle altre tipologie consentite da C 2011 6.5 paragrafo 71 è un comportamento indefinito.Sfortunatamente, puoi ancora programmare in questo modo, Forse ricevi alcuni avvisi, fallo compilare correttamente, solo per avere uno strano comportamento imprevisto quando esegui il codice.

(GCC appare in qualche modo incoerente nella sua capacità di fornire avvisi di aliasing, a volte dandoci un avviso amichevole e talvolta no.)

Per capire perché questo comportamento non è definito, dobbiamo pensare a cosa guadagna il compilatore con la rigorosa regola di aliasing.In sostanza, con questa regola, non si deve pensare ad inserire istruzioni per rinfrescare il contenuto buff ogni esecuzione del ciclo.Invece, durante l'ottimizzazione, con alcuni presupposti fastidiosamente non applicati sull'aliasing, può omettere quelle istruzioni, caricare buff[0] E buff[1] nei registri della CPU una volta prima dell'esecuzione del ciclo e accelera il corpo del ciclo.Prima che venisse introdotto l'aliasing rigoroso, il compilatore doveva vivere in uno stato di paranoia riguardo al contenuto di buff potrebbe cambiare in qualsiasi momento da qualsiasi luogo da chiunque.Quindi, per ottenere un ulteriore vantaggio in termini di prestazioni e presupponendo che la maggior parte delle persone non digiti puntatori di giochi di parole, è stata introdotta la rigorosa regola dell'aliasing.

Tieni presente che, se ritieni che l'esempio sia artificioso, ciò potrebbe anche accadere se stai passando un buffer a un'altra funzione che esegue l'invio per te, se invece lo hai fatto.

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

E abbiamo riscritto il nostro ciclo precedente per sfruttare questa comoda funzione

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

Il compilatore potrebbe o meno essere in grado o abbastanza intelligente da provare a incorporare SendMessage e potrebbe decidere o meno di caricare o meno il buff.Se SendMessage fa parte di un'altra API compilata separatamente, probabilmente contiene istruzioni per caricare il contenuto di buff.Poi di nuovo, forse sei in C++ e questa è un'implementazione solo di intestazione basata su modello che il compilatore ritiene di poter incorporare.O forse è solo qualcosa che hai scritto nel tuo file .c per tua comodità.In ogni caso potrebbe comunque verificarsi un comportamento indefinito.Anche quando sappiamo qualcosa di ciò che accade dietro le quinte, si tratta comunque di una violazione della regola, quindi non è garantito alcun comportamento ben definito.Quindi semplicemente racchiudendo una funzione che accetta il nostro buffer delimitato da parole non è necessariamente d'aiuto.

Allora come posso aggirare questo problema?

  • Usa un'unione.La maggior parte dei compilatori lo supporta senza lamentarsi dell'aliasing rigoroso.Ciò è consentito in C99 ed esplicitamente consentito in C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • Puoi disabilitare l'aliasing rigoroso nel compilatore (f[no-]aliasing rigoroso in gcc))

  • Puoi usare char* per l'alias invece della parola del tuo sistema.Le regole consentono un'eccezione per char* (Compreso signed char E unsigned char).Si dà sempre per scontato questo char* alias altri tipi.Tuttavia questo non funzionerà nel contrario:non si presume che la tua struttura alias un buffer di caratteri.

Principiante, attenzione

Questo è solo un potenziale campo minato quando si sovrappongono due tipi l'uno sull'altro.Dovresti anche informarti endianità, allineamento delle parole, e come affrontare i problemi di allineamento strutture di imballaggio correttamente.

Nota

1 I tipi a cui C 2011 6.5 7 consente l'accesso a un lvalue sono:

  • un tipo compatibile con il tipo effettivo dell'oggetto,
  • una versione qualificata di un tipo compatibile con il tipo effettivo dell'oggetto,
  • un tipo che è il tipo con segno o senza segno corrispondente al tipo effettivo dell'oggetto,
  • un tipo che è il tipo firmato o non firmato corrispondente a una versione qualificata del tipo effettivo dell'oggetto,
  • un tipo di aggregato o unione che include uno dei tipi sopra menzionati tra i suoi membri (incluso, ricorsivamente, un membro di un sottoaggregato o di un'unione contenuta), o
  • un tipo di carattere.

Altri suggerimenti

La migliore spiegazione che ho trovato è di Mike Acton, Comprendere l'aliasing rigoroso.Si concentra un po' sullo sviluppo di PS3, ma fondamentalmente è solo GCC.

Dall'articolo:

"L'aliasing rigoroso è un presupposto, fatto dal compilatore C (o C++), che i puntatori di dereferenziazione a oggetti di tipo diverso non faranno mai riferimento alla stessa posizione di memoria (cioèalias tra loro.)"

Quindi, in pratica, se hai un file int* indicando una memoria contenente un int e poi indichi a float* a quella memoria e usarla come a float infrangi la regola.Se il tuo codice non lo rispetta, molto probabilmente l'ottimizzatore del compilatore interromperà il tuo codice.

L'eccezione alla regola è a char*, che può puntare a qualsiasi tipo.

Questa è la rigorosa regola di aliasing, che si trova nella sezione 3.10 del C++03 standard (altre risposte forniscono una buona spiegazione, ma nessuna ha fornito la regola stessa):

Se un programma tenta di accedere al valore memorizzato di un oggetto tramite un lvalue diverso da uno dei seguenti tipi, il comportamento non è definito:

  • il tipo dinamico dell'oggetto,
  • una versione qualificata cv del tipo dinamico dell'oggetto,
  • un tipo che è il tipo con o senza segno corrispondente al tipo dinamico dell'oggetto,
  • un tipo che è il tipo con segno o senza segno corrispondente a una versione qualificata cv del tipo dinamico dell'oggetto,
  • un tipo di aggregato o unione che include uno dei suddetti tipi tra i suoi membri (incluso, ricorsivamente, un membro di un sottoaggregato o di un'unione contenuta),
  • un tipo che è un tipo di classe base (possibilmente qualificato cv) del tipo dinamico dell'oggetto,
  • UN char O unsigned char tipo.

C++11 E C++14 formulazione (modifiche enfatizzate):

Se un programma tenta di accedere al valore memorizzato di un oggetto tramite a gvalore di tipi diversi da quelli seguenti il ​​comportamento non è definito:

  • il tipo dinamico dell'oggetto,
  • una versione qualificata cv del tipo dinamico dell'oggetto,
  • un tipo simile (come definito in 4.4) al tipo dinamico dell'oggetto,
  • un tipo che è il tipo con o senza segno corrispondente al tipo dinamico dell'oggetto,
  • un tipo che è il tipo con segno o senza segno corrispondente a una versione qualificata cv del tipo dinamico dell'oggetto,
  • una tipologia aggregata o unione che comprende tra le sue una delle suddette tipologie elementi o membri dati non statici (incluso, ricorsivamente, an elemento o membro dati non statico di un sottoaggregato o di un'unione contenuta),
  • un tipo che è un tipo di classe base (possibilmente qualificato cv) del tipo dinamico dell'oggetto,
  • UN char O unsigned char tipo.

Due cambiamenti erano piccoli: gvalore invece di valore, e chiarimento del caso aggregato/unione.

La terza modifica rende una garanzia più forte (allenta la regola dell'aliasing forte):Il nuovo concetto di tipi simili che ora sono sicuri per gli alias.


Anche il C formulazione (C99;ISO/IEC 9899:1999 6.5/7;la stessa identica formulazione è utilizzata nella norma ISO/IEC 9899:2011 §6.5 §7):

Un oggetto deve avere il suo valore memorizzato accessibile solo da un'espressione di LValue che ha uno dei seguenti tipi 73) o 88):

  • un tipo compatibile con il tipo effettivo dell'oggetto,
  • Una versione qualificata di un tipo compatibile con il tipo efficace dell'oggetto,
  • un tipo che è il tipo firmato o non firmato corrispondente al tipo effettivo dell'oggetto,
  • un tipo che è il tipo firmato o non firmato corrispondente a una versione qualificata del tipo efficace dell'oggetto,
  • un tipo aggregato o sindacale che include uno dei tipi sopra menzionati tra i suoi membri (inclusi, ricorsivamente, un membro di un sottoaggregato o un sindacato contenuto) o
  • un tipo di carattere.

73) o 88) Lo scopo di questo elenco è specificare le circostanze in cui un oggetto può o meno avere un alias.

Nota

Questo è estratto dal mio "Qual è la regola rigorosa dell'aliasing e perché ci interessa?" Scrivilo.

Cos'è l'aliasing rigoroso?

In C e C++ l'aliasing ha a che fare con i tipi di espressione attraverso i quali è consentito accedere ai valori memorizzati.Sia in C che in C++ lo standard specifica quali tipi di espressione possono essere utilizzati come alias di quali tipi.Il compilatore e l'ottimizzatore possono presumere che seguiamo rigorosamente le regole di aliasing, da qui il termine regola rigorosa dell'aliasing.Se proviamo ad accedere a un valore utilizzando un tipo non consentito, viene classificato come comportamento indefinito(UB).Una volta che abbiamo un comportamento indefinito, tutte le scommesse vengono annullate, i risultati del nostro programma non sono più affidabili.

Sfortunatamente con severe violazioni dell'aliasing, otterremo spesso i risultati attesi, lasciando la possibilità che una versione futura di un compilatore con una nuova ottimizzazione rompa il codice che ritenevamo valido.Ciò non è auspicabile ed è un obiettivo utile comprendere le rigide regole di aliasing e come evitare di violarle.

Per comprendere meglio il motivo per cui ci preoccupiamo, discuteremo dei problemi che emergono quando si violano rigide regole di aliasing, dei giochi di parole sui tipi poiché le tecniche comuni utilizzate nei giochi di parole spesso violano rigide regole di aliasing e su come digitare correttamente i giochi di parole.

Esempi preliminari

Diamo un'occhiata ad alcuni esempi, poi possiamo parlare esattamente di ciò che dicono gli standard, esaminare alcuni ulteriori esempi e poi vedere come evitare un aliasing rigoroso e individuare le violazioni che ci sono sfuggite.Ecco un esempio che non dovrebbe sorprendere (esempio dal vivo):

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

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

Noi abbiamo un int* indicando la memoria occupata da un int e questo è un alias valido.L'ottimizzatore deve presupporre che le assegnazioni finiscano ip potrebbe aggiornare il valore occupato da X.

L'esempio successivo mostra l'aliasing che porta a un comportamento indefinito (esempio dal vivo):

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?
}

Nella funzione pippo prendiamo un int* e un galleggiante*, in questo esempio chiamiamo pippo e imposta entrambi i parametri in modo che puntino alla stessa posizione di memoria che in questo esempio contiene un file int.Notare la reinterpret_cast sta dicendo al compilatore di trattare l'espressione come se avesse il tipo specificato dal suo parametro di modello.In questo caso gli stiamo dicendo di trattare l'espressione &X come se avesse dei caratteri galleggiante*.Potremmo ingenuamente aspettarci il risultato del secondo cout essere 0 ma con l'ottimizzazione abilitata utilizzando -O2 sia gcc che clang producono il seguente risultato:

0
1

Il che potrebbe non essere previsto ma è perfettamente valido poiché abbiamo invocato un comportamento indefinito.UN galleggiante non può validamente alias an int oggetto.Pertanto l'ottimizzatore può assumere il costante 1 memorizzato durante il dereferenziamento io sarà il valore restituito da un negozio attraverso F non potrebbe influenzare validamente un int oggetto.L'inserimento del codice in Compiler Explorer mostra che questo è esattamente ciò che sta accadendo(esempio dal vivo):

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

L'ottimizzatore che utilizza Analisi degli alias basata sul tipo (TBAA) presuppone 1 verrà restituito e sposta direttamente il valore costante nel registro eax che porta il valore restituito.TBAA utilizza le regole della lingua su quali tipi sono consentiti per l'alias per ottimizzare carichi e negozi.In questo caso TBAA sa che a galleggiante non è possibile alias e int e ottimizza il carico di io.

Ora passiamo al regolamento

Cosa dice esattamente lo standard che possiamo e non possiamo fare?Il linguaggio standard non è semplice, quindi per ogni elemento cercherò di fornire esempi di codice che ne dimostrino il significato.

Cosa dice lo standard C11?

IL C11 lo standard dice quanto segue nella sezione 6.5 Espressioni paragrafo 7:

Un oggetto può avere accesso al suo valore memorizzato solo tramite un'espressione lvalue che ha uno dei seguenti tipi:88)— un tipo compatibile con il tipo effettivo dell'oggetto,

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

— una versione qualificata di un tipo compatibile con il tipo effettivo dell'oggetto,

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

— un tipo che sia il tipo con o senza segno corrispondente al tipo effettivo dell'oggetto,

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 ha un'estensione E Anche che consente l'assegnazione intero senza segno* A int* anche se non sono tipi compatibili.

— un tipo che sia il tipo firmato o non firmato corrispondente a una versione qualificata del tipo effettivo dell'oggetto,

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

— un tipo di aggregato o unione che include uno dei tipi summenzionati tra i suoi membri (incluso, ricorsivamente, un membro di un sottoaggregato o di un'unione contenuta), o

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 );

- un tipo di carattere.

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.

Cosa dice la bozza dello standard C++17

La bozza dello standard C++17 nella sezione [basic.lval] paragrafo 11 dice:

Se un programma tenta di accedere al valore memorizzato di un oggetto tramite un glvalue diverso da uno dei seguenti tipi, il comportamento non è definito:63(11.1) — il tipo dinamico dell'oggetto,

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) — una versione qualificata cv del tipo dinamico dell'oggetto,

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) — un tipo simile (come definito in 7.5) al tipo dinamico dell'oggetto,

(11.4) — un tipo che è il tipo con o senza segno corrispondente al tipo dinamico dell'oggetto,

// 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) — un tipo che è il tipo con o senza segno corrispondente a una versione qualificata cv del tipo dinamico dell'oggetto,

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

(11.6) — un tipo di aggregato o unione che include uno dei suddetti tipi tra i suoi elementi o membri di dati non statici (incluso, ricorsivamente, un elemento o un membro di dati non statici di un sottoaggregato o di un'unione contenuta),

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) — un tipo che è un tipo di classe base (possibilmente qualificato cv) del tipo dinamico dell'oggetto,

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) — un tipo char, unsigned char o 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
}

Vale la pena notare firmato car non è incluso nell'elenco sopra, questa è una differenza notevole rispetto a C che dice un tipo di carattere.

Cos'è il Type Punning

Siamo arrivati ​​​​a questo punto e potremmo chiederci, perché dovremmo voler fare un alias?La risposta in genere è a tipo gioco di parole, spesso i metodi utilizzati violano rigide regole di aliasing.

A volte vogliamo aggirare il sistema di tipi e interpretare un oggetto come un tipo diverso.Questo è chiamato tipo giochi di parole, per reinterpretare un segmento di memoria come di altro tipo. Digita il gioco di parole è utile per attività che desiderano accedere alla rappresentazione sottostante di un oggetto da visualizzare, trasportare o manipolare.Le aree tipiche in cui troviamo utilizzato il gioco di parole sono i compilatori, la serializzazione, il codice di rete, ecc...

Tradizionalmente ciò veniva ottenuto prendendo l'indirizzo dell'oggetto, trasmettendolo a un puntatore del tipo in cui vogliamo reinterpretarlo e quindi accedendo al valore, o in altre parole tramite aliasing.Per esempio:

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 ) ;

Come abbiamo visto in precedenza, questo non è un aliasing valido, quindi stiamo invocando un comportamento indefinito.Ma tradizionalmente i compilatori non sfruttavano rigide regole di aliasing e questo tipo di codice di solito funzionava e basta, gli sviluppatori purtroppo si sono abituati a fare le cose in questo modo.Un metodo alternativo comune per il gioco di parole è tramite le unioni, che è valido in C ma comportamento indefinito in C++ (vedere l'esempio dal vivo):

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

Questo non è valido in C++ e alcuni ritengono che lo scopo delle unioni sia esclusivamente quello di implementare tipi di varianti e ritengono che l'utilizzo delle unioni per il gioco di parole tra tipi sia un abuso.

Come digitiamo correttamente il gioco di parole?

Il metodo standard per tipo giochi di parole sia in C che in C++ lo è memcpy.Questo può sembrare un po' pesante, ma l'ottimizzatore dovrebbe riconoscerne l'uso memcpy per tipo giochi di parole e ottimizzarlo e generare un registro per registrare lo spostamento.Ad esempio, se lo sappiamo int64_t ha le stesse dimensioni di Doppio:

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

possiamo usare memcpy:

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

Ad un livello di ottimizzazione sufficiente qualsiasi compilatore moderno decente genera codice identico a quello menzionato in precedenza reinterpret_cast metodo o unione metodo per tipo giochi di parole.Esaminando il codice generato vediamo che utilizza semplicemente il registro mov (live Esempio di Explorer del compilatore).

C++20 e bit_cast

In C++20 potremmo guadagnare bit_cast (implementazione disponibile nel collegamento dalla proposta) che fornisce un modo semplice e sicuro per digitare il gioco di parole oltre ad essere utilizzabile in un contesto constexpr.

Di seguito è riportato un esempio di come utilizzare bit_cast per digitare il gioco di parole a intero senza segno A galleggiante, (vederlo dal vivo):

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

Nel caso in cui A E Da i tipi non hanno la stessa dimensione, è necessario utilizzare una struttura intermedia15.Useremo una struttura contenente a sizeof( unsigned int ) matrice di caratteri (presuppone 4 byte unsigned int) essere il Da tipo e intero senza segno come il A tipo.:

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 ;
}

È un peccato che abbiamo bisogno di questo tipo intermedio, ma questo è il vincolo attuale bit_cast.

Rilevamento di gravi violazioni di aliasing

Non disponiamo di molti strumenti validi per rilevare l'aliasing rigoroso in C++, gli strumenti di cui disponiamo rileveranno alcuni casi di violazioni di aliasing rigoroso e alcuni casi di carichi e archivi disallineati.

gcc utilizzando il flag -fstrict-aliasing E -Wstrict-aliasing può individuare alcuni casi anche se non senza falsi positivi/negativi.Ad esempio, i seguenti casi genereranno un avviso in gcc (vederlo dal vivo):

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)));

anche se non rileverà questo caso aggiuntivo (vederlo dal vivo):

int *p;

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

Sebbene clang consenta questi flag, apparentemente non implementa effettivamente gli avvisi.

Un altro strumento che abbiamo a disposizione è ASan che può rilevare carichi e magazzini disallineati.Sebbene queste non siano direttamente gravi violazioni di aliasing, sono un risultato comune di gravi violazioni di aliasing.Ad esempio, i seguenti casi genereranno errori di runtime se compilati con clang using -fsanitize=indirizzo

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]

L'ultimo strumento che consiglierò è specifico per C++ e non strettamente uno strumento ma una pratica di codifica, non consente cast in stile C.Sia gcc che clang produrranno una diagnostica per i cast in stile C utilizzando -Cast in stile Wold.Ciò costringerà qualsiasi gioco di parole di tipo non definito a utilizzare reinterpret_cast, in generale reinterpret_cast dovrebbe essere un flag per una revisione più ravvicinata del codice.È anche più semplice cercare reinterpret_cast nel codice base per eseguire un controllo.

Per C abbiamo già tutti gli strumenti trattati e abbiamo anche tis-interpreter, un analizzatore statico che analizza in modo esaustivo un programma per un ampio sottoinsieme del linguaggio C.Date le versioni C dell'esempio precedente in cui si utilizza -fstrict-aliasing manca un caso (vederlo dal vivo)

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 è in grado di catturarli tutti e tre, l'esempio seguente invoca tis-kernal come tis-interpreter (l'output è modificato per brevità):

./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.

Finalmente c'è TySan che è attualmente in fase di sviluppo.Questo disinfettante aggiunge informazioni sul controllo del tipo in un segmento di memoria shadow e controlla gli accessi per vedere se violano le regole di aliasing.Lo strumento dovrebbe potenzialmente essere in grado di rilevare tutte le violazioni di aliasing, ma potrebbe avere un notevole sovraccarico in termini di tempo di esecuzione.

L'aliasing rigoroso non si riferisce solo ai puntatori, ma influenza anche i riferimenti, ho scritto un articolo a riguardo per la wiki degli sviluppatori boost ed è stato accolto così bene che l'ho trasformato in una pagina sul mio sito web di consulenza.Spiega completamente di cosa si tratta, perché confonde così tanto le persone e cosa fare al riguardo. Libro bianco sull'aliasing rigoroso.In particolare spiega perché le unioni sono un comportamento rischioso per C++ e perché l'uso di memcpy è l'unica soluzione portabile sia in C che in C++.Spero che questo sia utile.

In aggiunta a ciò che Doug T.Già ha scritto, ecco un semplice caso di test che probabilmente lo innesca con GCC:

controlla.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;
}

Compila con gcc -O2 -o check check.c .Di solito (con la maggior parte delle versioni di gcc che ho provato) questo restituisce un "problema di aliasing rigoroso", perché il compilatore presuppone che "h" non possa essere lo stesso indirizzo di "k" nella funzione "check".Per questo motivo il compilatore ottimizza il file if (*h == 5) via e chiama sempre il printf.

Per coloro che sono interessati ecco il codice assembler x64, prodotto da gcc 4.6.3, in esecuzione su Ubuntu 12.04.2 per x64:

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

Quindi la condizione if è completamente scomparsa dal codice assembler.

Digita il gioco di parole tramite il cast di puntatori (invece di usare un'unione) è un importante esempio di violazione dell'aliasing rigoroso.

Secondo la logica C89, gli autori dello standard non volevano richiedere che i compilatori fornissero codice come:

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

dovrebbe essere richiesto di ricaricare il valore di x tra l'assegnazione e l'istruzione return in modo da consentire la possibilità che p potrebbe indicare x, e l'assegnazione a *p potrebbe di conseguenza alterare il valore di x.L'idea secondo cui un compilatore dovrebbe avere il diritto di presumere che non ci saranno aliasing in situazioni come quelle sopra non era controverso.

Sfortunatamente, gli autori del C89 hanno scritto la loro regola in un modo che, se letto alla lettera, farebbe sì che anche la seguente funzione invochi un comportamento non definito:

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

perché utilizza un lvalue di tipo int per accedere a un oggetto di tipo struct S, E int non è tra i tipi che possono essere utilizzati per accedere a a struct S.Poiché sarebbe assurdo trattare tutti gli usi di membri di strutture e unioni non di tipo carattere come comportamento indefinito, quasi tutti riconoscono che ci sono almeno alcune circostanze in cui un lvalue di un tipo può essere utilizzato per accedere a un oggetto di un altro tipo .Sfortunatamente, il C Standards Committee non è riuscito a definire quali siano queste circostanze.

Gran parte del problema è il risultato del Defect Report #028, che chiedeva informazioni sul comportamento di un programma come:

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);
}

Il rapporto sui difetti n. 28 afferma che il programma richiama il comportamento non definito perché l'azione di scrivere un membro dell'unione di tipo "double" e leggerne uno di tipo "int" richiama il comportamento definito dall'implementazione.Tale ragionamento non ha senso, ma costituisce la base per le regole del Tipo Effettivo che complicano inutilmente il linguaggio senza fare nulla per affrontare il problema originale.

Il modo migliore per risolvere il problema originale sarebbe probabilmente quello di trattare la nota a piè di pagina sullo scopo della regola come se fosse normativa e ha reso la regola inapplicabile se non nei casi che comportano effettivamente accessi contrastanti usando gli alias.Dato qualcosa del tipo:

 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;
 }

Non c'è conflitto interiore inc_int perché tutti gli accessi allo spazio di archiviazione sono accessibili tramite *p vengono eseguiti con un lvalue di tipo int, e non c'è conflitto test Perché p è visibilmente derivato da a struct S, e per la prossima volta s viene utilizzato, tutti gli accessi a tale spazio di archiviazione che verranno mai effettuati p sarà già successo.

Se il codice venisse leggermente modificato...

 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;
 }

Qui c'è un conflitto di aliasing tra p e l'accesso a s.x sulla linea segnata perché in quel punto dell'esecuzione esiste un altro riferimento che verrà utilizzato per accedere allo stesso archivio.

Se il Defect Report 028 avesse detto che l'esempio originale invocava UB a causa della sovrapposizione tra la creazione e l'uso dei due puntatori, ciò avrebbe reso le cose molto più chiare senza dover aggiungere "Tipi effettivi" o altra complessità simile.

Dopo aver letto molte delle risposte, sento il bisogno di aggiungere qualcosa:

Aliasing rigoroso (che descriverò tra poco) è importante perché:

  1. L'accesso alla memoria può essere costoso (in termini di prestazioni), ecco perché i dati vengono manipolati nei registri della CPU prima di essere riscritti nella memoria fisica.

  2. Se i dati in due diversi registri della CPU verranno scritti nello stesso spazio di memoria, non possiamo prevedere quali dati "sopravviveranno" quando codifichiamo in C.

    In assembly, dove codifichiamo manualmente il caricamento e lo scaricamento dei registri della CPU, sapremo quali dati rimangono intatti.Ma C (per fortuna) elimina questo dettaglio.

Poiché due puntatori possono puntare alla stessa posizione nella memoria, ciò potrebbe comportare codice complesso che gestisce possibili collisioni.

Questo codice aggiuntivo è lento e danneggia le prestazioni poiché esegue operazioni di lettura/scrittura di memoria aggiuntiva che sono più lente e (possibilmente) inutili.

IL Una rigorosa regola di aliasing ci consente di evitare codice macchina ridondante nei casi in cui esso dovrebbe essere è lecito ritenere che due puntatori non puntino allo stesso blocco di memoria (vedere anche il file restrict parola chiave).

L'aliasing Strict afferma che è lecito ritenere che i puntatori a tipi diversi puntino a posizioni diverse nella memoria.

Se un compilatore nota che due puntatori puntano a tipi diversi (ad esempio, an int * e un float *), presumerà che l'indirizzo di memoria sia diverso e it non lo farà proteggere dalle collisioni degli indirizzi di memoria, con conseguente codice macchina più veloce.

Per esempio:

Assumiamo la seguente funzione:

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

Per gestire il caso in cui a == b (entrambi i puntatori puntano alla stessa memoria), dobbiamo ordinare e testare il modo in cui carichiamo i dati dalla memoria ai registri della CPU, quindi il codice potrebbe risultare così:

  1. carico a E b dalla memoria.

  2. aggiungere a A b.

  3. salva b E ricaricare a.

    (salva dal registro della CPU alla memoria e carica dalla memoria al registro della CPU).

  4. aggiungere b A a.

  5. salva a (dal registro della CPU) alla memoria.

Il passaggio 3 è molto lento perché deve accedere alla memoria fisica.Tuttavia, è necessario per proteggersi dai casi in cui a E b puntano allo stesso indirizzo di memoria.

Un aliasing rigoroso ci consentirebbe di prevenire ciò dicendo al compilatore che questi indirizzi di memoria sono nettamente diversi (il che, in questo caso, consentirà un'ulteriore ottimizzazione che non può essere eseguita se i puntatori condividono un indirizzo di memoria).

  1. Questo può essere comunicato al compilatore in due modi, utilizzando tipi diversi a cui puntare.cioè.:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. Usando il restrict parola chiave.cioè.:

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

Ora, soddisfacendo la regola Strict Aliasing, il passaggio 3 può essere evitato e il codice verrà eseguito molto più velocemente.

Infatti, aggiungendo il restrict parola chiave, l'intera funzione potrebbe essere ottimizzata per:

  1. carico a E b dalla memoria.

  2. aggiungere a A b.

  3. salva il risultato entrambi in a e a b.

Questa ottimizzazione non avrebbe potuto essere eseguita prima, a causa della possibile collisione (dove a E b verrebbe triplicato invece che raddoppiato).

L'aliasing rigoroso non consente tipi di puntatori diversi agli stessi dati.

Questo articolo dovrebbe aiutarti a comprendere il problema in tutti i dettagli.

Tecnicamente in C++, la rigida regola dell'aliasing probabilmente non è mai applicabile.

Si noti la definizione di indiretto (*operatore):

L'operatore unario * esegue l'indirizzamento:L'espressione a cui è applicato deve essere un puntatore a un tipo di oggetto o un puntatore a un tipo di funzione e il risultato è un lvalue riferito all'oggetto o funzione a cui punta l'espressione.

Anche da la definizione di glvalue

Un glvalue è un'espressione la cui valutazione determina l'identità di un oggetto, (... snip)

Quindi in qualsiasi traccia di programma ben definita, un glvalue si riferisce a un oggetto. Quindi la cosiddetta regola dell'aliasing rigido non si applica mai. Questo potrebbe non essere ciò che i progettisti volevano.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top