¿Imprimir punteros nulos con% p es un comportamiento indefinido?

93

¿Es un comportamiento indefinido imprimir punteros nulos con el %pespecificador de conversión?

#include <stdio.h>

int main(void) {
    void *p = NULL;

    printf("%p", p);

    return 0;
}

La pregunta se aplica al estándar C y no a las implementaciones C.

Dror K.
fuente
En realidad, no creo que a nadie (incluido el comité C) le importe demasiado. Es un problema bastante artificial, sin (o casi nulo) significado práctico.
P__J__
es como printf solo muestra el valor, y no toca (en el sentido de leer o escribir el objeto puntiagudo) - no puede ser UB i el puntero tiene un valor válido para su tipo (NULL es el valor válido )
P__J__
3
@PeterJ digamos que lo que está diciendo es cierto (aunque claramente el estándar establece lo contrario), el solo hecho de que estemos debatiendo sobre esto hace que la pregunta sea válida y correcta, ya que parece que la parte citada a continuación del estándar hace Es muy difícil de entender para un desarrollador regular qué diablos está pasando .. Es decir: la pregunta no merece el voto negativo, ¡porque este problema requiere una aclaración!
Peter Varo
2
@PeterJ esa es una historia diferente entonces, gracias por la aclaración :)
Peter Varo

Respuestas:

93

Este es uno de esos casos raros en los que estamos sujetos a las limitaciones del idioma inglés y a la estructura inconsistente del estándar. Entonces, en el mejor de los casos, puedo hacer un contraargumento convincente, ya que es imposible probarlo :) 1


El código de la pregunta muestra un comportamiento bien definido.

Como [7.1.4] es la base de la pregunta, comencemos por ahí:

Cada una de las siguientes declaraciones se aplica a menos que se indique explícitamente lo contrario en las descripciones detalladas que siguen: Si un argumento de una función tiene un valor no válido ( como un valor fuera del dominio de la función, o un puntero fuera del espacio de direcciones del programa, o un puntero nulo , [... otros ejemplos ...] ) [...] el comportamiento no está definido. [... otras declaraciones ...]

Este es un lenguaje torpe. Una interpretación es que los elementos de la lista son UB para todas las funciones de la biblioteca, a menos que se anulen por las descripciones individuales. Pero la lista comienza con "como", lo que indica que es ilustrativa, no exhaustiva. Por ejemplo, no menciona la terminación nula correcta de cadenas (crítica para el comportamiento de eg strcpy).

Por lo tanto, está claro que la intención / alcance de 7.1.4 es simplemente que un "valor no válido" conduce a UB (a menos que se indique lo contrario ). Tenemos que mirar la descripción de cada función para determinar qué cuenta como "valor no válido".

Ejemplo 1 - strcpy

[7.21.2.3] dice solo esto:

La strcpyfunción copia la cadena a la que apunta s2(incluido el carácter nulo de terminación) en la matriz apuntada por s1. Si la copia tiene lugar entre objetos que se superponen, el comportamiento no está definido.

No hace mención explícita de punteros nulos, pero tampoco menciona terminadores nulos. En cambio, se infiere de "cadena apuntada por s2" que los únicos valores válidos son cadenas (es decir, punteros a matrices de caracteres terminadas en nulo).

De hecho, este patrón se puede ver en todas las descripciones individuales. Algunos otros ejemplos:

  • [7.6.4.1 (fenv)] almacenar el medio ambiente de punto flotante actual en el objeto apuntado porenvp

  • [7.12.6.4 (frexp)] almacena el entero en el objeto int apuntado porexp

  • [7.19.5.1 (fclose)] la secuencia apuntada porstream

Ejemplo 2 - printf

[7.19.6.1] dice esto sobre %p:

p- El argumento será un puntero a void. El valor del puntero se convierte en una secuencia de caracteres de impresión, de una manera definida por la implementación.

Null es un valor de puntero válido, y esta sección no menciona explícitamente que null es un caso especial, ni que el puntero tiene que apuntar a un objeto. Por tanto, se define como comportamiento.


1. A menos que se presente un autor de estándares, o que podamos encontrar algo similar a un documento de fundamento que aclare las cosas.

