web-dev-qa-db-fra.com

Quelle est la règle de crénelage stricte?

Lorsqu'on leur demande à propos de comportement indéfini commun en C , les utilisateurs font parfois référence à la règle de repliement de spectre strict.
De quoi parlent-ils?

759
Benoit

Une situation typique où vous rencontrez des problèmes de crénelage strictes est de superposer une structure (comme un périphérique/un message réseau) sur un tampon de la taille du mot de votre système (comme un pointeur sur uint32_ts ou uint16_ts). Lorsque vous superposez une structure sur un tel tampon, ou un tampon sur une telle structure par le biais de la diffusion de pointeur, vous pouvez facilement enfreindre les règles de crénelage strictes.

Donc, dans ce type de configuration, si je veux envoyer un message à quelque chose, il faut que deux pointeurs incompatibles pointent vers le même bloc de mémoire. Je pourrais alors coder naïvement 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'aliasing 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 de l'un des autres types autorisés par C 2011 6.5, paragraphe 71 est un comportement indéfini. Malheureusement, vous pouvez toujours coder de cette façon, peut-être obtenir des avertissements, compiler correctement, mais avoir un comportement inattendu étrange lorsque vous exécutez le code.

(GCC semble quelque peu incohérent dans sa capacité à donner des avertissements de repliement, parfois un avertissement amical et parfois non.)

Pour voir pourquoi ce comportement est indéfini, nous devons réfléchir à ce que la règle d'aliasing stricte achète au compilateur. Fondamentalement, avec cette règle, il n'est pas nécessaire de penser à insérer des instructions pour actualiser le contenu de buff à chaque exécution de la boucle. Lors de l'optimisation, avec quelques hypothèses gênantes non appliquées sur le repliement, il peut omettre ces instructions, charger buff[0] et buff[1] dans les registres de la CPU une fois avant l'exécution de la boucle, et accélérer le corps de la boucle. . Avant l’introduction du pseudonyme strict, le compilateur devait vivre dans un état de paranoïa tel que le contenu de buff puisse changer à tout moment, de n’importe où. Donc, pour obtenir une performance supplémentaire, et en supposant que la plupart des gens ne pointent pas les pointeurs, une règle de repliement strict a été introduite.

Gardez à l'esprit que 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 le faites plutôt.

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

Et réécrivez 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 être ou ne pas être capable ou assez intelligent pour essayer d’envoyer SendMessage en ligne et il peut décider ou non de charger ou de ne pas charger le buff à nouveau. 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 en-tête basée sur un modèle que le compilateur pense pouvoir insérer en ligne. Ou peut-être que c'est simplement quelque chose que vous avez écrit dans votre fichier .c pour votre propre commodité. Quoi qu’il en soit, un comportement indéfini pourrait encore se produire. Même lorsque nous connaissons une partie de ce qui se passe sous le capot, cela reste une violation de la règle, de sorte qu'aucun comportement bien défini n'est garanti. Donc, le simple fait d’emballer une fonction qui prend notre tampon délimité par Word n’aide pas nécessairement.

Alors, comment puis-je contourner cela?

  • Utilisez un syndicat. La plupart des compilateurs le supportent sans se plaindre d'un aliasing strict. Ceci est autorisé dans C99 et explicitement autorisé dans C11.

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

  • Vous pouvez utiliser char* pour créer un alias au lieu de Word. Les règles autorisent une exception pour char* (y compris signed char et unsigned char). Il est toujours supposé que char* alias d'autres types. Cependant, cela ne fonctionnera pas dans l'autre sens: il n'y a aucune hypothèse que votre structure alias un tampon de caractères.

Débutant, méfiez-vous

Ce n'est qu'un champ de mines potentiel lorsque deux types sont superposés. Vous devriez également vous renseigner sur endianness , Alignement de Word , et sur la façon de traiter les problèmes d'alignement par le biais de assemblage de structures .

Note de bas de page

1 Les types auxquels C 2011 6.5 7 autorise l'accès à une lvalue sont les suivants:

  • un type compatible avec le type effectif 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 à une version qualifiée du type effectif de l'objet,
  • un type d'agrégat ou d'union qui inclut l'un des types mentionnés ci-dessus parmi ses membres (y compris, de manière récurrente, un membre d'une union sous-agrégée ou confinée), ou
  • un type de caractère.
