web-dev-qa-db-fra.com

Le masquage avant le décalage à gauche non signé en C / C ++ est-il trop paranoïaque?

Cette question est motivée par moi qui implémente des algorithmes cryptographiques (par exemple SHA-1) en C/C++, écrit du code portable indépendant de la plate-forme et évite soigneusement comportement indéfini .

Supposons qu'un algorithme de chiffrement standardisé vous demande de l'implémenter:

b = (a << 31) & 0xFFFFFFFF

a et b sont des entiers 32 bits non signés. Notez que dans le résultat, nous supprimons tous les bits supérieurs aux 32 bits les moins significatifs.


Comme première approximation naïve, nous pourrions supposer que int a une largeur de 32 bits sur la plupart des plates-formes, nous écririons donc:

unsigned int a = (...);
unsigned int b = a << 31;

Nous savons que ce code ne fonctionnera pas partout car int a une largeur de 16 bits sur certains systèmes, 64 bits sur d'autres et peut-être même 36 bits. Mais en utilisant stdint.h, Nous pouvons améliorer ce code avec le type uint32_t:

uint32_t a = (...);
uint32_t b = a << 31;

Nous avons donc terminé, non? C'est ce que je pensais depuis des années. ... Pas assez. Supposons que sur une certaine plateforme, nous ayons:

// stdint.h
typedef unsigned short uint32_t;

La règle pour effectuer des opérations arithmétiques en C/C++ est que si le type (tel que short) est plus étroit que int, alors il est élargi à int si toutes les valeurs peuvent fit, ou unsigned int sinon.

Disons que le compilateur définit short comme 32 bits (signé) et int comme 48 bits (signé). Ensuite, ces lignes de code:

uint32_t a = (...);
uint32_t b = a << 31;

signifiera effectivement:

unsigned short a = (...);
unsigned short b = (unsigned short)((int)a << 31);

Notez que a est promu en int car tout ushort (ie uint32) Tient dans int (ie int48).

Mais maintenant nous avons un problème: décaler les bits non nuls vers la gauche dans le bit de signe d'un type entier signé est un comportement non défini . Ce problème est survenu parce que notre uint32 A été promu int48 - au lieu d'être promu uint48 (Où le décalage à gauche serait correct).


Voici mes questions:

  1. Mon raisonnement est-il correct et est-ce un problème légitime en théorie?

  2. Ce problème est-il sûr à ignorer parce que sur chaque plate-forme, le type d'entier suivant est le double de la largeur?

  3. Est-ce une bonne idée de se défendre correctement contre cette situation pathologique en pré-masquant l'entrée comme ceci?: b = (a & 1) << 31;. (Cela sera nécessairement correct sur chaque plate-forme. Mais cela pourrait rendre un algorithme de chiffrement critique plus lent que nécessaire.)

Clarifications/modifications:

  • J'accepte les réponses pour C ou C++ ou les deux. Je veux connaître la réponse pour au moins une des langues.

  • La logique de pré-masquage peut nuire à la rotation des bits. Par exemple, GCC compilera b = (a << 31) | (a >> 1); en une instruction de rotation de bits 32 bits en langage assembleur. Mais si nous pré-masquons le décalage à gauche, il est possible que la nouvelle logique ne soit pas traduite en rotation de bits, ce qui signifie que maintenant 4 opérations sont effectuées au lieu de 1.

71
Nayuki

Prenant un indice de cette question sur UB possible dans uint32 * uint32 arithmétique, l'approche simple suivante devrait fonctionner en C et C++:

uint32_t a = (...);
uint32_t b = (uint32_t)((a + 0u) << 31);

La constante entière 0u a le type unsigned int. Cela favorise l'ajout a + 0u à uint32_t ou unsigned int, la valeur la plus large étant retenue. Étant donné que le type a un rang int ou supérieur, aucune promotion supplémentaire ne se produit et le décalage peut être appliqué avec l'opérande gauche étant uint32_t ou unsigned int.

Le casting final revient à uint32_t supprimera simplement les avertissements potentiels concernant une conversion plus étroite (par exemple, si int est de 64 bits).

