c pointers undefined-behavior

c - ¿Qué tratamiento puede sufrir un puntero y seguir siendo válido?



pointers undefined-behavior (3)

1) Fundido a vacío y puntero hacia atrás.

Esto produce un puntero válido igual al original. El párrafo 6.3.2.3/1 de la norma es claro al respecto:

Un puntero para anular se puede convertir ao desde un puntero a cualquier tipo de objeto. Un puntero a cualquier tipo de objeto se puede convertir en un puntero para anularlo y viceversa; El resultado se comparará igual al puntero original.

2) Emitir a enteros de tamaño apropiado y volver

3) Un par de operaciones enteras triviales

4) Operaciones con números enteros no lo suficientemente triviales como para ocultar la procedencia, pero que, sin embargo, dejarán el valor sin cambios.

5) Más operaciones enteras indirectas que dejarán el valor sin cambios

[...] Obviamente, el caso 1 es válido, y el caso 2 seguramente también debe serlo. Por otro lado, me topé con una publicación de Chris Lattner, que desafortunadamente no puedo encontrar ahora, diciendo que el caso 5 no es válido, que la licencia estándar del compilador simplemente lo compila en un bucle infinito.

C requiere una conversión cuando se convierte de cualquier manera entre los punteros y los enteros, y ha omitido algunos de ellos en su código de ejemplo. En ese sentido, sus ejemplos (2) - (5) no son conformes, pero para el resto de esta respuesta pretenderé que los moldes necesarios están ahí.

Aún así, siendo muy pedantes, todos estos ejemplos tienen un comportamiento definido por la implementación, por lo que no son estrictamente conformes. Por otro lado, el comportamiento "definido por la implementación" sigue siendo un comportamiento definido; si eso significa que su código es "válido" o no depende de lo que quiere decir con ese término. En cualquier caso, el código que el compilador podría emitir para cualquiera de los ejemplos es un asunto aparte.

Estas son las disposiciones relevantes de la norma de la sección 6.3.2.3 (énfasis agregado):

Un entero se puede convertir a cualquier tipo de puntero. Excepto como se especificó anteriormente, el resultado está definido por la implementación , podría no estar correctamente alineado, podría no apuntar a una entidad del tipo referenciado y podría ser una representación de captura.

Cualquier tipo de puntero se puede convertir en un tipo entero. Excepto como se especificó anteriormente, el resultado está definido por la implementación . Si el resultado no se puede representar en el tipo entero, el comportamiento es indefinido. El resultado no necesita estar en el rango de valores de cualquier tipo de entero.

La definición de uintptr_t también es relevante para su código de ejemplo particular. El estándar lo describe de esta manera (C2011, 7.20.1.4/1; énfasis agregado):

un tipo de entero sin signo con la propiedad de que cualquier puntero válido para anular se puede convertir a este tipo, luego se puede volver a convertir en puntero a anular , y el resultado se comparará igual al puntero original.

Se está convirtiendo de ida y vuelta entre int * y uintptr_t . int * no es void * , por lo que 7.20.1.4/1 no se aplica a estas conversiones, y el comportamiento está definido por la implementación según la sección 6.3.2.3.

Sin embargo, suponga que se convierte de un lado a otro a través de un void * intermedio void * :

uintptr_t b = (uintptr_t)(void *)a; a = (int *)(void *)b;

En una implementación que proporciona uintptr_t (que es opcional), eso haría que sus ejemplos (2 - 5) se ajusten estrictamente. En ese caso, el resultado de las conversiones de entero a puntero depende solo del valor del objeto uintptr_t , no de cómo se obtuvo ese valor.

En cuanto a las reclamaciones que atribuye a Chris Lattner, son sustancialmente incorrectas. Si los ha representado con precisión, tal vez reflejen una confusión entre el comportamiento definido por la implementación y el comportamiento no definido. Si el código exhibió un comportamiento indefinido, entonces la reclamación podría contener algo de agua, pero ese no es, de hecho, el caso.

Independientemente de cómo se obtuvo su valor, b tiene un valor definido de tipo uintptr_t , y el bucle debe incrementar eventualmente i a ese valor, en cuyo punto se ejecutará el bloque if . En principio, el comportamiento definido por la implementación de una conversión de uintptr_t directamente a int * podría ser una locura, como saltarse la siguiente afirmación (causando así un bucle infinito), pero tal comportamiento es completamente inverosímil. Todas las implementaciones que conozca alguna vez fallarán en ese punto o almacenarán algún valor en la variable a , y luego, si no falla, ejecutará la declaración de return .

