Quelle est la stricte aliasing règle?

en posant des questions sur comportement commun non défini dans C , les âmes plus éclairées que je me suis référé à la règle stricte d'alias.

De quoi parlent-ils?

690
demandé sur Community 2008-09-19 05:30:27

11 réponses

une situation typique où vous rencontrez des problèmes d'alias stricts est lorsque vous superposez une structure (comme un périphérique/réseau msg) sur un tampon de la taille du mot de votre système (comme un pointeur vers uint32_t s ou uint16_t s). Quand vous superposez une structure sur un tel buffer, ou un buffer sur une telle structure via un pointer casting, vous pouvez facilement violer des règles d'alias strictes.

donc dans ce genre de configuration, si je veux envoyer un message à quelque chose je dois avoir deux pointeurs incompatibles pointant vers le même morceau de mémoire. Je pourrais alors naïvement coder quelque chose comme ceci:

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érencement d'un pointeur qui aliase 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 obtenez quelques avertissements, faites-le compiler fine, seulement pour avoir un comportement inattendu bizarre quand vous exécutez le code.

(GCC semble quelque peu incompatible dans sa capacité à donner de l'aliasing mises en garde, parfois donnant un avertissement amical et parfois pas.)

pour voir pourquoi ce comportement n'est pas défini, nous devons penser à ce que la règle d'alias stricte achète le compilateur. Fondamentalement, avec cette règle, il ne doit pas penser à insérer des instructions pour rafraîchir le contenu de buff à chaque passage de la boucle. Au lieu de cela, lors de l'optimisation, avec quelques hypothèses fâcheusement non confirmées sur l'aliasing, il peut omettre ces instructions, charger buff[0] et buff[1 ] dans les registres CPU une fois avant que la boucle soit lancée, et accélérer le corps de la boucle. Avant d'introduire l'alias strict, le compilateur devait vivre dans un État de paranoïa que le contenu de buff pouvait changer à tout moment de n'importe où par n'importe qui. Donc pour obtenir un supplément performances de pointe, et en supposant que la plupart des gens ne sont pas de type jeu de pointeurs, de la stricte aliasing règle a été introduite.

gardez à l'esprit, si vous pensez que l'exemple est inventé, cela pourrait même se produire si vous passez un tampon à une autre fonction faisant l'envoi pour vous, si à la place vous avez.

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 commode

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

, Le compilateur peut ou ne pas être capable ou assez intelligent pour essayer D'inline SendMessage et il peut ou ne pas décider de charger ou de ne pas charger buff à nouveau. Si SendMessage fait partie d'une autre API compilée séparément, il y a probablement des instructions pour charger le contenu de buff. Encore une fois, peut-être que vous êtes en C++ et qu'il s'agit d'une implémentation d'en-tête templée que le compilateur pense pouvoir mettre en ligne. Ou peut-être que c'est juste quelque chose que tu as écrit dans ton .fichier c pour votre propre convenance. De toute façon un comportement indéfini pourrait encore s'ensuivre. Même si nous savons ce qui se passe sous le capot, c'est quand même une violation de la règle, donc aucun comportement bien défini n'est garanti. Donc juste en enveloppant dans une fonction qui prend notre tampon délimité par mot n'aide pas nécessairement.

alors comment je m'en sors?

  • utiliser un syndicat. La plupart des compilateurs soutiennent cela sans se plaindre de l'alias strict. Cela est autorisé dans C99 et explicitement autorisé en C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • Vous pouvez désactiver stricte de l'aliasing dans votre compilateur ( f[n-]strict-aliasing dans gcc))

  • vous pouvez utiliser char* pour l'alias au lieu du mot de votre système. Les règles prévoient une exception pour char* (y compris signed char et unsigned char ). Il est toujours supposé que char* aliase d'autres types. Toutefois, cela ne travailler de l'autre côté: il n'y a pas de supposition que votre struct aliase un tampon de caractères.

Débutant méfiez-vous

il ne s'agit que d'un champ de mines potentiel lorsqu'on en superpose deux types l'un sur l'autre. Vous devriez également en savoir plus sur endianness , word alignment , et comment traiter les questions d'alignement à travers structures d'emballage correctement.

note de bas de page