Un compilateur C décent devrait être en mesure de voir que l'ajout de zéro est un no-op, ce qui est moins onéreux que de voir qu'un pré-masque n'a aucun effet après un décalage non signé.

12
Nayuki

Parlant du côté C du problème,

  1. Mon raisonnement est-il correct et est-ce un problème légitime en théorie?

C'est un problème que je n'avais pas envisagé auparavant, mais je suis d'accord avec votre analyse. C définit le comportement du << opérateur en termes de type de l'opérande prom gauche, et il est concevable que les promotions entières aboutissent à ce qu'il soit (signé) int lorsque le type d'origine de cet opérande est uint32_t. Je ne m'attends pas à voir cela en pratique sur n'importe quelle machine moderne, mais je suis tout à fait pour la programmation au niveau réel par opposition à mes attentes personnelles.

  1. Ce problème est-il sûr à ignorer parce que sur chaque plate-forme, le type d'entier suivant est le double de la largeur?

C n'exige pas une telle relation entre les types entiers, bien qu'il soit omniprésent dans la pratique. Si vous êtes déterminé à vous fier uniquement à la norme, c'est-à-dire si vous vous efforcez d'écrire du code strictement conforme, vous ne pouvez pas compter sur une telle relation.

  1. Est-ce une bonne idée de se défendre correctement contre cette situation pathologique en pré-masquant l'entrée comme ceci?: B = (a & 1) << 31 ;. (Cela sera nécessairement correct sur chaque plate-forme. Mais cela pourrait rendre un algorithme de chiffrement critique plus lent que nécessaire.)

Le type unsigned long est garanti d'avoir au moins 32 bits de valeur, et il n'est soumis à aucune promotion vers un autre type dans le cadre des promotions entières. Sur de nombreuses plates-formes courantes, il a exactement la même représentation que uint32_t, et peut même être du même type. Ainsi, je serais enclin à écrire l'expression comme ceci:

uint32_t a = (...);
uint32_t b = (unsigned long) a << 31;

Ou si vous avez besoin de a uniquement comme valeur intermédiaire dans le calcul de b, alors déclarez-le comme unsigned long pour commencer.

24
John Bollinger

Q1: Masquer avant le décalage empêche un comportement indéfini qui inquiète OP.

Q2: "... parce que sur chaque plate-forme, le type d'entier suivant est le double de la largeur?" -> non. Le type entier "suivant" peut être inférieur à 2x ou même avoir la même taille.

Ce qui suit est bien défini pour tous les compilateurs C conformes qui ont uint32_t.

uint32_t a; 
uint32_t b = (a & 1) << 31;

Q3: uint32_t a; uint32_t b = (a & 1) << 31; ne devrait pas générer de code qui exécute un masque - il n'est pas nécessaire dans l'exécutable - juste dans la source. Si un masque se produit, obtenez un meilleur compilateur si la vitesse est un problème.

Comme suggéré , mieux vaut souligner la non-signature avec ces changements.

uint32_t b = (a & 1U) << 31;

@ John Bollinger une bonne réponse explique bien comment gérer le problème spécifique d'OP.

Le problème général est de savoir comment former un nombre d'au moins n bits, une certaine signe-ness et ne font pas l'objet de promotions entières surprenantes - au cœur du dilemme d'OP. Ce qui suit remplit cela en invoquant une opération unsigned qui ne modifie pas la valeur - effective un no-op autre que le type concerne. Le produit aura au moins la largeur de unsigned ou uint32_t. La coulée, en général, peut réduire le type. Le moulage doit être évité à moins qu'il ne soit certain que le rétrécissement ne se produira pas. Un compilateur d'optimisation ne créera pas de code inutile.

uint32_t a;
uint32_t b = (a + 0u) << 31;
uint32_t b = (a*1u) << 31;
19
chux

Pour éviter une promotion indésirable, vous pouvez utiliser le type supérieur avec certains typedef, comme

using my_uint_at_least32 = std::conditional_t<(sizeof(std::uint32_t) < sizeof(unsigned)),
                                              unsigned,
                                              std::uint32_t>;
10
Jarod42