545
Doug T.

La meilleure explication que j'ai trouvée est celle de Mike Acton, nderstanding Strict Aliasing . Il se concentre un peu sur le développement de la PS3, mais il s’agit essentiellement de GCC.

De l'article:

"Le pseudonyme strict est une hypothèse, faite par le compilateur C (ou C++), selon lequel les pointeurs de déréférencement vers des objets de types différents ne feront jamais référence au même emplacement mémoire (c.-à-d. Se pseudonyme)."

Donc, fondamentalement, si vous avez un int* pointant sur une mémoire contenant un int, puis que vous pointez un float* vers cette mémoire et que vous l'utilisez comme un float, vous enfreignez la règle. Si votre code ne respecte pas cela, alors l'optimiseur du compilateur cassera votre code.

L'exception à la règle est un char*, qui est autorisé à pointer sur n'importe quel type.

228
Niall

Il s'agit de la règle de crénelage strict, décrite à la section 3.10 du standard C++ (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 différente de l'un des types suivants, le comportement est indé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 de cv du type dynamique de l'objet,
  • un type d'agrégat ou d'union qui inclut l'un des types mentionnés ci-dessus parmi ses membres (y compris, de manière récurrente, un membre d'une union sous-agrégée ou confinée),
  • un type qui est un type de classe de base (éventuellement qualifié cv) du type dynamique de l'objet,
  • un type char ou unsigned char.

C++ 11 et C++ 14 libellé (modifications mises en évidence):

Si un programme tente d'accéder à la valeur stockée d'un objet par le biais d'une glvalue d'un autre type 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 de 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 des membres de données non statiques (y compris, de manière récursive, un élément ou membre de données non statique 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,
  • un type char ou unsigned char.

Deux changements étaient mineurs: glvalue au lieu de lvalue , et clarification de la cas global/union.

La troisième modification apporte une garantie plus forte (assouplit la règle de repliement fort): Le nouveau concept de types similaires qui sont maintenant sécurisés pour les alias.


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

Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui possède 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 mentionnés ci-dessus parmi ses membres (y compris, de manière récurrente, un membre d'une union sous-agrégée ou confinée), 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 avoir un alias.

131
Ben Voigt

Remarque

Ceci est extrait de mon "Qu'est-ce que la règle de pseudonyme stricte et pourquoi nous nous en soucions?" Rédaction.

Qu'est-ce qu'un aliasing strict?

En C et C++, l'aliasing a à voir avec les types d'expression par lesquels 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 de repliement, d'où le terme règle de repliement strict. Si nous essayons d'accéder à une valeur en utilisant un type non autorisé, il est classé comme comportement indéfini (UB). Une fois que nous avons un comportement indéfini, tous les paris sont ouverts, les résultats de notre programme ne sont plus fiables.

Malheureusement, avec des violations de crénelage strictes, nous obtiendrons souvent les résultats escomptés, laissant ainsi la possibilité qu'une future version d'un compilateur avec une nouvelle optimisation rompra le code que nous pensions valide. Cela n’est pas souhaitable et c’est un objectif louable de comprendre les règles strictes en matière de crénelage et d’éviter de les enfreindre.

Pour mieux comprendre pourquoi nous nous soucions de nous, nous aborderons les problèmes pouvant survenir lors de la violation des règles strictes en matière de crénelage, tapez punning puisque les techniques courantes utilisées dans ce type enfreignent souvent les règles strictes en matière de crénelage et expliquent comment taper correctement.

Exemples préliminaires

Regardons quelques exemples, puis nous pourrons parler exactement de ce que disent les normes, examinons d’autres exemples, puis voyons comment éviter le crénelage 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 il s'agit d'un aliasing valide. L’optimiseur doit supposer que les assignations 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 appelez foo et définissez les deux paramètres pour qu'ils pointent vers le même emplacement mémoire qui, dans cet exemple, contient un int. Notez que reinterpret_cast indique au compilateur de traiter l'expression comme si elle avait le type spécifié par son paramètre template. Dans ce cas, nous lui disons de traiter l'expression & x comme si elle avait le type float *. On peut naïvement s'attendre à ce que le résultat du deuxième cout soit 0 mais avec optimisation activée en utilisant - O2 gcc et clang produisent le résultat suivant:

0
1

Ce qui peut ne pas être attendu mais est parfaitement valide puisque nous avons invoqué un comportement indéfini. Un float ​​ne peut valablement aliaser un objet int. Par conséquent, l’optimiseur peut supposer que la constante 1 enregistrée lors du déréférencement i sera la valeur renvoyée puisqu’elle est enregistrée f n'a pas pu affecter valablement un objet int. Brancher le code dans Compiler Explorer 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 qui utilise Analyse d’alias de type (TBAA) suppose 1 est renvoyé et déplace directement la valeur constante dans le registre eax qui porte la valeur de retour. TBAA utilise les règles de langage relatives aux 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, au livre de règles

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, je vais donc essayer de fournir pour chaque élément des exemples de code illustrant le sens.

Que dit la norme C11?

La norme C11 dit ce qui suit dans la section 6.5 Expressions paragraph 7:

La valeur stockée d'un objet doit être accessible uniquement par une expression lvalue qui possède 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'assigner nsigned int * à int * même s'ils ne le sont pas 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, de manière récursive, un membre d'une union sous-agrégée ou confinée), 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.

Que dit le projet de norme C++ 17

Le projet de norme C++ 17 de la section [basic.lval] paragraphe 11 indique:

Si un programme tente d'accéder à la valeur stockée d'un objet via une valeur glval autre que l'un des types suivants, le comportement est indé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 de 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 de 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 des membres de données non statiques (y compris, de manière récursive, un élément ou un membre de données non statique d'une union sous-agrégée ou confinée),

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 du type dynamique de l'objet (éventuellement qualifié par cv),

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 de caractère, un caractère non signé ou un type 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
}