¿Cuál de las siguientes formas de tratar e intentar recuperar un puntero C, tiene la garantía de ser válida?

1) Fundido a vacío y puntero hacia atrás.

int f(int *a) { void *b = a; a = b; return *a; }

2) Emitir a enteros de tamaño apropiado y volver

int f(int *a) { uintptr_t b = a; a = (int *)b; return *a; }

3) Un par de operaciones enteras triviales

int f(int *a) { uintptr_t b = a; b += 99; b -= 99; a = (int *)b; return *a; }

4) Operaciones con números enteros no lo suficientemente triviales como para ocultar la procedencia, pero que, sin embargo, dejarán el valor sin cambios.

int f(int *a) { uintptr_t b = a; char s[32]; // assume %lu is suitable sprintf(s, "%lu", b); b = strtoul(s); a = (int *)b; return *a; }

5) Más operaciones enteras indirectas que dejarán el valor sin cambios

int f(int *a) { uintptr_t b = a; for (uintptr_t i = 0;; i++) if (i == b) { a = (int *)i; return *a; } }

Obviamente el caso 1 es válido, y el caso 2 seguramente debe serlo también. Por otro lado, me encontré con un post de Chris Lattner, que desafortunadamente no puedo encontrar ahora, diciendo que algo similar al caso 5 no es válido, que el estándar compila al compilador para que simplemente lo compile en un bucle infinito. Sin embargo, cada caso parece una extensión inobjetable de la anterior.

¿Dónde se traza la línea entre un caso válido y uno inválido?

Agregado según la discusión en los comentarios: aunque todavía no puedo encontrar la publicación que inspiró el caso 5, no recuerdo qué tipo de puntero estaba involucrado; en particular, podría haber sido un puntero a función, que podría ser la razón por la que ese caso demostró un código no válido mientras que mi caso 5 es un código válido.

Segunda adición: está bien, aquí hay otra fuente que dice que hay un problema, y ​​a esta tengo un enlace. https://www.cl.cam.ac.uk/~pes20/cerberus/notes30.pdf - la discusión sobre la procedencia del puntero - dice, y respalda con evidencia, que no, si el compilador pierde la pista de dónde vino el puntero de, es un comportamiento indefinido.


Debido a que los diferentes campos de aplicación requieren la capacidad de manipular los punteros de diferentes maneras, y debido a que las mejores implementaciones para algunos propósitos pueden ser totalmente inadecuadas para otros, el Estándar C trata el soporte (o la falta del mismo) para varios tipos de manipulaciones como una Calidad de Cuestión de implementación . En general, las personas que escriben una implementación para un campo de aplicación en particular deben estar más familiarizadas que los autores del Estándar con las características que serían útiles para los programadores en ese campo, y las personas que realizan un esfuerzo fidedigno para producir implementaciones de calidad adecuadas para aplicaciones de escritura en ese campo admitirá dichas características, ya sea que el Estándar lo requiera o no.

En el lenguaje pre-estándar inventado por Dennis Ritchie, todos los punteros de un tipo particular que se identificaron en la misma dirección eran equivalentes. Si cualquier secuencia de operaciones en un puntero terminaría produciendo otro puntero del mismo tipo que identificó la misma dirección, ese puntero, esencialmente por definición, sería equivalente al primero. El Estándar C especifica algunas situaciones, sin embargo, donde los punteros pueden identificar la misma ubicación en el almacenamiento y ser indistinguibles entre sí sin ser equivalentes. Por ejemplo, dado:

int foo[2][4] = {0}; int *p = foo[0]+4, *q=foo[1];

tanto p como q se compararán iguales entre sí, y para foo[0]+4 , y para foo[1] . Por otro lado, a pesar del hecho de que la evaluación de p[-1] y q[0] habría definido el comportamiento, la evaluación de p[0] o q[-1] invocaría UB. Desafortunadamente, aunque el Estándar deja en claro que p y q no serían equivalentes, no hace nada para aclarar si realizar varias secuencias de operaciones en, por ejemplo, p , producirá un puntero que se puede usar en todos los casos en que p era utilizable, un puntero que se puede usar en todos los casos en los que p o q serían utilizables, un puntero que solo se puede usar en los casos en que q sería utilizable, o un puntero que solo se puede usar en los casos en que tanto p como q serían utilizables.

