Quando si chiede un comportamento indefinito comune in C , le persone a volte fanno riferimento alla rigida regola di aliasing.
Di cosa stanno parlando?
Quando si chiede un comportamento indefinito comune in C , le persone a volte fanno riferimento alla rigida regola di aliasing.
Di cosa stanno parlando?
Risposte:
Una situazione tipica in cui si verificano problemi di aliasing rigoroso è quando si sovrappone uno struct (come un dispositivo / network msg) su un buffer delle dimensioni della parola del proprio sistema (come un puntatore a uint32_t
s o uint16_t
s). Quando si sovrappone una struttura a un tale buffer o un buffer a una tale struttura tramite il cast di puntatori, è possibile violare facilmente le rigide regole di aliasing.
Quindi, in questo tipo di installazione, se voglio inviare un messaggio a qualcosa, dovrei avere due puntatori incompatibili che puntano allo stesso pezzo di memoria. Potrei quindi ingenuamente codificare qualcosa del genere (su un sistema con sizeof(int) == 2
):
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 rigorosa regola di aliasing rende illegale questa configurazione: la dereferenziazione di un puntatore che alias un oggetto non di tipo compatibile o uno degli altri tipi consentiti da C 2011 6.5 paragrafo 7 1 è un comportamento indefinito. Sfortunatamente, puoi ancora codificare in questo modo, forse ricevere alcuni avvisi, farlo compilare bene, solo per avere strani comportamenti inattesi 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 ciò che la rigida regola di alias acquista il compilatore. Fondamentalmente, con questa regola, non deve pensare di inserire istruzioni per aggiornare il contenuto di buff
ogni esecuzione del ciclo. Invece, durante l'ottimizzazione, con alcune assunzioni fastidiosamente non forzate sull'aliasing, può omettere quelle istruzioni, caricare buff[0]
e buff[1
] nei registri della CPU una volta prima che il ciclo venga eseguito e accelerare il corpo del ciclo. Prima che fosse introdotto il rigoroso alias, il compilatore doveva vivere in uno stato di paranoia che i contenuti buff
potevano cambiare in qualsiasi momento e dovunque da chiunque. Quindi, per ottenere un vantaggio in più in termini di prestazioni, e supponendo che la maggior parte delle persone non digiti puntatori di tipo pun, è stata introdotta la rigida regola di aliasing.
Tieni presente che se pensi che l'esempio sia inventato, ciò potrebbe accadere anche 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 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 può o meno essere in grado o abbastanza intelligente da provare a incorporare SendMessage e può o meno decidere di caricare o non caricare nuovamente buff. Se SendMessage
fa parte di un'altra API compilata separatamente, probabilmente ha istruzioni per caricare i contenuti di buff. Poi di nuovo, forse sei in C ++ e questa è un'implementazione basata solo su un'intestazione basata su modelli che il compilatore pensa di poter incorporare. O forse è solo qualcosa che hai scritto nel tuo file .c per tua comodità. In ogni caso potrebbe comunque derivarne un comportamento indefinito. Anche quando sappiamo qualcosa di ciò che accade sotto il cofano, è ancora 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 aiuta necessariamente.
Quindi, come posso aggirare questo?
Usa un'unione. Molti compilatori lo supportano senza lamentarsi di un aliasing rigoroso. Ciò è consentito in C99 e esplicitamente consentito in C11.
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
Puoi disabilitare l'aliasing rigoroso nel tuo compilatore ( f [no-] aliasing rigoroso in gcc))
È possibile utilizzare char*
per l'aliasing anziché la parola del sistema. Le regole consentono un'eccezione per char*
(compreso signed char
e unsigned char
). Si presume sempre che gli char*
alias siano di altri tipi. Tuttavia, ciò non funzionerà diversamente: 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. Si dovrebbe anche conoscere endianness , l'allineamento di parola , e come affrontare i problemi di allineamento con l'imballaggio le strutture in modo corretto.
1 I tipi a cui C 2011 6.5 7 consente l'accesso a un valore sono:
unsigned char*
essere usato lontano char*
invece? Tendo a utilizzare unsigned char
piuttosto che char
come tipo sottostante per byte
perché i miei byte non sono firmati e non voglio la stranezza del comportamento firmato (in particolare wrt a overflow)
unsigned char *
va bene.
uint32_t* buff = malloc(sizeof(Msg));
dell'Unione e quelle successive unsigned int asBuffer[sizeof(Msg)];
avranno dimensioni diverse e nessuna delle due è corretta. La malloc
chiamata si basa sull'allineamento di 4 byte sotto il cofano (non farlo) e l'unione sarà 4 volte più grande di quanto deve essere ... Capisco che è per chiarezza ma non mi dà alcun bug meno ...
La migliore spiegazione che ho trovato è di Mike Acton, Understanding Strict Aliasing . Si concentra un po 'sullo sviluppo di PS3, ma sostanzialmente è solo GCC.
Dall'articolo:
"Il rigoroso aliasing è un presupposto, fatto dal compilatore C (o C ++), che dereferenziare i puntatori a oggetti di tipi diversi non farà mai riferimento alla stessa posizione di memoria (cioè alias a vicenda.)"
Quindi, fondamentalmente, se si ha un int*
puntamento a un po 'di memoria contenente un int
e quindi si punta float*
a quel ricordo e lo si usa come float
infrangere la regola. Se il tuo codice non lo rispetta, molto probabilmente l'ottimizzatore del compilatore romperà il tuo codice.
L'eccezione alla regola è a char*
, che può indicare qualsiasi tipo.
Questa è la regola di aliasing rigorosa, trovata nella sezione 3.10 dello standard C ++ 03 (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 attraverso un valore 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 di tipo con o senza segno corrispondente al tipo dinamico dell'oggetto,
- un tipo di tipo con o senza segno corrispondente a una versione con certificazione cv del tipo dinamico dell'oggetto,
- un tipo aggregato o sindacale che include uno dei tipi summenzionati tra i suoi membri (incluso, ricorsivamente, un membro di un'unione sottogruppo o contenuta),
- un tipo che è un tipo di classe base (possibilmente qualificato in cv) del tipo dinamico dell'oggetto,
- a
char
ounsigned char
digitare.
Formulazione C ++ 11 e C ++ 14 (modifiche enfatizzate):
Se un programma tenta di accedere al valore memorizzato di un oggetto attraverso un valore 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 simile (come definito in 4.4) al tipo dinamico dell'oggetto,
- un tipo di tipo con o senza segno corrispondente al tipo dinamico dell'oggetto,
- un tipo di tipo con o senza segno corrispondente a una versione con certificazione cv del tipo dinamico dell'oggetto,
- un tipo aggregato o di unione che include uno dei tipi sopra menzionati tra i suoi elementi o membri di dati non statici (incluso, ricorsivamente, un elemento o un membro di dati non statici di un'unione sottoaggregata o contenuta),
- un tipo che è un tipo di classe base (possibilmente qualificato in cv) del tipo dinamico dell'oggetto,
- a
char
ounsigned char
digitare.
Due cambiamenti erano piccoli: glvalue anziché lvalue e chiarimento del caso aggregato / sindacato.
Il terzo cambiamento fornisce una garanzia più forte (rilassa la forte regola di aliasing): il nuovo concetto di tipi simili che ora sono alias sicuri.
Anche la formulazione C (C99; ISO / IEC 9899: 1999 6.5 / 7; la stessa identica formulazione viene utilizzata in ISO / IEC 9899: 2011 §6.5 ¶7):
Un oggetto deve avere il suo valore memorizzato accessibile solo da un'espressione lvalue che ha uno dei seguenti tipi 73) o 88) :
- un tipo compatibile con il tipo effettivo dell'oggetto,
- una versione quali fi cata di un tipo compatibile con il tipo effettivo dell'oggetto,
- un tipo di tipo con o senza segno corrispondente al tipo effettivo dell'oggetto,
- un tipo di tipo con o senza segno corrispondente a una versione quali fi cata del tipo effettivo dell'oggetto,
- un tipo di aggregato o unione che include uno dei tipi di cui sopra tra i suoi membri (incluso, ricorsivamente, un membro di un'unione subaggregata o contenuta), o
- un tipo di carattere.
73) o 88) L'intento di questo elenco è di specificare le circostanze in cui un oggetto può o meno essere aliasato.
wow(&u->s1,&u->s2)
dovrebbe essere legale anche quando si utilizza un puntatore per modificare u
e ciò annullerebbe la maggior parte delle ottimizzazioni che il la regola di aliasing è stata progettata per facilitare.
Questo è tratto dal mio "Cos'è la regola di aliasing rigorosa e perché ci preoccupiamo?" Scrivilo.
In C e C ++ l'aliasing ha a che fare con quali tipi di espressioni siamo autorizzati ad accedere ai valori memorizzati. In C e C ++ lo standard specifica quali tipi di espressioni sono autorizzati ad alias quali tipi. Il compilatore e l'ottimizzatore possono assumere che seguiamo rigorosamente le regole di aliasing, da cui il termine regola di aliasing rigoroso . Se tentiamo di 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 sono disattivate, i risultati del nostro programma non sono più affidabili.
Sfortunatamente con violazioni rigorose dell'aliasing, otterremo spesso i risultati che ci aspettiamo, lasciando la possibilità che una versione futura di un compilatore con una nuova ottimizzazione rompa il codice che ritenevamo valido. Questo è indesiderabile ed è un obiettivo utile comprendere le rigide regole di aliasing e come evitare di violarle.
Per capire di più sul perché ci preoccupiamo, discuteremo dei problemi che sorgono quando si violano le rigide regole di aliasing, la punzonatura di tipo poiché le tecniche comuni utilizzate nella punzonatura di tipo spesso violano le rigide regole di aliasing e come digitare correttamente il gioco di parole.
Diamo un'occhiata ad alcuni esempi, quindi possiamo parlare esattamente di ciò che dicono gli standard, esaminare alcuni ulteriori esempi e quindi vedere come evitare aliasing rigorosi e rilevare le violazioni che abbiamo perso. 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";
Abbiamo un int * che punta alla memoria occupata da un int e questo è un alias valido. L'ottimizzatore deve presumere che le assegnazioni tramite ip potrebbero aggiornare il valore occupato da x .
L'esempio seguente mostra l'aliasing che porta a comportamenti indefiniti ( esempio live ):
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 foo prendiamo un int * e un float * , in questo esempio chiamiamo foo e impostiamo entrambi i parametri in modo che puntino alla stessa posizione di memoria che in questo esempio contiene un int . Nota, reinterpret_cast sta dicendo al compilatore di trattare l'espressione come se avesse il tipo specificato dal suo parametro template. In questo caso gli stiamo dicendo di trattare l'espressione & x come se avesse tipo float * . Possiamo aspettarci ingenuamente che il risultato del secondo cout sia 0 ma con l'ottimizzazione abilitata usando -O2 sia gcc che clang producono il seguente risultato:
0
1
Il che non può essere previsto, ma è perfettamente valido poiché abbiamo invocato un comportamento indefinito. Un float non può validamente alias un oggetto int . Pertanto, l'ottimizzatore può assumere la costante 1 memorizzata durante la dereferenziazione i sarà il valore restituito poiché un archivio tramite f non può influire validamente su un oggetto int . Collegando il codice in Compiler Explorer si vede che è esattamente ciò che sta accadendo ( esempio live ):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
L'ottimizzatore utilizzando Type-Based Analysis alias (TBAA) assume 1 verrà restituito e sposta direttamente il valore costante nel registro eax che porta il valore restituito. TBAA utilizza le regole delle lingue su quali tipi sono autorizzati all'alias per ottimizzare carichi e negozi. In questo caso TBAA sa che un float non può alias e int e ottimizza il carico di i .
Cosa dice esattamente lo standard che siamo autorizzati e non autorizzati a fare? Il linguaggio standard non è semplice, quindi per ogni articolo cercherò di fornire esempi di codice che dimostrino il significato.
Lo standard C11 dice quanto segue nella sezione 6.5 Espressioni paragrafo 7 :
Un oggetto deve avere il suo valore memorizzato accessibile solo da 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 di 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 ed inoltre che permette di assegnare unsigned int * a int * anche se non sono tipi compatibili.
- un tipo di tipo con o senza segno 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 aggregato o sindacale che includa uno dei tipi summenzionati tra i suoi membri (incluso, ricorsivamente, un membro di un'unione sottaggregata o 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.
La bozza di standard C ++ 17 nella sezione [basic.lval] paragrafo 11 dice:
Se un programma tenta di accedere al valore memorizzato di un oggetto attraverso un valore 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 di 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 di 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 tipi di cui sopra tra i suoi elementi o membri di dati non statici (incluso, ricorsivamente, un elemento o un membro di dati non statici di un'unione sottaggregata o 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 in 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 carattere 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 che il carattere firmato non è incluso nell'elenco sopra, questa è una notevole differenza da C che indica un tipo di carattere .
Siamo arrivati a questo punto e potremmo chiederci, perché dovremmo voler alias? La risposta è in genere digitare pun , spesso i metodi utilizzati violano le rigide regole di aliasing.
A volte vogliamo eludere il sistema dei tipi e interpretare un oggetto come un tipo diverso. Questo si chiama punzonatura , per reinterpretare un segmento di memoria come un altro tipo. La punzonatura del tipo è utile per le attività che desiderano visualizzare, trasportare o manipolare la rappresentazione sottostante di un oggetto. Le aree tipiche che troviamo come tipo di punzonatura utilizzate sono compilatori, serializzazione, codice di rete, ecc ...
Tradizionalmente questo è stato ottenuto prendendo l'indirizzo dell'oggetto, lanciandolo su un puntatore del tipo che vogliamo reinterpretare come e quindi accedendo al valore, o in altre parole con l'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 alias valido, quindi stiamo invocando un comportamento indefinito. Ma tradizionalmente i compilatori non sfruttano le rigide regole di aliasing e questo tipo di codice di solito ha appena funzionato, purtroppo gli sviluppatori si sono abituati a fare le cose in questo modo. Un metodo alternativo comune per la punzonatura di tipo è tramite i sindacati, che è valido in C ma comportamento indefinito in C ++ ( vedi 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
Ciò non è valido in C ++ e alcuni considerano lo scopo dei sindacati esclusivamente per l'implementazione di tipi di varianti e ritengono che l'uso dei sindacati per la punzonatura dei tipi sia un abuso.
Il metodo standard per la punzonatura di tipo in C e C ++ è memcpy . Questo può sembrare un po 'pesante ma l'ottimizzatore dovrebbe riconoscere l'uso di memcpy per la punzonatura del tipo e ottimizzarlo e generare un registro per registrare lo spostamento. Ad esempio, se sappiamo che int64_t ha le stesse dimensioni di double :
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);
//...
A un livello di ottimizzazione sufficiente, qualsiasi compilatore moderno decente genera un codice identico al metodo reinterpret_cast menzionato in precedenza o al metodo union per la punzonatura dei tipi . Esaminando il codice generato, vediamo che utilizza solo il registro mov ( esempio di Explorer compilatore live ).
In C ++ 20 potremmo ottenere bit_cast ( implementazione disponibile nel link dalla proposta ) che offre un modo semplice e sicuro per digitare-pun ed essere utilizzabile in un contesto constexpr.
Il seguente è un esempio di come usare bit_cast per digitare pun un int senza segno su float , ( vederlo dal vivo ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
Nel caso in cui i tipi To e From non abbiano le stesse dimensioni, è necessario utilizzare una struttura intermedia15. Useremo uno struct contenente un array di caratteri sizeof (unsigned int) ( presuppone che 4 byte unsigned int ) sia il tipo From e un unsigned int come il tipo A .:
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 è l'attuale vincolo di bit_cast .
Non disponiamo di molti buoni strumenti per rilevare un aliasing rigoroso in C ++, gli strumenti in nostro possesso rileveranno alcuni casi di violazioni rigorose dell'aliasing e alcuni casi di carichi e negozi non allineati.
gcc usando il flag -fstrict-aliasing e -Wstrict-aliasing può catturare 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)));
sebbene non rileverà questo caso aggiuntivo ( vederlo dal vivo ):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Sebbene clang consenta queste bandiere, a quanto pare non implementa effettivamente gli avvisi.
Un altro strumento che abbiamo a disposizione è ASan che può catturare carichi e depositi disallineati. Sebbene non si tratti di violazioni di aliasing direttamente rigide, sono un risultato comune di violazioni di aliasing rigorose. Ad esempio, i seguenti casi genereranno errori di runtime quando generati con clang usando -fsanitize = address
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 del C ++ e non strettamente uno strumento ma una pratica di codifica, non consentire cast in stile C. Sia gcc che clang produrranno una diagnostica per i cast in stile C usando -Wold-style-cast . Questo costringerà tutti i giochi di parole di tipo indefinito a usare reinterpret_cast, in generale reinterpret_cast dovrebbe essere un flag per una più stretta revisione del codice. È anche più facile cercare nella base di codici reinterpret_cast per eseguire un controllo.
Per C abbiamo già tutti gli strumenti coperti e abbiamo anche tis-interprete, un analizzatore statico che analizza in modo esauriente un programma per un ampio sottoinsieme del linguaggio C. Dato un verion C dell'esempio precedente in cui l'uso di -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 catturare tutti e tre, l'esempio seguente invoca tis-kernal come tis-interprete (l'output viene 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.
Infine 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 potenzialmente dovrebbe essere in grado di rilevare tutte le violazioni dell'aliasing ma potrebbe avere un sovraccarico di runtime elevato.
reinterpret_cast
potrebbe fare o cosa cout
potrebbe significare. (Va bene menzionare C ++, ma la domanda originale era su C e IIUC, questi esempi potevano essere validamente scritti in C.)
Il rigoroso aliasing non si riferisce solo ai puntatori, influisce anche sui riferimenti, ne ho scritto un articolo per il wiki dello sviluppatore boost ed è stato così ben accolto 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. White paper rigoroso di aliasing . In particolare spiega perché i sindacati sono comportamenti rischiosi per C ++ e perché l'uso di memcpy è l'unica soluzione portatile sia in C sia in C ++. Spero sia utile.
Come addendum a ciò che Doug T. ha già scritto, ecco un semplice caso di prova che probabilmente lo innesca con 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;
}
Compila con gcc -O2 -o check check.c
. Di solito (con la maggior parte delle versioni di gcc che ho provato) questo genera 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 fileif (*h == 5)
away e chiama sempre 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.
long long*
e int64_t
*). Ci si potrebbe aspettare che un compilatore sano dovrebbe riconoscere che long long*
e int64_t*
potrebbe accedere allo stesso archivio se sono archiviati in modo identico, ma tale trattamento non è più di moda.
La punzonatura di tipo tramite cast di puntatori (invece di utilizzare un sindacato) è un esempio importante di rottura di un aliasing rigoroso.
fpsync()
direttiva tra la scrittura come fp e la lettura come int o viceversa [sulle implementazioni con pipeline e cache separate di interi e FPU , una direttiva del genere potrebbe essere costosa, ma non così costosa come fare in modo che il compilatore esegua tale sincronizzazione su ogni accesso al sindacato]. Oppure un'implementazione potrebbe specificare che il valore risultante non sarà mai utilizzabile se non in circostanze che utilizzano Sequenze iniziali comuni.
Secondo la logica C89, gli autori dello Standard non volevano richiedere ai compilatori un codice come:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
dovrebbe essere richiesto di ricaricare il valore x
tra l'assegnazione e la dichiarazione di reso in modo da consentire la possibilità che p
potrebbe indicare x
, e l'incarico *p
potrebbe di conseguenza modificare il valore di x
. L'idea che un compilatore dovrebbe avere il diritto di presumere che non ci sarà aliasing in situazioni come quelle sopra è stata controversa.
Sfortunatamente, gli autori del C89 hanno scritto la loro regola in modo che, se letto alla lettera, anche la seguente funzione invocherebbe un comportamento indefinito:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
perché utilizza un valore di tipo int
per accedere a un oggetto di tipo struct S
e int
non è tra i tipi che possono essere utilizzati accedendo astruct S
. Poiché sarebbe assurdo trattare ogni uso di membri di tipo non carattere di strutture e sindacati come comportamento indefinito, quasi tutti riconoscono che ci sono almeno alcune circostanze in cui un valore di un tipo può essere usato per accedere a un oggetto di un altro tipo . Sfortunatamente, il C Standards Committee non è riuscito a definire quali siano tali circostanze.
Gran parte del problema è il risultato del Rapporto difetti n. 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 comportamenti indefiniti perché l'azione di scrivere un membro del sindacato di tipo "doppio" e leggere uno del tipo "int" invoca il comportamento definito dall'implementazione. Tale ragionamento è privo di senso, ma costituisce la base per le regole del tipo efficace che complicano inutilmente la lingua 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 rendere la regola inapplicabile, tranne nei casi che implicano effettivamente accessi contrastanti mediante alias. Dato qualcosa di simile:
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 vi sono conflitti all'interno inc_int
perché tutti gli accessi alla memoria a cui si accede *p
vengono effettuati con un valore di tipo int
e non vi sono conflitti test
perché p
è visibilmente derivato da una struct S
e, al successivo s
utilizzo, tutti gli accessi a quella memoria che verranno mai effettuati attraverso p
sarà già successo.
Se il codice fosse 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, esiste un conflitto di aliasing tra p
e l'accesso alla s.x
riga contrassegnata perché a quel punto dell'esecuzione esiste un altro riferimento che verrà utilizzato per accedere alla stessa memoria .
Se il rapporto sui difetti 028 affermasse che l'esempio originale aveva invocato 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 efficaci" o altra complessità del genere.
Dopo aver letto molte delle risposte, sento la necessità di aggiungere qualcosa:
Il rigoroso aliasing (che descriverò tra poco) è importante perché :
L'accesso alla memoria può essere costoso (dal punto di vista delle prestazioni), motivo per cui i dati vengono manipolati nei registri della CPU prima di essere riscritti nella memoria fisica.
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) estrae questo dettaglio.
Poiché due puntatori possono puntare alla stessa posizione nella memoria, ciò potrebbe comportare un 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 sia più lente che (possibilmente) non necessarie.
La regola di aliasing rigorosa ci consente di evitare il codice macchina ridondante nei casi in cui dovrebbe essere sicuro supporre che due puntatori non puntino allo stesso blocco di memoria (vedere anche la restrict
parola chiave).
L'aliasing rigoroso afferma che è sicuro supporre 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 a float *
), supporrà che l'indirizzo di memoria sia diverso e non proteggerà dalle collisioni dell'indirizzo 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 cariciamo i dati dalla memoria nei registri della CPU, quindi il codice potrebbe finire così:
carica a
e b
dalla memoria.
aggiungere a
a b
.
salvare b
e ricaricare a
.
(salva dal registro CPU nella memoria e carica dalla memoria al registro CPU).
aggiungere b
a a
.
salva a
(dal registro CPU) nella memoria.
Il passaggio 3 è molto lento perché deve accedere alla memoria fisica. Tuttavia, è necessario per la protezione da istanze in cui a
e b
punta allo stesso indirizzo di memoria.
Un aliasing rigoroso ci consentirebbe di impedirlo dicendo al compilatore che questi indirizzi di memoria sono nettamente diversi (che, in questo caso, consentirà un'ulteriore ottimizzazione che non può essere eseguita se i puntatori condividono un indirizzo di memoria).
Questo può essere detto al compilatore in due modi, usando diversi tipi a cui puntare. vale a dire:
void merge_two_numbers(int *a, long *b) {...}
Usando la restrict
parola chiave. vale a dire:
void merge_two_ints(int * restrict a, int * restrict b) {...}
Ora, soddisfacendo la regola di aliasing rigoroso, è possibile evitare il passaggio 3 e il codice verrà eseguito in modo significativamente più veloce.
Infatti, aggiungendo la restrict
parola chiave, l'intera funzione potrebbe essere ottimizzata per:
carica a
e b
dalla memoria.
aggiungere a
a b
.
salva il risultato sia a a
che a b
.
Questa ottimizzazione non avrebbe potuto essere eseguita prima, a causa della possibile collisione (dove a
e b
sarebbe triplicata anziché raddoppiata).
b
(non ricaricandolo) e ricaricandolo a
. Spero sia più chiaro ora.
restrict
, ma penso che quest'ultimo nella maggior parte dei casi sarebbe più efficace, e allentando alcuni vincoli register
gli permetterebbe di riempire alcuni dei casi in cui restrict
non sarebbe utile. Non sono sicuro che sia mai stato "importante" trattare lo Standard come una descrizione completa di tutti i casi in cui i programmatori dovrebbero aspettarsi che i compilatori riconoscano le prove di aliasing, piuttosto che semplicemente descrivere i luoghi in cui i compilatori devono presumere aliasing anche quando non esistono prove particolari di esso .
restrict
parola chiave minimizza non solo la velocità delle operazioni, ma anche il loro numero, il che potrebbe essere significativo ... Voglio dire, dopo tutto, l'operazione più veloce non è affatto un'operazione :)
Alias rigoroso non consente a tipi di puntatore diversi di utilizzare gli stessi dati.
Questo articolo dovrebbe aiutarti a comprendere il problema in dettaglio.
int
E una struttura che contiene un int
).
Tecnicamente in C ++, la rigida regola di aliasing non è probabilmente mai applicabile.
Nota la definizione di indiretta ( * operatore ):
L'operatore unario * esegue il riferimento indiretto: l'espressione a cui viene applicata deve essere un puntatore a un tipo di oggetto o un puntatore a un tipo di funzione e il risultato è un valore che si riferisce all'oggetto o alla funzione a cui punta l'espressione .
Anche dalla 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 valore di valore si riferisce a un oggetto. Quindi la cosiddetta regola di aliasing rigoroso non si applica mai. Questo potrebbe non essere quello che volevano i designer.
int foo;
, a cosa accede l'espressione lvalue *(char*)&foo
? È un oggetto di tipo char
? Quell'oggetto nasce contemporaneamente foo
? Scriverebbe per foo
modificare il valore memorizzato di quel suddetto oggetto di tipo char
? In tal caso, esiste una regola che consenta di char
accedere al valore memorizzato di un oggetto di tipo usando un valore di tipo int
?
int i;
crea quattro oggetti di ogni tipo di carattere in addition to one of type
int ? I see no way to apply a consistent definition of "object" which would allow for operations on both
* (char *) & i` e i
. Infine, non c'è nulla nello standard che consenta persino a un volatile
puntatore qualificato di accedere ai registri hardware che non soddisfano la definizione di "oggetto".
c
ec++faq
.