1 les types d'accès autorisés par la loi c 20116. 57 sont les suivants:

  • un type compatible avec le type effectif de l'objet,
  • version qualifiée d'un type compatible avec le type effectif de l'objet,
  • type qui est le type signé ou non signé correspondant au type effectif de l'objet,
  • type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l'objet,
  • un agrégat ou un type d'union qui comprend l'un des types susmentionnés parmi ses membres (y compris, de façon récursive, un membre d'un sous-agrégat ou d'une union contenue), ou
  • un type de caractère.
496
répondu Doug T. 2018-06-09 14:32:04

La meilleure explication que j'ai trouvé est par Mike Acton, Compréhension Stricte Alias . Il est un peu axé sur le Développement PS3, mais c'est essentiellement juste GCC.

de l'article:

Strict "de l'aliasing est une supposition faite par le C (ou C++) du compilateur, qui déréférencement de pointeurs vers des objets de types différents ne sera jamais reportez-vous à la même emplacement de mémoire (c'est à dire alias les uns des autres.) "

donc si vous avez un int* pointant vers une mémoire contenant un int et que vous pointez un float* vers cette mémoire et que vous l'utilisez comme un float vous violez 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.

219
répondu Niall 2017-10-16 14:46:56

il s'agit de la règle d'aliasing stricte, qui se trouve à 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 d'un objet à travers une lvalue d'autre que l'un des types suivants le comportement est indéfini:

  • le type dynamique de l'objet,
  • un version cv-qualified du type dynamique de l'objet,
  • type qui est le type signé ou non signé correspondant au type dynamique de l'objet,
  • type qui est le type signé ou non signé correspondant à une version Certifiée cv du type dynamique de l'objet,
  • un groupe ou un type d'union qui comprend l'un des types susmentionnés parmi ses membres (y compris, de façon récursive, un membre d'un sous-groupe). ou confiné),
  • un type qui est une (peut-être de cv qualifiés) de la classe de base de type de le type dynamique de l'objet,
  • a char ou unsigned char type.

C++11 et C++14 libellé (changements soulignés):

si un programme tente d'accéder à la valeur stockée d'un objet via un glvalue de l'autre que l'un des types suivants le comportement est indéfini:

  • le type dynamique de l'objet,
  • un cv qualifiés version 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 la objet,
  • type qui est le type signé ou non signé correspondant à une version Certifiée cv du type dynamique de l'objet,
  • un agrégat ou un type d'union qui comprend l'un des types susmentionnés parmi ses éléments ou membres de données non statiques (y compris, de façon récursive, un élément ou membre de données non statiques d'un sous-agrégat ou d'une union contenue),
  • un type c'est un type de classe de base (éventuellement qualifié cv) du type dynamique de l'objet,
  • a char ou unsigned char type.

deux modifications étaient mineures: glvalue au lieu de lvalue , et clarification de l'affaire agrégée/union.

le troisième changement apporte une garantie plus forte (assouplit la règle de l'aliasing fort): le nouveau concept de similaires qui sont maintenant à l'abri de l'alias.


"également le C libellé (C99; ISO/IEC 9899:1999 6.5/7; le même libellé est utilisé dans ISO / IEC 9899:2011 §6.5 ¶7):

un objet ne doit avoir accès à sa valeur stockée que par une valeur l expression ayant l'un des types suivants 73) ou 88) :

  • un type compatible avec l'efficacité du type de l'objet,
  • une version qualifié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 à un version qualifiée du type effectif de l'objet,
  • un agrégat ou union type qui comprend l'un des susvisés types parmi ses membres (y compris, récursivement, un membre d'un sous-groupe ou union contenue), ou
  • un type de caractère.

73) ou 88) le but de cette liste est de préciser les circonstances dans lesquelles un objet peut ou ne peut pas être aliasé.

125
répondu Ben Voigt 2017-10-19 22:30:34

aliasing Strict ne se réfère pas seulement aux pointeurs, il affecte les références aussi bien, j'ai écrit un papier à ce sujet pour le développeur de boost wiki et il a été si bien reçu que je l'ai transformé en une page sur mon site de consultation. Il explique ce qu'il est, pourquoi il confond tellement les gens et quoi faire à ce sujet. Strict Aliasing Livre Blanc . En particulier, cela explique pourquoi les syndicats sont un comportement risqué pour C++, et pourquoi l'utilisation de memcpy est la seule solution C et c++. Espérons que cela est utile.

