c undefined-behavior c11

¿Qué significan las diferentes clasificaciones de comportamiento indefinido?



undefined-behavior c11 (5)

" Estaba leyendo el estándar C11. Según el estándar C11, el comportamiento indefinido se clasifica en cuatro tipos diferentes " .

Me pregunto qué estás leyendo en realidad. El estándar ISO C 2011 no menciona estas cuatro clasificaciones diferentes de comportamiento indefinido. De hecho, es bastante explícito al no hacer distinciones entre diferentes tipos de comportamiento indefinido.

Aquí está ISO C11 sección 4 párrafo 2:

Si se viola un requisito "debe" o "no debe" que aparece fuera de una restricción o se restringe la restricción de tiempo de ejecución, el comportamiento no está definido. El comportamiento indefinido se indica de otro modo en esta Norma Internacional por las palabras "comportamiento indefinido" o por la omisión de cualquier definición explícita de comportamiento. No hay diferencia en el énfasis entre estos tres; todos ellos describen "comportamiento que es indefinido".

Todos los ejemplos que cita son comportamientos indefinidos , que, en lo que respecta a la Norma, no significan nada más ni menos que:

comportamiento, en el uso de una construcción de programa no portátil o errónea o de datos erróneos, para los cuales esta Norma Internacional no impone requisitos

Si tiene alguna otra referencia, que discuta diferentes tipos de comportamiento indefinido, actualice su pregunta para citarla. Su pregunta sería entonces sobre qué significa ese documento por su sistema de clasificación, no (solo) sobre el estándar ISO C.

Parte de la redacción de su pregunta es similar a la información en el Anexo L de C11, "Análisis" (que es opcional para las implementaciones de C11 conformes), pero su primer ejemplo se refiere a "Comportamiento indefinido (se necesita información / confirmación)", y La palabra "confirmación" no aparece en ninguna parte en el estándar ISO C.

Estaba leyendo el estándar C11. Según el estándar C11, el comportamiento indefinido se clasifica en cuatro tipos diferentes. Los números entre paréntesis se refieren a la subcláusula del Estándar C (C11) que identifica el comportamiento indefinido.

Ejemplo 1: el programa intenta modificar una cadena literal (6.4.5). Este comportamiento indefinido se clasifica como: Comportamiento indefinido (se necesita información / confirmación)

Ejemplo 2: un valor de l no designa un objeto cuando se evalúa (6.3.2.1). Este comportamiento indefinido se clasifica como: Comportamiento crítico indefinido

Ejemplo 3: un objeto tiene su valor almacenado al que se accede por un valor de l de un tipo permitido (6.5). Este comportamiento indefinido se clasifica como: Comportamiento indefinido acotado

Ejemplo 4: la cadena señalada por el argumento de mode en una llamada a la función fopen no coincide exactamente con una de las secuencias de caracteres especificadas (7.21.5.3). Este comportamiento indefinido se clasifica como: Posible extensión del lenguaje conforme

¿Cuál es el significado de las clasificaciones? ¿Qué le transmiten estas clasificaciones al programador?


Algunos programas están destinados únicamente para su uso con entradas que se sabe que son válidas, o al menos provienen de fuentes confiables. Otros no lo son. Ciertos tipos de optimizaciones que pueden ser útiles cuando se procesan solo datos confiables son estúpidos y peligrosos cuando se usan con datos no confiables. Desafortunadamente, los autores del Anexo L lo escribieron de manera excesivamente vaga, pero la clara intención es permitir que los compiladores no hagan ciertas "optimizaciones" que son estúpidas y peligrosas cuando usan datos de fuentes no confiables.

Considere la función (suponga que "int" es de 32 bits):

int32_t triplet_may_be_interesting(int32_t a, int32_t b, int32_t c) { return a*b > c; }

invocado desde el contexto:

#define SCALE_FACTOR 123456 int my_array[20000]; int32_t foo(uint16_t x, uint16_t y) { if (x < 20000) my_array[x]++; if (triplet_may_be_interesting(x, SCALE_FACTOR, y)) return examine_triplet(x, SCALE_FACTOR, y); else return 0; }

