Comportamiento no especificado, indefinido y definido para implementación WIKI para C

Aunque hay abundantes enlaces sobre este tema en SO, creo que falta algo: una explicación clara en lenguaje sencillo de cuáles son las diferencias entre el comportamiento no especificado (UsB), el comportamiento indefinido (UB) y el comportamiento definido en la implementación (BID ) con una explicación detallada pero fácil de cualquier caso de uso y ejemplo.

Nota: Hice el acrónimo de UsB en aras de la compacidad en este WIKI, pero no espero verlo en otro lugar.

Sé que esto puede parecer un duplicado de otras publicaciones (la que más se acerca es esta ), pero antes de que alguien marque esto como un duplicado , por favor considere cuáles son los problemas con todo el material que ya encontré (y voy a hacer una comunidad WIKI fuera de este post):

  • Demasiados ejemplos dispersos. Los ejemplos no son malos, por supuesto, pero a veces uno no puede encontrar un ejemplo que se adapte bien a su problema, por lo que puede ser confuso (especialmente para los novatos).

  • Los ejemplos suelen ser solo códigos con pocas explicaciones. En asuntos tan delicados, especialmente para los novatos (relativos), un enfoque más vertical podría ser mejor: primero una explicación clara y simple con una descripción abstracta (pero no legalista), luego algunos ejemplos simples con explicaciones sobre por qué desencadenan algún comportamiento .

  • Algunas publicaciones suelen tener una combinación de ejemplos de C y C ++. C y C ++ a veces no están de acuerdo con lo que consideran UsB, UB y BID, por lo que un ejemplo puede ser engañoso para alguien que no domine ambos idiomas.

  • Cuando se da una definición de UsB, UB e IDB, generalmente es una cita simple de los estándares, que a veces puede ser confusa o demasiado difícil de digerir para los novatos.

  • A veces, la cita de los estándares es parcial. Muchas publicaciones citan el estándar solo para las partes que son útiles para el problema en cuestión, lo cual es bueno, pero carece de generalidad. Además, la cita de los estándares a menudo no va acompañada de ninguna explicación (mala para los principiantes).

Como yo no soy un súper experto en este tema, haré un WIKI comunitario para que cualquier persona interesada pueda contribuir y mejorar la respuesta.

Para no estropear mi propósito de crear un WIKI amigable para principiantes estructurado , me gustaría que los carteles sigan un par de pautas simples al editar el WIKI:

  • Categoriza tu caso de uso. Intente colocar su ejemplo / código en una categoría ya existente, si corresponde, de lo contrario, cree una nueva.

  • Primero la descripción de palabras simples. Primero describa con palabras simples (sin simplificar en exceso, por supuesto, ¡calidad primero!) El ejemplo o el punto que intenta hacer. Luego ponga muestras de código o citas.

  • Cite los estándares por referencia. No publique fragmentos de varios estándares, pero proporcione referencias claras (por ejemplo, C99 WG14 / N … sección 1.4.7, párrafo …) y publique un enlace al recurso relevante, si es posible.

  • Prefiere recursos gratuitos en línea. Si desea citar libros o recursos no disponibles que están bien (y puede mejorar la calidad del WIKI), pero intente agregar también algunos enlaces a recursos gratuitos. Esto es realmente importante especialmente para los estándares ISO. Le invitamos a agregar enlaces a los estándares oficiales, pero intente agregar un enlace equivalente a los borradores disponibles de manera gratuita también. Y no reemplace los enlaces a borradores con referencias a estándares oficiales, añádalos . Incluso algunos departamentos de informática de algunas universidades no tienen una copia de los estándares ISO, ¡y mucho menos de la mayoría de los progtwigdores en general!

  • No publiques código a menos que sea realmente necesario. Código postal solo si una explicación que usa solo inglés simple sería incómoda o poco clara. Intenta limitar las muestras de código a líneas únicas. Publique enlaces a otras preguntas y respuestas de SO en su lugar.

  • No publiques ejemplos de C ++. Me gustaría que esto se convierta en un tipo de preguntas frecuentes para C (si alguien quiere comenzar un doble hilo para C ++ que sería genial, sin embargo). Las diferencias relevantes con C ++ son bienvenidas, pero solo como notas secundarias. Es decir, después de explicar el caso C a fondo, puede agregar un par de afirmaciones sobre C ++ si esto ayudaría a un progtwigdor C al cambiar a C ++, pero no quisiera ver ejemplos con algo más que, por ejemplo, 20% C ++. Por lo general, una nota simple como “(C ++ se comporta de manera diferente en este caso)” más un enlace relevante debería ser suficiente.

Como soy bastante nuevo en SO, espero no romper ninguna regla comenzando un Q & A de esta manera. Lo siento si este es el caso. Los mods son bienvenidos para hacerme saber al respecto.