40
répondu Patrick 2011-06-19 23:46:55

comme addendum à ce que Doug T. a déjà écrit, ici est un simple cas de test qui le déclenche probablement avec gcc :

vérifier.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;
}

compilé avec gcc -O2 -o check check.c . D'habitude (avec la plupart des versions gcc j'ai essayé) cette sortie "problème d'alias strict", parce que 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) et appelle toujours le printf.

pour ceux qui sont intéressés ici est le code assembleur x64, produit par gcc 4.6.3, en cours d'exécution sur ubuntu 12.04.2 pour x64:

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

donc la condition if est complètement partie du code de l'assembleur.

31
répondu Ingo Blackman 2013-05-14 02:37:04

Note

Ceci est un extrait de mon "Quelle est la Stricte Aliasing Règle et Pourquoi faisons-nous des soins?" écriture-up.

Qu'est-ce que l'alias strict?

en C et c++ l'aliasing a à voir avec quels types d'expression Nous sommes autorisés à accéder aux valeurs stockées à travers. Dans C et c++, le standard spécifie les types d'expressions autorisés à alias quels types. Le compilateur et l'optimiseur sont autorisés à supposons que nous suivons strictement les règles d'alias, d'où le terme 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 éteints, les résultats de notre programme ne sont plus fiables.

malheureusement avec des violations d'alias strictes, nous obtiendrons souvent les résultats que nous attendons, laissant la possibilité que la future version d'un compilateur avec une nouvelle optimisation brise le code que nous pensions valide. Cela n'est pas souhaitable et cela vaut la peine de comprendre les règles d'alias strictes et comment éviter de les violer.

pour comprendre plus sur pourquoi nous nous soucions, nous allons discuter des questions qui surgissent lors de la violation des règles d'aliasing strictes, tapez le punning puisque les techniques courantes utilisées dans le punning de type violent souvent les règles d'aliasing strictes et la façon de taper jeu de mots correctement.

exemples préliminaires

regardons quelques exemples, alors nous pouvons parler de ce que dit exactement la(Les) norme (s), examiner quelques exemples supplémentaires et ensuite voir comment éviter l'alias strict et les violations des captures que nous avons manqué. Voici un exemple qui ne devrait pas être surprenant ( live exemple ):

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

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

Nous avons une int* pointant vers la mémoire occupée par une int et c'est valable aliasing. L'optimiseur doit supposer que les assignations par ip pourraient mettre à jour la valeur occupée par x .

l'exemple suivant montre un alias qui conduit à un comportement non défini ( live example ):

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 de mémoire qui dans cet exemple contient un int . Note, le reinterpret_cast dit au compilateur de traiter l'expression comme si elle avait le type spécifié par son paramètre template. Dans ce cas, nous disons à traiter l'expression &x comme si elle avait le type float* . On peut s'attendre naïvement au résultat du second Cut d'être 0 mais avec optimisation activée en utilisant - O2 les deux gcc et clang produisent le résultat suivant:

0
1

qui ne peut pas être attendu mais est parfaitement valide puisque nous avons invoqué un comportement non défini. Un float ne peut valablement alias un int objet. Par conséquent, l'optimiseur peut supposer que la constante 1 stockée lors du déréférencement i sera la valeur de retour car un stock à travers f ne pourrait pas affecter valablement un objet int . Le fait de brancher le code dans L'Explorateur de compilateurs montre que c'est exactement ce qui se passe( live example ):

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

L'optimiseur à l'aide de En Fonction de leur Type Analyse d'Alias (terre aart) assume 1 sera retourné et se déplace directement la valeur de la constante dans le registre eax qui porte la valeur de retour. TBAA utilise les règles de langues sur les types autorisés à alias pour optimiser les charges et les magasins. Dans ce cas, TBAA sait qu'un float ne peut pas alias et int et optimise la charge de i .

Maintenant, à la Règle-Livre

Qu'est-ce que la norme dit exactement que nous sommes autorisés et non autorisés à faire? Le langage standard n'est pas simple, donc pour chaque item je vais essayer de fournir des exemples de code qui montrent le sens.

