fno - Richtiger, portabler Weg, den Puffer als Struktur zu interpretieren




fno-strict-aliasing (2)

Der Kontext meines Problems liegt in der Netzwerkprogrammierung. Angenommen, ich möchte Nachrichten zwischen zwei Programmen über das Netzwerk senden. Der Einfachheit halber wollen wir sagen, Nachrichten sehen so aus, und die Reihenfolge der Bytes spielt keine Rolle. Ich möchte eine korrekte, portable und effiziente Möglichkeit finden, diese Nachrichten als C-Strukturen zu definieren. Ich kenne vier Ansätze dafür: explizites Casting, Casting durch eine Union, Kopieren und Marshalling.

struct message {
    uint16_t logical_id;
    uint16_t command;
};

Explizites Casting:

void send_message(struct message *msg) {
    uint8_t *bytes = (uint8_t *) msg;
    /* call to write/send/sendto here */
}

void receive_message(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message *msg = (struct message*) bytes;
    /* And now use the message */
    if (msg->command == SELF_DESTRUCT)
        /* ... */
}

Mein Verständnis ist, dass send_message keine Aliasing-Regeln verletzt, weil ein Byte / Char-Zeiger einen beliebigen Typ send_message kann. Die Umkehrung ist jedoch nicht richtig, und so verletzt receive_message Aliasing-Regeln und hat daher undefiniertes Verhalten.

Casting durch eine Union:

union message_u {
    struct message m;
    uint8_t bytes[sizeof(struct message)];
};

void receive_message_union(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    union message_u *msgu = bytes;
    /* And now use the message */
    if (msgu->m.command == SELF_DESTRUCT)
        /* ... */
}

Dies scheint jedoch die Vorstellung zu verletzen, dass eine Gewerkschaft zu irgendeinem Zeitpunkt nur eines ihrer Mitglieder enthält. Darüber hinaus scheint dies zu Ausrichtungsproblemen führen, wenn der Quellpuffer nicht auf einer Wort / Halbwort-Grenze ausgerichtet ist.

Kopieren:

void receive_message_copy(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message msg;
    memcpy(&msg, bytes, sizeof msg);
    /* And now use the message */
    if (msg.command == SELF_DESTRUCT)
        /* ... */
}

Dies scheint garantiert das richtige Ergebnis zu liefern, aber natürlich würde ich es vorziehen, die Daten nicht kopieren zu müssen.

Marshalling

void send_message(struct message *msg) {
    uint8_t bytes[4];
    bytes[0] = msg.logical_id >> 8;
    bytes[1] = msg.logical_id & 0xff;
    bytes[2] = msg.command >> 8;
    bytes[3] = msg.command & 0xff;
    /* call to write/send/sendto here */
}

void receive_message_marshal(uint8_t *bytes, size_t len) {
    /* No longer relying on the size of the struct being meaningful */
    assert(len >= 4);    
    struct message msg;
    msg.logical_id = (bytes[0] << 8) | bytes[1];    /* Big-endian */
    msg.command = (bytes[2] << 8) | bytes[3];
    /* And now use the message */
    if (msg.command == SELF_DESTRUCT)
        /* ... */
}

Immer noch kopieren, aber jetzt entkoppelt von der Darstellung der Struktur. Aber jetzt müssen wir mit der Position und Größe jedes Mitglieds explizit sein, und Endian-Sein ist ein viel offensichtlicheres Problem.

Verwandte Informationen:

Was ist die strenge Aliasing-Regel?

Aliasing-Array mit Pointer-to-Struct ohne Verletzung des Standards

Wann ist char * sicher für striktes Pointer-Aliasing?

http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

Beispiel der realen Welt

Ich habe nach Beispielen für Netzwerkcode gesucht, um zu sehen, wie diese Situation anderswo gehandhabt wird. Die leichte IP hat ein paar ähnliche Fälle. In der Datei udp.c liegt der folgende Code:

/**
 * Process an incoming UDP datagram.
 *
 * Given an incoming UDP datagram (as a chain of pbufs) this function
 * finds a corresponding UDP PCB and hands over the pbuf to the pcbs
 * recv function. If no pcb is found or the datagram is incorrect, the
 * pbuf is freed.
 *
 * @param p pbuf to be demultiplexed to a UDP PCB (p->payload pointing to the UDP header)
 * @param inp network interface on which the datagram was received.
 *
 */
void
udp_input(struct pbuf *p, struct netif *inp)
{
  struct udp_hdr *udphdr;

  /* ... */

  udphdr = (struct udp_hdr *)p->payload;

  /* ... */
}

struct udp_hdr eine gepackte Repräsentation eines udp-Headers ist und p->payload vom Typ void * . Wenn ich auf mein Verständnis und this Antwort zugreife, dann ist dies definitiv das [Bearbeiten-] Nicht-Brechen von Strict-Aliasing und hat somit undefiniertes Verhalten.