À noter caractère signé n'est pas inclus dans la liste ci-dessus, il s'agit d'une différence notable par rapport à C qui indique n type de caractère.

Quel est le type Punning

Nous en sommes arrivés à ce point et nous nous demandons peut-être pourquoi nous voudrions alias? La réponse est généralement: type pun], les méthodes utilisées violent souvent les règles strictes en matière de crénelage.

Parfois, nous voulons contourner le système de types et interpréter un objet comme un type différent. Cela s'appelle type punning, pour réinterpréter un segment de mémoire en un autre type. Type punning est utile pour les tâches qui souhaitent accéder à la représentation sous-jacente d'un objet à afficher, transporter ou manipuler. Les domaines typiques utilisés sont le compilateur, la sérialisation, le code de réseau, etc.

Traditionnellement, cela a été accompli en prenant l'adresse de l'objet, en la convertissant en un pointeur du type que nous souhaitons 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 indéfini. Mais traditionnellement, les compilateurs ne tiraient pas parti des règles strictes en matière de crénelage 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 le typage est le type union, qui est valide en C mais comportement indé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

Cela n’est pas valable en C++ et certains considèrent que les syndicats ont pour seul but de mettre en œuvre des types de variantes et estiment que l’utilisation de syndicats pour le typage est un abus.

Comment pouvons-nous taper Pun correctement?

La méthode standard pour type punning en C et C++ est la suivante memcpy . Cela peut sembler un peu lourd, mais l’optimiseur devrait reconnaître l’utilisation de memcpy pour type punning et l’optimiser et générer un registre pour enregistrer déplacer. 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); 
  //...

A un niveau d'optimisation suffisant, tout compilateur moderne correct génère un code identique à la méthode mentionnée précédemment (reinterpret_cast ou à la méthode nion pour tapez punning. En examinant le code généré, nous constatons qu’il utilise simplement l’enregistrement mov ( Exemple d’explorateur de compilateur dynamique ).

C++ 20 et bit_cast

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 ainsi que d’être utilisable dans un contexte constexpr.

Voici un exemple d'utilisation de bit_cast pour saisir pun a nsigned int ​​à float , ( 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 est nécessaire d'utiliser une structure intermédiaire15. Nous allons utiliser une structure contenant un sizeof (unsigned int) un tableau de caractères (suppose que 4 octets unsigned int) est le - De tapez et nsigned int ​​en tant que To 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 .

