c++ gcc undefined-behavior

c++ - ¿Por qué este bucle produce "advertencia: la iteración 3u invoca un comportamiento indefinido" y genera más de 4 líneas?



gcc undefined-behavior (5)

Compilando esto:

#include <iostream> int main() { for (int i = 0; i < 4; ++i) std::cout << i*1000000000 << std::endl; }

y gcc produce la siguiente advertencia:

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations] std::cout << i*1000000000 << std::endl; ^

Entiendo que hay un desbordamiento de entero con signo.

Lo que no puedo obtener es por qué el valor se rompe por esa operación de desbordamiento?

He leído las respuestas a ¿Por qué el desbordamiento de enteros en x86 con GCC causa un ciclo infinito? , pero todavía no tengo claro por qué sucede esto, entiendo que "indefinido" significa que "puede pasar cualquier cosa", pero ¿cuál es la causa subyacente de este comportamiento específico ?

En línea: http://ideone.com/dMrRKR

Compilador: gcc (4.8)


Lo que no puedo obtener es por qué el valor se rompe por esa operación de desbordamiento?

Parece que el desbordamiento de enteros ocurre en la 4ta iteración (para i = 3 ). signed desbordamiento de entero con signo invoca el comportamiento indefinido . En este caso, nada puede predecirse. ¡El ciclo puede iterar solo 4 veces o puede ir al infinito o a cualquier otra cosa!
El resultado puede variar de compilador a compilador o incluso para versiones diferentes del mismo compilador.

C11: 1.3.24 comportamiento indefinido:

comportamiento para el cual esta Norma Internacional no impone requisitos
[Nota: se puede esperar un comportamiento indefinido cuando esta Norma Internacional omite cualquier definición explícita de comportamiento o cuando un programa usa una construcción errónea o datos erróneos. El comportamiento indefinido permitido va desde ignorar la situación completamente con resultados impredecibles, hasta comportarse durante la traducción o la ejecución del programa de una manera documentada característica del entorno (con o sin la emisión de un mensaje de diagnóstico) hasta terminar una traducción o ejecución (con la emisión de un mensaje de diagnóstico) . Muchos constructos erróneos del programa no engendran un comportamiento indefinido; se requiere que sean diagnosticados. -finalizar nota]


El desbordamiento de enteros con signo (en sentido estricto, no existe el "desbordamiento de entero sin signo") significa un comportamiento indefinido . Y esto significa que cualquier cosa puede pasar, y discutir por qué sucede bajo las reglas de C ++ no tiene sentido.

Proyecto C ++ 11 N3337: §5.4: 1

Si durante la evaluación de una expresión, el resultado no está definido matemáticamente o no está en el rango de valores representables para su tipo, el comportamiento no está definido. [Nota: la mayoría de las implementaciones existentes de C ++ ignoran los sobre-flujos de enteros. El tratamiento de la división por cero, formando un resto utilizando un divisor cero, y todas las excepciones de punto flotante varían entre máquinas, y generalmente es ajustable por una función de biblioteca. -finalizar nota]

Su código compilado con g++ -O3 emite una advertencia (incluso sin -Wall )

a.cpp: In function ''int main()'': a.cpp:11:18: warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations] std::cout << i*1000000000 << std::endl; ^ a.cpp:9:2: note: containing loop for (int i = 0; i < 4; ++i) ^

La única forma en que podemos analizar lo que el programa está haciendo es leyendo el código ensamblador generado.

Aquí está la lista de montaje completo:

