¿Cuál es la razón para que el bash shell no te advierta del desbordamiento aritmético, etc.

9

Hay límites establecidos para las capacidades de evaluación aritmética del bashshell. El manual es breve sobre este aspecto de la aritmética de shell pero establece :

La evaluación se realiza en enteros de ancho fijo sin verificación de desbordamiento, aunque la división por 0 queda atrapada y marcada como un error. Los operadores y su precedencia, asociatividad y valores son los mismos que en el lenguaje C.

A qué número entero de ancho fijo se refiere esto es realmente sobre qué tipo de datos se usa (y los detalles de por qué esto está más allá de esto), pero el valor límite se expresa /usr/include/limits.hde esta manera:

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

Y una vez que sepa eso, puede confirmar este estado de hecho así:

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

Este es un entero de 64 bits y se traduce directamente en el shell en el contexto de la evaluación aritmética:

# 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

Entonces, entre 2 63 y 2 64 -1, obtienes enteros negativos que te muestran qué tan lejos de ULONG_MAX estás 1 . Cuando la evaluación alcanza ese límite y se desborda, en cualquier orden que sea, no recibe ninguna advertencia y esa parte de la evaluación se restablece a 0, lo que puede generar un comportamiento inusual con algo como la exponenciación asociativa correcta, por ejemplo:

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
...

El uso sh -c 'command'no cambia nada, así que debo suponer que esta es una salida normal y compatible. Ahora que creo que tengo una comprensión básica pero concreta del rango y límite aritmético y lo que significa en el shell para la evaluación de expresiones, pensé que podría echar un vistazo rápidamente a qué tipos de datos usa el otro software en Linux. Usé algunas bashfuentes que tenía para complementar la entrada de este comando:

{ 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)

Hay más resultados con las ifdeclaraciones y también puedo buscar un comando como awketc. Noto que la expresión regular que utilicé no capta nada sobre las herramientas de precisión arbitraria que tengo como bcy dc.


Preguntas

  1. ¿Cuál es la razón para no advertirte (como awkcuando evalúas 2 ^ 1024) cuando tu evaluación aritmética se desborda? ¿Por qué los enteros negativos entre 2 63 y 2 64 -1 están expuestos al usuario final cuando está evaluando algo?
  2. ¿He leído en alguna parte que un poco de sabor de UNIX puede cambiar interactivamente ULONG_MAX? ¿Alguien ha oído hablar de esto?
  3. Si alguien cambia arbitrariamente el valor del máximo entero sin signo limits.h, luego vuelve a compilar bash, ¿qué podemos esperar que suceda?

Nota

1. Quería ilustrar más claramente lo que vi, ya que es algo empírico muy simple. Lo que noté es que:

  • (a) Cualquier evaluación que dé <2 ^ 63-1 es correcta
  • (b) Cualquier evaluación que dé => 2 ^ 63 hasta 2 ^ 64 da un número entero negativo:
    • El rango de ese entero es x a y. x = -9223372036854775808 e y = 0.

Considerando esto, una evaluación que es como (b) puede expresarse como 2 ^ 63-1 más algo dentro de x..y. Por ejemplo, si literalmente se nos pide evaluar (2 ^ 63-1) +100 002 (pero podría ser cualquier número menor que en (a)) obtenemos -9223372036854675807. Solo estoy afirmando lo obvio, supongo, pero esto también significa que las dos siguientes expresiones:

  • (2 ^ 63-1) + 100 002 Y;
  • (2 ^ 63-1) + (LLONG_MAX - {lo que nos da el shell ((2 ^ 63-1) + 100 002), que es -9223372036854675807}), usando valores positivos que tenemos;
    • (2 ^ 63-1) + (9223372036854775807 - 9223372036854675807 = 100 000)
    • = 9223372036854775807 + 100 000

están muy cerca de hecho. La segunda expresión es "2" aparte de (2 ^ 63-1) + 100 002, es decir, lo que estamos evaluando. Esto es lo que quiero decir con que obtienes enteros negativos que te muestran qué tan lejos estás de 2 ^ 64. Quiero decir, con esos enteros negativos y conocimiento de los límites, bueno, no puedes terminar la evaluación dentro del rango x..y en el shell bash, pero puedes hacerlo en otro lugar: los datos se pueden usar hasta 2 ^ 64 en ese sentido (podría agregar póngalo en papel o úselo en bc). Más allá de eso, sin embargo, el comportamiento es similar al de 6 ^ 6 ^ 6 ya que el límite se alcanza como se describe a continuación en la Q ...


fuente
55
Mi conjetura es que la razón se reduce a "el shell no es la herramienta adecuada para las matemáticas". No está diseñado para eso y no trata de lidiar con gracia como lo muestra. ¡Demonios, la mayoría de los proyectiles ni siquiera se ocupan de carrozas!
terdon
@terdon Aunque la forma en que el shell maneja los números en este caso es exactamente la misma que en todos los idiomas de alto nivel que he escuchado. Los tipos enteros son de tamaño fijo y pueden desbordarse.
Ricitos
@terdon De hecho, mientras investigaba esto desde el 6 ^ 6 ^ 6 momento Q me di cuenta de eso. También adiviné que la razón por la que no pude encontrar mucho contenido fue porque esto tenía que ver con C, o incluso con C99. Como no soy un desarrollador ni una persona de TI, tengo que aceptar todo el conocimiento que sustenta estos supuestos. Seguramente alguien que requiere precisión arbitraria sabe sobre el tipo de datos, pero obviamente no soy esa persona :) (pero noté el comportamiento de awk @ 2 ^ 53 + 1, es decir, flotante doble; solo es precisión e interna versus impresión, etc., está más allá de mí) !).
1
Si quieres trabajar con grandes números en la cáscara, el uso bc, por ejemplo: $num=$(echo 6^6^6 | bc). Desafortunadamente, bcpone saltos de línea, por lo que debe hacerlo num=$(echo $num | sed 's/\\\s//g')después; Si lo hace en una tubería, hay caracteres de nueva línea reales, que son incómodos con sed, aunque num=$(echo 6^6^3 | bc | perl -pne 's/\\\s//g')funciona. En cualquiera de los casos que ahora tiene un número entero que puede ser utilizado, por ejemplo, num2=$(echo "$num * 2" | bc).
Ricitos
1
... Alguien aquí señaló que puede desactivar esta función de salto de línea bcconfigurando BC_LINE_LENGTH=0.
Ricitos