Cuando se escribió C89, la forma más común en que un compilador de 32 bits procesaría ese código habría sido realizar una multiplicación de 32 bits y luego hacer una comparación firmada con y. Sin embargo, son posibles algunas optimizaciones, especialmente si un compilador alinea la invocación de la función:

  1. En las plataformas donde las comparaciones sin signo son más rápidas que las comparadas con signo, un compilador podría inferir que, dado que ninguno de a , b o c puede ser negativo, el valor aritmético de a*b no es negativo, y por lo tanto puede usar un comparador sin signo. de una comparación firmada. Esta optimización sería permisible incluso si __STDC_ANALYZABLE__ no es cero.

  2. Un compilador también podría inferir que si x no es cero, el valor aritmético de x*123456 será mayor que cualquier valor posible de y , y si x es cero, entonces x*123456 no será mayor que cualquier otro. Por lo tanto, podría reemplazar la segunda condición if (x) simplemente con if (x) . Esta optimización también es permisible incluso si __STDC_ANALYzABLE__ no es cero.

  3. Un compilador cuyos autores pretenden usarlo solo con datos confiables, o bien cree erróneamente que la astucia y la estupidez son antónimos, podría inferir que dado que cualquier valor de x mayor que 17395 resultará en un desbordamiento de un entero, se puede suponer que x es seguro 17395 o menos. Por lo tanto, podría realizar my_array[x]++; incondicionalmente Es posible que un compilador no defina __STDC_ANALYZABLE__ con un valor distinto de cero si realiza esta optimización. Es este último tipo de optimización que el Anexo L está diseñado para abordar. Si una implementación puede garantizar que el efecto del desbordamiento se limitará a generar un valor posiblemente sin sentido, puede ser más barato y más fácil para el código tratar con la posibilidad de que el valor carezca de significado que evitar el desbordamiento. Sin embargo, si el desbordamiento pudiera hacer que los objetos se comportaran como si sus valores estuvieran dañados por futuros cómputos, no habría forma de que un programa pudiera lidiar con cosas como desbordamiento después del hecho, incluso en los casos en que el resultado del cómputo terminaría irrelevante.

En este ejemplo, si el efecto del desbordamiento de enteros se limitaría a producir un valor posiblemente sin sentido, y si llamar a examine_triplet() innecesariamente perdería tiempo pero sería inofensivo, un compilador podría optimizar de forma útil triplet_may_be_interesting no sería posible si se escribiera para evitar el desbordamiento de enteros a toda costa. La "optimización" agresiva resultará en un código menos eficiente de lo que sería posible con un compilador que, en cambio, utilizó su libertad para ofrecer algunas garantías de comportamiento flexibles.

El Anexo L sería mucho más útil si permitiera que las implementaciones ofrezcan garantías de comportamiento específicas (por ejemplo, el desbordamiento producirá un resultado posiblemente sin sentido, pero no tendrá otros efectos secundarios). Ningún conjunto de garantías único sería óptimo para todos los programas, pero la cantidad de texto del Anexo L gastado en su mecanismo de trampeo propuesto poco práctico podría haberse gastado en la especificación de macros para indicar qué garantías podrían ofrecer varias implementaciones.


Según cppreference :

Comportamiento crítico indefinido

La UB crítica es un comportamiento indefinido que puede realizar una escritura de memoria o una lectura de memoria volátil fuera de los límites de cualquier objeto. Un programa que tiene un comportamiento crítico indefinido puede ser susceptible a ataques de seguridad.

Solo los siguientes comportamientos indefinidos son críticos:

  • acceso a un objeto fuera de su vida útil (por ejemplo, a través de un puntero que cuelga)
  • escribir en un objeto cuyas declaraciones no son compatibles
  • función llamada a través de un puntero a función cuyo tipo no es compatible con el tipo de función al que apunta
  • lvalue expresión se evalúa, pero no designa un objeto intento de modificación de una cadena literal
  • desreferenciación de un puntero no válido (nulo, indeterminado, etc.) o pasado al final
  • Modificación de un objeto const a través de un puntero no const.
  • llamar a una función de biblioteca estándar o macro con un argumento no válido
  • llamar a una función de biblioteca estándar variadic con tipo de argumento inesperado (por ejemplo, call a printf con un argumento del tipo que no coincide con su especificador de conversión)
  • longjmp donde no hay un setjmp para el alcance de la llamada, a través de subprocesos, o dentro del alcance de un tipo de máquina virtual.
  • Cualquier uso del puntero que fue desasignado por free o realloc
  • cualquier cadena o función de biblioteca de cadena ancha accede a una matriz fuera de límites

