Lorsqu'on pose des questions sur un comportement non défini commun en C , les gens se réfèrent parfois à la règle stricte d'alias.
De quoi parlent-ils?
Lorsqu'on pose des questions sur un comportement non défini commun en C , les gens se réfèrent parfois à la règle stricte d'alias.
De quoi parlent-ils?
Réponses:
Une situation typique où vous rencontrez des problèmes d'alias stricts est lors de la superposition d'une structure (comme un périphérique / réseau msg) sur un tampon de la taille de mot de votre système (comme un pointeur vers uint32_t
s ou uint16_t
s). Lorsque vous superposez une structure sur un tel tampon, ou un tampon sur une telle structure via la conversion de pointeur, vous pouvez facilement violer des règles d'aliasing strictes.
Donc, dans ce type de configuration, si je veux envoyer un message à quelque chose, je devrais avoir deux pointeurs incompatibles pointant vers le même bloc de mémoire. Je pourrais alors naïvement coder quelque chose comme ça (sur un système avec 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 règle d'alias stricte rend cette configuration illégale: déréférencer un pointeur qui alias un objet qui n'est pas d'un type compatible ou l'un des autres types autorisés par C 2011 6.5 paragraphe 7 1 est un comportement non défini. Malheureusement, vous pouvez toujours coder de cette façon, peut - être obtenir des avertissements, le faire compiler correctement, seulement pour avoir un comportement inattendu étrange lorsque vous exécutez le code.
(GCC semble quelque peu incohérent dans sa capacité à donner des avertissements de pseudonyme, nous donnant parfois un avertissement amical et parfois non.)
Pour voir pourquoi ce comportement n'est pas défini, nous devons réfléchir à ce que la règle d'aliasing stricte achète le compilateur. Fondamentalement, avec cette règle, il n'a pas à penser à insérer des instructions pour actualiser le contenu de buff
chaque exécution de la boucle. Au lieu de cela, lors de l'optimisation, avec certaines hypothèses ennuyeusement non appliquées sur l'alias, il peut omettre ces instructions, charger buff[0]
et buff[1
] dans les registres du processeur une fois avant l'exécution de la boucle et accélérer le corps de la boucle. Avant l'introduction d'un aliasing strict, le compilateur devait vivre dans un état de paranoïa dont le contenu buff
pouvait changer à tout moment et de n'importe où par n'importe qui. Donc, pour obtenir un avantage supplémentaire en termes de performances et en supposant que la plupart des gens ne tapent pas de pointeurs de pun, la règle d'aliasing stricte a été introduite.
Gardez à l'esprit, si vous pensez que l'exemple est artificiel, cela peut même arriver si vous passez un tampon à une autre fonction effectuant l'envoi pour vous, si vous l'avez à la place.
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
Et réécrit notre boucle précédente pour profiter de cette fonction pratique
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
Le compilateur peut ou non être en mesure ou suffisamment intelligent pour essayer d'inclure SendMessage et il peut ou non décider de charger ou de ne pas charger à nouveau buff. Si SendMessage
fait partie d'une autre API compilée séparément, elle contient probablement des instructions pour charger le contenu de buff. Là encore, vous êtes peut-être en C ++ et il s'agit d'une implémentation basée uniquement sur un modèle que le compilateur pense pouvoir incorporer. Ou peut-être que c'est juste quelque chose que vous avez écrit dans votre fichier .c pour votre propre convenance. Quoi qu'il en soit, un comportement indéfini pourrait encore se produire. Même lorsque nous savons ce qui se passe sous le capot, c'est toujours une violation de la règle, donc aucun comportement bien défini n'est garanti. Donc, simplement en encapsulant une fonction qui prend notre tampon délimité par des mots n'aide pas nécessairement.
Alors, comment puis-je contourner cela?
Utilisez un syndicat. La plupart des compilateurs prennent en charge cela sans se plaindre d'un alias strict. Ceci est autorisé en C99 et explicitement autorisé en C11.
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
Vous pouvez désactiver l'alias strict dans votre compilateur ( f [no-] strict-aliasing dans gcc))
Vous pouvez utiliser l' char*
alias au lieu du mot de votre système. Les règles autorisent une exception pour char*
(y compris signed char
et unsigned char
). Il est toujours supposé que les char*
alias d'autres types. Cependant, cela ne fonctionnera pas dans l'autre sens: il n'y a pas d'hypothèse que votre structure alias un tampon de caractères.
Attention aux débutants
Ce n'est qu'un champ de mines potentiel lors de la superposition de deux types l'un sur l'autre. Vous devriez également vous renseigner sur l' endianité , l' alignement des mots et la façon de traiter les problèmes d'alignement via correctement les des structures d'emballage .
1 Les types auxquels C 2011 6.5 7 permet à une valeur d'accéder sont:
unsigned char*
être être utilisé à la char*
place? J'ai tendance à utiliser unsigned char
plutôt que char
comme type sous-jacent byte
car mes octets ne sont pas signés et je ne veux pas l'étrangeté du comportement signé (notamment wrt to overflow)
unsigned char *
est acceptable.
uint32_t* buff = malloc(sizeof(Msg));
et les unsigned int asBuffer[sizeof(Msg)];
déclarations de tampon d' union suivantes auront des tailles différentes et aucune n'est correcte. L' malloc
appel repose sur l'alignement de 4 octets sous le capot (ne le faites pas) et l'union sera 4 fois plus grande qu'elle ne devrait l'être ... Je comprends que c'est pour plus de clarté mais cela me dérange quand même - moins ...
La meilleure explication que j'ai trouvée est de Mike Acton, Understanding Strict Aliasing . Il se concentre un peu sur le développement de la PS3, mais c'est essentiellement GCC.
De l'article:
"Un aliasing strict est une hypothèse, faite par le compilateur C (ou C ++), que le déréférencement de pointeurs vers des objets de différents types ne fera jamais référence au même emplacement de mémoire (c'est-à-dire un alias entre eux.)"
Donc, fondamentalement, si vous avez un int*
pointage vers une mémoire contenant un int
, puis que vous pointez un float*
vers cette mémoire et que float
vous l' utilisez comme vous enfreignez la règle. Si votre code ne respecte pas cela, alors l'optimiseur du compilateur cassera très probablement votre code.
L'exception à la règle est un char*
, qui est autorisé à pointer vers n'importe quel type.
Il s'agit de la règle d'alias stricte, trouvée dans la section 3.10 de la norme C ++ 03 (d'autres réponses fournissent une bonne explication, mais aucune n'a fourni la règle elle-même):
Si un programme tente d'accéder à la valeur stockée d'un objet via une valeur l autre que l'un des types suivants, le comportement n'est pas défini:
- le type dynamique de l'objet,
- une version qualifiée cv du type dynamique de l'objet,
- un type qui est le type signé ou non signé correspondant au type dynamique de l'objet,
- un type qui est le type signé ou non signé correspondant à une version qualifiée cv du type dynamique de l'objet,
- un type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses membres (y compris, récursivement, un membre d'une union sous-agrégée ou contenue),
- un type qui est un type de classe de base (éventuellement qualifié cv) du type dynamique de l'objet,
- a
char
ouunsigned char
tapez.
Formulation C ++ 11 et C ++ 14 (changements mis en évidence):
Si un programme tente d'accéder à la valeur stockée d'un objet via une valeur gl autre que l'un des types suivants, le comportement n'est pas défini:
- le type dynamique de l'objet,
- une version qualifiée cv du type dynamique de l'objet,
- un type similaire (tel que défini en 4.4) au type dynamique de l'objet,
- un type qui est le type signé ou non signé correspondant au type dynamique de l'objet,
- un type qui est le type signé ou non signé correspondant à une version qualifiée cv du type dynamique de l'objet,
- un type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses éléments ou membres de données non statiques (y compris, récursivement, un élément ou un membre de données non statiques d'une union sous-agrégée ou contenue),
- un type qui est un type de classe de base (éventuellement qualifié cv) du type dynamique de l'objet,
- a
char
ouunsigned char
tapez.
Deux changements étaient mineurs : glvalue au lieu de lvalue et clarification du cas d'agrégation / d'union.
Le troisième changement apporte une garantie plus forte (assouplit la règle de l'alias fort): le nouveau concept de types similaires qui sont désormais sûrs pour l'alias.
Aussi le libellé C (C99; ISO / IEC 9899: 1999 6.5 / 7; le même libellé exact est utilisé dans ISO / IEC 9899: 2011 §6.5 §7):
Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui a l'un des types suivants 73) ou 88) :
- un type compatible avec le type effectif de l'objet,
- une version quali fi ée d'un type compatible avec le type effectif de l'objet,
- un type qui est le type signé ou non signé correspondant au type effectif de l'objet,
- un type qui est le type signé ou non signé correspondant à une version quali fi ée du type effectif de l'objet,
- un type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses membres (y compris, récursivement, un membre d'une union sous-agrégée ou contenue), ou
- un type de caractère.
73) ou 88) Le but de cette liste est de spécifier les circonstances dans lesquelles un objet peut ou non être replié.
wow(&u->s1,&u->s2)
devrait être légal même lorsqu'un pointeur est utilisé pour modifier u
, et cela annulerait la plupart des optimisations que le règle d'aliasing a été conçue pour faciliter.
Ceci est extrait de mon "Qu'est-ce que la règle de repliement strict et pourquoi nous en soucions-nous?"rédiger.
En C et C ++, l'aliasing a à voir avec quels types d'expression nous sommes autorisés à accéder aux valeurs stockées. En C et C ++, la norme spécifie quels types d'expression sont autorisés à alias quels types. Le compilateur et l'optimiseur sont autorisés à supposer que nous suivons strictement les règles d'alias, d'où le terme de règle d'alias stricte . Si nous tentons d'accéder à une valeur en utilisant un type non autorisé, elle est classée comme comportement non défini ( UB ). Une fois que nous avons un comportement indéfini, tous les paris sont désactivés, les résultats de notre programme ne sont plus fiables.
Malheureusement, avec des violations d'alias strictes, nous obtiendrons souvent les résultats escomptés, laissant la possibilité qu'une future version d'un compilateur avec une nouvelle optimisation casse le code que nous pensions être valide. Ce n'est pas souhaitable et c'est un objectif valable de comprendre les règles strictes d'alias et comment éviter de les violer.
Pour mieux comprendre pourquoi nous nous soucions, nous discuterons des problèmes qui surviennent lors de la violation de règles d'aliasing strictes, de la punition de type car les techniques courantes utilisées dans la punition de type violent souvent les règles d'aliasing strictes et comment taper correctement pun.
Regardons quelques exemples, puis nous pourrons parler exactement de ce que disent les normes, examiner d'autres exemples et voir comment éviter l'aliasing strict et les violations de capture que nous avons manquées. Voici un exemple qui ne devrait pas surprendre ( exemple en direct ):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
Nous avons un int * pointant vers la mémoire occupée par un int et ceci est un aliasing valide. L'optimiseur doit supposer que les affectations via ip pourraient mettre à jour la valeur occupée par x .
L'exemple suivant montre un alias qui conduit à un comportement indéfini ( exemple en direct ):
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?
}
Dans la fonction foo, nous prenons un int * et un float * , dans cet exemple, nous appelons foo et définissons les deux paramètres pour pointer vers le même emplacement mémoire qui dans cet exemple contient un int . Remarque, le reinterpret_cast indique au compilateur de traiter l'expression comme si elle avait le type spécifié par son paramètre de modèle. Dans ce cas, nous lui disons de traiter l'expression & x comme si elle avait le type float * . Nous pouvons naïvement nous attendre à ce que le résultat du deuxième cout soit 0, mais avec l'optimisation activée en utilisant -O2, gcc et clang produisent le résultat suivant:
0
1
Ce qui n'est peut-être pas prévu, mais est parfaitement valide car nous avons invoqué un comportement indéfini. Un flottant ne peut pas alias valablement un objet int . Par conséquent, l'optimiseur peut supposer que la constante 1 stockée lorsque le déréférencement i sera la valeur de retour puisqu'un magasin via f n'a pas pu affecter valablement un objet int . Brancher le code dans l'Explorateur de compilateur montre que c'est exactement ce qui se passe ( exemple en direct ):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
L'optimiseur utilisant l' analyse d'alias basée sur le type (TBAA) suppose que 1 sera renvoyé et déplace directement la valeur constante dans le registre eax qui contient la valeur de retour. TBAA utilise les règles de langues sur les types autorisés à alias pour optimiser les chargements et les magasins. Dans ce cas, TBAA sait qu'un float ne peut pas alias et int et optimise la charge de i .
Que dit exactement la norme que nous sommes autorisés et non autorisés à le faire? Le langage standard n'est pas simple, donc pour chaque élément, je vais essayer de fournir des exemples de code qui en démontrent le sens.
La norme C11 dit ce qui suit dans la section 6.5 Expressions, paragraphe 7 :
Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui a l'un des types suivants: 88) - un type compatible avec le type effectif de l'objet,
int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
- une version qualifiée d'un type compatible avec le type effectif de l'objet,
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 type qui est le type signé ou non signé correspondant au type effectif de l'objet,
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 a une extension et qui permet également d' assigner int * non signé à int * même s'il ne s'agit pas de types compatibles.
- un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l'objet,
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 type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses membres (y compris, récursivement, un membre d'une union sous-agrégée ou contenue), ou
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 type de caractère.
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.
Le projet de norme C ++ 17 dans la section [basic.lval] paragraphe 11 dit:
Si un programme tente d'accéder à la valeur stockée d'un objet via une valeur gl autre que l'un des types suivants, le comportement n'est pas défini: 63 (11.1) - le type dynamique de l'objet,
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) - une version qualifiée cv du type dynamique de l'objet,
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 type similaire (tel que défini en 7.5) au type dynamique de l'objet,
(11.4) - un type qui est le type signé ou non signé correspondant au type dynamique de l'objet,
// 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 type qui est le type signé ou non signé correspondant à une version qualifiée cv du type dynamique de l'objet,
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6) - un type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses éléments ou membres de données non statiques (y compris, récursivement, un élément ou un membre de données non statique d'une union sous-agrégée ou contenue),
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 type qui est un type de classe de base (éventuellement qualifié cv) du type dynamique de l'objet,
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 type char, unsigned char ou 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
}
Il convient de noter que le caractère signé n'est pas inclus dans la liste ci-dessus, c'est une différence notable par rapport à C qui dit un type de caractère .
Nous sommes arrivés à ce point et nous nous demandons peut-être pourquoi voudrions-nous nous alias? La réponse est généralement de taper calembour , souvent les méthodes utilisées violent les règles strictes d'alias.
Parfois, nous voulons contourner le système de types et interpréter un objet comme un type différent. C'est ce qu'on appelle le type punning , pour réinterpréter un segment de mémoire comme un autre type. Le repérage de type est utile pour les tâches qui souhaitent accéder à la représentation sous-jacente d'un objet à visualiser, transporter ou manipuler. Les domaines typiques que nous trouvons le type punning utilisé sont les compilateurs, la sérialisation, le code réseau, etc.
Traditionnellement, cela a été accompli en prenant l'adresse de l'objet, en le convertissant en un pointeur du type auquel nous voulons le réinterpréter, puis en accédant à la valeur, ou en d'autres termes par un aliasing. Par exemple:
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 ) ;
Comme nous l'avons vu précédemment, ce n'est pas un aliasing valide, nous invoquons donc un comportement non défini. Mais traditionnellement, les compilateurs ne profitaient pas de règles d'alias strictes et ce type de code fonctionnait généralement bien, les développeurs se sont malheureusement habitués à faire les choses de cette façon. Une méthode alternative courante pour la punition de type consiste à utiliser les unions, ce qui est valide en C mais un comportement non défini en C ++ ( voir l'exemple en direct ):
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
Cela n'est pas valide en C ++ et certains considèrent que le but des unions est uniquement d'implémenter des types de variantes et estiment que l'utilisation des unions pour le punning de type est un abus.
La méthode standard pour la punition de type en C et C ++ est memcpy . Cela peut sembler un peu lourd, mais l'optimiseur doit reconnaître l'utilisation de memcpy pour le type punning et l'optimiser et générer un registre pour enregistrer le mouvement. Par exemple, si nous savons que int64_t a la même taille que double :
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
nous pouvons utiliser memcpy :
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
À un niveau d'optimisation suffisant, tout compilateur moderne décent génère un code identique à la méthode reinterpret_cast ou la méthode d' union mentionnée précédemment pour le découpage de type . En examinant le code généré, nous voyons qu'il utilise simplement register mov ( exemple de l'explorateur de compilation en direct ).
En C ++ 20, nous pouvons gagner bit_cast ( implémentation disponible dans le lien de la proposition ) qui donne un moyen simple et sûr de taper-pun ainsi que d'être utilisable dans un contexte constexpr.
Ce qui suit est un exemple d'utilisation de bit_cast pour taper pun un entier non signé à flotter ( voir en direct ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
Dans le cas où les types To et From n'ont pas la même taille, il nous faut utiliser une structure intermédiaire15. Nous utiliserons une structure contenant un tableau de caractères sizeof (int non signé) ( suppose un entier non signé de 4 octets ) comme étant le type From et un entier non signé comme type To :
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 ;
}
Il est regrettable que nous ayons besoin de ce type intermédiaire mais c'est la contrainte actuelle de bit_cast .
Nous n'avons pas beaucoup de bons outils pour intercepter l'aliasing strict en C ++, les outils dont nous disposons vont détecter certains cas de violations d'aliasing strictes et certains cas de chargements et de magasins mal alignés.
gcc utilisant l'indicateur -fstrict-aliasing et -Wstrict-aliasing peut intercepter certains cas, mais pas sans faux positifs / négatifs. Par exemple, les cas suivants généreront un avertissement dans gcc ( voir en direct ):
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)));
bien qu'il n'attrapera pas ce cas supplémentaire ( voir en direct ):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Bien que clang autorise ces indicateurs, il n'applique apparemment pas les avertissements.
Un autre outil dont nous disposons est ASan qui peut capturer des charges et des magasins mal alignés. Bien qu'il ne s'agisse pas directement de violations d'alias strictes, elles sont le résultat commun de violations d'alias strictes. Par exemple, les cas suivants génèrent des erreurs d'exécution lorsqu'ils sont créés avec clang en utilisant -fsanitize = adresse
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]
Le dernier outil que je recommanderai est spécifique au C ++ et pas strictement un outil mais une pratique de codage, n'autorisez pas les conversions de style C. Gcc et clang produiront un diagnostic pour les modèles de style C à l'aide de -Wold-style-cast . Cela forcera tous les jeux de mots de type non défini à utiliser reinterpret_cast, en général, reinterpret_cast devrait être un indicateur pour une révision plus approfondie du code. Il est également plus facile de rechercher dans votre base de code reinterpret_cast pour effectuer un audit.
Pour C, nous avons tous les outils déjà couverts et nous avons également tis-interpreter, un analyseur statique qui analyse de manière exhaustive un programme pour un grand sous-ensemble du langage C. Étant donné un C verions de l'exemple précédent où l'utilisation de -fstrict-aliasing manque un cas ( voir en direct )
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 est capable d'attraper les trois, l'exemple suivant appelle tis-kernal comme tis-interpreter (la sortie est éditée pour plus de concision):
./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.
Enfin, il y a TySan qui est actuellement en développement. Cet assainisseur ajoute des informations de vérification de type dans un segment de mémoire fantôme et vérifie les accès pour voir s'ils violent les règles d'alias. L'outil devrait potentiellement être en mesure de détecter toutes les violations d'alias, mais il peut avoir un gros temps d'exécution.
reinterpret_cast
pourrait faire ou de ce qui cout
pourrait signifier. (Il est bon de mentionner C ++ mais la question initiale portait sur C et IIUC, ces exemples pourraient tout aussi bien être écrits en C.)
L'aliasing strict ne fait pas uniquement référence aux pointeurs, il affecte également les références, j'ai écrit un article à ce sujet pour le wiki du développeur boost et il a été si bien reçu que je l'ai transformé en une page sur mon site Web de consultation. Il explique complètement ce que c'est, pourquoi il confond tellement les gens et ce qu'il faut faire à ce sujet. Livre blanc sur l'alias strict . En particulier, il explique pourquoi les unions sont un comportement risqué pour C ++ et pourquoi l'utilisation de memcpy est le seul correctif portable à la fois en C et C ++. J'espère que cela vous sera utile.
En complément de ce que Doug T. a déjà écrit, voici un cas de test simple qui le déclenche probablement avec 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;
}
Compilez avec gcc -O2 -o check check.c
. Habituellement (avec la plupart des versions de gcc que j'ai essayées), cela génère un "problème d'alias strict", car le compilateur suppose que "h" ne peut pas être la même adresse que "k" dans la fonction "check". Pour cette raison, le compilateur optimise le if (*h == 5)
away et appelle toujours le printf.
Pour ceux qui sont intéressés, voici le code assembleur x64, produit par gcc 4.6.3, fonctionnant sur ubuntu 12.04.2 pour x64:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
Ainsi, la condition if a complètement disparu du code assembleur.
long long*
et int64_t
*). On pourrait s'attendre à ce qu'un compilateur sensé reconnaisse que a long long*
et int64_t*
puisse accéder au même stockage s'ils sont stockés de manière identique, mais un tel traitement n'est plus à la mode.
La punition de type via des transtypages de pointeurs (par opposition à l'utilisation d'une union) est un exemple majeur de rupture de l'aliasing strict.
fpsync()
directive entre l'écriture comme fp et la lecture comme int ou vice versa [sur les implémentations avec des pipelines et des caches FPU et entiers séparés , une telle directive pourrait être coûteuse, mais pas aussi coûteuse que de demander au compilateur d'effectuer une telle synchronisation sur chaque accès à l'union]. Ou une implémentation pourrait spécifier que la valeur résultante ne sera jamais utilisable sauf dans des circonstances utilisant des séquences initiales communes.
Selon la justification C89, les auteurs de la norme ne voulaient pas exiger que les compilateurs reçoivent un code comme:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
devrait être requis pour recharger la valeur de x
entre l'affectation et l'instruction return afin de tenir compte de la possibilité qui p
pourrait pointer vers x
, et l'affectation à *p
pourrait par conséquent modifier la valeur de x
. L'idée qu'un compilateur devrait avoir le droit de présumer qu'il n'y aura pas d'alias dans des situations comme celles ci-dessus n'était pas controversée.
Malheureusement, les auteurs du C89 ont écrit leur règle d'une manière qui, si elle était lue littéralement, inciterait même la fonction suivante à invoquer un comportement indéfini:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
car il utilise une valeur l de type int
pour accéder à un objet de type struct S
et int
ne fait pas partie des types pouvant être utilisés pour accéder à a struct S
. Parce qu'il serait absurde de traiter toute utilisation de membres non structurés de structures et d'unions comme un comportement indéfini, presque tout le monde reconnaît qu'il existe au moins certaines circonstances où une valeur l d'un type peut être utilisée pour accéder à un objet d'un autre type . Malheureusement, le Comité des normes C n'a pas défini quelles sont ces circonstances.
Une grande partie du problème est le résultat du rapport de défaut # 028, qui a posé des questions sur le comportement d'un programme comme:
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);
}
Le rapport de défauts # 28 indique que le programme appelle un comportement indéfini car l'action d'écrire un membre d'union de type "double" et de lire un membre de type "int" appelle un comportement défini par l'implémentation. Un tel raisonnement n'a pas de sens, mais constitue la base des règles de type effectif qui compliquent inutilement le langage tout en ne faisant rien pour résoudre le problème d'origine.
La meilleure façon de résoudre le problème d'origine serait probablement de traiter la note de bas de page sur le but de la règle comme si elle était normative et de la rendre inapplicable, sauf dans les cas qui impliquent réellement des accès conflictuels à l'aide d'alias. Étant donné quelque chose comme:
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;
}
Il n'y a pas de conflit à l'intérieur inc_int
parce que tous les accès au stockage accédé via *p
se font avec une valeur de type l int
, et il n'y a pas de conflit test
parce qu'il p
est visiblement dérivé d'un struct S
, et à la prochaine s
utilisation, tous les accès à ce stockage qui seront jamais effectués à travers p
aura déjà eu lieu.
Si le code a été légèrement modifié ...
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;
}
Ici, il existe un conflit d'alias entre p
et l'accès à s.x
la ligne marquée car à ce stade de l'exécution, il existe une autre référence qui sera utilisée pour accéder au même stockage .
Si le rapport de défaut 028 avait indiqué que l'exemple d'origine invoquait UB en raison du chevauchement entre la création et l'utilisation des deux pointeurs, cela aurait rendu les choses beaucoup plus claires sans avoir à ajouter des "types efficaces" ou une autre complexité de ce type.
Après avoir lu bon nombre des réponses, je ressens le besoin d'ajouter quelque chose:
Un aliasing strict (que je décrirai un peu) est important car :
L'accès à la mémoire peut être coûteux (en termes de performances), c'est pourquoi les données sont manipulées dans les registres du processeur avant d'être réécrites dans la mémoire physique.
Si les données de deux registres CPU différents seront écrites dans le même espace mémoire, nous ne pouvons pas prédire quelles données "survivront" lorsque nous coderons en C.
En assemblage, où nous codons le chargement et le déchargement des registres CPU manuellement, nous saurons quelles données restent intactes. Mais C (heureusement) résume ce détail.
Étant donné que deux pointeurs peuvent pointer vers le même emplacement dans la mémoire, cela peut entraîner un code complexe qui gère les collisions possibles .
Ce code supplémentaire est lent et nuit aux performances car il effectue des opérations de lecture / écriture de mémoire supplémentaires qui sont à la fois plus lentes et (éventuellement) inutiles.
La règle d'alias stricte nous permet d'éviter le code machine redondant dans les cas où il devrait être sûr de supposer que deux pointeurs ne pointent pas vers le même bloc de mémoire (voir aussi le restrict
mot - clé).
L'aliasing strict indique qu'il est sûr de supposer que les pointeurs vers différents types pointent vers différents emplacements dans la mémoire.
Si un compilateur remarque que deux pointeurs pointent vers des types différents (par exemple, un int *
et un float *
), il supposera que l'adresse mémoire est différente et ne protégera pas contre les collisions d'adresses mémoire, ce qui accélérera le code machine.
Par exemple :
Supposons la fonction suivante:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
Afin de gérer le cas dans lequel a == b
(les deux pointeurs pointent vers la même mémoire), nous devons ordonner et tester la façon dont nous chargeons les données de la mémoire vers les registres du processeur, de sorte que le code puisse se retrouver comme suit:
charger a
et b
de la mémoire.
ajouter a
à b
.
enregistrer b
et recharger a
.
(sauvegarde du registre CPU dans la mémoire et chargement de la mémoire dans le registre CPU).
ajouter b
à a
.
enregistrer a
(du registre CPU) dans la mémoire.
L'étape 3 est très lente car elle doit accéder à la mémoire physique. Cependant, il est nécessaire de se protéger contre les instances où a
et b
pointer vers la même adresse mémoire.
Un aliasing strict nous permettrait d'éviter cela en indiquant au compilateur que ces adresses mémoire sont distinctement différentes (ce qui, dans ce cas, permettra une optimisation encore plus poussée qui ne peut pas être effectuée si les pointeurs partagent une adresse mémoire).
Cela peut être dit au compilateur de deux manières, en utilisant différents types pour pointer. c'est à dire:
void merge_two_numbers(int *a, long *b) {...}
En utilisant le restrict
mot - clé. c'est à dire:
void merge_two_ints(int * restrict a, int * restrict b) {...}
Désormais, en satisfaisant à la règle de repli strict, l'étape 3 peut être évitée et le code s'exécutera beaucoup plus rapidement.
En fait, en ajoutant le restrict
mot - clé, toute la fonction pourrait être optimisée pour:
charger a
et b
de la mémoire.
ajouter a
à b
.
enregistrer le résultat vers a
et vers b
.
Cette optimisation n'aurait pas pu être effectuée auparavant, en raison de la collision possible (où a
et b
serait triplé au lieu de doublé).
b
(pas le recharger) et recharger a
. J'espère que c'est plus clair maintenant.
restrict
, mais je pense que ce dernier serait dans la plupart des cas plus efficace, et le relâchement de certaines contraintes register
lui permettrait de remplir certains des cas où restrict
cela ne serait pas utile. Je ne suis pas sûr qu'il ait jamais été "important" de traiter la norme comme décrivant pleinement tous les cas où les programmeurs devraient s'attendre à ce que les compilateurs reconnaissent les preuves d'alias, plutôt que de simplement décrire les endroits où les compilateurs doivent présumer l'alias même lorsqu'aucune preuve particulière n'existe .
restrict
mot - clé minimise non seulement la vitesse des opérations mais aussi leur nombre, ce qui pourrait être significatif ... Je veux dire, après tout, l'opération la plus rapide n'est pas une opération du tout :)
Un alias strict n'autorise pas différents types de pointeurs vers les mêmes données.
Cet article devrait vous aider à comprendre le problème en détail.
int
et une structure qui contient un int
).
Techniquement en C ++, la règle d'aliasing stricte n'est probablement jamais applicable.
Notez la définition de l'indirection ( opérateur * ):
L'opérateur unaire * effectue une indirection: l'expression à laquelle il est appliqué doit être un pointeur vers un type d'objet, ou un pointeur vers un type de fonction et le résultat est une valeur l se référant à l'objet ou à la fonction vers laquelle l'expression pointe .
Également de la définition de glvalue
Une glvalue est une expression dont l'évaluation détermine l'identité d'un objet, (... snip)
Ainsi, dans toute trace de programme bien définie, une valeur gl fait référence à un objet. Donc, la soi-disant règle d'aliasing stricte ne s'applique jamais. Ce n'est peut-être pas ce que les concepteurs voulaient.
int foo;
, à quoi accède l'expression lvalue *(char*)&foo
? Est-ce un objet de type char
? Cet objet existe-t-il en même temps que foo
? Est-ce que l'écriture foo
changerait la valeur stockée de cet objet de type susmentionné char
? Dans l'affirmative, existe-t-il une règle permettant d' char
accéder à la valeur stockée d'un objet de type à l'aide d'une valeur de type l int
?
int i;
crée-t-elle quatre objets de chaque type de caractère 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` et i
. Enfin, rien dans la norme ne permet même à un volatile
pointeur qualifié d'accéder à des registres matériels qui ne répondent pas à la définition d '"objet".
c
etc++faq
.