¿Es legal que el código fuente que contiene un comportamiento indefinido bloquee el compilador?

85

Digamos que voy a compilar un código fuente de C ++ mal escrito que invoca un comportamiento indefinido, y por lo tanto (como dicen) "cualquier cosa puede pasar".

Desde la perspectiva de lo que la especificación del lenguaje C ++ considera aceptable en un compilador "conforme", "cualquier cosa" en este escenario incluye que el compilador falle (o robe mis contraseñas, o se comporte mal o cometa errores en tiempo de compilación), o es alcance del comportamiento indefinido limitado específicamente a lo que puede suceder cuando se ejecuta el ejecutable resultante?

Jeremy Friesner
fuente
22
"UB es UB. Vive con eso" ... No, espera. "Publique un MCVE". ... No, espera. Me encanta la pregunta por todos los reflejos que desencadena de manera inapropiada. :-)
Yunnosch
14
Realmente no hay limitación, por lo que se dice que UB puede convocar demonios nasales .
Un tipo programador
15
UB puede hacer que el autor publique una pregunta en SO. : P
Tanveer Badar
45
Independientemente de lo que diga el estándar C ++, si yo fuera un escritor de compiladores, ciertamente lo consideraría un error en mi compilador. Entonces, si está viendo esto, presente un informe de defectos.
juan
9
@LeifWillerts Esto fue en los 80. No recuerdo la construcción exacta, pero creo que dependía del uso de un tipo de variable intrincado. Después de colocar un reemplazo tuve un momento de "¿qué estaba pensando? Las cosas no funcionan de esa manera". No culpé al compilador por rechazar la construcción, solo por reiniciar la máquina. Dudo que alguien se encuentre hoy con ese compilador. Fue el compilador cruzado HP C para el HP 64000 dirigido al microprocesador 68000.
Avi Berger

Respuestas:

71

La definición normativa de comportamiento indefinido es la siguiente:

[defns.undefined]

comportamiento para el que 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 utiliza una construcción o datos erróneos. El comportamiento indefinido admisible va desde ignorar la situación por completo con resultados impredecibles, a comportarse durante la traducción o ejecución del programa de 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). Muchas construcciones de programas erróneas no generan un comportamiento indefinido; deben ser diagnosticados. La evaluación de una expresión constante nunca muestra un comportamiento explícitamente especificado como indefinido. - nota final]

Si bien la nota en sí no es normativa, describe una variedad de comportamientos que se sabe que exhiben las implementaciones. Por lo tanto, bloquear el compilador (que está terminando la traducción abruptamente) es legítimo según esa nota. Pero realmente, como dice el texto normativo, el estándar no establece límites ni para la ejecución ni para la traducción. Si una implementación roba sus contraseñas, no es una violación de ningún contrato establecido en el estándar.

StoryTeller - Unslander Monica
fuente
43
Dicho esto, si realmente puede hacer que un compilador ejecute código arbitrario en tiempo de compilación, sin ningún espacio aislado, entonces varias personas de seguridad estarían muy interesadas en saberlo. Lo mismo ocurre con la segmentación del compilador.
Kevin
67
Lo mismo ocurre con lo que dijo Kevin. Como ingeniero de compiladores de C / C ++ / etc en una carrera anterior, nuestra posición era que un comportamiento indefinido podría bloquear su programa , arruinar sus datos de salida, incendiar su casa, lo que sea. Pero el compilador nunca debería fallar sin importar cuál sea la entrada. (Puede que no brinde mensajes de error útiles, pero debería producir algún tipo de diagnóstico y salida en lugar de simplemente gritar CTHULHU TAKE THE WHEEL y segfaulting.)
Ti Strga
8
@TiStrga Apuesto a que Cthulhu sería un piloto de F1 increíble.
zeta-band
35
"Si una implementación roba sus contraseñas, no es una violación de ningún contrato establecido en el estándar". Eso es cierto independientemente de si el código tiene UB, ¿no es así? El estándar solo dicta lo que debe hacer el programa compilado: un compilador que compile correctamente el código pero robe sus contraseñas en el proceso no estaría desobedeciendo el estándar.
Carmeister
8
@Carmeister, oooh, ese es un buen punto, me aseguraré de recordarle a la gente eso cada vez que aparezcan esos argumentos de "UB da permiso al compilador para comenzar una guerra nuclear". De nuevo.
ilkkachu
8

La mayoría de los tipos de UB de los que normalmente nos preocupamos, como NULL-deref o dividir por cero, son UB en tiempo de ejecución . Compilar una función que causaría UB en tiempo de ejecución si se ejecuta no debe causar que el compilador se bloquee. A menos que tal vez pueda probar que la función (y esa ruta a través de la función) definitivamente será ejecutada por el programa.

(Segundo pensamiento: tal vez no he considerado la evaluación requerida de template / constexpr en el momento de la compilación. Posiblemente UB durante eso puede causar rarezas arbitrarias durante la traducción, incluso si nunca se llama a la función resultante).

La parte de comportamiento durante la traducción de la cita ISO C ++ en la respuesta de @ StoryTeller es similar al lenguaje utilizado en el estándar ISO C. C no incluye plantillas ni constexprevaluaciones obligatorias en tiempo de compilación.

Pero dato curioso: ISO C dice en una nota que si se termina la traducción, debe ser con un mensaje de diagnóstico. O "comportarse durante la traducción ... de manera documentada". No creo que "ignorar la situación por completo" pueda interpretarse como una interrupción de la traducción.