Comportamiento indefinido acotado

La UB limitada es un comportamiento indefinido que no puede realizar una escritura de memoria no válida, aunque puede interceptarse y puede producir o almacenar valores indeterminados.

Todos los comportamientos no definidos que no figuran como críticos están limitados, incluidos

  • carreras de datos multiproceso
  • Uso de valores indeterminados con duración de almacenamiento automático.
  • violaciones estrictas aliasing
  • acceso a objetos desalineados
  • Desbordamiento de enteros con signo
  • los efectos secundarios no posteriores modifican el mismo escalar o modifican y leen el mismo escalar
  • Desbordamiento de conversión de flotante a entero o de puntero a entero
  • Desplazamiento a nivel de bits por un conteo de bits negativo o demasiado grande
  • división entera por cero
  • uso de una expresión vacía
  • asignación directa o memcpy de objetos superpuestos inexactamente
  • restringir violaciones
  • Todo el comportamiento indefinido no está en la lista crítica.

Solo tengo acceso a un borrador de la norma, pero por lo que estoy leyendo, parece que esta clasificación de comportamiento no definido no es obligatoria en la norma y solo importa desde la perspectiva de los compiladores y entornos que indican específicamente que desean para crear programas de C que puedan analizarse más fácilmente para diferentes clases de errores. (Estos entornos deben definir un símbolo especial __STDC_ANALYZABLE__ .)

Parece que la idea clave aquí es una "escritura fuera de límites", que se define como una operación de escritura que modifica los datos que de otro modo no se asignan como parte de un objeto. Por ejemplo, si pulsa accidentalmente los bytes de una variable existente, eso no es una escritura fuera de límites, pero si saltó a una región aleatoria de la memoria y la decoró con su patrón de bits favorito, estaría realizando una escritura fuera de límites .

Un comportamiento específico es un comportamiento indefinido acotado si el resultado no está definido, pero nunca hará una escritura fuera de límites. En otras palabras, el comportamiento no está definido, pero no saltará a una dirección aleatoria que no esté asociada con ningún objeto o espacio asignado y no coloque bytes allí. Un comportamiento es un comportamiento crítico no definido si obtiene un comportamiento indefinido que no puede prometer que no hará una escritura fuera de los límites.

Luego, el estándar continúa hablando sobre lo que puede llevar a un comportamiento crítico indefinido. Por defecto, los comportamientos indefinidos son limitados, pero existen excepciones para UB que resultan de errores de memoria como acceder a la memoria desasignada o usar un puntero no inicializado, que tiene un comportamiento crítico indefinido. Sin embargo, recuerde que estas clasificaciones solo existen y tienen un significado en el contexto de las implementaciones de C que eligen separar específicamente este tipo de comportamientos. ¡A menos que su entorno C garantice que es analizable, todos los comportamientos indefinidos pueden hacer absolutamente cualquier cosa!

Supongo que esto está destinado a entornos como compiladores o complementos del kernel en los que te gustaría poder analizar un fragmento de código y decir "bueno, si vas a dispararle a alguien en el pie, es mejor que lo hagas". el pie que estás disparando y no el mío ”. Si compilas un programa en C con estas restricciones, el entorno de ejecución puede instrumentar las muy pocas operaciones que pueden ser de comportamiento crítico indefinido y hacer que esas operaciones queden atrapadas en el sistema operativo, y asumir que todos los demás comportamientos indefinidos destruirán a lo sumo la memoria que está asociada específicamente con el programa en sí.