que dit la norme C11?

la norme C11 stipule ce qui suit dans la section 6.5 Expressions paragraphe 7 :

un objet doit avoir sa valeur stockée accessible seulement par une expression de valeur L 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 aussi qui permet d'affecter int non signé* à int* même s'ils ne sont pas des 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 agrégat ou un type d'union qui comprend l'un des types susmentionnés parmi ses membres (y compris, de façon récursive, un membre d'un sous-agrégat ou d'une union 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.

Ce que le C++17 Projet de norme say

Le C++17 projet de norme dans la section [de base.lval] paragraphe 11 dit:

si un programme tente d'accéder à la valeur stockée d'un objet par le biais d'une glvalue 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 Certifié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 semblable (tel que défini au paragraphe 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 Certifié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 agrégat ou un type d'union qui comprend l'un des types susmentionnés parmi ses éléments ou ses membres de données non statiques (y compris, récursivement, un élément ou un membre de données non statiques d'un sous-agrégat ou d'une union 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 cv-qualified) type de classe de base 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 char, unsigned char, ou std::type octet.

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
}

mérite de noter char signé n'est pas inclus dans la liste ci-dessus, il s'agit d'une différence notable de C qui dit un type de caractère .

quoi est le Type de beaucoup les jeux de mots

nous sommes arrivés à ce point et nous nous demandons peut-être, pourquoi voulons-nous alias pour? La réponse est typiquement type jeu de mots , souvent les méthodes utilisées violent des règles d'alias strictes.

parfois nous voulons contourner le système de type et interpréter un objet comme un type différent. C'est ce qu'on appelle type punning , pour réinterpréter un segment de mémoire comme un autre type. Tapez est utile pour les tâches qui veulent accéder à la représentation sous-jacente d'un objet à visualiser, transporter ou manipuler. Les domaines typiques que nous trouvons le pointage de type utilisé sont les compilateurs, la sérialisation, le code de réseau, etc ...

traditionnellement ceci a été accompli en prenant l'adresse de l'objet, en le jetant à un pointeur du type que nous voulons le réinterpréter comme et puis en accédant à la valeur, ou en d'autres termes en aliasing. 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 plus haut, il ne s'agit pas d'un alias valide, donc nous invoquons un comportement non défini. Mais traditionnellement les compilateurs n'ont pas profité des règles d'alias strictes et ce type de code a généralement juste fonctionné, les développeurs se sont malheureusement habitués à faire les choses de cette façon. Une méthode alternative courante pour le pointage de type est par les unions, qui est valide en C mais comportement non défini en C++ ( voir 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

ceci n'est pas valable en C++ et certains considèrent que le but des syndicats est uniquement de mettre en œuvre des variantes de types et pensent que l'utilisation de syndicats pour le poinçonnage de type est un abus.

comment dactylographier correctement un jeu de mots?

la méthode standard pour type de poinçonnage en C et c++ est memcpy . Cela peut sembler un peu lourd, mais l'optimiseur doit reconnaître l'utilisation de memcpy pour type de poinçonnage et l'optimiser et générer un registre pour enregistrer le mouvement. Par exemple, si nous savons que int64_t est 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 n'importe quel compilateur moderne décent produit identique code de la méthode susmentionnée reinterpret_cast ou union méthode pour type punning . En examinant le code généré, nous voyons qu'il utilise just register mov ( Live Compiler Explorer exemple ).

C++20 et bit_cast

En C++20 nous pouvons gagner bit_cast ( la mise en œuvre disponible dans le lien de la proposition ) qui donne une façon simple et sûre de taper-calembour ainsi que d'être utilisable dans un contexte de constexpr.

ce qui suit est un exemple d'utilisation de bit_cast pour dactylographier un non signé int à float , ( voir en direct ):

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

dans le cas où à et de types ne ont la même taille, il nous faut utiliser une structure intermediaire15. Nous utiliserons une structure contenant un sizeof( unsigned int ) character array ( suppose 4 byte unsigned int ) pour être le de type et unsigned int comme le à type.:

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 .

Rattrapage Stricte Aliasing Violations

