Quelle est la raison pour laquelle le shell bash ne vous avertit pas d'un débordement arithmétique, etc.?

9

Des limites sont fixées pour les capacités d'évaluation arithmétique du bashshell. Le manuel est succinct sur cet aspect de l'arithmétique des coques mais déclare :

L'évaluation est effectuée dans des entiers à largeur fixe sans vérification de débordement, bien que la division par 0 soit interceptée et signalée comme une erreur. Les opérateurs et leur priorité, associativité et valeurs sont les mêmes que dans le langage C.

Le nombre entier à largeur fixe auquel il se réfère concerne vraiment le type de données utilisé (et les raisons pour lesquelles cela se situe au-delà), mais la valeur limite est exprimée /usr/include/limits.hde cette manière:

#  if __WORDSIZE == 64
#   define ULONG_MAX     18446744073709551615UL
#  ifdef __USE_ISOC99
#  define LLONG_MAX       9223372036854775807LL
#  define ULLONG_MAX    18446744073709551615ULL

Et une fois que vous le savez, vous pouvez confirmer cet état de fait comme suit:

# getconf -a | grep 'long'
LONG_BIT                           64
ULONG_MAX                          18446744073709551615

Il s'agit d'un entier de 64 bits et cela se traduit directement dans le shell dans le contexte de l'évaluation arithmétique:

# echo $(((2**63)-1)); echo $((2**63)); echo $(((2**63)+1)); echo $((2**64))
9223372036854775807        //the practical usable limit for your everyday use
-9223372036854775808       //you're that much "away" from 2^64
-9223372036854775807     
0
# echo $((9223372036854775808+9223372036854775807))
-1

Ainsi, entre 2 63 et 2 64 -1, vous obtenez des entiers négatifs vous montrant à quelle distance de ULONG_MAX vous êtes 1 . Lorsque l'évaluation atteint cette limite et déborde, quel que soit l'ordre, vous ne recevez aucun avertissement et cette partie de l'évaluation est réinitialisée à 0, ce qui peut produire un comportement inhabituel avec quelque chose comme l' exponentiation associative à droite, par exemple:

echo $((6**6**6))                      0   // 6^46656 overflows to 0
echo $((6**6**6**6))                   1   // 6^(6^46656) = 6^0 = 1
echo $((6**6**6**6**6))                6   // 6^(6(6^46656)) = 6^(6^0) = 6^1
echo $((6**6**6**6**6**6))         46656   // 6^(6^(6^(6^46656))) = 6^6
echo $((6**6**6**6**6**6**6))          0   // = 6^6^6^1 = 0
...

L'utilisation sh -c 'command'ne change rien, je dois donc supposer qu'il s'agit d'une sortie normale et conforme. Maintenant que je pense avoir une compréhension basique mais concrète de la plage et de la limite arithmétiques et de ce que cela signifie dans le shell pour l'évaluation de l'expression, j'ai pensé que je pouvais rapidement jeter un œil aux types de données utilisés par les autres logiciels sous Linux. J'ai utilisé quelques bashsources que je devais compléter l'entrée de cette commande:

{ shopt -s globstar; for i in /path/to/source_bash-4.2/include/**/*.h /usr/include/**/*.h; do grep -HE '\b(([UL])|(UL)|())LONG|\bFLOAT|\bDOUBLE|\bINT' $i; done; } | grep -iE 'bash.*max'

bash-4.2/include/typemax.h:#    define LLONG_MAX   TYPE_MAXIMUM(long long int)
bash-4.2/include/typemax.h:#    define ULLONG_MAX  TYPE_MAXIMUM(unsigned long long int)
bash-4.2/include/typemax.h:#    define INT_MAX     TYPE_MAXIMUM(int)

Il y a plus de sortie avec les ifinstructions et je peux rechercher une commande comme awkaussi etc. Je remarque que l'expression régulière que j'ai utilisée ne détecte rien sur les outils de précision arbitraire que j'ai comme bcet dc.


