web-dev-qa-db-fra.com

Détection d'un débordement signé en C / C ++

À première vue, cette question peut sembler être un doublon de Comment détecter un débordement d'entier? , mais elle est en réalité significativement différente.

J'ai trouvé que bien que détecter un débordement d'entier non signé soit assez trivial, détecter un débordement signé en C/C++ est en fait plus difficile que la plupart des gens ne le pensent.

La façon la plus évidente, mais naïve, de le faire serait quelque chose comme:

int add(int lhs, int rhs)
{
 int sum = lhs + rhs;
 if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) {
  /* an overflow has occurred */
  abort();
 }
 return sum; 
}

Le problème avec ceci est que selon la norme C, le débordement d'entier signé est un comportement indéfini. En d'autres termes, selon la norme, dès que vous même provoquer un débordement signé, votre programme est tout aussi invalide que si vous déréférencé un pointeur nul. Vous ne pouvez donc pas provoquer un comportement indéfini, puis essayer de détecter le débordement après coup, comme dans l'exemple de vérification de post-condition ci-dessus.

Même si la vérification ci-dessus est susceptible de fonctionner sur de nombreux compilateurs, vous ne pouvez pas compter dessus. En fait, parce que la norme C dit que le débordement d'entier signé n'est pas défini, certains compilateurs (comme GCC) optimisent la vérification ci-dessus lorsque les indicateurs d'optimisation sont définis, car le compilateur suppose qu'un débordement signé est impossible. Cela rompt totalement la tentative de vérification du débordement.

Ainsi, une autre façon possible de vérifier le débordement serait:

int add(int lhs, int rhs)
{
 if (lhs >= 0 && rhs >= 0) {
  if (INT_MAX - lhs <= rhs) {
   /* overflow has occurred */
   abort();
  }
 }
 else if (lhs < 0 && rhs < 0) {
  if (lhs <= INT_MIN - rhs) {
   /* overflow has occurred */
   abort();
  }
 }

 return lhs + rhs;
}

Cela semble plus prometteur, car nous n'ajoutons pas les deux entiers ensemble jusqu'à ce que nous nous assurions à l'avance que l'exécution d'une telle addition n'entraînera pas de débordement. Ainsi, nous ne provoquons aucun comportement indéfini.

Cependant, cette solution est malheureusement beaucoup moins efficace que la solution initiale, car vous devez effectuer une opération de soustraction juste pour tester si votre opération d'addition fonctionnera. Et même si vous ne vous souciez pas de ce (petit) coup de performance, je ne suis toujours pas entièrement convaincu que cette solution est adéquate. L'expression lhs <= INT_MIN - rhs ressemble exactement au type d'expression que le compilateur pourrait optimiser, pensant qu'un débordement signé est impossible.

Alors, y a-t-il une meilleure solution ici? Quelque chose qui est garanti 1) ne provoque pas de comportement indéfini et 2) ne fournit pas au compilateur la possibilité d'optimiser les vérifications de dépassement de capacité? Je pensais qu'il pourrait y avoir un moyen de le faire en convertissant les deux opérandes en non signés et en effectuant des vérifications en roulant votre propre arithmétique à deux compléments, mais je ne sais pas vraiment comment faire.

75
Channel72

Votre approche avec soustraction est correcte et bien définie. Un compilateur ne peut pas l'optimiser.

Une autre approche correcte, si vous disposez d'un type entier plus grand, consiste à effectuer l'arithmétique dans le type plus grand, puis à vérifier que le résultat correspond au type plus petit lors de la reconversion.

int sum(int a, int b)
{
    long long c;
    assert(LLONG_MAX>INT_MAX);
    c = (long long)a + b;
    if (c < INT_MIN || c > INT_MAX) abort();
    return c;
}

Un bon compilateur doit convertir la totalité de l'addition et de l'instruction if en une addition de taille int et un seul saut conditionnel de dépassement et ne jamais effectuer réellement la plus grande addition.

Edit: Comme Stephen l'a souligné, j'ai du mal à obtenir un compilateur (pas si bon), gcc, pour générer le bon asm. Le code qu'il génère n'est pas terriblement lent, mais certainement sous-optimal. Si quelqu'un connaît des variantes de ce code qui permettront à gcc de faire ce qu'il faut, j'adorerais les voir.

23
R..

Non, votre 2ème code n'est pas correct, mais vous êtes proche: si vous définissez

int half = INT_MAX/2;
int half1 = half + 1;

le résultat d'un ajout est INT_MAX. (INT_MAX est toujours un nombre impair). C'est donc une entrée valide. Mais dans votre routine, vous aurez INT_MAX - half == half1 et vous avorteriez. Un faux positif.

