C - Konvertierungsverhalten zwischen zwei Zeigern

Dec 11 2020

Update 2020-12-11: Danke @ "Some programmer dude" für den Vorschlag im Kommentar. Mein zugrunde liegendes Problem ist, dass unser Team eine dynamische Speicher-Engine implementiert. Wir weisen mehrere Zeichenarray-Puffer [PAGE_SIZE] mit 16 Ausrichtungen zu, um dynamische Datentypen zu speichern (es gibt keine feste Struktur). Aus Effizienzgründen können wir keine Bytecodierung durchführen oder zusätzlichen Speicherplatz zuweisen memcpy.

Da die Ausrichtung bestimmt wurde (dh 16), besteht der Rest darin, die Umwandlung des Zeigers zu verwenden, um auf Objekte des angegebenen Typs zuzugreifen, zum Beispiel:

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

Unser Team bestreitet jedoch, ob es sich bei dieser Operation um ein undefiniertes Verhalten handelt.


Ich habe viele ähnliche Fragen gelesen, wie z

  • Warum warnt -Wcast-align nicht vor der Umwandlung von char * nach int * auf x86?
  • Wie wird das char-Array an einer nicht ausgerichteten Position in int umgewandelt?
  • C undefiniertes Verhalten. Strenge Aliasing-Regel oder falsche Ausrichtung?
  • SEI CERT C CS EXP36-C

Aber diese unterscheiden sich von meiner Interpretation des C-Standards. Ich möchte wissen, ob es mein Missverständnis ist.

Die Hauptverwirrung betrifft den Abschnitt 6.3.2.3 # 7 von C11:

Ein Zeiger auf einen Objekttyp kann in einen Zeiger auf einen anderen Objekttyp konvertiert werden. Wenn der resultierende Zeiger für den referenzierten Typ nicht korrekt ausgerichtet ist 68), ist das Verhalten undefiniert.

68) Im Allgemeinen ist das Konzept "korrekt ausgerichtet" transitiv: Wenn ein Zeiger auf Typ A für einen Zeiger auf Typ B korrekt ausgerichtet ist, der wiederum für einen Zeiger auf Typ C korrekt ausgerichtet ist, dann ein Zeiger auf Typ A ist für einen Zeiger auf Typ C korrekt ausgerichtet.

Bezieht sich der resultierende Zeiger hier auf Zeigerobjekt oder Zeigerwert ?

Meiner Meinung nach ist die Antwort das Zeigerobjekt , aber mehr Antworten scheinen den Zeigerwert anzuzeigen .


Interpretation A: Zeigerobjekt

Meine Gedanken sind wie folgt: Ein Zeiger selbst ist ein Objekt. Gemäß 6.2.5 # 28 können unterschiedliche Zeiger unterschiedliche Darstellungs- und Ausrichtungsanforderungen haben. Daher können gemäß 6.3.2.3 # 7 , solange zwei Zeiger dieselbe Ausrichtung haben, sie ohne undefiniertes Verhalten sicher konvertiert werden, es gibt jedoch keine Garantie dafür, dass sie dereferenziert werden können. Drücken Sie diese Idee in einem Programm aus:

#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");
    }
}

Interpretation B: Zeigerwert

Wenn jedoch der C11 - Standard spricht Zeigerwert für referenzierte Typ anstatt Zeigerobjekt . Die Ausrichtungsprüfung des obigen Codes ist bedeutungslos. Drücken Sie diese Idee in einem Programm aus:

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

Welche Interpretation ist richtig?

Antworten

6 dbush Dec 11 2020 at 01:36

Interpretation B ist richtig. Der Standard spricht von einem Zeiger auf ein Objekt, nicht auf das Objekt selbst. "Resultierender Zeiger" bezieht sich auf das Ergebnis der Umwandlung, und eine Umwandlung erzeugt keinen l-Wert, daher bezieht sie sich auf den Zeigerwert nach der Umwandlung.

Unter den Code in Ihrem Beispiel an , dass ein intauf einer 4 - Byte - Grenze ausgerichtet sein muss, dh es Adresse ein Vielfaches von 4 sein muss , wenn die Adresse bufwird 0x1001dann die Adresse Umwandlung zu int *ungültig ist , weil der Zeigerwert nicht richtig ausgerichtet ist. Wenn die Adresse bufwird 0x1000dann die Umstellung auf int *gültig ist .

Aktualisieren:

Der Code, den Sie hinzugefügt haben, behebt das Ausrichtungsproblem, daher ist diesbezüglich alles in Ordnung. Es hat jedoch ein anderes Problem: Es verstößt gegen striktes Aliasing.

Das von Ihnen definierte Array enthält Objekte vom Typ char. Indem Sie die Adresse in einen anderen Typ umwandeln und anschließend den konvertierten Typentyp dereferenzieren, greifen Sie auf Objekte eines Typs als Objekte eines anderen Typs zu. Dies ist nach dem C-Standard nicht zulässig.