Des questions

  1. Quelle est la raison de ne pas vous avertir (comme awklors de l'évaluation de 2 ^ 1024) lorsque votre évaluation arithmétique déborde? Pourquoi les entiers négatifs entre 2 63 et 2 64 -1 sont-ils exposés à l'utilisateur final lorsqu'il évalue quelque chose?
  2. J'ai lu quelque part qu'une certaine saveur d'UNIX peut changer interactivement ULONG_MAX? Est-ce que quelqu'un a entendu parler de ça?
  3. Si quelqu'un modifie arbitrairement la valeur du nombre entier non signé dans limits.h, puis recompile bash, à quoi pouvons-nous nous attendre?

Remarque

1. Je voulais illustrer plus clairement ce que j'ai vu, car il s'agit de choses empiriques très simples. Ce que j'ai remarqué, c'est que:

  • (a) Toute évaluation qui donne <2 ^ 63-1 est correcte
  • (b) Toute évaluation qui donne => 2 ^ 63 jusqu'à 2 ^ 64 donne un entier négatif:
    • La plage de cet entier est de x à y. x = -9223372036854775808 et y = 0.

Compte tenu de cela, une évaluation qui est comme (b) peut être exprimée comme 2 ^ 63-1 plus quelque chose dans x..y. Par exemple, si on nous demande littéralement d'évaluer (2 ^ 63-1) +100 002 (mais pourrait être un nombre inférieur à celui de (a)), nous obtenons -9223372036854675807. Je dis juste l'évidence, je suppose, mais cela signifie également que les deux expressions suivantes:

  • (2 ^ 63-1) + 100 002 ET;
  • (2 ^ 63-1) + (LLONG_MAX - {ce que le shell nous donne ((2 ^ 63-1) + 100 002), ce qui est -9223372036854675807}) eh bien, en utilisant des valeurs positives que nous avons;
    • (2 ^ 63-1) + (9223372036854775807 - 9223372036854675807 = 100 000)
    • = 9223372036854775807 + 100 000

sont très proches en effet. La deuxième expression est "2" à part (2 ^ 63-1) + 100 002 c'est-à-dire ce que nous évaluons. C'est ce que je veux dire par vous obtenez des entiers négatifs vous montrant à quelle distance de 2 ^ 64 vous êtes. Je veux dire avec ces entiers négatifs et la connaissance des limites, eh bien vous ne pouvez pas terminer l'évaluation dans la plage x..y dans le shell bash mais vous pouvez ailleurs - les données sont utilisables jusqu'à 2 ^ 64 dans ce sens (je pourrais ajouter sur papier ou utilisez-le en bc). Au-delà de cela, cependant, le comportement est similaire à celui de 6 ^ 6 ^ 6 car la limite est atteinte comme décrit ci-dessous dans le Q ...


la source
5
Je suppose que la justification se résume à "la coquille n'est pas le bon outil pour les mathématiques". Il n'est pas conçu pour cela et n'essaie pas de le gérer avec élégance comme vous le montrez. Enfer, la plupart des obus ne traitent même pas avec des flotteurs!
terdon
@terdon Bien que la façon dont le shell traite les nombres dans ce cas est exactement la même que toutes les langues de haut niveau dont j'ai jamais entendu parler. Les types entiers sont de taille fixe et peuvent déborder.
goldilocks
@terdon En effet, alors que je faisais des recherches sur ce sujet depuis le 6 ^ 6 ^ 6 timing QI en est venu à réaliser cela. J'ai également deviné que la raison pour laquelle je ne trouvais pas beaucoup de contenu était parce que cela avait à voir avec C, ou même C99. Comme je ne suis ni développeur ni informaticien, je dois accepter toutes les connaissances qui fondent ces hypothèses. Certes, quelqu'un qui nécessite une précision arbitraire connaît le type de données, mais je ne suis évidemment pas cette personne :) (mais j'ai remarqué le comportement de awk @ 2 ^ 53 + 1 c'est-à-dire flottant double; c'est juste la précision et interne vs l'impression, etc., c'est au-delà de moi !).
1
Si vous voulez travailler avec de grands nombres dans la coquille, l' utilisation bc, par exemple: $num=$(echo 6^6^6 | bc). Malheureusement, bcmet en ligne des sauts, vous devez donc num=$(echo $num | sed 's/\\\s//g')après; si vous le faites dans une pipe, il y a de vrais caractères de nouvelle ligne, qui sont gênants avec sed, bien que cela num=$(echo 6^6^3 | bc | perl -pne 's/\\\s//g')fonctionne. Dans les deux cas , vous avez maintenant un entier qui peut être utilisé, par exemple, num2=$(echo "$num * 2" | bc).
goldilocks
1
... Quelqu'un ici a souligné que vous pouvez désactiver cette fonction de saut de ligne bcen définissant BC_LINE_LENGTH=0.
goldilocks

Réponses:

11

Donc entre 2 ^ 63 et 2 ^ 64-1, vous obtenez des entiers négatifs vous montrant à quelle distance de ULONG_MAX vous êtes.

Non , comment voulez - vous? Par votre propre exemple, le maximum est:

> max=$((2**63 - 1)); echo $max
9223372036854775807

Si "débordement" signifiait "vous obtenez des entiers négatifs vous montrant à quelle distance de ULONG_MAX vous êtes", alors si nous en ajoutons un, ne devrions-nous pas obtenir -1? Mais plutôt:

> echo $(($max + 1))
-9223372036854775808

Peut-être voulez-vous dire que c'est un nombre que vous pouvez ajouter $maxpour obtenir une différence négative, car:

> echo $(($max + 1 + $max))
-1

Mais cela ne reste pas vrai en fait:

> echo $(($max + 2 + $max))
0

En effet, le système utilise le complément à deux pour implémenter des entiers signés. 1 La valeur résultant d'un débordement n'est pas une tentative de vous fournir une différence, une différence négative, etc. Il est littéralement le résultat de tronquer une valeur à un nombre limité de bits, puis l' avoir interprété comme deux de l'entier signé du complément . Par exemple, la raison $(($max + 1 + $max))apparaît comme -1 parce que la valeur la plus élevée dans le complément à deux est tous les bits définis sauf le bit le plus élevé (qui indique négatif); les ajouter ensemble signifie essentiellement porter tous les bits vers la gauche pour que vous vous retrouviez avec (si la taille était de 16 bits, et non 64):

11111111 11111110

Le bit haut (signe) est maintenant défini car il a été reporté dans l'addition. Si vous en ajoutez un de plus (00000000 00000001) à cela, vous avez alors tous les bits définis , qui en complément à deux est -1.

Je pense que cela répond partiellement à la deuxième moitié de votre première question - "Pourquoi les entiers négatifs ... sont-ils exposés à l'utilisateur final?". D'abord, parce que c'est la valeur correcte selon les règles des nombres complémentaires 64 bits deux. C'est la pratique conventionnelle de la plupart des (autres) langages de programmation de haut niveau à usage général (je ne peux pas penser à un qui ne fait pas cela), donc bashadhère à la convention. Quelle est également la réponse à la première partie de la première question - "Quelle est la justification?": C'est la norme dans la spécification des langages de programmation.

WRT la 2ème question, je n'ai pas entendu parler de systèmes qui changent interactivement ULONG_MAX.

Si quelqu'un modifie arbitrairement la valeur du nombre entier non signé dans limits.h, puis recompile bash, que pouvons-nous attendre?

Cela ne ferait aucune différence dans la façon dont l'arithmétique sort, car ce n'est pas une valeur arbitraire qui est utilisée pour configurer le système - c'est une valeur de commodité qui stocke une constante immuable reflétant le matériel. Par analogie, vous pouvez redéfinir c à 55 mph, mais la vitesse de la lumière sera toujours de 186 000 miles par seconde. c n'est pas un nombre utilisé pour configurer l'univers - c'est une déduction sur la nature de l'univers.

ULONG_MAX est exactement le même. Il est déduit / calculé en fonction de la nature des nombres à N bits. La modifier limits.hserait une très mauvaise idée si cette constante est utilisée quelque part en supposant qu'elle est censée représenter la réalité du système .

Et vous ne pouvez pas changer la réalité imposée par votre matériel.


1. Je ne pense pas que cela (le moyen de représentation entière) soit réellement garanti par bash, car cela dépend de la bibliothèque C sous-jacente et le standard C ne le garantit pas. Cependant, c'est ce qui est utilisé sur la plupart des ordinateurs modernes normaux.

boucle d'or
la source
Je suis très reconnaissant! Se réconcilier avec l'éléphant dans la pièce et réfléchir. Oui dans la première partie c'est surtout des mots. J'ai mis à jour mon Q pour montrer ce que je voulais dire. Je vais chercher pourquoi le complément à deux décrit une partie de ce que j'ai vu et votre réponse est inestimable pour comprendre cela! En ce qui concerne UNIX Q, je dois avoir mal lu quelque chose sur ARG_MAX avec AIX ici . À votre santé!
1
En fait, vous pouvez utiliser le complément à deux pour déterminer la valeur si vous êtes sûr que vous êtes dans la plage> 2 * $max, comme vous le décrivez. Mes points sont 1) ce n'est pas le but, 2) assurez-vous de comprendre si vous voulez le faire, 3) ce n'est pas très utile en raison de l'applicabilité très limitée, 4) selon la note de bas de page, il n'est pas réellement garanti que le système fonctionne utilisez le complément à deux. En bref, essayer d'exploiter cela dans le code du programme serait considéré comme une très mauvaise pratique. Il existe des bibliothèques / modules "en grand nombre" (pour les shells sous POSIX, bc) - utilisez-les si vous en avez besoin.
goldilocks
Ce n'est que récemment que j'ai regardé quelque chose qui a exploité le complément des deux pour implémenter une ALU avec un additionneur binaire 4 bits avec IC de transport rapide; il y avait même une comparaison avec son complément (pour voir à quel point c'était différent). Votre explication a contribué à me permettre de nommer et de relier ce que j'ai vu ici avec ce qui a été discuté dans ces vidéos , augmentant la chance que je puisse vraiment saisir toutes les implications sur toute la ligne une fois que tout s'enfonce. Merci encore pour cela! À votre santé!