.file "a.cpp" .section .text$_ZNKSt5ctypeIcE8do_widenEc,"x" .linkonce discard .align 2 LCOLDB0: LHOTB0: .align 2 .p2align 4,,15 .globl __ZNKSt5ctypeIcE8do_widenEc .def __ZNKSt5ctypeIcE8do_widenEc; .scl 2; .type 32; .endef __ZNKSt5ctypeIcE8do_widenEc: LFB860: .cfi_startproc movzbl 4(%esp), %eax ret $4 .cfi_endproc LFE860: LCOLDE0: LHOTE0: .section .text.unlikely,"x" LCOLDB1: .text LHOTB1: .p2align 4,,15 .def ___tcf_0; .scl 3; .type 32; .endef ___tcf_0: LFB1091: .cfi_startproc movl $__ZStL8__ioinit, %ecx jmp __ZNSt8ios_base4InitD1Ev .cfi_endproc LFE1091: .section .text.unlikely,"x" LCOLDE1: .text LHOTE1: .def ___main; .scl 2; .type 32; .endef .section .text.unlikely,"x" LCOLDB2: .section .text.startup,"x" LHOTB2: .p2align 4,,15 .globl _main .def _main; .scl 2; .type 32; .endef _main: LFB1084: .cfi_startproc leal 4(%esp), %ecx .cfi_def_cfa 1, 0 andl $-16, %esp pushl -4(%ecx) pushl %ebp .cfi_escape 0x10,0x5,0x2,0x75,0 movl %esp, %ebp pushl %edi pushl %esi pushl %ebx pushl %ecx .cfi_escape 0xf,0x3,0x75,0x70,0x6 .cfi_escape 0x10,0x7,0x2,0x75,0x7c .cfi_escape 0x10,0x6,0x2,0x75,0x78 .cfi_escape 0x10,0x3,0x2,0x75,0x74 xorl %edi, %edi subl $24, %esp call ___main L4: movl %edi, (%esp) movl $__ZSt4cout, %ecx call __ZNSolsEi movl %eax, %esi movl (%eax), %eax subl $4, %esp movl -12(%eax), %eax movl 124(%esi,%eax), %ebx testl %ebx, %ebx je L15 cmpb $0, 28(%ebx) je L5 movsbl 39(%ebx), %eax L6: movl %esi, %ecx movl %eax, (%esp) addl $1000000000, %edi call __ZNSo3putEc subl $4, %esp movl %eax, %ecx call __ZNSo5flushEv jmp L4 .p2align 4,,10 L5: movl %ebx, %ecx call __ZNKSt5ctypeIcE13_M_widen_initEv movl (%ebx), %eax movl 24(%eax), %edx movl $10, %eax cmpl $__ZNKSt5ctypeIcE8do_widenEc, %edx je L6 movl $10, (%esp) movl %ebx, %ecx call *%edx movsbl %al, %eax pushl %edx jmp L6 L15: call __ZSt16__throw_bad_castv .cfi_endproc LFE1084: .section .text.unlikely,"x" LCOLDE2: .section .text.startup,"x" LHOTE2: .section .text.unlikely,"x" LCOLDB3: .section .text.startup,"x" LHOTB3: .p2align 4,,15 .def __GLOBAL__sub_I_main; .scl 3; .type 32; .endef __GLOBAL__sub_I_main: LFB1092: .cfi_startproc subl $28, %esp .cfi_def_cfa_offset 32 movl $__ZStL8__ioinit, %ecx call __ZNSt8ios_base4InitC1Ev movl $___tcf_0, (%esp) call _atexit addl $28, %esp .cfi_def_cfa_offset 4 ret .cfi_endproc LFE1092: .section .text.unlikely,"x" LCOLDE3: .section .text.startup,"x" LHOTE3: .section .ctors,"w" .align 4 .long __GLOBAL__sub_I_main .lcomm __ZStL8__ioinit,1,1 .ident "GCC: (i686-posix-dwarf-rev1, Built by MinGW-W64 project) 4.9.0" .def __ZNSt8ios_base4InitD1Ev; .scl 2; .type 32; .endef .def __ZNSolsEi; .scl 2; .type 32; .endef .def __ZNSo3putEc; .scl 2; .type 32; .endef .def __ZNSo5flushEv; .scl 2; .type 32; .endef .def __ZNKSt5ctypeIcE13_M_widen_initEv; .scl 2; .type 32; .endef .def __ZSt16__throw_bad_castv; .scl 2; .type 32; .endef .def __ZNSt8ios_base4InitC1Ev; .scl 2; .type 32; .endef .def _atexit; .scl 2; .type 32; .endef

Apenas puedo leer el ensamblaje, pero incluso puedo ver la línea addl $1000000000, %edi . El código resultante se parece más a

for(int i = 0; /* nothing, that is - infinite loop */; i += 1000000000) std::cout << i << std::endl;

Este comentario de @TC:

Sospecho que es algo así como: (1) porque cada iteración con i de cualquier valor mayor que 2 tiene un comportamiento indefinido -> (2) podemos suponer que i <= 2 para fines de optimización -> (3) la condición de ciclo es siempre true -> (4) se optimiza en un bucle infinito.