Todos estos son casos en los que el comportamiento no está definido, es decir, el estándar "no impone requisitos" . Tradicionalmente, dentro de un comportamiento indefinido y considerando una implementación (es decir, compilador C + biblioteca estándar C), uno podría ver dos tipos de comportamiento indefinido:

  • construcciones para las cuales el comportamiento no se documentaría, o se documentaría para causar un bloqueo, o el comportamiento sería errático,
  • construye que el estándar quedó indefinido pero para el cual la implementación define algún comportamiento útil.

A veces, estos pueden ser controlados por compiladores. Por ejemplo, el ejemplo 1 generalmente causa un mal comportamiento: una trampa, un bloqueo o modifica un valor compartido. Las versiones anteriores de GCC permitían tener literales de cadena modificables con -fwritable-strings ; por lo tanto, si se dio ese cambio, la implementación definió el comportamiento en ese caso.

C11 agregó una clasificación ortogonal opcional: comportamiento indefinido acotado y comportamiento indefinido crítico . El comportamiento indefinido acotado es aquel que no realiza un almacén fuera de límites , es decir, no puede causar que los valores se escriban en ubicaciones arbitrarias en la memoria. Cualquier comportamiento indefinido que no esté limitado a un comportamiento indefinido es un comportamiento crítico no definido .

Si se define __STDC_ANALYZABLE__ , la implementación se ajustará al apéndice L , que tiene esta lista definitiva de comportamiento crítico indefinido:

  • Se hace referencia a un objeto fuera de su duración ( 6.2.4 ).
  • Se realiza un almacenamiento a un objeto que tiene dos declaraciones incompatibles ( 6.2.7 ),
  • Se usa un puntero para llamar a una función cuyo tipo no es compatible con el tipo referenciado ( 6.2.7 , 6.3.2.3 , 6.5.2.2 ).
  • Un valor de l no designa un objeto cuando se evalúa ( 6.3.2.1 ).
  • El programa intenta modificar una cadena literal ( 6.4.5 ).
  • El operando del operador unario * tiene un valor no válido ( 6.5.3.2 ).
  • La adición o sustracción de un puntero a, o simplemente más allá, un objeto de matriz y un tipo entero produce un resultado que apunta más allá del objeto de matriz y se usa como el operando de un operador unario * que se evalúa ( 6.5.6 ).
  • Se intenta modificar un objeto definido con un tipo de calificación constante mediante el uso de un lvalue con el tipo de calificación no constante ( 6.7.3 ).
  • Un argumento para una función o macro definida en la biblioteca estándar tiene un valor no válido o un tipo no esperado por una función con número variable de argumentos ( 7.1.4 ).
  • La función longjmp se llama con un argumento jmp_buf donde la invocación más reciente de la macro setjmp en la misma invocación del programa con el argumento jmp_buf correspondiente es inexistente, o la invocación fue de otro hilo de ejecución, o la función que contiene la invocación tiene la ejecución terminada en el interino, o la invocación estaba dentro del alcance de un identificador con un tipo modificado de forma variable y la ejecución ha dejado ese alcance en el interino ( 7.13.2.1 ).
  • Se utiliza el valor de un puntero que se refiere al espacio desasignado por una llamada a la función libre o realloc ( 7.22.3 ).
  • Una cadena o una función de utilidad de cadena ancha accede a una matriz más allá del final de un objeto ( 7.24.1 , 7.29.4 ).

Para el comportamiento indefinido acotado, el estándar no impone requisitos distintos de los que no se permite que ocurra una escritura fuera de los límites .

El ejemplo 1: modificación de una cadena literal también es. Clasificado como comportamiento crítico indefinido. El ejemplo 4 también es un comportamiento crítico no definido : el valor no es el esperado por la biblioteca estándar.

Para el ejemplo 4, el estándar sugiere que si bien el comportamiento no está definido en el caso de un modo que no está definido por el estándar, existen implementaciones que podrían definir el comportamiento de otras marcas. Por ejemplo, glibc admite muchos más indicadores de modo , como c , e , m y x , y permite configurar la codificación de caracteres de la entrada con ,ccs=charset modificador de ,ccs=charset (y poner la transmisión en modo ancho de inmediato).