Ist es gut definiert, einen falsch ausgerichteten Zeiger zu halten, solange Sie ihn nie dereferenzieren?

Lesezeit: 5 Minuten

Ich habe einen C-Code, der gepackte/nicht aufgefüllte Binärdaten analysiert, die aus dem Netzwerk kommen.

Dieser Code funktionierte/funktionierte gut unter Intel/x86, aber als ich ihn unter ARM kompilierte, stürzte er oft ab.

Der Übeltäter, wie Sie vielleicht vermutet haben, waren nicht ausgerichtete Zeiger – insbesondere der Parsing-Code würde fragwürdige Dinge wie die folgenden tun:

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord = *((int32_t *) &buf[5]);  // misaligned access -- can crash under ARM!

… das wird offensichtlich nicht in ARM-Land fliegen, also habe ich es so modifiziert, dass es eher so aussieht:

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t * pNextWord = (int32_t *) &buf[5];
int32 nextWord;
memcpy(&nextWord, pNextWord, sizeof(nextWord));  // slower but ARM-safe

Meine Frage (aus Sicht eines Sprachanwalts) lautet: Ist mein “ARM-fester” Ansatz nach den C-Sprachregeln gut definiert?

Meine Sorge ist, dass vielleicht sogar nur ein falsch ausgerichteter int32_t-Zeiger ausreichen könnte, um undefiniertes Verhalten hervorzurufen, selbst wenn ich ihn nie direkt dereferenziere. (Wenn meine Bedenken berechtigt sind, denke ich, dass ich das Problem durch eine Änderung beheben könnte pNextWord‘s Typ von (const int32_t *) zu (const char *)aber ich würde das lieber nicht tun, es sei denn, es ist tatsächlich notwendig, da dies bedeuten würde, einige Zeigerschritt-Arithmetik von Hand durchzuführen)

  • Zugriff auf den Inhalt von pNextWord ergibt unabhängig von der Ausrichtung eine strikte Aliasing-Verletzung. Sie haben hier also zwei Fälle von schwerem UB. Verwenden memcpy um diesen Fehler auch zu vermeiden.

    – Ludin

    6. Juli 2018 um 8:34 Uhr


  • Darauf könntest du verzichten pNextWord und mach einfach memcpy(&nextWord, &buf[5], sizeof(nextWord));

    – dbusch

    6. Juli 2018 um 14:35 Uhr

Antti Haapala -- Benutzeravatar von Слава Україні
Antti Haapala – Слава Україні

Nein, der neue Code hat immer noch undefiniertes Verhalten. C11 6.3.2.3p7:

  1. Ein Zeiger auf einen Objekttyp kann in einen Zeiger auf einen anderen Objekttyp umgewandelt werden. Wenn der resultierende Zeiger nicht richtig ausgerichtet ist 68) für den referenzierten Typ ist das Verhalten undefiniert. […]

Es sagt nichts über die Dereferenzierung des Zeigers aus – selbst die Konvertierung hat ein undefiniertes Verhalten.


In der Tat ist der geänderte Code, von dem Sie annehmen, dass ARM-sicher ist möglicherweise nicht einmal Intel-sicher. Es ist bekannt, dass Compiler Code für Intel generieren, der bei nicht ausgerichtetem Zugriff abstürzen kann. Obwohl dies nicht im verknüpften Fall der Fall ist, kann es sein, dass ein cleverer Compiler die Konvertierung als nachweisen dass die Adresse tatsächlich ausgerichtet ist, und verwenden Sie einen speziellen Code dafür memcpy.


Abgesehen von der Ausrichtung leidet Ihr erster Auszug auch unter einer strikten Aliasing-Verletzung. C11 6.5p7:

  1. Auf den gespeicherten Wert eines Objekts darf nur durch 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 Typ mit oder ohne Vorzeichen ist, der dem effektiven Typ des Objekts entspricht,
    • ein Typ, der der Typ mit oder ohne Vorzeichen ist, der einer qualifizierten Version des effektiven Typs des Objekts entspricht,
    • ein Aggregat- oder Vereinigungstyp, der einen der oben genannten Typen in seinen Mitgliedern enthält (einschließlich rekursiv eines Mitglieds eines Unteraggregats oder einer enthaltenen Vereinigung), oder
    • ein Zeichentyp.

Da das Array buf[2048] ist statisch getipptjedes Element ist charund daher sind die effektiven Typen der Elemente char; Sie können auf den Inhalt des Arrays zugreifen nur als Zeichen, nicht als int32_ts.

