Une situation typique dans laquelle vous rencontrez des problèmes d'aliasing strict est la superposition d'une structure (comme un message de périphérique/réseau) sur un tampon de la taille d'un mot de votre système (comme un pointeur à uint32_t
ou uint16_t
s). Lorsque vous superposez une structure sur un tel tampon, ou un tampon sur une telle structure par le biais d'un moulage de pointeur, vous pouvez facilement violer les règles strictes d'aliasing.
Ainsi, dans ce type de configuration, si je veux envoyer un message à quelque chose, je dois avoir deux pointeurs incompatibles pointant sur le même morceau 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 stricte de l'aliasing rend cette configuration illégale : le déréférencement d'un pointeur qui aliène un objet qui n'est pas d'une catégorie 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 quelques avertissements, avoir une bonne compilation, mais avoir un comportement bizarre et inattendu lorsque vous exécutez le code.
(GCC semble quelque peu incohérent dans sa capacité à donner des avertissements d'aliasing, nous donnant parfois un avertissement amical et parfois non).
Pour voir pourquoi ce comportement est indéfini, nous devons penser à ce que la règle d'aliasing strict achète au compilateur. Fondamentalement, avec cette règle, il n'a pas à penser à insérer des instructions pour rafraîchir le contenu de buff
à chaque exécution de la boucle. Au lieu de cela, lors de l'optimisation, avec quelques hypothèses non forcées et ennuyeuses sur l'aliasing, il peut omettre ces instructions, charger buff[0]
y buff[1]
dans les registres du CPU une fois avant l'exécution de la boucle, et accélérer le corps de la boucle. Avant l'introduction de l'aliasing strict, le compilateur devait vivre dans un état de paranoïa en craignant que le contenu de buff
pourrait être modifié par n'importe quel stockage mémoire précédent. Ainsi, pour obtenir un avantage supplémentaire en termes de performances, et en supposant que la plupart des gens ne tapent pas les pointeurs, la règle d'aliasing stricte a été introduite.
Gardez à l'esprit, si vous pensez que l'exemple est artificiel, que cela pourrait même se produire si vous passez un tampon à une autre fonction qui effectue l'envoi à votre place, si à la place vous avez.
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
Et nous avons réécrit notre boucle précédente pour tirer parti 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 capable ou assez intelligent pour essayer de mettre en ligne SendMessage et il peut ou non décider de charger ou non le tampon à nouveau. Si SendMessage
fait partie d'une autre API qui est compilée séparément, elle a probablement des instructions pour charger le contenu de Buff. Mais peut-être que vous êtes en C++ et qu'il s'agit d'une implémentation modèle d'en-tête seulement que le compilateur pense pouvoir mettre en ligne. Ou peut-être que c'est juste quelque chose que vous avez écrit dans votre fichier .c pour votre propre confort. Quoi qu'il en soit, un comportement indéfini peut toujours s'ensuivre. Même si nous savons ce qui se passe sous le capot, il s'agit toujours d'une violation de la règle et aucun comportement bien défini n'est garanti. Ainsi, le simple fait d'intégrer une fonction qui prend notre tampon délimité par des mots n'est pas forcément utile.
Alors comment puis-je contourner ce problème ?
-
Utilisez une union. La plupart des compilateurs supportent cela sans se plaindre de l'aliasing 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'aliasing strict dans votre compilateur ( f[no-]strict-aliasing dans gcc))
-
Vous pouvez utiliser char*
pour l'aliasing au lieu du mot de votre système. Les règles prévoient une exception pour char*
(y compris signed char
y unsigned char
). Il est toujours supposé que char*
alias d'autres types. Cependant, cela ne fonctionne pas dans l'autre sens : il n'est pas supposé que votre structure aliène un tampon de caractères.
Attention aux débutants
Ce n'est là qu'un des champs de mines potentiels lorsque l'on superpose deux types. Vous devez également vous renseigner sur endiveté , alignement des mots et comment traiter les problèmes d'alignement par Structures d'emballage correctement.
Note de bas de page
1 Les types auxquels C 2011 6.5 7 permet à une lvalue d'accéder 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 susmentionnés parmi ses membres (y compris, de manière récursive, un membre d'un sous-agrégat ou d'une union contenue), ou
- un type de caractère.
13 votes
@Ben Voigt : Les règles d'aliasing sont différentes pour c++ et c. Pourquoi cette question est étiquetée avec
c
yc++faq
.8 votes
@MikeMB : Si vous vérifiez l'historique, vous verrez que j'ai gardé les balises telles qu'elles étaient à l'origine, malgré la tentative de certains autres experts de modifier la question sous les réponses existantes. De plus, la dépendance du langage et de la version est une partie très importante de la réponse à "Quelle est la règle d'aliasing stricte ?" et connaître les différences est important pour les équipes qui migrent du code entre C et C++, ou qui écrivent des macros à utiliser dans les deux.
7 votes
@Ben Voigt : En fait - pour autant que je puisse dire - la plupart des réponses ne concernent que le c et non le c++ ; de plus, la formulation de la question indique que l'accent est mis sur les règles du c (ou le PO n'était tout simplement pas conscient, qu'il y a une différence). Pour la plupart, les règles et l'idée générale sont les mêmes bien sûr, mais surtout, en ce qui concerne les unions, les réponses ne s'appliquent pas à c++. Je suis un peu inquiet que certains programmeurs c++ recherchent la règle d'aliasing stricte et supposent que tout ce qui est énoncé ici s'applique également à c++.
0 votes
D'un autre côté, je suis d'accord pour dire qu'il est problématique de changer la question après que beaucoup de bonnes réponses aient été postées et que le problème est de toute façon mineur.
2 votes
@MikeMB : Je pense que vous verrez que l'accent mis sur le C dans la réponse acceptée, la rendant incorrecte pour le C++, a été édité par une tierce partie. Cette partie devrait probablement être révisée à nouveau.
2 votes
Vous pouvez également consulter un article que j'ai écrit récemment. Qu'est-ce que la règle d'aliénation stricte et pourquoi s'en soucier ? . Il couvre une grande partie du matériel qui n'est pas couvert ici ou, dans certains domaines, une approche plus moderne.
0 votes
J'ai a ajouté une réponse ci-dessous sur la base de mon écrit mentionné ci-dessus.