nous n'avons pas beaucoup de bons outils pour attraper l'alias strict EN C++, les outils que nous avons vont attraper quelques cas de violations de l'alias strict et quelques cas de chargements et de magasins mal alignés.

gcc en utilisant le drapeau -fstrict-aliasing et -Wstrict-aliasing peut capter certains cas, bien qu' non sans faux positifs/négatifs. Par exemple, les cas suivants vont générer 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 vivant ):

int *p;

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

bien que clang autorise ces drapeaux, il ne semble pas réellement mettre en œuvre les Avertissements.

un autre outil dont nous disposons est un peut attraper des charges et des provisions mal alignées. Bien qu'il ne s'agisse pas directement de violations de l'aliasing strictes, elles sont le résultat de violations de l'aliasing strictes. Par exemple, les cas suivants généreront des erreurs d'exécution lorsqu'ils seront construits 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 n'est pas strictement un outil mais une pratique de codage, n'autorisez pas les moulages en C-style. Gcc et clang produiront une diagnostic pour les moulages de style C utilisant -Wold-style-cast . Cela forcera n'importe quel type de jeu de mot non défini à utiliser reinterpret_cast, en général reinterpret_cast devrait être un drapeau pour la révision de code plus proche. Il est également plus facile de rechercher dans votre base de code réinterpret_cast pour effectuer un audit.

pour C nous avons tous les outils déjà couverts et nous avons aussi tis-interpreter, un analyseur statique qui analyse exhaustivement un programme pour un grand sous-ensemble du C langue. Compte tenu 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-interprète est capable d'attraper tous les trois, l'exemple suivant appelle tis-kernel comme tis-interprète (la sortie est édité par souci 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. Ce désinfectant ajoute des informations de vérification de type dans un segment de mémoire d'ombre et vérifie les accès pour voir s'ils violent les règles d'alias. L'outil devrait être capable de détecter toutes les violations de l'alias, mais il pourrait avoir un temps d'exécution important.

23
répondu Shafik Yaghmour 2018-08-20 13:14:43

type punning via pointer casts (par opposition à l'utilisation d'un syndicat) est un exemple majeur de rupture de l'alias strict.

15
répondu Chris Jester-Young 2014-07-06 18:31:53

selon la logique C89, les auteurs de la norme ne voulaient pas exiger que les compilateurs reçoivent un code du genre:

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

devrait être requis pour recharger la valeur de x entre la cession et la déclaration de retour afin de tenir compte de la possibilité que p puisse pointer à x , et la cession à *p pourrait par conséquent modifier la valeur de x . La notion qu'un compilateur devrait avoir le droit supposer qu'il n'y aura pas d'alias dans des situations comme celle-ci ne prête pas à controverse.

malheureusement, les auteurs du C89 ont écrit leur règle d'une manière qui, si elle est lue littéralement, ferait même la fonction suivante invoquer un comportement non défini:

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

parce qu'il utilise une valeur l de type int pour accéder à un objet de type struct S , et int n'est pas parmi les types qui peuvent être utilisés l'accès à un struct S . Parce qu'il serait absurde de traiter toute utilisation de membres de type non-caractère des structures et des syndicats comme un comportement non défini, presque tout le monde reconnaît qu'il y a 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 réussi à définir ce que sont ces circonstances.

une grande partie du problème est le résultat du rapport de défaut n ° 028, qui posait 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);
}

Defect Report #28 déclare que le programme invoque un comportement non défini parce que l'action d'écrire un membre de l'union de type "double" et la lecture d'un de type "int" invoque un comportement défini par la mise en œuvre. Un tel raisonnement est absurde, mais constitue la base des règles de Type efficaces qui compliquent inutilement le langage tout en ne faisant rien pour résoudre le problème initial.

La meilleure façon de résoudre le problème d'origine serait probablement pour traiter la note de bas de page sur le but de la règle comme si elle était normative, et fait la règle n'est pas applicable sauf dans les cas où il y a conflit d'accès par Alias. 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 dans inc_int parce que tous les accès au stockage accédé par *p sont faits avec une valeur de l de type int , et il n'y a pas de conflit dans test parce que p est visiblement dérivé d'un struct S , et à la prochaine fois s est utilisé, tous les accès à ce stockage qui ne sera jamais fait à 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 y a un conflit d'alias entre p et l'accès à s.x sur la ligne marquée parce qu'à ce point d'exécution une autre référence existe qui sera utilisé pour accéder au même stockage .