Los estándares C definen UsB, UB e IDB de una manera que se puede resumir de la siguiente manera:

Comportamiento no especificado ( UsB )

Este es un comportamiento para el cual el estándar brinda algunas alternativas entre las cuales la implementación debe elegir , pero no establece cómo y cuándo se tomará la decisión. En otras palabras, la implementación debe aceptar un código de usuario que active ese comportamiento sin errores y debe cumplir con una de las alternativas dadas por el estándar.

Tenga en cuenta que la implementación no es necesaria para documentar nada sobre las elecciones realizadas. Estas opciones también pueden ser no deterministas o dependientes (de forma no documentada) en las opciones del comstackdor.

Para resumir: la norma brinda algunas posibilidades para elegir, la implementación elige cuándo y cómo se selecciona y aplica la alternativa específica.

Tenga en cuenta que el estándar puede proporcionar una gran cantidad de alternativas. El ejemplo típico es el valor inicial de las variables locales que no se inicializan explícitamente. El estándar dice que este valor no se especifica siempre que sea un valor válido para el tipo de datos de la variable.

Para ser más específicos, considere una variable int : una implementación es libre de elegir cualquier valor int , y esta elección puede ser completamente aleatoria, no determinista o estar a merced de los caprichos de la implementación, que no se requiere para documentar nada sobre eso Siempre que la implementación se mantenga dentro de los límites establecidos por la norma, esto está bien y el usuario no puede presentar una queja.

Comportamiento indefinido ( UB )

Como el nombre indica, esta es una situación en la que el estándar C no impone ni garantiza lo que el progtwig debería o debería hacer. Todas las apuestas están cerradas. Tal situacion:

  • hace que un progtwig sea erróneo o no sea portable

  • no requiere absolutamente nada de la implementación

Esta es una situación realmente desagradable: siempre y cuando haya una porción de código que tenga un comportamiento indefinido, todo el progtwig se considera erróneo y el estándar permite que la implementación haga todo .

En otras palabras, la presencia de una causa de UB permite que la implementación ignore por completo el estándar, siempre que se trate del progtwig que desencadena el UB.

Tenga en cuenta que el comportamiento real en este caso puede abarcar un rango ilimitado de posibilidades, el siguiente no es de ninguna manera una lista exhaustiva:

  • Se puede emitir un error en tiempo de comstackción.
  • Se puede emitir un error en tiempo de ejecución.
  • El problema se ignora por completo (y esto puede dar lugar a errores del progtwig).
  • El comstackdor arroja silenciosamente el código UB como una optimización.
  • Su disco duro puede estar formateado.
  • Su computadora puede borrar su cuenta bancaria y pedirle una cita a su novia.

Espero que los últimos dos artículos ( semi- serios) puedan darte la sensación visceral correcta sobre la maldad de UB. Y a pesar de que la mayoría de las implementaciones no insertarán el código necesario para formatear su disco duro, ¡los comstackdores reales sí optimizan!

Nota de terminología: A veces las personas argumentan que algún fragmento de código que el estándar considere una fuente de UB en su implementación / sistema / entorno funciona de manera documentada, por lo tanto , no puede ser realmente UB. Este razonamiento es incorrecto , pero es un malentendido común (y algo comprensible): cuando el término UB (y también UsB e IDB) se utiliza en un contexto C se entiende como un término técnico cuyo significado preciso está definido por el estándar ( s). En particular, la palabra “indefinido” pierde su significado cotidiano. Por lo tanto, no tiene sentido mostrar ejemplos donde los progtwigs erróneos o no portadores producen un comportamiento “bien definido” como contraejemplos. Si lo intentas, realmente te pierdes el punto. UB significa que pierde todas las garantías del estándar. Si su implementación proporciona una extensión, sus garantías son solo las de su implementación. Si usa esa extensión, su progtwig ya no es un progtwig C conforme (en cierto sentido, ya no es un progtwig C, ya que ya no sigue el estándar).

Utilidad del comportamiento indefinido

Una pregunta común sobre UB es algo sobre estas líneas: “Si UB es tan desagradable, ¿por qué el estándar no exige que una implementación emita un error cuando se enfrenta con UB?”

Primero, optimizaciones. Permitir que las implementaciones no comprueben las posibles causas de UB permite muchas optimizaciones que hacen que un progtwig de C sea extremadamente eficiente. Esta es una de las características de C, aunque hace de C una fuente de muchos escollos para principiantes.

En segundo lugar, la existencia de UB en los estándares permite que una implementación conforme brinde extensiones a C sin que se lo considere no conforme como un todo.