Oliver Charlesworth
fuente
Los comentarios no son para una discusión extensa; esta conversación se ha movido al chat .
Bhargav Rao
1
"sin embargo, no menciona terminadores nulos" es débil en el Ejemplo 1 - strcpy ya que la especificación dice "copia la cadena ". La cadena se define explícitamente con un carácter nulo .
chux - Reincorporar a Monica
1
@chux: ese es mi punto, uno tiene que inferir lo que es válido / no válido del contexto, en lugar de asumir que la lista en 7.1.4 es exhaustiva. (Sin embargo, la existencia de esta parte de mi respuesta tenía algo más de sentido en el contexto de los comentarios que desde entonces se han eliminado, argumentando que strcpy era un contraejemplo).
Oliver Charlesworth
1
El quid de la cuestión es cómo el lector interpretará tales como . ¿Significa que algunos ejemplos de posibles valores no válidos son ? ¿Significa que algunos ejemplos que siempre son valores inválidos lo son ? Para que conste, voy con la primera interpretación.
ninjalj
1
@ninjalj - Sí, de acuerdo. Eso es esencialmente lo que estoy tratando de transmitir en mi respuesta aquí, es decir, "estos son ejemplos de los tipos de cosas que podrían ser valores inválidos". :)
Oliver Charlesworth
20

La respuesta corta

. La impresión de punteros nulos con el %pespecificador de conversión tiene un comportamiento indefinido. Habiendo dicho eso, no tengo conocimiento de ninguna implementación conforme existente que se comporte mal.

La respuesta se aplica a cualquiera de los estándares C (C89 / C99 / C11).


La respuesta larga

El %pespecificador de conversión espera que un argumento de tipo puntero se anule, la conversión del puntero a caracteres imprimibles está definida por la implementación. No indica que se espera un puntero nulo.

La introducción a las funciones de la biblioteca estándar establece que los punteros nulos como argumentos de las funciones (de la biblioteca estándar) se consideran valores no válidos, a menos que se indique explícitamente lo contrario.

C99 / C11 §7.1.4 p1