Catching Strict Aliasing Violations

Nous n'avons pas beaucoup de bons outils pour détecter les alias stricts en C++. Ils nous permettent également de détecter certains cas de violations d'alias strictes et certains cas de charges et de magasins mal alignés.

gcc en utilisant le drapeau - fstrict-aliasing et - Wstrict-aliasing peut attraper certains cas mais pas sans faux positifs/négatifs. Par exemple, les cas suivants généreront un avertissement dans gcc ( le 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'acceptera pas ce cas supplémentaire ( le voir en direct ):

int *p;

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

Bien que clang permette ces drapeaux, il ne semble apparemment pas implémenter les avertissements.

Un autre outil dont nous disposons est ASan, qui peut capturer des charges et des magasins mal alignés. Bien que ces violations ne soient pas directement contraignantes, elles sont un résultat courant des violations contraignantes. Par exemple, les cas suivants généreront des erreurs d’exécution lorsqu’ils sont générés avec clang en utilisant - 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]

Le dernier outil que je recommanderai est spécifique à C++ et n'est pas strictement un outil, mais une pratique de codage. N'autorisez pas les conversions en C. Gcc et clang produiront tous deux un diagnostic pour les conversions de style C en utilisant - Wold-style-cast . Cela forcera toutes les tables de type non définies à utiliser reinterpret_cast. En général, reinterpret_cast devrait être un indicateur pour une révision plus proche du code. Il est également plus facile de rechercher dans votre base de code la réinterprétation/diffusion pour effectuer un audit.

Pour C, tous les outils sont 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é les versions C de l'exemple précédent où utiliser - fstrict-aliasing manque un cas ( le 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 de saisir les trois, l’exemple suivant appelle tis-kernal en tant qu’interprète tis (la sortie est modifiée par souci de brièveté):

./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 fantôme et vérifie les accès pour vérifier s’ils enfreignent les règles de crénelage. L'outil devrait potentiellement être capable de détecter toutes les violations de repliement, mais peut avoir un temps système important.

59
Shafik Yaghmour

Les alias rigoureux ne se réfèrent pas uniquement aux pointeurs, ils affectent également les références. J'ai écrit un article à ce sujet pour le wiki de développeur boost et il a été si bien reçu que je l'ai transformé en une page de mon site de conseil. Il explique complètement ce que c'est, pourquoi cela déroute tellement les gens et quoi faire à ce sujet. Livre blanc sur le crénelage strict . En particulier, cela explique pourquoi les unions sont un comportement risqué pour C++ et pourquoi utiliser memcpy est le seul correctif portable à la fois en C et en C++. J'espère que c'est utile.

43
Patrick

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