Las implementaciones de calidad destinadas a la programación de bajo nivel generalmente deben procesar manipulaciones de punteros distintas de aquellas que involucran punteros de restrict de una manera que produzcan un puntero que se pueda utilizar en cualquier caso en el que un puntero que se compare igual pueda ser utilizable. Desafortunadamente, el Estándar no proporciona ningún medio por el cual un programa pueda determinar si está siendo procesado por implementaciones de calidad que sean adecuadas para la programación de bajo nivel y se niegue a ejecutarse si no lo está, por lo que la mayoría de las formas de programación de sistemas deben confiar en implementaciones de calidad procesar ciertas acciones de manera documentada y característica del entorno, incluso cuando la Norma no imponga requisitos.

Incidentalmente, incluso si las construcciones normales para manipular los punteros no tendrían ninguna forma de crear punteros donde los principios de equivalencia no deberían aplicarse, algunas plataformas pueden definir formas de crear punteros "interesantes". Por ejemplo, si una implementación que normalmente atraparía las operaciones en los punteros nulos se ejecutó en un entorno donde a veces podría ser necesario acceder a un objeto en la dirección cero, podría definir una sintaxis especial para crear un puntero que podría usarse para acceder cualquier dirección, incluido cero, dentro del contexto donde se creó. Un "puntero legítimo para direccionar cero" probablemente se compararía igual a un puntero nulo (aunque no sean equivalentes), pero realizar una conversión de ida y vuelta a otro tipo y viceversa probablemente convertiría lo que había sido un puntero legítimo a cero. en un puntero nulo. Si el Estándar hubiera ordenado que una conversión de ida y vuelta de cualquier puntero debe producir uno que se pueda utilizar de la misma manera que el original, eso requeriría que el compilador omita las trampas nulas en cualquier puntero que pudiera haber sido producido de tal manera, incluso si es mucho más probable que se hubieran producido al disparar un puntero nulo.

Incidentalmente, desde una perspectiva práctica, los compiladores "modernos", incluso en -fno-strict-aliasing , intentarán a veces rastrear la procedencia de los punteros a través de conversiones puntero-entero-puntero de tal manera que los punteros producidos al emitir enteros iguales a veces pueden Se supone incapaz de alias.

Por ejemplo, dado:

#include <stdint.h> extern int x[],y[]; int test(void) { if (!x[0]) return 999; uintptr_t upx = (uintptr_t)x; uintptr_t upy = (uintptr_t)(y+1); //Consider code with and without the following line if (upx == upy) upy = upx; if ((upx ^ ~upy)+1) // Will return if upx != upy return 123; int *py = (int*)upy; *py += 1; return x[0]; }

En ausencia de la línea marcada, gcc, icc y clang supondrán, incluso cuando se use -fno-strict-aliasing , que una operación en *py no puede afectar a *px , aunque la única forma en que el código podría se alcanzaría sería si upx y upy mantuvieran el mismo valor (lo que significa que tanto px como py se produjeron al emitir el mismo valor uintptr_t ). Agregar la línea marcada hace que icc y clang reconozcan que px y py podrían identificar el mismo objeto, pero gcc asume que la asignación se puede optimizar, incluso si eso significa que py se derivaría de px --a situación un compilador de calidad no debería tener problemas para reconocer que implica un posible aliasing.

No estoy seguro de qué beneficio real esperan los escritores de compiladores de sus esfuerzos por rastrear la procedencia de los valores de uintptr_t, dado que no veo mucho propósito para hacer tales conversiones fuera de los casos en que los resultados de las conversiones se pueden usar de maneras "interesantes". . Sin embargo, dado el comportamiento del compilador, no estoy seguro de ver alguna forma agradable de garantizar que las conversiones entre enteros y punteros se comporten de una manera coherente con los valores involucrados.


Según el proyecto de norma C11 :

Ejemplo 1

Válido, por §6.5.16.1, incluso sin una conversión explícita.

Ejemplo 2

Los tipos intptr_t y uintptr_t son opcionales. Asignar un puntero a un entero requiere una conversión explícita (§6.5.16.1), aunque gcc y clang solo te avisarán si no tienes uno. Con esas advertencias, la conversión de ida y vuelta es válida por §7.20.1.4. ETA: John Bellinger señala que el comportamiento solo se especifica cuando se realiza un lanzamiento intermedio para void* ambas formas. Sin embargo, tanto gcc como clang permiten la conversión directa como una extensión documentada.

Ejemplo 3