Siempre que una implementación se comporte como se exige para un progtwig conforme, ella misma se está conformando, aunque puede proporcionar instalaciones no estándar que pueden ser útiles en plataformas específicas. Por supuesto, los progtwigs que utilizan esas instalaciones no serán portables y se basarán en UB documentados , es decir, comportamiento que sea UB según el estándar, pero que una implementación documente como una extensión.

Comportamiento definido por la implementación ( BID )

Este es un comportamiento que puede describirse de forma similar a UsB: el estándar proporciona algunas alternativas y la implementación elije una, pero se requiere la implementación para documentar exactamente cómo se realiza la elección .

Esto significa que un usuario que lea la documentación de su comstackdor debe recibir suficiente información para predecir exactamente qué sucederá en el caso específico.

Tenga en cuenta que una implementación que no documenta completamente un BID no se puede considerar conforme. Una implementación conforme debe documentar exactamente lo que sucede en cualquier caso que la norma declare BID.

Ejemplos de comportamiento no especificado

Orden de evaluación

Argumentos de función

El orden de evaluación para los argumentos de la función no se especifica EXP30-C .

Por ejemplo, en c(a(), b()); no se especifica si la función a se llama antes o después de b . La única garantía es que ambos se llaman antes de la función c .

Ejemplos de comportamiento indefinido

Punteros

Desreferencia de puntero nulo

Los punteros nulos se utilizan para indicar que un puntero no apunta a la memoria válida. Como tal, no tiene mucho sentido tratar de leer o escribir en la memoria a través de un puntero nulo.

Técnicamente, este es un comportamiento indefinido. Sin embargo, dado que esta es una fuente muy común de errores, la mayoría de los entornos C aseguran que la mayoría de los bashs de desreferenciar un puntero nulo colgarán inmediatamente el progtwig (usualmente matándolo con un error de segmentación). Esta protección no es perfecta debido a la aritmética del puntero involucrada en las referencias a arreglos y / o estructuras, por lo que incluso con herramientas modernas, la eliminación de referencias a un puntero nulo puede formatear su disco duro.

Desreferencia de puntero no inicializado

Al igual que los punteros nulos, desreferenciar un puntero antes de establecer explícitamente su valor es UB. A diferencia de los punteros nulos, la mayoría de los entornos no ofrecen ninguna red de seguridad contra este tipo de error, excepto que el comstackdor puede advertir al respecto. Si comstack su código de todos modos, es probable que experimente toda la maldad de UB.

Desreferencia de punteros no válidos

Un puntero no válido es un puntero que contiene una dirección que no está dentro de ningún área de memoria asignada. Formas comunes de crear punteros no válidos es llamar a free() (después de la llamada, el puntero no será válido, que es más o menos el punto de llamar a free() ), o usar la aritmética del puntero para obtener una dirección que está más allá de los límites de un bloque de memoria asignado.

Esta es la variante más diabólica de la desreferenciación de punteros UB: no hay red de seguridad, no hay advertencia de comstackción, simplemente existe el hecho de que el código puede hacer cualquier cosa. Y comúnmente, lo hace: la mayoría de los ataques de malware utilizan este tipo de comportamiento UB en los progtwigs para que los progtwigs se comporten como quieren que se comporten (como instalar un troyano, registrador de teclas, encriptar su disco duro, etc.). ¡La posibilidad de un disco duro formateado se vuelve muy real con este tipo de UB!

Deshacerse de la constness

Si declaramos un objeto como const , le damos una promesa al comstackdor de que nunca cambiaremos el valor de ese objeto. En muchos contextos, los comstackdores detectarán una modificación no válida y nos gritarán. Pero si eliminamos la constness como en este fragmento:

 int const a = 42; ... int* ap0 = &a; //< error, compiler will tell us int* ap1 = (int*)a; //< silences the compiler ... *ap1 = 43; //< UB ==> program crash? 

Es posible que el comstackdor no pueda rastrear este acceso no válido, compile el código en un archivo ejecutable y solo en el momento de la ejecución se detectará el acceso no válido y se generará un locking del progtwig.

categoría 2

poner un título aquí!

pon tu explicación aquí!

Ejemplos de comportamiento definido por la implementación

Categoría 1

poner un título aquí!

pon tu explicación aquí!

N1570 es un borrador del estándar ISO C, muy parecido al documento ISO oficial.

N1256 es un borrador anterior, que incorpora el estándar C99 más los cambios de los tres Corrientes Técnicas.

El Anexo J tiene 5 secciones, cada una de las cuales reúne información dispersa por el rest del estándar:

  • J.1 Comportamiento no especificado
  • J.2 Comportamiento indefinido
  • J.3 Comportamiento definido por la implementación
  • J.4 Comportamiento específico de la configuración regional
  • J.5 Extensiones comunes