Respuesta anterior, escrita antes de que aprendiera sobre la UB en tiempo de traducción. Sin embargo, es cierto para runtime-UB y, por lo tanto, todavía es potencialmente útil.


No existe tal cosa como UB que suceda en tiempo de compilación. Puede ser visible para el compilador a lo largo de una determinada ruta de ejecución, pero en términos de C ++ no ha sucedido hasta que la ejecución alcanza esa ruta de ejecución a través de una función.

Los defectos en un programa que hacen imposible la compilación no son UB, son errores de sintaxis. Dicho programa "no está bien formado" en terminología C ++ (si tengo mi standardese correcto). Un programa puede estar bien formado pero contener UB. Diferencia entre comportamiento indefinido y mal formado, no se requiere mensaje de diagnóstico

A menos que no entienda algo, ISO C ++ requiere que este programa se compile y se ejecute correctamente, porque la ejecución nunca llega a la división por cero. (En la práctica ( Godbolt ), los buenos compiladores solo crean ejecutables que funcionan. Gcc / clang advierte x / 0pero no esto, incluso cuando se optimiza. Pero de todos modos, estamos tratando de decir qué tan bajo ISO C ++ permite que sea la calidad de implementación. Así que verificando gcc / clang no es una prueba útil más que para confirmar que escribí el programa correctamente).

int cause_UB() {
    int x=0;
    return 1 / x;      // UB if ever reached.
 // Note I'm avoiding  x/0  in case that counts as translation time UB.
 // UB still obvious when optimizing across statements, though.
}

int main(){
    if (0)
        cause_UB();
}

Un caso de uso para esto podría involucrar el preprocesador de C, o las constexprvariables y la ramificación en esas variables, lo que conduce a una tontería en algunas rutas que nunca se alcanzan para esas elecciones de constantes.

Se puede suponer que las rutas de ejecución que causan UB visibles en tiempo de compilación nunca se toman, por ejemplo, un compilador para x86 podría emitir una ud2(causar una excepción de instrucción ilegal) como la definición de cause_UB(). O dentro de una función, si un lado de un if()conduce a un UB demostrable , la rama se puede quitar.

Pero el compilador todavía tiene que compilar todo lo demás de una manera sana y correcta. Todas las rutas que no encuentran (o no se puede probar que encuentren) UB aún deben compilarse en un asm que se ejecuta como si la máquina abstracta de C ++ lo estuviera ejecutando.


Se podría argumentar que la UB incondicional visible en tiempo de compilación en maines una excepción a esta regla. O de lo contrario, comprobable en tiempo de compilación que la ejecución que comienza en main, de hecho, alcanza UB garantizado.

Todavía diría que los comportamientos legales del compilador incluyen producir una granada que explota si se ejecuta. O más plausiblemente, una definición de maineso consiste en una sola instrucción ilegal. Yo diría que si nunca ejecuta el programa, todavía no ha habido ningún UB. El compilador en sí no puede explotar, en mi opinión.


Funciones que contienen ramas internas de UB posibles o demostrables

UB a lo largo de cualquier ruta de ejecución determinada se remonta en el tiempo para "contaminar" todo el código anterior. Pero en la práctica, los compiladores solo pueden aprovechar esa regla cuando realmente pueden demostrar que las rutas de ejecución conducen a UB visibles en tiempo de compilación. p.ej

int minefield(int x) {
    if (x == 3) {
        *(char*)nullptr = x/0;
    }

    return x * 5;
}

El compilador tiene que hacer un asm que funcione para todos los xdemás que no sean 3, hasta los puntos donde x * 5causa UB de desbordamiento firmado en INT_MIN e INT_MAX. Si esta función nunca se llama con x==3, el programa, por supuesto, no contiene UB y debe funcionar como está escrito.

Bien podríamos haber escrito if(x == 3) __builtin_unreachable();en GNU C para decirle al compilador que xdefinitivamente no es 3.

En la práctica, hay un código de "campo minado" por todas partes en los programas normales. por ejemplo, cualquier división por un número entero promete al compilador que no es cero. Cualquier puntero deref promete al compilador que no es NULL.

Peter Cordes
fuente
3

¿Qué significa "legal" aquí? Todo lo que no contradiga el estándar C o el estándar C ++ es legal, de acuerdo con estos estándares. Si ejecuta una declaración i = i++;y, como resultado, los dinosaurios se apoderan del mundo, eso no contradice los estándares. Sin embargo, contradice las leyes de la física, por lo que no va a suceder :-)

Si un comportamiento indefinido bloquea su compilador, eso no viola el estándar C o C ++. Sin embargo, significa que la calidad del compilador podría (y probablemente debería) mejorarse.

En versiones anteriores del estándar C, había declaraciones que eran errores o no dependían de un comportamiento indefinido:

char* p = 1 / 0;

Se permite la asignación de una constante 0 a un carácter *. Permitir una constante distinta de cero no lo es. Dado que el valor de 1/0 es un comportamiento indefinido, es un comportamiento indefinido si el compilador debe o no aceptar esta declaración. (Hoy en día, 1/0 ya no cumple con la definición de "expresión constante entera").

gnasher729
fuente
3
Para ser precisos: los dinosaurios que se apoderan del mundo no contradice ninguna ley de la física (por ejemplo, la variación de Jurassic Park). Es muy poco probable. :)
monstruoso