Compiler avec gcc -O2 -o check check.c. Habituellement (avec la plupart des versions de gcc que j'ai essayées), ceci produit "un problème de repliement strict", car le compilateur suppose que "h" ne peut pas être la même adresse que "k" dans la fonction "check". De ce fait, le compilateur optimise la if (*h == 5) et appelle toujours le printf.

Pour ceux qui sont intéressés, voici le code assembleur x64, produit par gcc 4.6.3, tournant sous ubuntu 12.04.2 pour x64:

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

Donc, la condition if est complètement partie du code assembleur.

34
Ingo Blackman

Type punning via des conversions de pointeur (par opposition à l'utilisation d'une union) est un exemple majeur de suppression du crénelage strict.

16
Chris Jester-Young

Selon le raisonnement C89, les auteurs de la norme ne voulaient pas exiger que les compilateurs reçoivent un code tel que:

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

devrait être obligé de recharger la valeur de x entre l'affectation et l'instruction return afin de permettre la possibilité que p puisse pointer sur x et l'affectation sur *p pourrait par conséquent modifier la valeur de x. La notion selon laquelle un compilateur devrait avoir le droit de présumer qu'il n'y aura pas de repliement dans des situations comme celles décrites ci-dessus n'était pas controversée.

Malheureusement, les auteurs du C89 ont écrit leur règle de manière à ce que, même si elle était lue littéralement, la fonction suivante invoquerait le comportement non défini:

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

car il utilise une lvalue de type int pour accéder à un objet de type struct S, et int ne figure pas parmi les types pouvant être utilisés pour accéder à struct S. Parce qu'il serait absurde de traiter toute utilisation de membres non-de type caractère de structures et d'unions comme un comportement indéfini, presque tout le monde reconnaît qu'il existe au moins certaines circonstances dans lesquelles une valeur 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 provient du rapport de défauts n ° 028, qui portait sur le comportement d'un programme tel que:

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éfectuosité n ° 28 indique que le programme invoque le comportement non défini, car l'action d'écrire un membre d'union de type "double" et d'en lire un de type "int" appelle le comportement défini par Implémentation. Un tel raisonnement est absurde, mais constitue la base des règles de type Effective, qui compliquent inutilement le langage sans rien faire pour résoudre le problème initial.

Le meilleur moyen de résoudre le problème initial serait probablement de traiter la note de bas de page relative à l'objectif de la règle comme si elle était normative et de la rendre inapplicable, sauf dans les cas impliquant des accès conflictuels utilisant des 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 dans inc_int car tous les accès au stockage accessible via *p se font avec une lvalue 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 utilisation de s, tous les accès à ce stockage qui seront jamais effectués via p auront 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 de repliement entre p et l'accès à s.x sur 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éfauts 028 indiquait 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 telle complexité.

15
supercat

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

Aliasing strict (que je décrirai dans un peu) est important car:

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

  2. Si des données dans deux registres de CPU différents sont écrites sur le même espace mémoire, nous ne pouvons pas prédire quelles données vont "survivre" lorsque nous codons en C.

    Dans Assembly, où nous codons manuellement le chargement et le déchargement des registres de la CPU, nous saurons quelles données restent intactes. Mais C (heureusement) fait abstraction de ce détail.

Étant donné que deux pointeurs peuvent pointer vers le même emplacement dans la mémoire, cela pourrait entraîner code complexe gérant des collisions possibles.

Ce code supplémentaire est lent et nuit aux performances car il effectue des opérations supplémentaires de lecture/écriture en mémoire, qui sont à la fois plus lentes et (éventuellement) inutiles.

La règle de repliement stricte nous permet d'éviter le code machine redondant dans les cas où devrait être sûr de supposer que deux pointeurs ne ne pointez pas sur le même bloc de mémoire (voir aussi le mot clé restrict).

Le crénelage strict indique qu'il est prudent de supposer que les pointeurs sur 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 il ne sera pas protéger contre les collisions d’adresses mémoire, entraînant un code machine plus rapide.

Par exemple:

Assumons 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 de la CPU, afin que le code puisse se retrouver ainsi. :

  1. chargez a et b de la mémoire.

  2. ajoutez a à b.

  3. sauvegarderb et rechargera.

    (sauvegarde du registre de la CPU dans la mémoire et chargement de la mémoire dans le registre de la CPU).

  4. ajoutez b à a.

  5. enregistrez a (du registre de la 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 désignent la même adresse mémoire.

Un alias strict nous permettrait d'éviter cela en indiquant au compilateur que ces adresses mémoire sont nettement différentes (ce qui, dans ce cas, permettra une optimisation encore plus grande qui ne peut pas être effectuée si les pointeurs partagent une adresse mémoire).

  1. Cela peut être dit au compilateur de deux manières, en utilisant différents types de pointeurs. c'est à dire.:

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

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

Maintenant, en satisfaisant à la règle Strict Aliasing, l'étape 3 peut être évitée et le code s'exécutera beaucoup plus rapidement.

En fait, en ajoutant le mot clé restrict, la fonction entière pourrait être optimisée pour:

  1. chargez a et b de la mémoire.

  2. ajoutez a à b.

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

Cette optimisation n'aurait pas pu être réalisée auparavant, en raison d'une possible collision (où a et b seraient triplés au lieu de doublés).

10
Myst

Le crénelage strict n'autorise pas différents types de pointeurs sur les mêmes données.

Cet article devrait vous aider à comprendre le problème en détail.

6
Jason Dagit