c++ c x86 undefined-behavior integer-overflow

c++ - ¿El desbordamiento de enteros causa un comportamiento indefinido debido a la corrupción de la memoria?



x86 undefined-behavior (6)

Además de las consecuencias de la optimización esotérica, debe considerar otros problemas incluso con el código que ingenuamente espera que genere un compilador que no optimiza.

  • Incluso si sabe que la arquitectura es un complemento a dos (o lo que sea), una operación desbordada podría no establecer indicadores como se esperaba, por lo que una instrucción como if(a + b < 0) podría tomar la rama incorrecta: dados dos números positivos grandes, por lo cuando se suman, se desborda y el resultado, por lo que los puristas de dos complementos afirman, es negativo, pero la instrucción de adición en realidad no puede establecer el indicador negativo)

  • Una operación de múltiples pasos puede haber tenido lugar en un registro más amplio que sizeof (int), sin truncarse en cada paso, por lo que una expresión como (x << 5) >> 5 no puede cortar los cinco bits de la izquierda como usted asumir que lo harían

  • Las operaciones de multiplicar y dividir pueden usar un registro secundario para bits adicionales en el producto y el dividendo. Si se multiplica el "no se puede" desbordar, el compilador puede suponer que el registro secundario es cero (o -1 para productos negativos) y no reiniciarlo antes de dividirse. Entonces, una expresión como x * y / z puede usar un producto intermedio más amplio de lo esperado.

Algunos de estos parecen una precisión extra, pero su precisión extra no se espera, no se puede predecir ni se puede confiar, y viola su modelo mental de "cada operación acepta operandos de dos en dos de N bits y devuelve la N menos significativa bits del resultado para la próxima operación "

Recientemente leí que el desbordamiento de entero con signo en C y C ++ causa un comportamiento indefinido:

Si durante la evaluación de una expresión, el resultado no está matemáticamente definido o no está en el rango de valores representables para su tipo, el comportamiento no está definido.

Actualmente estoy tratando de entender la razón del comportamiento indefinido aquí. Pensé que el comportamiento indefinido ocurre aquí porque el entero comienza a manipular la memoria alrededor de sí mismo cuando se vuelve demasiado grande para ajustarse al tipo subyacente.

Así que decidí escribir un pequeño programa de prueba en Visual Studio 2015 para probar esa teoría con el siguiente código:

#include <stdio.h> #include <limits.h> struct TestStruct { char pad1[50]; int testVal; char pad2[50]; }; int main() { TestStruct test; memset(&test, 0, sizeof(test)); for (test.testVal = 0; ; test.testVal++) { if (test.testVal == INT_MAX) printf("Overflowing/r/n"); } return 0; }

Usé una estructura aquí para evitar cualquier problema de protección de Visual Studio en el modo de depuración, como el relleno temporal de las variables de pila, etc. El bucle sin fin debería causar varios desbordamientos de test.testVal , y de hecho, aunque sin otras consecuencias que el propio desbordamiento.

test.testVal un vistazo al volcado de memoria mientras ejecutaba las pruebas de desbordamiento con el siguiente resultado ( test.testVal tenía una dirección de memoria de 0x001CFAFC ):

0x001CFAE5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001CFAFC 94 53 ca d8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Como puede ver, la memoria alrededor del int que continuamente se desborda permanece "intacta". Probé esto varias veces con un resultado similar. Nunca hubo ningún recuerdo alrededor del desbordamiento int dañado.

¿Qué pasa aquí? ¿Por qué no se daña la memoria en torno a la variable test.testVal ? ¿Cómo puede esto causar un comportamiento indefinido?

Intento entender mi error y por qué no se corrompe la memoria durante un desbordamiento de enteros.


El comportamiento de desbordamiento de enteros no está definido por el estándar de C ++. Esto significa que cualquier implementación de C ++ es libre de hacer lo que quiera.

En la práctica esto significa: lo que sea más conveniente para el implementador. Y dado que la mayoría de los implementadores tratan int como un valor de complemento a dos, la implementación más común hoy en día es decir que una suma desbordada de dos números positivos es un número negativo que guarda cierta relación con el resultado verdadero. Esta es una respuesta incorrecta y está permitida por el estándar, porque el estándar permite cualquier cosa.

Existe un argumento para decir que el desbordamiento de entero debe tratarse como un error , al igual que la división entera por cero. La arquitectura ''86 incluso tiene la instrucción INTO para generar una excepción en caso de desbordamiento. En algún momento, ese argumento puede ganar suficiente peso como para convertirlo en compiladores convencionales, en cuyo punto un desbordamiento de entero puede causar un bloqueo. Esto también cumple con el estándar C ++, que permite que una implementación haga cualquier cosa.

Se podría imaginar una arquitectura en la que los números se representan como cadenas terminadas en nulo en forma de little-endian, con un byte cero que dice "fin de número". La adición se puede hacer añadiendo byte por byte hasta que se alcance un byte cero. En una arquitectura de este tipo, un desbordamiento de enteros podría sobrescribir un cero final con uno, haciendo que el resultado parezca mucho más largo y potencialmente dañe los datos en el futuro. Esto también se ajusta al estándar C ++.

Finalmente, como se señaló en algunas otras respuestas, una gran cantidad de generación y optimización de código depende del razonamiento del compilador sobre el código que genera y cómo se ejecutaría. En el caso de un desbordamiento de enteros, es totalmente lícito para el compilador (a) generar código para la suma que arroje resultados negativos al agregar números positivos grandes y (b) para informar su generación de código con el conocimiento de que además de números positivos grandes da un resultado positivo. Así, por ejemplo

if (a+b>0) x=a+b;

podría, si el compilador sabe que tanto a como b son positivos, no se molesta en realizar una prueba, sino que incondicionalmente agrega a b y pone el resultado en x . En una máquina de dos complementos, eso podría llevar a un valor negativo puesto en x , en aparente violación de la intención del código. Esto sería completamente en conformidad con el estándar.


El comportamiento indefinido no está definido. Puede bloquear tu programa. Puede que no haga nada en absoluto. Puede hacer exactamente lo que esperaba. Puede convocar demonios nasales. Puede eliminar todos tus archivos. El compilador puede emitir el código que desee (o ninguno) cuando encuentra un comportamiento indefinido.

Cualquier instancia de comportamiento indefinido hace que todo el programa no esté definido, no solo la operación no definida, por lo que el compilador puede hacer lo que quiera a cualquier parte de su programa. Incluyendo el viaje en el tiempo: el comportamiento indefinido puede ocasionar viajes en el tiempo (entre otras cosas, pero el viaje en el tiempo es el más funky) .

Hay muchas respuestas y publicaciones en blogs sobre el comportamiento indefinido, pero los siguientes son mis favoritos. Sugiero leerlos si quieres aprender más sobre el tema.


Los autores de Standard dejaron indefinido el desbordamiento de enteros debido a que algunas plataformas de hardware podrían atrapar en formas cuyas consecuencias podrían ser impredecibles (posiblemente incluyendo la ejecución de código aleatorio y la consiguiente corrupción de memoria). Aunque el hardware complementario de dos con manejo predecible de desbordamiento silencioso se estableció como estándar para cuando se publicó el estándar C89 (de las muchas arquitecturas de microordenador reprogramables que he examinado, cero uso de cualquier otra cosa) los autores del estándar no quería evitar que nadie produjera implementaciones C en máquinas más antiguas.

En las implementaciones que implementaron la semántica silenciosa envolvente de dos complementos común, como el código

int test(int x) { int temp = (x==INT_MAX); if (x+1 <= 23) temp+=2; return temp; }

sería, 100% confiable, devolver 3 cuando pasara un valor de INT_MAX, ya que agregar 1 a INT_MAX arrojaría INT_MIN, que por supuesto es menor que 23.

En la década de 1990, los compiladores utilizaron el hecho de que el desbordamiento de enteros era un comportamiento indefinido, en lugar de definirse como un complemento de dos, para permitir varias optimizaciones que significaban que los resultados exactos de los cálculos que se desbordaban no serían predecibles, sino aspectos del comportamiento que no Depender de los resultados exactos se mantendría en los rieles. Un compilador de 1990 dado el código anterior podría tratarlo como si agregar 1 a INT_MAX arrojara un valor numéricamente mayor que INT_MAX, haciendo que la función devuelva 1 en lugar de 3, o podría comportarse como los compiladores anteriores, produciendo 3. Nota que en el código anterior, dicho tratamiento podría guardar una instrucción en muchas plataformas, ya que (x + 1 <= 23) sería equivalente a (x <= 22). Un compilador puede no ser consistente en su elección de 1 o 3, pero el código generado no haría otra cosa que producir uno de esos valores.

Desde entonces, sin embargo, se ha puesto más de moda para los compiladores el hecho de que el estándar no imponga ningún requisito sobre el comportamiento del programa en caso de desbordamiento de enteros (un fallo motivado por la existencia de hardware cuyas consecuencias pueden ser genuinamente impredecibles) para justificar tener compiladores lanzar código completamente fuera de los rieles en caso de desbordamiento. Un compilador moderno podría notar que el programa invocará Comportamiento no definido si x == INT_MAX, y así concluirá que la función nunca pasará ese valor. Si la función nunca pasa ese valor, la comparación con INT_MAX se puede omitir. Si se llamó a la función anterior desde otra unidad de traducción con x == INT_MAX, podría devolver 0 o 2; si se llama desde dentro de la misma unidad de traducción, el efecto podría ser aún más extraño ya que un compilador extendería sus inferencias sobre x al llamador.

Con respecto a si el desbordamiento causaría daños en la memoria, en algún hardware antiguo podría tener. En compiladores más antiguos que se ejecutan en hardware moderno, no lo hará. En los compiladores hipermodernos, el desbordamiento niega la estructura del tiempo y la causalidad, por lo que todas las apuestas están desactivadas. El desbordamiento en la evaluación de x + 1 podría efectivamente corromper el valor de x que había sido visto por la comparación anterior contra INT_MAX, haciendo que se comportara como si el valor de x en la memoria se hubiera corrompido. Además, dicho comportamiento del compilador a menudo eliminará la lógica condicional que habría evitado otros tipos de daños en la memoria, lo que permitiría la corrupción de memoria arbitraria.


No está definido qué valor representa el int . No hay ''desbordamiento'' en la memoria como creías.


Usted no entiende la razón del comportamiento indefinido. La razón no es la corrupción de la memoria alrededor del número entero (siempre ocupará el mismo tamaño que ocupan los enteros) sino las aritméticas subyacentes.

Dado que no es necesario codificar los enteros con signo en el complemento a 2, no puede haber una orientación específica sobre lo que sucederá cuando se desborden. La codificación diferente o el comportamiento de la CPU pueden causar diferentes resultados de desbordamiento, que incluyen, por ejemplo, la muerte del programa debido a las trampas.

Y como ocurre con todos los comportamientos indefinidos, incluso si su hardware utiliza el complemento 2 para su aritmética y tiene reglas definidas para el desbordamiento, los compiladores no están obligados por ellos. Por ejemplo, durante mucho tiempo, GCC optimizó los controles que solo se harían realidad en un entorno de complemento a 2. Por ejemplo, if (x > x + 1) f() se eliminará del código optimizado, ya que el desbordamiento firmado es un comportamiento indefinido, lo que significa que nunca sucede (desde la vista del compilador, los programas nunca contienen código que produce un comportamiento indefinido), lo que significa x nunca puede ser mayor que x + 1 .