C - Comportamiento de conversión entre dos punteros
Actualización 2020-12-11: Gracias @ "Algún tipo programador" por la sugerencia en el comentario. Mi problema subyacente es que nuestro equipo está implementando un motor de almacenamiento de tipo dinámico. Asignamos múltiples búferes de matriz de caracteres [PAGE_SIZE] con 16 alineados para almacenar tipos dinámicos de datos (no hay una estructura fija). Por razones de eficiencia, no podemos realizar la codificación de bytes ni asignar espacio adicional para usar memcpy
.
Dado que se ha determinado la alineación (es decir, 16), el resto es utilizar la conversión de puntero para acceder a objetos del tipo especificado, por ejemplo:
int main() {
// simulate our 16-aligned malloc
_Alignas(16) char buf[4096];
// store some dynamic data:
*((unsigned long *) buf) = 0xff07;
*(((double *) buf) + 2) = 1.618;
}
Pero nuestro equipo discute si esta operación es un comportamiento indefinido.
He leído muchas preguntas similares, como
- ¿Por qué -Wcast-align no advierte sobre la conversión de char * a int * en x86?
- ¿Cómo lanzar una matriz de caracteres a int en una posición no alineada?
- C comportamiento indefinido. ¿Regla estricta de alias o alineación incorrecta?
- SEI CERT C CS EXP36-C
Pero estos son diferentes de mi interpretación del estándar C, quiero saber si es mi malentendido.
La principal confusión es sobre la sección 6.3.2.3 # 7 de C11:
Un puntero a un tipo de objeto se puede convertir en un puntero a un tipo de objeto diferente. Si el puntero resultante no está alineado correctamente 68) para el tipo referenciado, el comportamiento no está definido.
68) En general, el concepto '' alineado correctamente '' es transitivo: si un puntero al tipo A está alineado correctamente para un puntero al tipo B, que a su vez está alineado correctamente para un puntero al tipo C, entonces un puntero al tipo A está alineado correctamente para que un puntero escriba C.
¿El puntero resultante aquí se refiere al objeto puntero o al valor del puntero ?
En mi opinión, creo que la respuesta es el objeto puntero , pero más respuestas parecen indicar el valor del puntero .
Interpretación A: Objeto puntero
Mis pensamientos son los siguientes: Un puntero en sí mismo es un objeto. Según 6.2.5 # 28 , diferentes punteros pueden tener diferentes requisitos de representación y alineación. Por lo tanto, de acuerdo con 6.3.2.3 # 7 , siempre que dos punteros tengan la misma alineación, se pueden convertir de forma segura sin un comportamiento indefinido, pero no hay garantía de que se puedan desreferenciar. Exprese esta idea en un programa:
#include <stdio.h>
int main() {
char buf[4096];
char *pc = buf;
if (_Alignof(char *) == _Alignof(int *)) {
// cast safely, because they have the same alignment requirement?
int *pi = (int *) pc;
printf("pi: %p\n", pi);
} else {
printf("char * and int * don't have the same alignment.\n");
}
}
Interpretación B: Valor del puntero
Sin embargo, si el estándar C11 está hablando de Pointer Value para el tipo referenciado en lugar de Pointer Object . La verificación de alineación del código anterior no tiene sentido. Exprese esta idea en un programa:
#include <stdio.h>
int main() {
char buf[4096];
char *pc = buf;
/*
* undefined behavior, because:
* align of char is 1
* align of int is 4
*
* and we don't know whether the `value` of pc is 4-aligned.
*/
int *pi = (int *) pc;
printf("pi: %p\n", pi);
}
¿Qué interpretación es la correcta?
Respuestas
La interpretación B es correcta. El estándar se refiere a un puntero a un objeto, no al objeto en sí. "Puntero resultante" se refiere al resultado de la conversión, y una conversión no produce un valor l, por lo que se refiere al valor del puntero después de la conversión.
Tomando el código de su ejemplo, suponga que an int
debe estar alineado en un límite de 4 bytes, es decir, su dirección debe ser un múltiplo de 4. Si la dirección de buf
es 0x1001
entonces convertir esa dirección a int *
no es válida porque el valor del puntero no está alineado correctamente. Si la dirección de buf
es 0x1000
, la conversión a int *
es válida.
Actualizar:
El código que agregó aborda el problema de alineación, por lo que está bien en ese sentido. Sin embargo, tiene un problema diferente: viola el alias estricto.
La matriz que definió contiene objetos de tipo char
. Al convertir la dirección a un tipo diferente y posteriormente desreferenciar el tipo de tipo convertido, está accediendo a objetos de un tipo como objetos de otro tipo. Esto no está permitido por el estándar C.
Aunque el término "aliasing estricto" no se utiliza en la norma, el concepto se describe en la sección 6.5, párrafos 6 y 7:
6 El tipo efectivo de un objeto para acceder a su valor almacenado es el tipo declarado del objeto, si lo hubiera. 87) Si un valor se almacena en un objeto que no tiene un tipo declarado a través de un lvalue que tiene un tipo que no es un tipo de carácter, entonces el tipo del lvalue se convierte en el tipo efectivo del objeto para ese acceso y para los accesos subsiguientes que no lo tienen. modificar el valor almacenado. Si un valor se copia en un objeto que no tiene un tipo declarado usando
memcpy
omemmove
, o se copia como una matriz de tipo de carácter, entonces el tipo efectivo del objeto modificado para ese acceso y para los accesos posteriores que no modifican el valor es el tipo efectivo del objeto del que se copia el valor, si tiene uno. Para todos los demás accesos a un objeto que no tiene un tipo declarado, el tipo efectivo del objeto es simplemente el tipo de lvalue usado para el acceso.7 Un objeto tendrá acceso a su valor almacenado solo mediante una expresión lvalue que tenga uno de los siguientes tipos: 88)
- un tipo compatible con el tipo efectivo del objeto,
- una versión calificada de un tipo compatible con el tipo efectivo del objeto,
- un tipo que es el tipo firmado o no firmado correspondiente al tipo efectivo del objeto,
- un tipo que es el tipo firmado o no firmado correspondiente a una versión calificada del tipo efectivo del objeto,
- un tipo de agregación o unión que incluye uno de los tipos antes mencionados entre sus miembros (incluido, de forma recursiva, un miembro de una unión subagrupada o contenida), o
- un tipo de carácter.
...
87) Los objetos asignados no tienen un tipo declarado.
88) La intención de esta lista es especificar aquellas circunstancias en las que un objeto puede tener o no un alias.
En su ejemplo, está escribiendo una unsigned long
y una double
sobre char
objetos. Ninguno de estos tipos cumple las condiciones del párrafo 7.
Además de eso, la aritmética del puntero aquí no es válida:
*(((double *) buf) + 2) = 1.618;
Como lo está tratando buf
como una variedad de double
cuando no lo es. Como mínimo, necesitaría realizar la aritmética necesaria buf
directamente y emitir el resultado al final.
Entonces, ¿por qué es esto un problema para una char
matriz y no un búfer devuelto por malloc
? Porque la memoria devuelta nomalloc
tiene un tipo efectivo hasta que almacena algo en ella, que es lo que describen el párrafo 6 y la nota al pie 87.
Entonces, desde un punto de vista estricto del estándar, lo que está haciendo es un comportamiento indefinido. Pero dependiendo de su compilador, es posible que pueda deshabilitar el alias estricto para que esto funcione. Si está usando gcc, querrá pasar la -fno-strict-aliasing
bandera
El Estándar no requiere que las implementaciones consideren la posibilidad de que el código alguna vez observe un valor en a T*
que no esté alineado para el tipo T. En clang, por ejemplo, cuando se dirigen a plataformas cuyas instrucciones de carga / almacenamiento "más grandes" no admiten acceso no alineado, convertir un puntero en un tipo cuya alineación no satisface y luego usarlo memcpy
puede resultar en que el compilador genere código que fallará si el puntero no está alineado, aunque en memcpy
sí mismo no impondría ningún requisito de alineación.
Al apuntar a un ARM Cortex-M0 o Cortex-M3, por ejemplo, dado:
void test1(long long *dest, long long *src)
{
memcpy(dest, src, sizeof (long long));
}
void test2(char *dest, char *src)
{
memcpy(dest, src, sizeof (long long));
}
void test3(long long *dest, long long *src)
{
*dest = *src;
}
clang generará código tanto para test1 como para test3 que fallaría si src
o dest
no estuviera alineado, pero test2
generará código que es más grande y más lento, pero que admitirá la alineación arbitraria de los operandos de origen y destino.
Sin duda, incluso con un sonido metálico, el acto de convertir un puntero no alineado en un puntero long long*
no hará que suceda nada extraño por sí solo, pero es el hecho de que dicha conversión produciría UB lo que exime al compilador de cualquier responsabilidad de manejar el caso de puntero no alineado en test1
.