¿Cuáles son las semánticas de los objetos superpuestos en C?

25

Considere la siguiente estructura:

struct s {
  int a, b;
};

Típicamente 1 , esta estructura tendrá tamaño 8 y alineación 4.

¿Qué pasa si creamos dos struct sobjetos (más precisamente, escribimos en el almacenamiento asignado dos de estos objetos), con el segundo objeto superpuesto al primero?

char *storage = malloc(3 * sizeof(struct s));
struct s *o1 = (struct s *)storage; // offset 0
struct s *o2 = (struct s *)(storage + alignof(struct s)); // offset 4

// now, o2 points half way into o1
*o1 = (struct s){1, 2};
*o2 = (struct s){3, 4};

printf("o2.a=%d\n", o2->a);
printf("o2.b=%d\n", o2->b);
printf("o1.a=%d\n", o1->a);
printf("o1.b=%d\n", o1->b);

¿Hay algo sobre este programa de comportamiento indefinido? Si es así, ¿dónde queda indefinido? Si no es UB, ¿se garantiza que siempre imprima lo siguiente?

o2.a=3
o2.b=4
o1.a=1
o1.b=3

En particular, quiero saber qué sucede con el objeto señalado por o1cuándo o2, que se superpone, está escrito. ¿Todavía está permitido acceder a la parte no ocultada ( o1->a)? ¿Acceder a la parte con capa es o1->bsimplemente lo mismo que acceder o2->a?

¿Cómo se aplica el tipo efectivo aquí? Las reglas son lo suficientemente claras cuando habla de objetos no superpuestos y punteros que apuntan a la misma ubicación que la última tienda, pero cuando comienza a hablar sobre el tipo efectivo de porciones de objetos u objetos superpuestos, es menos claro.

¿Cambiaría algo si la segunda escritura fuera de un tipo diferente? Si los miembros fueran decir inty shortno dos ints?

Aquí hay un godbolt si quieres jugar con él allí.


1 Esta respuesta se aplica a plataformas donde este no es el caso también: por ejemplo, algunas pueden tener tamaño 4 y alineación 2. En una plataforma donde el tamaño y la alineación son las mismas, esta pregunta no se aplicaría ya que los objetos alineados y superpuestos ser imposible, pero no estoy seguro de si hay alguna plataforma como esa.

BeeOnRope
fuente
2
Estoy bastante seguro de que es UB, pero dejaré que un abogado de idiomas proporcione capítulos y versículos.
Barmar
Creo que el compilador de C en los viejos sistemas de vectores Cray forzó la alineación y el tamaño para que fueran iguales, con un modelo ILP64 y una alineación forzada de 64 bits (las direcciones son palabras de 64 bits, sin direccionamiento de bytes). Por supuesto, esto generó muchos otros problemas ...
John D McCalpin

Respuestas:

15

Básicamente, esta es toda el área gris en el estándar; La estricta regla de alias especifica casos básicos y deja que el lector (y los proveedores del compilador) completen los detalles.

Se han realizado esfuerzos para escribir una regla mejor, pero hasta ahora no han dado lugar a ningún texto normativo y no estoy seguro de cuál es el estado de esto para C2x.

Como mencioné en mi respuesta a su pregunta anterior, la interpretación más común es que p->qsignifica (*p).qy el tipo efectivo se aplica a todos *p, a pesar de que luego procedemos a aplicar .q.

Según esta interpretación, printf("o1.a=%d\n", o1->a);causaría un comportamiento indefinido ya que el tipo efectivo de la ubicación *o1no lo es s(ya que parte de ella se ha sobrescrito).

La justificación de esta interpretación se puede ver en una función como:

void f(s* s1, s* s2)
{
    s2->a = 5;
    s1->b = 6;
    printf("%d\n", s2->a);
}

Con esta interpretación, la última línea podría optimizarse puts("5");, pero sin ella, el compilador tendría que considerar que la llamada a la función puede haber sido f(o1, o2);y, por lo tanto, perder todos los beneficios que supuestamente proporciona la estricta regla de alias.

Un argumento similar se aplica a dos tipos de estructura no relacionados que tienen un intmiembro con un desplazamiento diferente.

MM
fuente
1
Con f(s* s1, s* s2), sin restrict, el compilador no puede asumir s1y s2son punteros diferentes. Creo que , una vez más restrict, ni siquiera puede suponer que no se superponen parcialmente. IAC, no veo que la f()analogía demuestre bien la preocupación de OP . Buena suerte sin gruñir. UV para la primera mitad.
chux
@ chux-ReinstateMonica sin restricción, s1 == s2se permitiría, pero no una superposición parcial. (La optimización en mi ejemplo de código todavía podría realizarse si s1 == s2)
MM
@ chux-ReinstateMonica también podría considerar el mismo problema con solo en intlugar de structs (y un sistema con _Alignof(int) < sizeof(int)).
MM
3
El estado de este tipo de preguntas sobre el tipo efectivo para C2x es bastante abierto y aún está sujeto a debate en el grupo de estudio. Sin embargo, tenga cuidado al reclamar la equivalencia de p->qy (*p).q. Esto puede ser cierto para la interpretación de tipo como usted dice, pero no es cierto desde un punto de vista operativo. Es importante para los accesos concurrentes a la misma estructura que el acceso de un miembro no implique el acceso de ningún otro miembro.
Jens Gustedt
La estricta regla de alias es sobre el acceso . La expresión del lado izquierdo en la E1.E2expresión no realiza el acceso (me refiero a la E1expresión completa . Algunas de sus subexpresiones pueden realizar el acceso. Es decir, si E1es así (*p), leer el valor del puntero al evaluar el pacceso, pero la evaluación de *po (*p)no realiza ninguna acceso). La regla de alias estricta no se aplica en caso de que no haya acceso.
Abogado de idiomas