que - ¿Es compatible con la norma C para acceder a la dirección del puntero nulo?
punteros c++ pdf (5)
Como OP ha concluido correctamente en su respuesta a su propia pregunta :
No hay una forma estándar porque no hay una dirección (arquitectónica) en el documento C estándar. Esto es cierto para todas las direcciones (arquitectónicas), no solo para int2ptr (0).
Sin embargo, es probable que una situación en la que uno quisiera acceder directamente a la memoria sea en la que se emplee un script de enlace personalizado. (Es decir, algún tipo de sistema de sistemas embebidos). Por lo tanto, diría que la forma estándar de hacer lo que OP solicita sería exportar un símbolo para la dirección (arquitectónica) en el script del vinculador, y no molestarse con la dirección exacta en el El código C en sí.
Una variación de ese esquema sería definir un símbolo en la dirección cero y simplemente usarlo para derivar cualquier otra dirección requerida. Para hacer eso, agregue algo como lo siguiente a la sección de SECTIONS
de la secuencia de comandos del vinculador (asumiendo la sintaxis de ld de GNU):
_memory = 0;
Y luego en tu código C:
extern char _memory[];
Ahora es posible, por ejemplo, crear un puntero a la dirección cero utilizando, por ejemplo, char *p = &_memory[0];
(o simplemente char *p = _memory;
), sin convertir nunca un int en un puntero. Del mismo modo, int addr = ...; char *p_addr = &_memory[addr];
int addr = ...; char *p_addr = &_memory[addr];
creará un puntero a la dirección addr
sin lanzar técnicamente un int a un puntero.
(Por supuesto, esto evita la pregunta original, porque el vinculador es independiente del estándar C y del compilador C, y cada vinculador puede tener una sintaxis diferente para su secuencia de comandos del vinculador. Además, el código generado puede ser menos eficiente, ya que el compilador no es consciente de la dirección a la que se está accediendo. Pero creo que esto aún agrega una perspectiva interesante a la pregunta, así que, por favor, perdonen la respuesta ligeramente fuera de tema ...)
En C, diferir el puntero nulo es un comportamiento indefinido, sin embargo, el valor del puntero nulo tiene una representación de bits que en algunas arquitecturas hace que apunte a una dirección válida (por ejemplo, la dirección 0).
Llamemos a esta dirección la dirección de puntero nulo , para mayor claridad.
Supongamos que quiero escribir una pieza de software en C, en un entorno con acceso ilimitado a la memoria. Supongamos que además quiero escribir algunos datos en la dirección del puntero nulo: ¿cómo lograría eso de una manera compatible con el estándar?
Caso de ejemplo (IA32e):
#include <stdint.h>
int main()
{
uintptr_t zero = 0;
char* p = (char*)zero;
return *p;
}
Este código cuando se compila con gcc con -O3 para IA32e se transforma en
movzx eax, BYTE PTR [0]
ud2
debido a UB (0 es la representación de bits del puntero nulo).
Dado que C está cerca de la programación de bajo nivel, creo que debe haber una manera de acceder a la dirección del puntero nulo y evitar UB.
Solo para aclarar
Estoy preguntando qué tiene que decir el estándar acerca de esto, NO cómo lograr esto de una manera definida por la implementación.
Sé la respuesta para este último.
Cualquiera que sea la solución dependerá de la implementación. Con necesidad ISO C no describe el entorno en el que se ejecuta un programa de C; más bien, cómo se ve un programa C conforme a una variedad de entornos («sistemas de procesamiento de datos»). El Estándar no puede garantizar lo que obtendría al acceder a una dirección que no es una matriz de objetos, es decir, algo que se asignó de manera visible , no el entorno.
Por lo tanto, usaría algo que el estándar deja como definido por la implementación (e incluso como soportado condicionalmente) en lugar de por un comportamiento no definido *: Ensamblaje en línea. Para GCC / clang:
asm volatile("movzx 0, %%eax;") // *(int*)0;
También vale la pena mencionar los entornos independientes, en el que parece estar. El estándar dice acerca de este modelo de ejecución (el énfasis es mío):
§ 5.1.2
Se definen dos entornos de ejecución: independientes y alojados. [...]
§ 5.1.2.1, coma 1
En un entorno independiente ( en el que la ejecución del programa C puede realizarse sin ningún beneficio de un sistema operativo ), el nombre y el tipo de la función llamada al inicio del programa están definidos por la implementación. Todas las instalaciones de la biblioteca disponibles para un programa independiente, distintas del conjunto mínimo requerido por la cláusula 4, están definidas por la implementación. [...]
Observe que no dice que puede acceder a ninguna dirección a voluntad.
Lo que sea que eso pueda significar. Las cosas son un poco diferentes cuando se trata de la implementación que controlan los delegados estándar.
Todas las citas son del borrador N. 1570.
El Estándar C no requiere que las implementaciones tengan direcciones que se asemejen a los enteros de ninguna forma o forma; todo lo que requiere es que si los tipos uintptr_t e intptr_t existen, el acto de convertir un puntero a uintptr_t o intptr_t producirá un número, y convertir ese número directamente al mismo tipo que el puntero original dará un puntero igual al original.
Si bien se recomienda que las plataformas que utilizan direcciones que se asemejan a enteros deben definir conversiones entre enteros y direcciones de una manera que no sorprenda a alguien familiarizado con dicha asignación, eso no es un requisito, y el código que se basa en dicha recomendación no sería estrictamente conforme
No obstante, sugeriría que si una implementación de calidad especifica que realiza una conversión de entero a puntero mediante un mapeo simple a nivel de bits, y si puede haber razones plausibles por las cuales el código querría acceder a la dirección cero, debería considerar las siguientes afirmaciones:
*((uint32_t volatile*)0) = 0x12345678;
*((uint32_t volatile*)x) = 0x12345678;
como una solicitud para escribir en la dirección cero y la dirección x, en ese orden, incluso si x resulta ser cero, e incluso si la implementación normalmente quedaría atrapada en los accesos de puntero nulo. Tal comportamiento no es "estándar", en la medida en que el Estándar no dice nada sobre el mapeo entre los punteros y los enteros, pero una implementación de buena calidad debería comportarse sensiblemente.
Leí (parte de) el estándar C99 para aclarar mi mente. Encontré las secciones que son de interés para mi propia pregunta y estoy escribiendo esto como referencia.
RENUNCIA
Soy un principiante absoluto, el 90% o más de lo que he escrito está mal, no tiene sentido o puede romperte la tostadora. También trato de hacer una justificación fuera del estándar, a menudo con resultados desastrosos e ingenuos (como se indica en el comentario).
No leer
Consulte a @Olaf, para una respuesta formal y profesional.
Para lo siguiente, el término dirección de arquitectura diseñó una dirección de memoria como la ve el procesador (dirección lógica, virtual, lineal, física o de bus). En otras palabras, las direcciones que usaría en el montaje.
En la sección 6.3.2.3. se lee
Una expresión de constante entera con el valor 0, o una expresión de este tipo convertida en tipo
void *
, se denomina constante de puntero nula . Si una constante de puntero nulo se convierte en un tipo de puntero , se garantiza que el puntero resultante, llamado puntero nulo , comparará desigual a un puntero con cualquier objeto o función.
y en cuanto a la conversión de entero a puntero
Un entero se puede convertir a cualquier tipo de puntero. Excepto como se especificó previamente [es decir, para el caso de una constante de puntero nulo] , 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 trampa † .
Esto implica que el compilador, para ser compatible, solo necesita implementar una función int2ptr desde el entero hasta los punteros que
- int2ptr (0) es, por definición, el puntero nulo .
Tenga en cuenta que int2ptr (0) no tiene el mandato de ser 0. Puede ser cualquier representación de bit. - * int2ptr (n! = 0) no tiene restricciones.
Tenga en cuenta que esto significa que int2ptr no necesita ser la función de identidad, ni una función que devuelva punteros válidos.
Dado el código de abajo
char* p = (char*)241;
El estándar no hace ninguna garantía de que la expresión *p = 56;
Escribirá a la dirección arquitectónica 241 .
Y, por lo tanto, no ofrece una forma directa de acceder a ninguna otra dirección de arquitectura (incluida int2ptr (0) , la dirección diseñada por un puntero nulo, si es válida).
En pocas palabras, el estándar no se ocupa de las direcciones arquitectónicas, sino de los punteros, sus comparaciones, conversiones y sus operaciones ‡ .
Cuando escribimos código como char* p = (char*)K
no le estamos diciendo al compilador que haga que p
apunte a la dirección arquitectónica K , le estamos diciendo que haga un puntero fuera del entero K , o en otro término para hacer p
apunta a la dirección (C abstracta) K.
El puntero nulo y la dirección (arquitectónica) 0x0 no son lo mismo (cit.), Por lo que es cierto para cualquier otro puntero creado a partir del entero K y la dirección (arquitectónica) K.
Por algunas razones, las herencias de la infancia, pensé que los literales enteros en C podían usarse para expresar direcciones arquitectónicas, en cambio me equivoqué y eso solo es (algo) correcto en los compiladores que estaba usando.
La respuesta a mi propia pregunta es simple: no hay una forma estándar porque no hay una dirección (arquitectónica) en el documento estándar de C. Esto es cierto para todas las direcciones (arquitectónicas), no solo para int2ptr (0) one 1 .
Nota sobre el return *(volatile char*)0;
La norma dice que
Si un valor no válido [un valor de puntero nulo es un valor no válido] se ha asignado al puntero, el comportamiento del operador unario * no está definido.
y eso
Por lo tanto, cualquier expresión que se refiera a tal objeto [volátil] se evaluará estrictamente de acuerdo con las reglas de la máquina abstracta.
La máquina abstracta dice que *
no está definido para valores de puntero nulos, por lo que el código no debe diferir de este
return *(char*)0;
que también es indefinido.
De hecho, no difieren , al menos con GCC 4.9, ambos compilan las instrucciones que figuran en mi pregunta.
La forma definida de implementación para acceder a la dirección arquitectónica 0 es, para GCC, el uso del indicador de desreferencia -fno-isolate-erróneo-caminos- que produce el código de ensamblaje "esperado".
† Las funciones de mapeo para convertir un puntero a un entero o un entero a un puntero pretenden ser consistentes con la estructura de direccionamiento del entorno de ejecución.
‡ Desafortunadamente dice que la &
produce la dirección de su operando, creo que esto es un poco impropio, yo diría que produce un puntero a su operando. Considere una variable a
que se sabe que reside en la dirección 0xf1 en un espacio de direcciones de 16 bits y considere un compilador que implementa int2ptr (n) = 0x8000 | n . &a
produciría un puntero cuya representación de bit es 0x80f1 que no es la dirección de a
.
1 Lo que fue especial para mí porque era el único, en mis implementaciones, al que no se podía acceder.
Supongo que la pregunta que está haciendo es:
¿Cómo accedo a la memoria de modo que un puntero a esa memoria tenga la misma representación que el puntero nulo?
Según una lectura literal de la Norma, esto no es posible. 6.3.2.3/3 dice que cualquier puntero a un objeto debe comparar desigual al puntero nulo.
Por lo tanto, este puntero del que estamos hablando no debe apuntar a un objeto. Pero el operador de deferencia *
, aplicado a un puntero de objeto, solo especifica el comportamiento en el caso de que apunte a un objeto.
Dicho esto, el modelo de objetos en C nunca se ha especificado con rigor, por lo que no pondría demasiado peso en la interpretación anterior. Sin embargo, me parece que cualquier solución que surja tendrá que depender de un comportamiento no estándar del compilador que esté en uso.
Vemos un ejemplo de esto en las otras respuestas en las que el optimizador de gcc detecta un puntero de todos los bits en cero en una etapa tardía de procesamiento y lo marca como UB.