Obwohl der Begriff "striktes Aliasing" in der Norm nicht verwendet wird, wird das Konzept in Abschnitt 6.5 Absätze 6 und 7 beschrieben:

6 Der effektive Typ eines Objekts für den Zugriff auf seinen gespeicherten Wert ist der deklarierte Typ des Objekts, falls vorhanden. 87) Wenn ein Wert in einem Objekt ohne deklarierten Typ über einen l-Wert mit einem Typ gespeichert wird, der kein Zeichentyp ist, wird der Typ des l-Werts zum effektiven Typ des Objekts für diesen Zugriff und für nachfolgende Zugriffe, die dies nicht tun Ändern Sie den gespeicherten Wert. Wenn ein Wert mit memcpyoder in ein Objekt ohne deklarierten Typ memmovekopiert wird oder als Array mit Zeichentyp kopiert wird, ist der effektive Typ des geänderten Objekts für diesen Zugriff und für nachfolgende Zugriffe, die den Wert nicht ändern, der effektive Typ des Objekts, von dem der Wert kopiert wird, falls vorhanden. Bei allen anderen Zugriffen auf ein Objekt ohne deklarierten Typ ist der effektive Typ des Objekts einfach der Typ des für den Zugriff verwendeten l-Werts.

7 Auf einen gespeicherten Wert eines Objekts darf nur über einen lvalue-Ausdruck zugegriffen werden, der einen der folgenden Typen hat: 88)

  • ein Typ, der mit dem effektiven Typ des Objekts kompatibel ist,
  • eine qualifizierte Version eines Typs, der mit dem effektiven Typ des Objekts kompatibel ist;
  • ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der dem effektiven Typ des Objekts entspricht;
  • ein Typ, der der vorzeichenbehaftete oder nicht vorzeichenbehaftete Typ ist, der einer qualifizierten Version des effektiven Typs des Objekts entspricht;
  • ein Aggregat- oder Vereinigungstyp, der einen der oben genannten Typen unter seinen Mitgliedern enthält (einschließlich rekursiv eines Mitglieds eines Unteraggregats oder einer enthaltenen Vereinigung), oder
  • ein Zeichentyp.

...

87) Zugeordnete Objekte haben keinen deklarierten Typ.

88) Mit dieser Liste sollen die Umstände angegeben werden, unter denen ein Objekt möglicherweise einen Alias ​​aufweist oder nicht.

In Ihrem Beispiel schreiben Sie ein unsigned longund ein doubleauf charObjekte. Keiner dieser Typen erfüllt die Bedingungen von Absatz 7.

Darüber hinaus ist die Zeigerarithmetik hier nicht gültig:

 *(((double *) buf) + 2) = 1.618;

Wie Sie bufals Array behandeln, doublewenn es nicht ist. Zumindest müssten Sie die erforderliche Arithmetik bufdirekt ausführen und das Ergebnis am Ende umwandeln.

Warum ist dies ein Problem für ein charArray und nicht für einen von zurückgegebenen Puffer malloc? Da kehrten Speicher aus mallochaben keine effektive Art , bis Sie speichern etwas in ihm, das ist das, was Absatz 6 und Fußnote 87 beschreiben.

Aus strenger Sicht des Standards ist das, was Sie tun, undefiniertes Verhalten. Abhängig von Ihrem Compiler können Sie möglicherweise das strikte Aliasing deaktivieren, damit dies funktioniert. Wenn Sie gcc verwenden, möchten Sie die -fno-strict-aliasingFlagge übergeben

1 supercat Dec 11 2020 at 05:09

Der Standard verlangt nicht, dass Implementierungen die Möglichkeit berücksichtigen, dass Code jemals einen Wert in a beobachtet T*, der nicht für Typ T ausgerichtet ist. Das Konvertieren eines Zeigers in einen Typ, dessen Ausrichtung nicht erfüllt, und die anschließende Verwendung memcpykann dazu führen, dass der Compiler Code generiert, der fehlschlägt, wenn der Zeiger nicht ausgerichtet ist, obwohl er memcpyselbst sonst keine Ausrichtungsanforderungen auferlegt.

Wenn Sie beispielsweise auf einen ARM Cortex-M0 oder Cortex-M3 abzielen, geben Sie Folgendes an:

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 generiert sowohl für test1- als auch für test3-Code, der fehlschlagen würde, wenn er ausgerichtet wäre srcoder destnicht, aber für test2ihn wird Code generiert, der größer und langsamer ist, aber eine willkürliche Ausrichtung der Quell- und Zieloperanden unterstützt.

Selbst wenn das Konvertieren eines nicht ausgerichteten Zeigers in einen Zeiger long long*im Allgemeinen nicht dazu führt, dass etwas Seltsames von selbst passiert, ist es die Tatsache, dass eine solche Konvertierung UB erzeugt, die den Compiler von jeglicher Verantwortung für den Umgang mit dem Zeiger befreit Fall eines nicht ausgerichteten Zeigers in test1.