Der einzig richtige Weg ist, wie Sie vermutet haben, die Daten aus dem char in Ihre Struktur zu kopieren. Ihre anderen Alternativen verletzen die strengen Alias-Regeln oder die Ein-Mitglied-der-Union-aktive Regel.

Ich möchte noch einen Moment nehmen, um dich daran zu erinnern, dass selbst wenn du dies auf einem einzelnen Host machst und die Byte-Reihenfolge egal ist, du trotzdem sicherstellen musst, dass beide Enden der Verbindung mit den gleichen Optionen und der Struktur aufgebaut sind ist auf die gleiche Weise gepolstert, die Typen haben die gleiche Größe, etc. Ich schlage vor, unter Berücksichtigung einer echten Serialisierungsimplementierung zumindest eine kleine Zeit zu nehmen, so dass, wenn Sie jemals eine breitere Reihe von Bedingungen unterstützen müssen, Sie keine haben großes Update vor dir dann.


Ich denke, das ist, was ich versucht habe zu vermeiden, aber ich ging schließlich und schaute selbst auf den C99-Standard . Hier ist, was ich gefunden habe (Hervorhebung hinzugefügt):
§6.3.2.2 ungültig

1 Der (nicht vorhandene) Wert eines void-Ausdrucks (ein Ausdruck, der den Typ void hat) darf in keiner Weise verwendet werden, und implizite oder explizite Konvertierungen (mit Ausnahme von void) dürfen nicht auf einen solchen Ausdruck angewendet werden. Wenn ein Ausdruck eines anderen Typs als void-Ausdruck ausgewertet wird, wird sein Wert oder Bezeichner verworfen. (Ein ungültiger Ausdruck wird auf seine Nebenwirkungen hin bewertet.)

§6.3.2.3 Zeiger

1 Ein Zeiger auf void kann in oder von einem Zeiger in einen unvollständigen oder Objekttyp konvertiert werden . Ein Zeiger auf einen unvollständigen oder Objekttyp kann in einen Zeiger auf void und zurück konvertiert werden; Das Ergebnis muss mit dem ursprünglichen Zeiger verglichen werden.

Und §3.14

1 Objekt
Bereich des Datenspeichers in der Ausführungsumgebung, dessen Inhalt Werte darstellen kann

§6.5

Auf einen gespeicherten Wert eines Objekts darf nur ein Lvalue-Ausdruck zugreifen, der einen der folgenden Typen aufweist:
- ein Typ, der mit dem effektiven Objekttyp kompatibel ist,
- eine quali fi zierte Version eines Typs, der mit dem effektiven Objekttyp kompatibel ist,
- ein Typ, bei dem es sich um den Typ mit oder ohne Vorzeichen handelt, der dem effektiven Typ des Objekts entspricht,
- ein Typ, bei dem es sich um den Typ mit oder ohne Vorzeichen handelt, der einer quali fi zierten Version des effektiven Objekttyps entspricht,
- Ein Aggregat- oder Unionstyp, der einen der oben genannten Typen enthält
Mitglieder (einschließlich, rekursiv, ein Mitglied eines Unteraggregats oder enthaltenen Union), oder
- ein Zeichentyp

§6.5

Der effektive Typ eines Objekts für einen Zugriff auf seinen gespeicherten Wert ist der deklarierte Typ des Objekts
Objekt, falls vorhanden. Wenn ein Wert in einem Objekt gespeichert wird, das keinen deklarierten Typ über einen L-Wert mit einem Typ hat, 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 das nicht modifizieren gespeicherter Wert . Wenn ein Wert mit memcpy oder memmove in ein Objekt ohne deklarierten Typ kopiert wird oder als Array eines Zeichentyps 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 Effektiver Typ des Objekts, von dem der Wert kopiert wird, falls er einen Wert hat. Für alle anderen Zugriffe auf ein Objekt ohne deklarierten Typ ist der effektive Typ des Objekts einfach der Typ des für den Zugriff verwendeten lvalue.

§J.2 Undefiniertes Verhalten

- Es wird versucht, den Wert eines void-Ausdrucks zu verwenden, oder eine implizite oder explizite Konvertierung (mit Ausnahme von void) wird auf einen void-Ausdruck angewendet (6.3.2.2).

Fazit

Es ist in Ordnung (gut definiert), nach und von einer void* zu werfen void* , aber nicht OK, um einen Wert vom Typ void in C99 zu verwenden . Daher ist das "reale Beispiel" kein undefiniertes Verhalten. Daher kann die explizite Casting-Methode mit der folgenden Änderung verwendet werden, solange für Ausrichtung, Auffüllung und Byte-Reihenfolge gesorgt wird:

void receive_message(void *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message *msg = (struct message*) bytes;
    /* And now use the message */
    if (msg->command == SELF_DESTRUCT)
        /* ... */
}




strict-aliasing