Cette erreur peut être réparée en mettant < au lieu de <= dans les deux chèques.

Mais votre code n'est pas non plus optimal. Ce qui suit ferait:

int add(int lhs, int rhs)
{
 if (lhs >= 0) {
  if (INT_MAX - lhs < rhs) {
   /* would overflow */
   abort();
  }
 }
 else {
  if (rhs < INT_MIN - lhs) {
   /* would overflow */
   abort();
  }
 }
 return lhs + rhs;
}

Pour voir que cela est valide, vous devez ajouter symboliquement lhs des deux côtés des inégalités, et cela vous donne exactement les conditions arithmétiques selon lesquelles votre résultat est hors limites.

34
Jens Gustedt

À mon humble avis, la façon la plus à l'est de traiter le code C++ sentsitive de débordement est d'utiliser SafeInt<T>. Il s'agit d'un modèle C++ multiplateforme hébergé sur du code plex qui offre les garanties de sécurité que vous désirez ici.

Je le trouve très intuitif à utiliser car il fournit la plupart des mêmes schémas d'utilisation que les opérations numériques normales et exprime des débits supérieurs et inférieurs via des exceptions.

16
JaredPar

Pour le cas gcc, à partir de Notes de version gcc 5. nous pouvons voir qu'il fournit maintenant un __builtin_add_overflow pour vérifier le débordement en plus:

Un nouvel ensemble de fonctions intégrées pour l'arithmétique avec vérification de débordement a été ajouté: __builtin_add_overflow, __builtin_sub_overflow et __builtin_mul_overflow et pour la compatibilité avec clang ainsi que d'autres variantes. Ces commandes internes ont deux arguments intégraux (qui n'ont pas besoin d'avoir le même type), les arguments sont étendus à un type signé de précision infinie, +, - ou * est exécuté sur ceux-ci, et le résultat est stocké dans une variable entière pointée vers par le dernier argument. Si la valeur stockée est égale au résultat de précision infinie, les fonctions intégrées renvoient false, sinon true. Le type de la variable entière qui contiendra le résultat peut être différent des types des deux premiers arguments.

Par exemple:

__builtin_add_overflow( rhs, lhs, &result )

Nous pouvons voir dans le document gcc Fonctions intégrées pour effectuer l'arithmétique avec vérification de débordement que:

[...] ces fonctions intégrées ont un comportement entièrement défini pour toutes les valeurs d'argument.

clang fournit également un ensemble de contrôles arithmétiques vérifiés :

Clang fournit un ensemble de fonctions intégrées qui implémentent l'arithmétique vérifiée pour les applications critiques de sécurité d'une manière qui est rapide et facilement exprimable en C.

dans ce cas, le builtin serait:

__builtin_sadd_overflow( rhs, lhs, &result )
13
Shafik Yaghmour

Si vous utilisez l'assembleur en ligne, vous pouvez vérifier le indicateur de débordement . Une autre possibilité est que vous pouvez utiliser un type de données int sécurisé . Je vous recommande de lire ce document sur sécurité entière .

10
rook

Le moyen le plus rapide possible est d'utiliser le module intégré GCC:

int add(int lhs, int rhs) {
    int sum;
    if (__builtin_add_overflow(lhs, rhs, &sum))
        abort();
    return sum;
}

Sur x86, GCC compile ceci en:

    mov %edi, %eax
    add %esi, %eax
    jo call_abort 
    ret
call_abort:
    call abort

qui utilise la détection de débordement intégrée du processeur.

Si vous n'êtes pas d'accord avec l'utilisation des générateurs GCC, le prochain moyen le plus rapide consiste à utiliser des opérations binaires sur les bits de signe. Un débordement signé se produit en outre lorsque:

  • les deux opérandes ont le même signe, et
  • le résultat a un signe différent de celui des opérandes.

Le bit de signe de ~(lhs ^ rhs) est sur si les opérandes ont le même signe, et le bit de signe de lhs ^ sum Est sur si le résultat a un signe différent de celui des opérandes. Vous pouvez donc faire l'ajout sous forme non signée pour éviter un comportement indéfini, puis utiliser le bit de signe de ~(lhs ^ rhs) & (lhs ^ sum):

int add(int lhs, int rhs) {
    unsigned sum = (unsigned) lhs + (unsigned) rhs;
    if ((~(lhs ^ rhs) & (lhs ^ sum)) & 0x80000000)
        abort();
    return (int) sum;
}