Dh sogar

int32_t nextWord = *((int32_t *) &buf[_Alignof(int32_t)]);

hat undefiniertes Verhalten.

  • Sie interpretieren C11 6.3.2.3p7 falsch, das von einer Fehlausrichtung gegenüber dem referenzierten Typ spricht, dh Integer, Struct usw., und nicht von einer Fehlausrichtung des Speicherzugriffs.

    – DewiW

    6. Juli 2018 um 14:00 Uhr

  • Abgesehen von der Ausrichtung glaube ich, dass der Zugriff als Inhalt von gut definiert ist buf sind nicht zugegriffen wird als int. pNextWord wird in a umgewandelt void * bei Übergabe an die memcpy Funktion, die dann die Bytes auf sichere Weise kopiert.

    – dbusch

    6. Juli 2018 um 14:34 Uhr

  • @DewiW Ich interpretiere nichts falsch. Ich werde klären.

    – Antti Haapala – Слава Україні

    6. Juli 2018 um 15:38 Uhr

  • @dbush du hast recht. Fehlende Klarstellung dort, ich meinte den Originalcode.

    – Antti Haapala – Слава Україні

    6. Juli 2018 um 15:39 Uhr

Benutzeravatar von lee qiaoping
lee qiaoping

Um Multi-Byte-Ganzzahlen sicher über Compiler/Plattformen hinweg zu analysieren, können Sie jedes Byte extrahieren und sie entsprechend dem Endian zu Ganzzahlen zusammenfügen. So lesen Sie beispielsweise eine 4-Byte-Ganzzahl aus dem Big-Endian-Puffer:

uint8_t* buf = any address;

uint32_t val = 0;
uint32_t  b0 = buf[0];
uint32_t  b1 = buf[1];
uint32_t  b2 = buf[2];
uint32_t  b3 = buf[3];

val = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;

  • obwohl dieser Code auch undefiniertes Verhalten hat: D (aber zum Beispiel GCC garantiert korrektes Verhalten). b0 würde befördert werden zu a unterzeichnet int und dann könnte 1 auf das Vorzeichenbit des 32-Bit verschoben werden int – besser alle b0 – b3 als zu deklarieren uint32_t.

    – Antti Haapala – Слава Україні

    6. Juli 2018 um 9:10 Uhr


  • Beispielcode wurde umgestaltet, danke @ Antti Haapala für den Hinweis, die meiste meiner Arbeit betrifft Linux/Windows, ^^.

    – lee qiaoping

    6. Juli 2018 um 9:43 Uhr

Einige Compiler können davon ausgehen, dass kein Zeiger jemals einen Wert enthalten wird, der nicht richtig für seinen Typ ausgerichtet ist, und führen Optimierungen durch, die darauf beruhen. Betrachten Sie als einfaches Beispiel:

void copy_uint32(uint32_t *dest, uint32_t *src)
{
  memcpy(dest, src, sizeof (uint32_t));
}

Wenn beides dest und src 32-Bit-ausgerichtete Adressen halten, könnte die obige Funktion sogar auf Plattformen, die nicht ausgerichtete Zugriffe nicht unterstützen, auf ein Laden und ein Speichern optimiert werden. Wenn die Funktion deklariert wurde, Argumente vom Typ zu akzeptieren void*jedoch wäre eine solche Optimierung auf Plattformen nicht erlaubt, wo sich nicht ausgerichtete 32-Bit-Zugriffe anders verhalten würden als eine Folge von Byte-Zugriffen, Verschiebungen und bitweisen Operationen.

Wie in der Antwort von Antti Haapala erwähnt, führt das einfache Konvertieren eines Zeigers in einen anderen Typ, wenn der resultierende Zeiger nicht richtig ausgerichtet ist, zu undefiniertem Verhalten gemäß Abschnitt 6.3.2.3p7 des C-Standards.

Ihr geänderter Code verwendet nur pNextWord zu übergehen memcpywo es in a umgewandelt wird void *Sie brauchen also nicht einmal eine Variable vom Typ uint32_t *. Übergeben Sie einfach die Adresse des ersten Bytes im Puffer, aus dem Sie lesen möchten memcpy. Dann brauchen Sie sich überhaupt keine Gedanken über die Ausrichtung zu machen.

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord;
memcpy(&nextWord, &buf[5], sizeof(nextWord));

1392400cookie-checkIst es gut definiert, einen falsch ausgerichteten Zeiger zu halten, solange Sie ihn nie dereferenzieren?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy