¿Por qué el desbordamiento de enteros en x86 con GCC causa un bucle infinito?

129

El siguiente código entra en un bucle infinito en GCC:

#include <iostream>
using namespace std;

int main(){
    int i = 0x10000000;

    int c = 0;
    do{
        c++;
        i += i;
        cout << i << endl;
    }while (i > 0);

    cout << c << endl;
    return 0;
}

Así que aquí está el trato: el desbordamiento de entero firmado es un comportamiento técnicamente indefinido. Pero GCC en x86 implementa aritmética de enteros utilizando instrucciones de enteros x86, que se ajustan al desbordamiento.

Por lo tanto, hubiera esperado que se ajustara al desbordamiento, a pesar del hecho de que es un comportamiento indefinido. Pero claramente ese no es el caso. ¿Así que ... qué me perdí?

Compilé esto usando:

~/Desktop$ g++ main.cpp -O2

Salida de GCC:

~/Desktop$ ./a.out
536870912
1073741824
-2147483648
0
0
0

... (infinite loop)

Con las optimizaciones deshabilitadas, no hay un bucle infinito y la salida es correcta. Visual Studio también compila correctamente esto y da el siguiente resultado:

Salida correcta:

~/Desktop$ g++ main.cpp
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
3

Aquí hay algunas otras variaciones:

i *= 2;   //  Also fails and goes into infinite loop.
i <<= 1;  //  This seems okay. It does not enter infinite loop.

Aquí está toda la información relevante de la versión:

~/Desktop$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/x86_64-linux-gnu/gcc/x86_64-linux-gnu/4.5.2/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ..

...

Thread model: posix
gcc version 4.5.2 (Ubuntu/Linaro 4.5.2-8ubuntu4) 
~/Desktop$ 

Entonces la pregunta es: ¿Es esto un error en GCC? ¿O entendí mal algo acerca de cómo GCC maneja la aritmética de enteros?

* También etiqueto este C, porque supongo que este error se reproducirá en C. (aún no lo he verificado).

EDITAR:

Aquí está el ensamblaje del bucle: (si lo reconocí correctamente)

.L5:
addl    %ebp, %ebp
movl    $_ZSt4cout, %edi
movl    %ebp, %esi
.cfi_offset 3, -40
call    _ZNSolsEi
movq    %rax, %rbx
movq    (%rax), %rax
movq    -24(%rax), %rax
movq    240(%rbx,%rax), %r13
testq   %r13, %r13
je  .L10
cmpb    $0, 56(%r13)
je  .L3
movzbl  67(%r13), %eax
.L4:
movsbl  %al, %esi
movq    %rbx, %rdi
addl    $1, %r12d
call    _ZNSo3putEc
movq    %rax, %rdi
call    _ZNSo5flushEv
cmpl    $3, %r12d
jne .L5
Místico
fuente
10
Esto sería mucho más responsable si incluyese el código de ensamblado generado gcc -S.
Greg Hewgill
El montaje es sorprendentemente largo. ¿Aún debería editarlo?
Mysticial
Solo las partes relevantes para su ciclo, por favor.
Greg Hewgill
12
-1. Usted dice que esto es estrictamente un comportamiento indefinido y pregunta si se trata de un comportamiento indefinido. así que esta no es una pregunta real para mí.
Johannes Schaub - litb
8
@ JohannesSchaub-litb Gracias por comentar. Probablemente mala redacción de mi parte. Haré todo lo posible para aclarar de alguna manera para ganar su voto negativo (y editaré la pregunta en consecuencia). Básicamente, sé que es UB. Pero también sé que GCC en x86 usa instrucciones enteras x86, que se ajustan al desbordamiento. Por lo tanto, esperaba que se ajustara a pesar de ser UB. Sin embargo, no fue así y eso me confundió. De ahí la pregunta.
Mysticial

Respuestas:

178

Cuando el estándar dice que es un comportamiento indefinido, lo dice en serio . Cualquier cosa puede suceder. "Cualquier cosa" incluye "generalmente enteros, pero en ocasiones ocurren cosas extrañas".

Sí, en las CPU x86, los enteros generalmente se ajustan a la forma esperada. Esta es una de esas excepciones. El compilador supone que no provocará un comportamiento indefinido y optimiza la prueba de bucle. Si realmente quiere envolvente, pasar -fwrapva la g++o gccal compilar; esto le proporciona una semántica de desbordamiento bien definida (dos complementos), pero puede afectar el rendimiento.

bdonlan
fuente
24
Oh wow. No estaba al tanto -fwrapv. Gracias por señalar esto.
Mysticial
1
¿Hay una opción de advertencia que intente notar bucles infinitos accidentales?
Jeff Burdges el
55
Encontré -Wunsafe-loop-optimizaciones mencionadas aquí: stackoverflow.com/questions/2982507/…
Jeff Burdges el
1
-1 "Sí, en las CPU x86, los enteros generalmente se ajustan de la manera esperada". eso está mal. Pero es sutil. Según recuerdo, es posible hacerlos atrapar en el desbordamiento, pero eso no es de lo que estamos hablando aquí , y nunca lo he visto hecho. aparte de eso, y sin tener en cuenta las operaciones x86 bcd (representación no permitida en C ++), las operaciones enteras x86 siempre se ajustan, porque son el complemento de dos. está confundiendo g ++ con una optimización defectuosa (o extremadamente poco práctica y sin sentido) para una propiedad de operaciones enteras x86.
Saludos y hth. - Alf
55
@ Cheersandhth.-Alf, por 'en CPU x86' quiero decir 'cuando estás desarrollando CPUs x86 usando un compilador de C'. ¿Realmente necesito explicarlo? Obviamente, todo lo que hablo sobre compiladores y GCC es irrelevante si está desarrollando en ensamblador, en cuyo caso la semántica para el desbordamiento de enteros está muy bien definida.
bdonlan
18

Es simple: el comportamiento indefinido, especialmente con la optimización ( -O2) activada, significa que cualquier cosa puede suceder.

Su código se comporta como (usted) esperaba sin el -O2interruptor.

Por cierto, funciona bastante bien con icl y tcc, pero no puedes confiar en cosas así ...

De acuerdo con esto , la optimización de gcc en realidad explota el desbordamiento de enteros con signo. Esto significaría que el "error" es por diseño.

Dennis
fuente
Es un poco horrible que un compilador opte por un bucle infinito de todas las cosas para un comportamiento indefinido.
Inverso
27
@ Inverso: no estoy de acuerdo. Si ha codificado algo con un comportamiento indefinido, ore por un bucle infinito. Hace que sea más fácil detectar ...
Dennis
Quiero decir, si el compilador está buscando activamente UB, ¿por qué no insertar una excepción en lugar de tratar de optimizar el código roto?
Inverso
15
@Inverse: el compilador no está buscando activamente un comportamiento indefinido , se supone que no ocurre. Esto permite que el compilador optimice el código. Por ejemplo, en lugar de computar for (j = i; j < i + 10; ++j) ++k;, simplemente se configurará k = 10, ya que esto siempre será cierto si no se produce un desbordamiento firmado.
Dennis
@Inverse El compilador no "optó" por nada. Escribiste el bucle en tu código. El compilador no lo inventó.
Carreras de ligereza en órbita el
13

Lo importante a tener en cuenta aquí es que los programas C ++ están escritos para la máquina abstracta C ++ (que generalmente se emula a través de instrucciones de hardware). El hecho de que esté compilando para x86 es totalmente irrelevante para el hecho de que esto tiene un comportamiento indefinido.

El compilador es libre de usar la existencia de un comportamiento indefinido para mejorar sus optimizaciones (al eliminar un condicional de un bucle, como en este ejemplo). No hay mapeo garantizado, ni siquiera útil, entre construcciones de nivel C ++ y construcciones de código de máquina de nivel x86, aparte del requisito de que el código de máquina, cuando se ejecute, produzca el resultado exigido por la máquina abstracta de C ++.