Cela se compile en:

    lea (%rsi,%rdi), %eax
    xor %edi, %esi
    not %esi
    xor %eax, %edi
    test %edi, %esi
    js call_abort
    ret
call_abort:
    call abort

ce qui est beaucoup plus rapide que la conversion vers un type 64 bits sur une machine 32 bits (avec gcc):

    Push %ebx
    mov 12(%esp), %ecx
    mov 8(%esp), %eax
    mov %ecx, %ebx
    sar $31, %ebx
    clt
    add %ecx, %eax
    adc %ebx, %edx
    mov %eax, %ecx
    add $-2147483648, %ecx
    mov %edx, %ebx
    adc $0, %ebx
    cmp $0, %ebx
    ja call_abort
    pop %ebx
    ret
call_abort:
    call abort
6
tbodt

Que diriez-vous:

int sum(int n1, int n2)
{
  int result;
  if (n1 >= 0)
  {
    result = (n1 - INT_MAX)+n2; /* Can't overflow */
    if (result > 0) return INT_MAX; else return (result + INT_MAX);
  }
  else
  {
    result = (n1 - INT_MIN)+n2; /* Can't overflow */
    if (0 > result) return INT_MIN; else return (result + INT_MIN);
  }
}

Je pense que cela devrait fonctionner pour tout INT_MIN et INT_MAX (symétrique ou non); la fonction comme les clips montrés, mais il devrait être évident comment obtenir d'autres comportements).

2
supercat

Vous aurez peut-être plus de chance de convertir en entiers 64 bits et de tester des conditions similaires comme celle-ci. Par exemple:

#include <stdint.h>

...

int64_t sum = (int64_t)lhs + (int64_t)rhs;
if (sum < INT_MIN || sum > INT_MAX) {
    // Overflow occurred!
}
else {
    return sum;
}

Vous voudrez peut-être regarder de plus près comment fonctionnera l'extension de signe ici, mais je pense que c'est correct.

1
Jonathan

Pour moi, la vérification la plus simple serait de vérifier les signes des opérandes et des résultats.

Examinons la somme: le débordement peut se produire dans les deux sens, + ou -, uniquement lorsque les deux opérandes ont le même signe. Et, évidemment, le débordement se produira lorsque le signe du résultat ne sera pas le même que le signe des opérandes.

Donc, un chèque comme celui-ci sera suffisant:

int a, b, sum;
sum = a + b;
if  (((a ^ ~b) & (a ^ sum)) & 0x80000000)
    detect_oveflow();

Edit: comme Nils l'a suggéré, c'est la bonne condition if:

((((unsigned int)a ^ ~(unsigned int)b) & ((unsigned int)a ^ (unsigned int)sum)) & 0x80000000)

Et depuis quand l'instruction

add eax, ebx 

conduit à un comportement indéfini? Il n'y a rien de tel dans la référence du jeu d'instructions Intel x86.

0
ruslik

La solution évidente est de convertir en non signé, pour obtenir le comportement de débordement non signé bien défini:

int add(int lhs, int rhs) 
{ 
   int sum = (unsigned)lhs + (unsigned)rhs; 
   if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) { 
      /* an overflow has occurred */ 
      abort(); 
   } 
   return sum;  
} 

Cela remplace le comportement de débordement signé non défini par la conversion définie par l'implémentation des valeurs hors limites entre signé et non signé, vous devez donc vérifier la documentation de votre compilateur pour savoir exactement ce qui se passera, mais il devrait au moins être bien défini, et devrait faire la bonne chose sur n'importe quelle machine à deux compléments qui n'émet pas de signaux sur les conversions, ce qui est à peu près toutes les machines et tous les compilateurs C construits au cours des 20 dernières années.

0
Chris Dodd

En cas d'ajout de deux valeurs long, le code portable peut diviser la valeur long en parties basse et haute int (ou en parties short dans le cas long a la même taille que int):

static_assert(sizeof(long) == 2*sizeof(int), "");
long a, b;
int ai[2] = {int(a), int(a >> (8*sizeof(int)))};
int bi[2] = {int(b), int(b >> (8*sizeof(int))});
... use the 'long' type to add the elements of 'ai' and 'bi'

L'utilisation de l'assemblage en ligne est le moyen le plus rapide si vous ciblez un processeur particulier:

long a, b;
bool overflow;
#ifdef __AMD64__
    asm (
        "addq %2, %0; seto %1"
        : "+r" (a), "=ro" (overflow)
        : "ro" (b)
    );
#else
    #error "unsupported CPU"
#endif
if(overflow) ...
// The result is stored in variable 'a'
0
atomsymbol