me dio la idea de comparar el código ensamblador del código OP con el código ensamblador del siguiente código, sin un comportamiento indefinido.

#include <iostream> int main() { // changed the termination condition for (int i = 0; i < 3; ++i) std::cout << i*1000000000 << std::endl; }

Y, de hecho, el código correcto tiene una condición de terminación.

; ...snip... L6: mov ecx, edi mov DWORD PTR [esp], eax add esi, 1000000000 call __ZNSo3putEc sub esp, 4 mov ecx, eax call __ZNSo5flushEv cmp esi, -1294967296 // here it is jne L7 lea esp, [ebp-16] xor eax, eax pop ecx ; ...snip...

Dios mío, eso no es completamente obvio. ¡No es justo! ¡Exijo juicio por fuego!

Ocúpate de eso, escribiste el código de buggy y deberías sentirte mal. Llevar las consecuencias.

... o, como alternativa, hacer un uso adecuado de mejores diagnósticos y mejores herramientas de depuración, para eso sirven:

  • habilitar todas las advertencias

    • -Wall es la opción gcc que habilita todas las advertencias útiles sin falsos positivos. Este es un mínimo que siempre debes usar.
    • gcc tiene muchas otras opciones de advertencia , sin embargo, no están habilitadas con -Wall ya que pueden advertir sobre falsos positivos
    • Desafortunadamente, Visual C ++ se está quedando atrás con la capacidad de dar advertencias útiles. Al menos el IDE habilita algunos de forma predeterminada.
  • usar indicadores de depuración para la depuración

    • para el desbordamiento de enteros -ftrapv atrapa el programa en desbordamiento,
    • El compilador de Clang es excelente para esto: -fcatch-undefined-behavior capta muchos casos de comportamiento indefinido (nota: "a lot of" != "all of them" )

¡Tengo un desastre de spaghetti de un programa que no he escrito y que debe enviarse mañana! AYUDA !!!!!! 111oneone

Use gcc''s -fwrapv

Esta opción indica al compilador que asuma que el desbordamiento aritmético firmado de suma, resta y multiplicación se completa con la representación de complemento a dos.

1 - esta regla no se aplica al "desbordamiento de enteros sin signo", ya que el §3.9.1.4 dice que

Los enteros sin signo, declarados sin signo, obedecerán las leyes del módulo aritmético 2 n donde n es el número de bits en la representación del valor de ese tamaño particular del número entero.

y, por ejemplo, el resultado de UINT_MAX + 1 está matemáticamente definido - por las reglas del módulo aritmético 2 n


Otro ejemplo de este error que se informa en gcc es cuando tiene un bucle que se ejecuta para un número constante de iteraciones, pero está usando la variable de contador como un índice en una matriz que tiene menos de esa cantidad de elementos, como:

int a[50], x; for( i=0; i < 1000; i++) x = a[i];

El compilador puede determinar que este bucle intentará acceder a la memoria fuera de la matriz ''a''. El compilador se queja de esto con este mensaje bastante críptico:

iteración xxu invoca comportamiento indefinido [-Werror = optimizaciones de bucle agresivo]


Respuesta corta, gcc específicamente ha documentado este problema, podemos verlo en las notas de la versión de gcc 4.8 que dice ( énfasis mío en el futuro ):

GCC ahora usa un análisis más agresivo para derivar un límite superior para el número de iteraciones de bucles utilizando restricciones impuestas por estándares de lenguaje . Esto puede provocar que los programas no conformes ya no funcionen como se esperaba, como SPEC CPU 2006 464.h264ref y 416.gamess. Se agregó una nueva opción, optimizaciones -fno-aggressive-loop-para inhabilitar este análisis agresivo. En algunos bucles que han conocido un número constante de iteraciones, pero se sabe que ocurre un comportamiento indefinido en el bucle antes de alcanzar o durante la última iteración, GCC advertirá sobre el comportamiento indefinido en el bucle en lugar de derivar el límite superior inferior del número de iteraciones para el bucle La advertencia se puede desactivar con optimizaciones de -Wno-aggressive-loop-optimization.

y, de hecho, si usamos -fno-aggressive-loop-optimizations el comportamiento del bucle infinito debería cesar y lo hace en todos los casos que he probado.