Mankarse
fuente
5
i += i;

// el desbordamiento no está definido.

Con -fwrapv es correcto. -fwrapv

lostyzd
fuente
3

Por favor, gente, el comportamiento indefinido es exactamente eso, indefinido . Significa que cualquier cosa podría suceder. En la práctica (como en este caso), el compilador es libre de asumir que noser llamado, y hacer lo que le plazca si eso puede hacer que el código sea más rápido / pequeño. Lo que sucede con el código que no debería ejecutarse es una incógnita. Dependerá del código circundante (dependiendo de eso, el compilador podría generar un código diferente), variables / constantes utilizadas, indicadores del compilador, ... Ah, y el compilador podría actualizarse y escribir el mismo código de manera diferente, o podría obtenga otro compilador con una vista diferente sobre la generación de código. O simplemente obtenga una máquina diferente, incluso otro modelo en la misma línea de arquitectura podría tener su propio comportamiento indefinido (busque códigos de operación indefinidos, algunos programadores emprendedores descubrieron que algunas de esas primeras máquinas a veces hacían cosas útiles ...) . No hay"el compilador da un comportamiento definido en el comportamiento indefinido". Hay áreas que están definidas por la implementación, y debería poder contar con que el compilador se comporte de manera consistente.

vonbrand
fuente
1
Sí, sé muy bien qué es el comportamiento indefinido. Pero cuando sabe cómo se implementan ciertos aspectos del lenguaje para un entorno particular, puede esperar ver ciertos tipos de UB y no otros. Sé que GCC implementa la aritmética de enteros como la aritmética de enteros x86, que se envuelve en el desbordamiento. Entonces asumí el comportamiento como tal. Lo que no esperaba era que GCC hiciera algo más, como respondió bdonlan.
Mysticial
77
Incorrecto. Lo que sucede es que GCC puede suponer que no invocarás un comportamiento indefinido, por lo que solo emite código como si no pudiera suceder. Si no sucede, las instrucciones para hacer lo que pide con ningún comportamiento indefinido se ejecutan, y el resultado es lo que hace la CPU. Es decir, en x86 se hace x86 cosas. Si es otro procesador, podría hacer algo totalmente diferente. O el compilador podría ser lo suficientemente inteligente como para darse cuenta de que está recurriendo a un comportamiento indefinido e iniciar nethack (sí, algunas versiones antiguas de gcc hicieron exactamente eso).
vonbrand
44
Creo que leíste mal mi comentario. Le dije: "Lo que no esperaba", razón por la cual hice la pregunta en primer lugar. No esperaba que GCC hiciera ningún truco.
Mysticial
1

Incluso si un compilador especificara que el desbordamiento de enteros debe considerarse una forma "no crítica" de Comportamiento indefinido (como se define en el Anexo L), el resultado de un desbordamiento de enteros debería, en ausencia de una promesa de plataforma específica de un comportamiento más específico, ser como mínimo considerado como un "valor parcialmente indeterminado". Bajo tales reglas, agregar 1073741824 + 1073741824 podría considerarse arbitrariamente que produce 2147483648 o -2147483648 o cualquier otro valor que sea congruente con 2147483648 mod 4294967296, y los valores obtenidos por las adiciones podrían considerarse arbitrariamente como cualquier valor que sea congruente con 0 mod 4294967296.

Las reglas que permiten que el desbordamiento produzca "valores parcialmente indeterminados" estarían suficientemente bien definidas para cumplir con la letra y el espíritu del Anexo L, pero no evitarían que un compilador hiciera las mismas inferencias generalmente útiles que se justificaría si los desbordamientos no estuvieran restringidos Comportamiento indefinido. Evitaría que un compilador realice algunas "optimizaciones" falsas cuyo efecto principal en muchos casos es requerir que los programadores agreguen desorden adicional al código cuyo único propósito es evitar tales "optimizaciones"; si eso sería algo bueno o no depende del punto de vista de uno.

Super gato
fuente