[...] Si un argumento de una función tiene un valor no válido (como [...] un puntero nulo, [...] el comportamiento no está definido.

Ejemplos de funciones (biblioteca estándar) que esperan punteros nulos como argumentos válidos:

  • fflush() utiliza un puntero nulo para eliminar "todos los flujos" (que correspondan).
  • freopen() utiliza un puntero nulo para indicar el archivo "actualmente asociado" con la secuencia.
  • snprintf() permite pasar un puntero nulo cuando 'n' es cero.
  • realloc() utiliza un puntero nulo para asignar un nuevo objeto.
  • free() permite pasar un puntero nulo.
  • strtok() utiliza un puntero nulo para llamadas posteriores.

Si aceptamos el caso snprintf(), tiene sentido permitir pasar un puntero nulo cuando 'n' es cero, pero este no es el caso de otras funciones (biblioteca estándar) que permiten un cero 'n' similar. Por ejemplo: memcpy(), memmove(), strncpy(), memset(), memcmp().

No solo se especifica en la introducción a la biblioteca estándar, sino también una vez más en la introducción a estas funciones:

C99 §7.21.1 p2 / C11 §7.24.1 p2

Donde un argumento declarado como size_tn especifica la longitud de la matriz para una función, n puede tener el valor cero en una llamada a esa función. A menos que se indique explícitamente lo contrario en la descripción de una función particular en esta subcláusula, los argumentos de puntero en dicha llamada aún tendrán valores válidos como se describe en 7.1.4.


¿Es intencional?

No sé si el UB de %pcon un puntero nulo es de hecho intencional, pero dado que el estándar establece explícitamente que los punteros nulos se consideran valores no válidos como argumentos para funciones de biblioteca estándar, y luego especifica explícitamente los casos en los que un nulo puntero es un argumento válido (snprintf, libre, etc.), y luego se va y una vez más se repite el requisito de que los argumentos sean válidos incluso en cero 'n' casos ( memcpy, memmove, memset), entonces yo creo que es razonable suponer que la El comité de estándares de C no está demasiado preocupado por tener tales cosas sin definir.

Dror K.
fuente
Los comentarios no son para una discusión extensa; esta conversación se ha movido al chat .
Bhargav Rao
1
@JeroenMostert: ¿Cuál es la intención de este argumento? La cita dada de 7.1.4 es bastante clara, ¿no es así? ¿Qué hay que discutir sobre "a menos que se indique explícitamente lo contrario" cuando no se dice lo contrario? ¿Qué hay para discutir sobre el hecho de que la biblioteca de funciones de cadena (no relacionada) tiene una redacción similar, por lo que la redacción no parece ser accidental? Creo que esta respuesta (aunque no es realmente útil en la práctica ) es lo más correcta posible.
Damon
3
@Damon: Su hardware mítico no es mítico, hay muchas arquitecturas donde los valores que no representan direcciones válidas pueden no cargarse en los registros de direcciones. Sin embargo, todavía se requiere pasar punteros nulos como argumentos de función para trabajar en esas plataformas como mecanismo general. El simple hecho de poner uno en la pila no hará explotar las cosas.
Jeroen Mostert
1
@anatolyg: en los procesadores x86, las direcciones tienen dos partes: un segmento y un desplazamiento. En el 8086, cargar un registro de segmento es como cargar cualquier otro, pero en todas las máquinas posteriores obtiene un descriptor de segmento. La carga de un descriptor no válido provoca una trampa. Una gran cantidad de código para los procesadores 80386 y posteriores, sin embargo, sólo se utiliza un segmento, y por lo tanto nunca cargas registros de segmento en absoluto .
supercat
1
Creo que todos estarían de acuerdo en que imprimir un puntero nulo con %pno se supone que sea un comportamiento indefinido
MM
-1

Los autores de la Norma C no hicieron ningún esfuerzo por enumerar exhaustivamente todos los requisitos de comportamiento que una implementación debe cumplir para ser adecuada para un propósito en particular. En cambio, esperaban que las personas que escribían compiladores ejercieran una cierta cantidad de sentido común, ya sea que la Norma lo requiera o no.

La cuestión de si algo invoca a UB rara vez es útil en sí misma. Las verdaderas cuestiones de importancia son:

  1. ¿Alguien que esté intentando escribir un compilador de calidad debería hacer que se comporte de forma predecible? Para el escenario descrito, la respuesta es claramente sí.

  2. ¿Deberían los programadores tener derecho a esperar que los compiladores de calidad para cualquier cosa que se parezca a las plataformas normales se comporten de manera predecible? En el escenario descrito, diría que la respuesta es sí.

  3. ¿Podrían algunos escritores de compiladores obtusos estirar la interpretación del Estándar para justificar hacer algo extraño? Espero que no, pero no lo descartaría.

  4. ¿Deberían los compiladores de desinfección chillar sobre el comportamiento? Eso dependería del nivel de paranoia de sus usuarios; un compilador desinfectante probablemente no debería de forma predeterminada quejarse de tal comportamiento, pero quizás proporcione una opción de configuración para hacerlo en caso de que los programas puedan ser portados a compiladores "inteligentes" / tontos que se comportan de manera extraña.

Si una interpretación razonable del Estándar implicaría que se define un comportamiento, pero algunos redactores de compiladores estiran la interpretación para justificar hacer lo contrario, ¿realmente importa lo que diga el Estándar?

Super gato
fuente
1. No es raro que los programadores encuentren que las suposiciones hechas por optimizadores modernos / agresivos están en desacuerdo con lo que consideran "razonable" o "calidad". 2. Cuando se trata de ambigüedades en la especificación, no es raro que los implementadores estén en desacuerdo en cuanto a las libertades que pueden asumir. 3. Cuando se trata de miembros del comité de estándares C, incluso ellos no siempre están de acuerdo en cuál es la interpretación "correcta", y mucho menos en cuál debería ser. Teniendo en cuenta lo anterior, ¿cuál interpretación razonable debemos seguir?
Dror K.
6
Responder a la pregunta "este fragmento de código en particular invoca a UB o no" con una disertación sobre lo que piensas sobre la utilidad de UB o cómo deberían comportarse los compiladores es un mal intento de respuesta, especialmente porque puedes copiar y pegar esto como una respuesta a casi cualquier pregunta sobre UB en particular. Como réplica a su floritura retórica: sí, realmente importa lo que diga el Estándar, sin importar lo que hagan algunos redactores de compiladores o lo que piense de ellos por hacer eso, porque el Estándar es de donde parten tanto los programadores como los redactores de compiladores.
Jeroen Mostert
1
@JeroenMostert: La respuesta a "¿X invoca un comportamiento indefinido" a menudo dependerá de lo que uno quiera decir con la pregunta? Si se considera que un programa tiene un comportamiento indefinido si el estándar no impondría requisitos sobre el comportamiento de una implementación conforme, casi todos los programas invocan UB. Los autores del Estándar claramente permiten que las implementaciones se comporten de manera arbitraria si un programa anida llamadas de función demasiado profundamente, siempre que una implementación pueda procesar correctamente al menos un texto fuente (posiblemente artificial) que ejerza los límites de traducción en el Stadard.
supercat
@supercat: muy interesante, pero ¿es printf("%p", (void*) 0)un comportamiento indefinido o no, según el Estándar? Las llamadas a funciones profundamente anidadas son tan relevantes para esto como el precio del té en China. Y sí, UB es muy común en programas del mundo real, ¿qué pasa con eso?
Jeroen Mostert
1
@JeroenMostert: Dado que el Estándar permitiría una implementación obtusa para considerar que casi cualquier programa tiene UB, lo que debería importar será el comportamiento de las implementaciones no obtusas. En caso de que no se haya dado cuenta, no solo escribí una copia / pega sobre UB, sino que respondí la pregunta sobre %ppara cada posible significado de la pregunta.
supercat