(OU dactylographie, comportement vague et alignement, oh mon Dieu!)Amis, il reste très peu de temps avant le lancement d'un nouveau fil de discussion sur le cours
"Développeur C ++" . Il est temps de publier une traduction de la deuxième partie du matériel, qui raconte ce qu'un jeu de mots tape.
Qu'est-ce qu'une typification de jeu de mots?Nous avons atteint le point où nous pouvons nous demander pourquoi nous pourrions avoir besoin de pseudonymes? Habituellement pour la mise en œuvre de la saisie de calembours, car les méthodes fréquemment utilisées violent les règles strictes d'alias.

Parfois, nous voulons contourner le système de types et interpréter l'objet comme un autre type. La réinterprétation d'un segment de mémoire comme un autre type est appelée un
jeu de mots de type
punning . Les jeux de mots sont utiles pour les tâches qui nécessitent l'accès à la représentation de base d'un objet pour afficher, transporter ou manipuler les données fournies. Domaines typiques où nous pouvons rencontrer l'utilisation de jeux de mots: compilateurs, sérialisation, code réseau, etc.
Traditionnellement, cela a été réalisé en prenant l'adresse de l'objet, en le moulant vers un pointeur sur le type auquel nous voulons interpréter, puis en accédant à la valeur, ou en d'autres termes, en utilisant des alias. Par exemple:
int x = 1 ; // C float *fp = (float*)&x ; // // C++ float *fp = reinterpret_cast<float*>(&x) ; // printf( “%f\n”, *fp ) ;
Comme nous l'avons vu précédemment, il s'agit d'un alias inacceptable, cela entraînera un comportement indéfini. Mais traditionnellement, les compilateurs n'utilisaient pas de règles d'aliasing strictes, et ce type de code fonctionnait généralement juste, et les développeurs, malheureusement, sont habitués à autoriser de telles choses. Une méthode alternative courante de frappe est d'utiliser l'union, qui est valide en C, mais provoquera un comportement indéfini en C ++ (
voir l'exemple ):
union u1 { int n; float f; } ; union u1 u; uf = 1.0f; printf( "%d\n”, un ); // UB(undefined behaviour) C++ “n is not the active member”
Ceci est inacceptable en C ++, et certains pensent que les unions sont uniquement destinées à implémenter des types de variantes, et considèrent que l'utilisation des unions pour taper des jeux de mots est un abus.
Comment mettre en œuvre un jeu de mots?La méthode bénie standard pour taper des jeux de mots en C et C ++ est memcpy. Cela peut sembler un peu compliqué, mais l'optimiseur doit reconnaître l'utilisation de memcpy pour le jeu de mots, 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
Nous pouvons utiliser
memcpy
:
void func1( double d ) { std::int64_t n; std::memcpy(&n, &d, sizeof d); //…
Avec un niveau d'optimisation suffisant, tout compilateur moderne décent génère du code identique à la méthode reinterpret_cast ou à la méthode join précédemment mentionnée pour obtenir un jeu de mots. En étudiant le code généré, nous voyons qu'il utilise uniquement le registre mov (
exemple ).
Types et tableaux de calemboursMais que se passe-t-il si nous voulons implémenter le jeu de mots d'un tableau de caractères non signé dans une série d'int entier non signé, puis effectuer une opération sur chaque valeur int non signée? Nous pouvons utiliser memcpy pour transformer un tableau de caractères non signé en un type int temporaire non signé. L'optimiseur sera toujours en mesure de tout voir via memcpy et d'optimiser à la fois l'objet temporaire et la copie, et de travailler directement avec les données sous-jacentes (
exemple ):
// , int foo( unsigned int x ) { return x ; } // , len sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = 0; std::memcpy( &ui, &p[index], sizeof(unsigned int) ); result += foo( ui ) ; } return result; }
Dans cet exemple, nous prenons
char*p
, supposons qu'il pointe vers plusieurs fragments de taille de données
sizeof(unsigned int)
, interprétons chaque fragment de données comme
unsigned int
, calculons
foo()
pour chaque fragment du jeu de mots, résumons le résultat et retournons la valeur finale .
L'assemblage du corps de boucle montre que l'optimiseur transforme le corps en accès direct au tableau de base de caractères
unsigned int
tant
unsigned int
, en l'ajoutant directement à
eax
:
add eax, dword ptr [rdi + rcx]
Le même code, mais en utilisant
reinterpret_cast
pour implémenter un jeu de mots (viole l'aliasing strict):
// , len sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = *reinterpret_cast<unsigned int*>(&p[index]); result += foo( ui ); } return result; }
C ++ 20 et bit_castEn C ++ 20, nous avons
bit_cast
, qui fournit un moyen facile et sûr d'interpréter, et peut également être utilisé dans le contexte de
constexpr
.
Voici un exemple d'utilisation de
bit_cast
pour interpréter un entier non signé dans un
float
(
exemple ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //, sizeof(float) == sizeof(unsigned int)
Dans le cas où les types To et From n'ont pas la même taille, cela nous oblige à utiliser une structure intermédiaire. Nous utiliserons une structure contenant un tableau de caractères multiple de
sizeof(unsigned int)
(un
sizeof(unsigned int)
4 octets est supposé) comme type From, et
unsigned int
comme To. Type:
struct uint_chars { unsigned char arr[sizeof( unsigned int )] = {} ; // sizeof( unsigned int ) == 4 }; // len 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 ; }
Malheureusement, nous avons besoin de ce type intermédiaire - c'est la limitation actuelle de
bit_cast
.
AlignementDans les exemples précédents, nous avons vu que la violation de règles strictes d'alias peut entraîner l'exclusion du stockage lors de l'optimisation. La violation d'un alias strict peut également entraîner une violation des exigences d'alignement. Les normes C et C ++ indiquent que les objets sont soumis à des exigences d'alignement qui limitent l'endroit où les objets peuvent être placés (en mémoire) et donc accessibles.
C11 section 6.2.8 Alignement des objets indique :
Les types complets d'objets ont des exigences d'alignement qui imposent des restrictions sur les adresses auxquelles les objets de ce type peuvent être placés. L'alignement est une valeur entière définie par l'implémentation qui représente le nombre d'octets entre des adresses consécutives auxquelles cet objet peut être placé. Le type de l'objet impose une exigence d'alignement à chaque objet de ce type: un alignement plus strict peut être demandé à l'aide du
_Alignas
.
La norme de projet C ++ 17 dans la section 1 [basic.align] :
Les types d'objets ont des exigences d'alignement (6.7.1, 6.7.2) qui imposent des restrictions sur les adresses auxquelles un objet de ce type peut être placé. L'alignement est une valeur entière définie par l'implémentation qui représente le nombre d'octets entre des adresses consécutives auxquelles un objet donné peut être placé. Un type d'objet impose une exigence d'alignement à chaque objet de ce type; Un alignement plus strict peut être demandé à l'aide du spécificateur d'alignement (10.6.2).
C99 et C11 indiquent explicitement qu'une conversion qui aboutit à un pointeur non aligné est un comportement indéfini, section 6.3.2.3.
Pointeurs dit:
Un pointeur vers un objet ou un type partiel peut être converti en pointeur vers un autre objet ou type partiel. Si le pointeur résultant n'est pas correctement aligné pour le type de pointeur, le comportement n'est pas défini. ...
Bien que C ++ ne soit pas si évident, je pense que cette phrase du paragraphe 1
[basic.align]
suffisante:
... Le type d'objet impose une exigence d'alignement à chaque objet de ce type; ...
ExempleSupposons donc:
- alignof (char) et alignof (int) sont respectivement 1 et 4
- sizeof (int) est 4
Ainsi, interpréter un tableau de caractères de taille 4 comme
int
viole l'alias strict et peut également violer les exigences d'alignement si le tableau a un alignement de 1 ou 2 octets.
char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; // 1 2 int x = *reinterpret_cast<int*>(arr); // Undefined behavior
Ce qui peut entraîner une baisse des performances ou une erreur de bus dans certaines situations. Alors que l'utilisation d'alignas pour forcer le même alignement pour un tableau dans int empêchera les exigences d'alignement de se casser:
alignas(alignof(int)) char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; int x = *reinterpret_cast<int*>(arr);
AtomicitéUne autre punition inattendue pour un accès déséquilibré est qu'elle viole l'atomicité de certaines architectures. Les magasins atomiques peuvent ne pas apparaître atomiques pour les autres threads de x86 s'ils ne sont pas alignés.
Détection des violations strictes d'aliasNous n'avons pas beaucoup de bons outils pour suivre l'aliasing strict en C ++. Les outils dont nous disposons permettront de détecter certains cas de violations et certains cas de chargement et de stockage incorrects.
gcc utilisant les
-fstrict-aliasing
et
-Wstrict-aliasing
peut
-Wstrict-aliasing
certains cas, mais pas sans faux positifs / problèmes. Par exemple, les cas suivants généreront un avertissement dans gcc (
exemple ):
int a = 1; short j; float f = 1.f; // , TIS , printf("%i\n", j = *(reinterpret_cast<short*>(&a))); printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
bien qu'il n'attrape pas ce cas supplémentaire (
exemple ):
int *p; p=&a; printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Bien que
clang
résolve ces indicateurs, il ne semble pas réellement implémenter l'avertissement.
Un autre outil que nous avons est ASan, qui peut capturer un enregistrement et un stockage mal alignés. Bien qu'il ne s'agisse pas de violations directes d'un alias strict, il s'agit d'un résultat assez courant. Par exemple, les cas suivants généreront des erreurs d'exécution lors de l'assemblage à l'aide de clang à l'aide de
-fsanitize=address
int *x = new int[2]; // 8 : [0,7]. int *u = (int*)((char*)x + 6); // x *u = 1; // [6-9] printf( "%d\n", *u ); // [6-9]
Le dernier outil que je recommande est spécifique à C ++ et, en fait, non seulement un outil, mais aussi une pratique de codage qui ne permet pas la
-Wold-style-cast
C.
gcc
et
clang
effectueront des diagnostics pour les
-Wold-style-cast
C à l'aide de
-Wold-style-cast
. Cela forcera tous les calembours de frappe non définis à utiliser reinterpret_cast. En général,
reinterpret_cast
devrait être une balise pour une analyse plus approfondie du code.
Il est également plus facile de rechercher dans la base de code reinterpret_cast pour effectuer un audit.
Pour C, nous avons tous les outils qui sont déjà décrits, et nous avons également
tis-interpreter
, un analyseur statique qui analyse de manière exhaustive le programme pour un grand sous-ensemble de C. Étant donné les versions C de l'exemple précédent, où l'utilisation de -fstrict-aliasing saute un cas (
exemple )
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));
L'interpréteur TIS peut intercepter les trois, l'exemple suivant appelle le noyau TIS en tant qu'interpréteur TIS (la sortie est modifiée 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.
Et enfin,
TySan , qui est en cours de développement. Cet assainisseur ajoute des informations de vérification de type au segment de mémoire fantôme et vérifie les accès pour déterminer s'ils violent les règles d'alias. L'outil devrait potentiellement être en mesure de suivre toutes les violations d'alias, mais peut avoir une surcharge importante au moment de l'exécution.
ConclusionNous avons appris les règles d'alias en C et C ++, ce qui signifie que le compilateur attend de nous que nous suivions strictement ces règles et acceptions les conséquences de ne pas les respecter. Nous avons découvert certains outils qui peuvent nous aider à identifier certains abus de pseudonymes. Nous avons vu que l'utilisation habituelle de l'aliasing est un jeu de mots de typification. Nous avons également appris à l'implémenter correctement.
Les optimiseurs améliorent progressivement l'analyse des alias basés sur le type et cassent déjà du code basé sur des violations strictes d'alias. Nous pouvons nous attendre à ce que les optimisations s'améliorent et cassent encore plus de code qui fonctionnait auparavant.
Nous avons des méthodes compatibles prêtes à l'emploi standard pour interpréter les types. Parfois, pour les versions de débogage, ces méthodes doivent être des abstractions libres. Nous avons plusieurs outils pour détecter les violations d'aliasing sévères, mais pour C ++ ils n'attraperont qu'une petite partie des cas, et pour C en utilisant le tis-interpreter, nous pouvons suivre la plupart des violations.
Merci à ceux qui ont commenté cet article: JF Bastien, Christopher Di Bella, Pascal Quoc, Matt P. Dziubinski, Patrice Roy et Olafur Vaage
Bien sûr, au final, toutes les erreurs appartiennent à l'auteur.
La traduction d'un document assez volumineux est donc terminée, dont la première partie peut être lue
ici . Et nous vous invitons traditionnellement à
la journée portes ouvertes , qui aura lieu le 14 mars par le chef du département de développement technologique chez Rambler & Co -
Dmitry Shebordaev.