Seguro, pero solo porque está usando una aritmética sin signo, que no puede desbordarse, y por lo tanto se garantiza que recupere la misma representación de objeto. Un intptr_t podría desbordarse! Si desea realizar aritmética de punteros de forma segura, puede convertir cualquier tipo de puntero a char* y luego agregar o restar desplazamientos dentro de la misma estructura o matriz. Recuerda, sizeof(char) siempre es 1 . ETA: El estándar garantiza que los dos punteros se comparan, pero su enlace a Chisnall et al. da ejemplos en los que los compiladores, sin embargo, asumen que los dos punteros no se alias entre sí.

Ejemplo 4

¡Siempre, siempre, siempre revise los desbordamientos de búfer cada vez que lea y especialmente cuando escriba en un búfer! ¿Si puede probar matemáticamente que el desbordamiento no puede ocurrir por análisis estático? Luego escriba las suposiciones que justifican eso, explícitamente, y assert() o static_assert() que no han cambiado. ¡Use snprintf() , no el sprintf() inseguro y en desuso! Si no recuerdas nada más de esta respuesta, ¡recuerda eso!

Para ser absolutamente pedante, la forma portátil de hacerlo sería usar los especificadores de formato en <inttypes.h> y definir la longitud del búfer en términos del valor máximo de cualquier representación de puntero. En el mundo real, imprimiría punteros con el formato %p .

Sin embargo, la respuesta a la pregunta que pretendía formular es sí: lo único que importa es que recupere la misma representación de objeto. Aquí hay un ejemplo menos artificial:

#include <assert.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { int i = 1; const uintptr_t u = (uintptr_t)(void*)&i; uintptr_t v; memcpy( &v, &u, sizeof(v) ); int* const p = (int*)(void*)v; assert(p == &i); *p = 2; printf( "%d = %d./n", i, *p ); return EXIT_SUCCESS; }

Todo lo que importa son los bits en su representación de objeto. Este código también sigue las estrictas reglas de alias en §6.5. Compila y funciona bien con los compiladores que causaron problemas a Chisnall et al .

Ejemplo 5

Esto funciona, igual que arriba.

Una nota a pie de página extremadamente pedante que nunca será relevante para su codificación: algún hardware esotérico obsoleto tiene una representación de signo-magnitud o de complemento-uno de enteros con signo, y en estos, puede haber un valor distinto de cero negativo que podría o podría no trampa En algunas CPU, esto podría ser un puntero válido o una representación de puntero nulo distinta del cero positivo. Y en algunas CPU, el cero positivo y el negativo pueden ser iguales.

PD

La norma dice:

Dos punteros se comparan igual si y solo si ambos son punteros nulos, ambos son punteros al mismo objeto (incluido un puntero a un objeto y un subobjeto al principio) o función, ambos son punteros al pasado del último elemento de la misma matriz objeto, o uno es un puntero a uno más allá del final de un objeto de matriz y el otro es un puntero al inicio de un objeto de matriz diferente que sucede a seguir inmediatamente el primer objeto de matriz en el espacio de direcciones.

Además, si los dos objetos de la matriz son filas consecutivas de la misma matriz multidimensional, uno más allá del final de la primera fila es un puntero válido al comienzo de la siguiente fila. Por lo tanto, incluso una implementación patológica que intencionalmente se proponga causar tantos errores como lo permita el estándar solo podría hacerlo si su puntero manipulado se compara con la dirección de un objeto de matriz, en cuyo caso la implementación podría en teoría decidir interpretarlo como One-past-the-end de algún otro objeto de matriz en su lugar.

El comportamiento previsto es claramente que el puntero que compara igual a &array1+1 y a &array2 es equivalente a ambos: significa permitirte compararlos con direcciones dentro de array1 o desreferenciarlo para obtener array2[0] . Sin embargo, la Norma en realidad no dice eso.

PPS

El comité de estándares ha abordado algunos de estos problemas y propone que el estándar C agregue explícitamente el lenguaje sobre la procedencia del indicador. Esto determinaría si una implementación conforme puede asumir que un puntero creado por la manipulación de bits no es un alias de otro puntero.

Específicamente, el corrigendum propuesto introduciría la procedencia del puntero y permitiría que los punteros con diferente procedencia no se comparen igual. También introduciría una -fno-provenance , que garantizaría que cualquiera de los dos punteros se comparen igual si y solo si tienen la misma dirección numérica. (Como se mencionó anteriormente, dos punteros de objeto que comparan alias iguales entre sí).