Was ist die Semantik überlappender Objekte in C?

25

Betrachten Sie die folgende Struktur:

struct s {
  int a, b;
};

Typischerweise hat 1 diese Struktur die Größe 8 und die Ausrichtung 4.

Was ist, wenn wir zwei struct sObjekte erstellen (genauer gesagt, wir schreiben zwei solche Objekte in den zugewiesenen Speicher), wobei das zweite Objekt das erste überlappt?

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);

Ist etwas an diesem Programm undefiniertes Verhalten? Wenn ja, wo wird es undefiniert? Wenn es nicht UB ist, wird garantiert immer Folgendes gedruckt:

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

Insbesondere möchte ich wissen, was mit dem Objekt passiert, auf das hingewiesen wird, o1wenn o2, was es überlappt, geschrieben wird. Ist es weiterhin erlaubt, auf den nicht überlasteten Teil ( o1->a) zuzugreifen ? Ist der Zugriff auf das überlastete Teil o1->beinfach der gleiche wie der Zugriff o2->a?

Wie gilt hier der effektive Typ ? Die Regeln sind klar genug, wenn Sie über nicht überlappende Objekte und Zeiger sprechen, die auf dieselbe Position wie der letzte Speicher verweisen. Wenn Sie jedoch über den effektiven Typ von Teilen von Objekten oder überlappenden Objekten sprechen, ist dies weniger klar.

Würde sich etwas ändern, wenn der zweite Schreibvorgang von einem anderen Typ wäre? Wenn die Mitglieder sagen würden intund shortnicht zwei ints?

Hier ist ein Godbolt, wenn Sie dort damit spielen möchten.


1 Diese Antwort gilt für Plattformen, auf denen dies ebenfalls nicht der Fall ist: Einige haben möglicherweise Größe 4 und Ausrichtung 2. Auf einer Plattform, auf der Größe und Ausrichtung gleich waren, würde diese Frage nicht zutreffen, da ausgerichtete, überlappende Objekte dies tun würden unmöglich sein, aber ich bin mir nicht sicher, ob es eine solche Plattform gibt.

BeeOnRope
quelle
2
Ich bin mir ziemlich sicher, dass es UB ist, aber ich werde einen Sprachanwalt Kapitel und Verse liefern lassen.
Barmar
Ich denke, dass der C-Compiler auf den alten Cray-Vektorsystemen die gleiche Ausrichtung und Größe erzwungen hat, mit einem ILP64-Modell und erzwungener 64-Bit-Ausrichtung (Adressen sind 64-Bit-Wörter - keine Byteadressierung). Natürlich verursachte dies viele andere Probleme ...
John D McCalpin

Antworten:

15

Grundsätzlich ist dies alles Grauzone im Standard; Die strikte Aliasing-Regel legt grundlegende Fälle fest und überlässt es dem Leser (und den Compiler-Anbietern), die Details einzugeben.

Es wurden Anstrengungen unternommen, um eine bessere Regel zu schreiben, aber bisher haben sie keinen normativen Text ergeben, und ich bin mir nicht sicher, wie der Status für C2x lautet.

Wie in meiner Antwort auf Ihre vorherige Frage erwähnt, ist die häufigste Interpretation das p->qMittel (*p).qund der effektive Typ gilt für alle *p, auch wenn wir uns dann weiter bewerben .q.

Nach dieser Interpretation printf("o1.a=%d\n", o1->a);würde dies zu undefiniertem Verhalten führen, da der effektive Typ des Standorts *o1nicht vorhanden ist s(da ein Teil davon überschrieben wurde).

Die Gründe für diese Interpretation können in einer Funktion gesehen werden wie:

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

Mit dieser Interpretation könnte die letzte Zeile optimiert werden puts("5");, aber ohne sie müsste der Compiler berücksichtigen, dass der Funktionsaufruf möglicherweise f(o1, o2);alle Vorteile gewesen ist und daher alle Vorteile verliert, die angeblich durch die strenge Aliasing-Regel bereitgestellt werden.

Ein ähnliches Argument gilt für zwei nicht verwandte Strukturtypen, bei denen beide zufällig ein intElement mit unterschiedlichem Versatz haben.

MM
quelle
1
Mit f(s* s1, s* s2), ohne restrictkann der Compiler keine unterschiedlichen Zeiger annehmen s1und s2sind diese. Ich denke , auch ohne restrictkann es nicht einmal annehmen, dass sie sich nicht teilweise überlappen. IAC, ich sehe nicht, dass die Besorgnis von OP durch die f()Analogie gut demonstriert wird . Viel Glück beim Entwirren. UV für die erste Hälfte.
chux - Monica
@ chux-ReinstateMonica ohne Einschränkung s1 == s2wäre erlaubt, aber keine teilweise Überlappung. (Die Optimierung in meinem Codebeispiel könnte noch durchgeführt werden, wenn s1 == s2)
MM
@ chux-ReinstateMonica Sie könnten das gleiche Problem auch mit nur intanstelle von Strukturen (und einem System mit _Alignof(int) < sizeof(int)) betrachten.
MM
3
Der Status dieser Art von Frage bezüglich des effektiven Typs für C2x ist ziemlich offen und wird in der Studiengruppe immer noch diskutiert. Seien Sie jedoch vorsichtig, wenn Sie die Gleichwertigkeit von p->qund beanspruchen (*p).q. Dies mag für die von Ihnen angegebene Typinterpreation zutreffen, aus betrieblicher Sicht jedoch nicht. Für gleichzeitige Zugriffe auf dieselbe Struktur ist es wichtig, dass ein Zugriff eines Mitglieds nicht den Zugriff eines anderen Mitglieds impliziert.
Jens Gustedt
Bei der strengen Aliasing-Regel geht es um den Zugriff . Der Ausdruck auf der linken Seite im E1.E2Ausdruck führt keinen Zugriff aus (ich meine den gesamten E1Ausdruck. Einige seiner Unterausdrücke führen möglicherweise den Zugriff aus. Wenn dies der Fall E1ist (*p), wird der Zeigerwert bei der Auswertung pals Zugriff gelesen , aber die Auswertung von *poder (*p)führt keinen aus Zugriff). Die strikte Aliasing-Regel gilt nicht, wenn kein Zugriff besteht.
Sprachanwalt