avait rapport de défaut 028 dit que l'exemple original invoqué UB en raison du chevauchement entre la création et l'utilisation des deux pointeurs, qui aurait fait les choses beaucoup plus clair sans avoir à ajouter des" Types efficaces " ou d'autre telle complexité.

10
répondu supercat 2018-03-10 00:42:31

après avoir lu beaucoup de réponses, je sens le besoin d'ajouter quelque chose:

aliasing Strict (que je vais décrire dans un peu) est important parce que :

  1. L'accès à la mémoire peut être coûteux (du point de vue des performances), c'est pourquoi les données sont manipulées dans les registres CPU avant d'être réécrites à la mémoire physique.

  2. si les données dans deux registres CPU différents seront écrites dans le même espace de mémoire, nous ne pouvons pas prédire quelles données "survivront" quand nous codons en C.

    en assemblage, où nous codons manuellement le chargement et le déchargement des registres CPU, nous saurons quelles données restent intactes. Mais C (heureusement) résume ce détail.

puisque deux pointeurs peuvent pointer vers le même endroit dans le mémoire, cela pourrait entraîner code complexe qui gère les collisions possibles .

ce code supplémentaire est lent et nuit à la performance car il effectue des opérations de lecture / écriture 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 on peut supposer que deux pointeurs ne pointent pas vers le même bloc mémoire (Voir Aussi Le mot-clé restrict ).

le pseudonyme Strict indique qu'il est sûr de supposer que les pointeurs vers différents types pointent vers différents endroits 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 il ne sera pas protégez - vous contre les collisions d'adresses mémoire, résultant en un code machine plus rapide.

par exemple :

laisse supposer 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 commander et tester la façon dont nous chargeons les données de la mémoire vers les registres CPU, de sorte que le code pourrait finir comme ceci:

  1. charge a et b à partir de la mémoire.

  2. ajouter a à b .

  3. enregistrer b et recharger a .

    (enregistrer du CPU au registre de la mémoire et de la charge de la mémoire de la CPU s'inscrire).

  4. ajouter b à a .

  5. enregistrer a (à partir de la CPU s'inscrire) à 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 cas où a et b pointent vers la même adresse mémoire.

aliasing Strict nous permettrait d'empêcher cela en disant au compilateur que ces adresses mémoire sont distinctement différentes (ce qui, dans ce cas, permettra encore plus d'optimisation qui ne peut pas être effectuée si les pointeurs partagent une adresse mémoire).

  1. cela peut être dit au compilateur de deux façons, en utilisant différents types pour pointer. c'est à dire:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. utilisant le mot-clé restrict . c'est à dire:

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

maintenant, en satisfaisant la règle D'Alias stricte, l'étape 3 peut être évitée et le code sera exécuté beaucoup plus rapidement.

en fait, en ajoutant le mot-clé restrict , l'ensemble de la fonction pourrait être optimisé à:

  1. charge a et b à partir de la mémoire.

  2. ajouter a à b .

  3. sauvegarder le résultat à la fois a et à b .

cette optimisation n'aurait pas pu être faite auparavant, en raison de la collision possible (où a et b seraient triplés au lieu de doubler).

9
répondu Myst 2018-01-16 14:11:07

aliasing Strict ne permet pas différents types de pointeur pour les mêmes données.

cet article devrait vous aider à comprendre la question en détail.

5
répondu Jason Dagit 2013-11-04 13:38:55

techniquement en C++, la règle d'alias stricte n'est probablement jamais applicable.

Note la définition d'indirection ( * l'opérateur ):

l'opérateur effectue indirectement: l'expression à laquelle il est demandé doit être un pointeur sur 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 fonction à que l'expression indique .

"de la 1519150920" la définition de glvalue

une glvalue est une expression dont l'évaluation détermine l'identité objet.( ,..snip)

ainsi, dans toute trace de programme bien définie, une glvalue se réfère à un objet. ainsi la règle dite d'alias strict ne s'applique pas, jamais. peut ne pas être ce que les concepteurs ont voulu.

-1
répondu curiousguy 2018-07-09 03:24:02