Respuestas:

11

Entonces, entre 2 ^ 63 y 2 ^ 64-1, obtienes enteros negativos que te muestran qué tan lejos estás de ULONG_MAX.

No. ¿Cómo te imaginas eso? Por su propio ejemplo, el máximo es:

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

Si "desbordamiento" significaba "obtienes enteros negativos que te muestran qué tan lejos estás de ULONG_MAX", entonces si agregamos uno a eso, ¿no deberíamos obtener -1? Pero en vez:

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

Quizás quiera decir que este es un número al que puede agregar $maxpara obtener una diferencia negativa, ya que:

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

Pero esto de hecho no sigue siendo cierto:

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

Esto se debe a que el sistema utiliza el complemento de dos para implementar enteros con signo. 1 El valor resultante de un desbordamiento NO es un intento de proporcionarle una diferencia, una diferencia negativa, etc. Es literalmente el resultado de truncar un valor a un número limitado de bits, y luego interpretarlo como un entero con signo de complemento a dos . Por ejemplo, la razón $(($max + 1 + $max))es que -1 es porque el valor más alto en el complemento de dos es todos los bits establecidos, excepto el bit más alto (que indica negativo); Sumar estos juntos básicamente significa llevar todos los bits a la izquierda para que termines con (si el tamaño fuera de 16 bits y no 64):

11111111 11111110

El bit alto (signo) ahora se establece porque se transfirió en la adición. Si agrega uno más (00000000 00000001) a eso, entonces tiene todos los bits establecidos , que en el complemento de dos es -1.

Creo que eso responde parcialmente a la segunda mitad de su primera pregunta: "¿Por qué los enteros negativos ... están expuestos al usuario final?". Primero, porque ese es el valor correcto de acuerdo con las reglas de los números de complemento a dos de 64 bits. Esta es la práctica convencional de la mayoría (otros) lenguajes de programación de alto nivel de propósito general (no puedo pensar en uno que no haga esto), por lo que bashse adhiere a la convención. ¿Cuál es también la respuesta a la primera parte de la primera pregunta: "¿Cuál es la razón?": Esta es la norma en la especificación de los lenguajes de programación.

WRT la segunda pregunta, no he oído hablar de sistemas que cambian interactivamente ULONG_MAX.

Si alguien cambia arbitrariamente el valor del máximo entero sin signo en los límites. H, luego vuelve a compilar bash, ¿qué podemos esperar que suceda?

No haría ninguna diferencia en cómo sale la aritmética, porque este no es un valor arbitrario que se utiliza para configurar el sistema, es un valor de conveniencia que almacena una constante inmutable que refleja el hardware. Por analogía, podría redefinir c a 55 mph, pero la velocidad de la luz seguirá siendo de 186,000 millas por segundo. c no es un número utilizado para configurar el universo, es una deducción sobre la naturaleza del universo.

ULONG_MAX es exactamente lo mismo. Se deduce / calcula en función de la naturaleza de los números de N bits. Cambiarlo limits.hsería una muy mala idea si esa constante se usa en algún lugar suponiendo que se supone que representa la realidad del sistema .

Y no puede cambiar la realidad impuesta por su hardware.


1. No creo que esto (el medio de representación de enteros) esté realmente garantizado bash, ya que depende de la biblioteca C subyacente y el estándar C no garantiza eso. Sin embargo, esto es lo que se usa en la mayoría de las computadoras modernas normales.

encerrada dorada
fuente
¡Estoy muy agradecido! Llegar a un acuerdo con el elefante en la habitación y pensar. Sí, en la primera parte se trata principalmente de palabras. He actualizado mi Q para mostrar lo que quise decir. ¡Investigaré por qué el complemento a dos describe algo de lo que vi y su respuesta es invaluable para entender eso! En lo que respecta a UNIX Q, debo haber leído mal algo sobre ARG_MAX con AIX aquí . ¡Salud!
1
De hecho, puede usar el complemento de dos para determinar el valor si está seguro de estar en el rango> 2 * $max, como lo describe. Mis puntos son 1) ese no es el propósito, 2) asegúrese de entender si quiere hacer eso, 3) no es muy útil debido a la aplicabilidad muy limitada, 4) según la nota al pie de página, en realidad no está garantizado que el sistema lo haga usa el complemento de dos. En resumen, tratar de explotar eso en el código del programa se consideraría una práctica muy pobre. Hay bibliotecas / módulos de "gran número" (para shells en POSIX bc). Úselos si es necesario.
Ricitos
Hace poco vi algo que aprovechó el complemento de los dos para implementar una ALU con un sumador binario de 4 bits con IC de transporte rápido; incluso hubo una comparación con el complemento de uno (para ver qué tan desagradable estaba). Su explicación fue instrumental en que pude nombrar y conectar lo que vi aquí con lo que se discutió en esos videos , aumentando la posibilidad de que realmente pueda comprender todas las implicaciones una vez que todo se asimile. ¡Gracias nuevamente por eso! ¡Salud!