¿&((Nombre de estructura*) NULL-> b) causa un comportamiento indefinido en C11?
language-lawyer offsetof (6)
Código de muestra:
struct name
{
int a, b;
};
int main()
{
&(((struct name *)NULL)->b);
}
¿Esto causa un comportamiento indefinido? Podríamos debatir si "desreferencia a nulo", sin embargo, C11 no define el término "desreferencia".
6.5.3.2/4 dice claramente que usar
*
en un puntero nulo provoca un comportamiento indefinido;
sin embargo, no dice lo mismo para
->
y tampoco define
a -> b
como
(*a).b
;
tiene definiciones separadas para cada operador.
La semántica de
->
en 6.5.2.3/4 dice:
Una expresión postfix seguida por el operador -> y un identificador designa un miembro de una estructura u objeto de unión. El valor es el del miembro nombrado del objeto al que apunta la primera expresión, y es un valor l.
Sin embargo,
NULL
no apunta a un objeto, por lo que la segunda oración parece poco especificada.
También relevante podría ser 6.5.3.2/1:
Restricciones:
El operando del operador unario
&
debe ser un designador de función, el resultado de un operador[]
o unario*
, o un valor l que designa un objeto que no es un campo de bits y no se declara con el especificador de clase de almacenamiento de registro .
Sin embargo, creo que el texto en negrita es defectuoso y debería leer lvalue que potencialmente designa un objeto , según 6.3.2.1/1 (definición de lvalue ) - C99 estropeó la definición de lvalue, por lo que C11 tuvo que reescribirlo y quizás esto La sección se perdió.
6.3.2.1/1 dice:
Un lvalue es una expresión (con un tipo de objeto distinto de void) que potencialmente designa un objeto; Si un valor no designa un objeto cuando se evalúa, el comportamiento es indefinido
sin embargo, el operador
&
evalúa su operando.
(No accede al valor almacenado pero eso es diferente).
Esta larga cadena de razonamiento parece sugerir que el código causa UB, sin embargo, es bastante tenue y no me queda claro qué pretendían los escritores de la Norma. Si de hecho tenían la intención de algo, en lugar de dejarnos a nosotros debatir :)
Comencemos con el operador de indirección
*
:
6.5.3.2 p4: El operador unario * denota indirección. Si el operando apunta a una función, el resultado es un designador de función; Si apunta a un objeto, el resultado es un valor l que designa el objeto. Si el operando tiene el tipo "puntero para escribir", el resultado tiene el tipo "tipo". Si se ha asignado un valor no válido al puntero, el comportamiento del operador unario
*
no está definido. 102)
* E, donde E es un puntero nulo, es un comportamiento indefinido.
Hay una nota al pie que dice:
102) Por lo tanto,
&*E
es equivalente a E (incluso si E es un puntero nulo) , y & (E1 [E2]) a ((E1) + (E2)). Siempre es cierto que si E es un designador de funciones o un valor l que es un operando válido del operador unario y único, * & E es un designador de funciones o un valor l igual a E. Si * P es un valor l y T es el nombre de un tipo de puntero de objeto, * (T) P es un valor l que tiene un tipo compatible con aquel al que apunta T.
Lo que significa que & * E, donde E es NULL, está definido, pero la pregunta es si lo mismo es cierto para & (* E) .m, donde E es un puntero nulo y su tipo es una estructura que tiene un miembro m ?
C Standard no define ese comportamiento.
Si se definiera, surgirían nuevos problemas, uno de los cuales se enumera a continuación. C Standard es correcto para mantenerlo indefinido, y proporciona un macro offsetof que maneja el problema internamente.
6.3.2.3 Punteros
- Una expresión constante entera con el valor 0, o tal expresión emitida para escribir void *, se llama constante de puntero nulo. 66) Si una constante de puntero nulo se convierte en un tipo de puntero, se garantiza que el puntero resultante, llamado puntero nulo, se comparará desigual a un puntero con cualquier objeto o función.
Esto significa que una expresión constante entera con el valor 0 se convierte en una constante de puntero nulo.
Pero el valor de una constante de puntero nulo no está definido como 0. El valor está definido por la implementación.
7.19 Definiciones comunes
- Las macros son NULL, que se expande a una constante de puntero nulo definida por la implementación
Esto significa que C permite una implementación donde el puntero nulo tendrá un valor en el que se establecen todos los bits y el uso del acceso de miembro en ese valor dará como resultado un desbordamiento que es un comportamiento indefinido
Otro problema es cómo evalúa & (* E) .m?
¿Se aplican los corchetes y
*
evalúa primero.
Mantenerlo indefinido resuelve este problema.
Desde el punto de vista de un abogado, la expresión
&(((struct name *)NULL)->b);
debería conducir a UB, ya que no podría encontrar una ruta en la que no hubiera UB.
En mi humilde opinión, la causa raíz es que en un momento se aplica el operador
->
en una expresión que no apunta a un objeto.
Desde el punto de vista del compilador, suponiendo que el programador del compilador no fuera demasiado complicado, está claro que la expresión devuelve el mismo valor que
offsetof(name, b)
, y estoy bastante seguro de que
siempre que se compile sin error
ningún compilador existente dará ese resultado.
Tal como está escrito, no podríamos culpar a un compilador que notaría que en la parte interna usa operador
->
en una expresión que no puede apuntar a un objeto (ya que es nulo) y emitir una advertencia o un error.
Mi conclusión es que hasta que haya un párrafo especial que diga que siempre que sea solo para tomar su dirección, es legal no hacer referencia a un puntero nulo, esta expresión no es legal C.
Nada en el estándar C impondría ningún requisito sobre lo que un sistema podría hacer con la expresión. Cuando se redactó el estándar, habría sido perfectamente razonable que causara la siguiente secuencia de eventos en tiempo de ejecución:
- El código carga un puntero nulo en la unidad de direccionamiento
-
El código le pide a la unidad de direccionamiento que agregue el desplazamiento del campo
b
. - La unidad de direccionamiento desencadena una trampa cuando intenta agregar un número entero a un puntero nulo (que debería ser, por robustez, una trampa en tiempo de ejecución, aunque muchos sistemas no la atrapen)
- El sistema comienza a ejecutar código esencialmente aleatorio después de ser enviado a través de un vector de trampa que nunca se configuró porque el código para configurarlo habría sido un desperdicio de memoria, ya que no deberían ocurrir las trampas de direccionamiento.
La esencia misma de lo que significaba Comportamiento Indefinido en ese momento.
Tenga en cuenta que la mayoría de los compiladores que han aparecido desde los primeros días de C considerarían que la dirección de un miembro de un objeto ubicado en una dirección constante es una constante de tiempo de compilación, pero no creo que ese comportamiento fuera obligatorio entonces, tampoco se ha agregado nada al estándar que obligue a que los cálculos de direcciones de tiempo de compilación que involucren punteros nulos se definan en casos donde los cálculos de tiempo de ejecución no lo harían.
No. Desarmemos esto:
&(((struct name *)NULL)->b);
es lo mismo que:
struct name * ptr = NULL;
&(ptr->b);
La primera línea es obviamente válida y bien definida.
En la segunda línea, calculamos la dirección de un campo relativo a la dirección
0x0
que también es perfectamente legal.
El Amiga, por ejemplo, tenía el puntero al núcleo en la dirección
0x4
.
Entonces, podría usar un método como este para llamar a las funciones del núcleo.
De hecho, se usa el mismo enfoque en el macro
offsetof
C (
wikipedia
):
#define offsetof(st, m) ((size_t)(&((st *)0)->m))
Entonces, la confusión aquí gira en torno al hecho de que los punteros NULL dan miedo.
Pero desde un compilador y un punto de vista estándar, la expresión es legal en C (C ++ es una bestia diferente ya que puede sobrecargar el operador
&
).
Primero, establezcamos que necesitamos un puntero a un objeto:
6.5.2.3 Estructura y miembros del sindicato
4 Una expresión postfix seguida del operador
->
y un identificador designa un miembro de una estructura u objeto de unión . El valor es el del miembro nombrado del objeto al que apunta la primera expresión, y es un valor l.96) Si la primera expresión es un puntero a un tipo calificado, el resultado tiene la versión calificada del tipo del miembro designado
Desafortunadamente, ningún puntero nulo apunta nunca a un objeto.
6.3.2.3 Punteros
3 Una expresión constante entera con el valor 0, o una expresión de este tipo convertida para escribir
void *
, se llama constante de puntero nulo .66) Si una constante de puntero nulo se convierte en un tipo de puntero, el puntero resultante, llamado puntero nulo , está garantizado para comparar desigual a un puntero a cualquier objeto o función .
Resultado: comportamiento indefinido.
Como nota al margen, algunas otras cosas para masticar:
6.3.2.3 Punteros
4 La conversión de un puntero nulo a otro tipo de puntero produce un puntero nulo de ese tipo. Dos punteros nulos se compararán igual.
5 Un entero se puede convertir a cualquier tipo de puntero. Excepto como se especificó previamente, 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.67)
6 Cualquier tipo de puntero se puede convertir a un tipo entero. Excepto como se especificó anteriormente, el resultado está definido por la implementación. Si el resultado no puede representarse en el tipo entero, el comportamiento es indefinido. El resultado no necesita estar en el rango de valores de ningún tipo de entero.67) Las funciones de mapeo para convertir un puntero en un entero o un entero en un puntero están destinadas a ser consistentes con la estructura de direccionamiento del entorno de ejecución.
Entonces, incluso si el UB fuera benigno esta vez , aún podría dar lugar a un número totalmente inesperado.
Sí, este uso de
->
tiene un comportamiento indefinido en el sentido directo del término inglés undefined.
El comportamiento solo se define si la primera expresión apunta a un objeto y no está definido (= indefinido) de lo contrario. En general, no debería buscar más en el término indefinido, significa solo eso: el estándar no proporciona un significado para su código. (A veces, señala explícitamente situaciones de este tipo que no define, pero esto no cambia el significado general del término).
Esta es una holgura que se introduce para ayudar a los creadores de compiladores a lidiar con las cosas.
Pueden
definir un comportamiento, incluso para el código que está presentando.
En particular, para la implementación de un compilador, está perfectamente bien usar dicho código o similar para la
offsetof
macro.
Hacer de este código una violación de restricción bloquearía esa ruta para las implementaciones del compilador.