La respuesta larga comienza con saber que el desbordamiento de entero con signo es un comportamiento indefinido mirando el borrador de la sección estándar de C ++ 5 Expresiones párrafo 4 que dice:

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 . [Nota: la mayoría de las implementaciones existentes de C ++ ignoran los desbordamientos de enteros. El tratamiento de la división por cero, formando un resto usando un divisor cero, y todas las excepciones de punto flotante varían entre las máquinas, y generalmente es ajustable por una función de biblioteca. Nota final

Sabemos que el estándar dice que el comportamiento indefinido es impredecible a partir de la nota que viene con la definición que dice:

[Nota: se puede esperar un comportamiento indefinido cuando esta Norma Internacional omite cualquier definición explícita de comportamiento o cuando un programa usa una construcción errónea o datos erróneos. El comportamiento indefinido permitido va desde ignorar la situación completamente con resultados impredecibles , hasta comportarse durante la traducción o la ejecución del programa de una manera documentada característica del entorno (con o sin la emisión de un mensaje de diagnóstico) hasta terminar una traducción o ejecución (con la emisión de un mensaje de diagnóstico). Muchos constructos erróneos del programa no engendran un comportamiento indefinido; se requiere que sean diagnosticados. -finalizar nota]

Pero, ¿qué gcc puede hacer el optimizador de gcc para convertir esto en un bucle infinito? Suena completamente loco. Pero afortunadamente, gcc nos da una pista para descubrirlo en la advertencia:

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations] std::cout << i*1000000000 << std::endl; ^

La clave son las Waggressive-loop-optimizations , ¿qué significa eso? Afortunadamente para nosotros esta no es la primera vez que esta optimización ha roto el código de esta manera y tenemos suerte porque John Regehr ha documentado un caso en el artículo GCC pre-4.8. Breaks Broken SPEC 2006 Benchmarks que muestra el siguiente código:

int d[16]; int SATD (void) { int satd = 0, dd, k; for (dd=d[k=0]; k<16; dd=d[++k]) { satd += (dd < 0 ? -dd : dd); } return satd; }

el articulo dice:

El comportamiento indefinido es acceder a d [16] justo antes de salir del bucle. En C99 es legal crear un puntero a un elemento una posición más allá del final de la matriz, pero ese puntero no se debe desreferenciar.

y luego dice:

En detalle, esto es lo que está pasando. El compilador de CA, al ver d [++ k], puede suponer que el valor incrementado de k está dentro de los límites de la matriz, ya que de lo contrario se produce un comportamiento indefinido. Para el código aquí, GCC puede inferir que k está en el rango 0..15. Un poco más tarde, cuando GCC ve k <16, se dice a sí mismo: "Aha- esa expresión siempre es verdadera, así que tenemos un ciclo infinito." La situación aquí, donde el compilador usa la asunción de la definición para inferir un hecho útil del flujo de datos,

Entonces, lo que el compilador debe estar haciendo en algunos casos es asumir, dado que el desbordamiento de enteros con signo es un comportamiento indefinido, entonces siempre debo ser menor que 4 y, por lo tanto, tenemos un ciclo infinito.

Él explica que esto es muy similar a la infame eliminación del null puntero del kernel de Linux donde al ver este código:

struct foo *s = ...; int x = s->f; if (!s) return ERROR;

gcc infirió que desde s se hizo referencia en s->f; y dado que eliminar referencias a un puntero nulo es un comportamiento indefinido, entonces s no debe ser nulo y, por lo tanto, optimiza la verificación if (!s) en la siguiente línea.

La lección aquí es que los optimizadores modernos son muy agresivos a la hora de explotar comportamientos indefinidos y lo más probable es que se vuelvan más agresivos. Claramente, con solo algunos ejemplos podemos ver que el optimizador hace cosas que parecen completamente irracionales para un programador, pero en retrospectiva desde la perspectiva de los optimizadores tiene sentido.


tl; dr El código genera una prueba cuyo entero + entero positivo == entero negativo . Por lo general, el optimizador no optimiza esto, pero en el caso específico de std::endl se utilizará a continuación, el compilador optimiza esta prueba. Aún no he descubierto qué tiene de especial endl .

Desde el código de ensamblado en -O1 y niveles superiores, está claro que gcc refactoriza el ciclo para:

i = 0; do { cout << i << endl; i += NUMBER; } while (i != NUMBER * 4)

El valor más grande que funciona correctamente es 715827882 , es decir, piso ( INT_MAX/3 ). El fragmento de ensamblaje en -O1 es:

L4: movsbl %al, %eax movl %eax, 4(%esp) movl $__ZSt4cout, (%esp) call __ZNSo3putEc movl %eax, (%esp) call __ZNSo5flushEv addl $715827882, %esi cmpl $-1431655768, %esi jne L6 // fallthrough to "return" code

Tenga en cuenta que el -1431655768 es 4 * 715827882 en complemento a 2.

Al -O2 optimiza a lo siguiente:

L4: movsbl %al, %eax addl $715827882, %esi movl %eax, 4(%esp) movl $__ZSt4cout, (%esp) call __ZNSo3putEc movl %eax, (%esp) call __ZNSo5flushEv cmpl $-1431655768, %esi jne L6 leal -8(%ebp), %esp jne L6 // fallthrough to "return" code

Entonces la optimización que se ha hecho es simplemente que el addl se movió más arriba.

Si volvemos a compilar con 715827883 entonces la versión -O1 es idéntica aparte del número cambiado y el valor de prueba. Sin embargo, -O2 luego realiza un cambio:

L4: movsbl %al, %eax addl $715827883, %esi movl %eax, 4(%esp) movl $__ZSt4cout, (%esp) call __ZNSo3putEc movl %eax, (%esp) call __ZNSo5flushEv jmp L2

Donde había cmpl $-1431655764, %esi en -O1 , esa línea se ha eliminado para -O2 . El optimizador debe haber decidido que agregar 715827883 a %esi nunca puede ser igual a -1431655764 .

Esto es bastante desconcertante. Agregar eso a INT_MIN+1 genera el resultado esperado, por lo que el optimizador debe haber decidido que %esi nunca puede ser INT_MIN+1 y no estoy seguro de por qué lo decidirá.

En el ejemplo de trabajo, parece que sería igualmente válido concluir que agregar 715827882 a un número no puede ser igual a INT_MIN + 715827882 - 2 ! (Esto solo es posible si se produce el envolvente), pero no optimiza la línea en ese ejemplo.

El código que estaba usando es:

#include <iostream> #include <cstdio> int main() { for (int i = 0; i < 4; ++i) { //volatile int j = i*715827883; volatile int j = i*715827882; printf("%d/n", j); std::endl(std::cout); } }

Si se std::endl(std::cout) , la optimización ya no se produce. De hecho, lo reemplazó con std::cout.put(''/n''); std::flush(std::cout); std::cout.put(''/n''); std::flush(std::cout); también hace que la optimización no suceda, aunque std::endl está en línea.

La incorporación de std::endl parece afectar la parte anterior de la estructura de bucle (que no entiendo muy bien qué es lo que está haciendo, pero la publicaré aquí en caso de que alguien más lo haga):

Con código original y -O2 :

L2: movl %esi, 28(%esp) movl 28(%esp), %eax movl $LC0, (%esp) movl %eax, 4(%esp) call _printf movl __ZSt4cout, %eax movl -12(%eax), %eax movl __ZSt4cout+124(%eax), %ebx testl %ebx, %ebx je L10 cmpb $0, 28(%ebx) je L3 movzbl 39(%ebx), %eax L4: movsbl %al, %eax addl $715827883, %esi movl %eax, 4(%esp) movl $__ZSt4cout, (%esp) call __ZNSo3putEc movl %eax, (%esp) call __ZNSo5flushEv jmp L2 // no test

Con mymanual enlining de std::endl , -O2 :

L3: movl %ebx, 28(%esp) movl 28(%esp), %eax addl $715827883, %ebx movl $LC0, (%esp) movl %eax, 4(%esp) call _printf movl $10, 4(%esp) movl $__ZSt4cout, (%esp) call __ZNSo3putEc movl $__ZSt4cout, (%esp) call __ZNSo5flushEv cmpl $-1431655764, %ebx jne L3 xorl %eax, %eax

Una diferencia entre estos dos es que %esi se usa en el original y %ebx en la segunda versión; ¿Hay alguna diferencia en la semántica definida entre %esi y %ebx en general? (No sé mucho sobre el ensamblaje x86).