Korrekte, portable Art, Puffer als Struktur zu interpretieren

Lesezeit: 8 Minuten

Benutzeravatar von Croyd
Croyd

Der Kontext meines Problems liegt in der Netzwerkprogrammierung. Angenommen, ich möchte Nachrichten zwischen zwei Programmen über das Netzwerk senden. Nehmen wir der Einfachheit halber an, dass Nachrichten so aussehen und die Byte-Reihenfolge keine Rolle spielt. Ich möchte einen korrekten, portablen und effizienten Weg 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 das send_message verstößt nicht gegen Aliasing-Regeln, da ein Byte/Char-Zeiger jeden Typ aliasieren kann. Die Umkehrung gilt jedoch nicht, und so receive_message verstößt gegen 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 gegen die Idee zu verstoßen, dass eine Gewerkschaft zu einem bestimmten Zeitpunkt nur eines ihrer Mitglieder enthält. Darüber hinaus scheint dies zu Ausrichtungsproblemen führen zu können, wenn der Quellpuffer nicht an 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.

Rangieren

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)
        /* ... */
}

Muss noch kopiert werden, aber jetzt von der Darstellung der Struktur entkoppelt. Aber jetzt müssen wir die Position und Größe jedes Mitglieds explizit angeben, und Endianness ist ein viel offensichtlicheres Problem.

Verwandte Informationen:

Was ist die strikte 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 aus der realen Welt

Ich habe nach Beispielen für Netzwerkcode gesucht, um zu sehen, wie diese Situation anderswo gehandhabt wird. Das leichtes ip hat ein paar ähnliche Fälle. In dem udp.c Datei liegt folgender 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;

  /* ... */
}

wo struct udp_hdr ist eine gepackte Darstellung eines UDP-Headers und p->payload ist vom Typ void *. Nach meinem Verständnis und dieser Antwort ist dies bestimmt [edit- not] bricht das strikte Aliasing und hat daher ein undefiniertes Verhalten.

  • Ich nehme an, der korrekteste (unkorrekt beabsichtigte) Weg wäre, eine neue Struktur zuzuweisen, die Werte separat zu lesen und sie einzeln auszufüllen.

    – Kninnug

    3. Oktober 2013 um 17:13 Uhr

  • @Kninnug ja, aber ich suche auch Effizienz & Eleganz. Ich hätte kein Problem damit, es für ein einfaches Beispiel wie dieses manuell zu tun, aber wenn es mehr Felder und nicht übereinstimmende Byte-Reihenfolgen gibt, wird es schnell hässlich.

    – Croyd

    3. Oktober 2013 um 17:17 Uhr

  • Da es Unterschiede in Endianness, Polsterung usw. geben könnte, wird es keinen glatten, einzeiligen, eleganten Weg geben, dies zu tun. Ich denke, @Kninnug hat recht damit, welcher Ansatz am robustesten wäre. Sie könnten eine Vereinigung oder eine Speicherkopie erstellen, aber dann müssen Sie einige starke Annahmen darüber treffen, was unter der Haube passiert, und es wird noch weniger portabel sein.

    – Schleicher

    3. Oktober 2013 um 17:20 Uhr

  • Bitte geben Sie getrennte Fragen für C und C++ ein und entfernen Sie eines der Tags aus dieser Frage. Die Antworten sind für die verschiedenen Sprachen unterschiedlich. Beispielsweise erlaubt C 1999 den Zugriff auf ein anderes Union-Member als das zuletzt gespeicherte (die Bytes werden als neuer Typ neu interpretiert), C++ jedoch nicht.

    – Eric Postpischil

    3. Oktober 2013 um 17:24 Uhr


  • Wenn Sie Probleme mit Endianness, Elementgröße, Polsterung und Ausrichtung nicht berücksichtigen, wäre es schwierig, das Ergebnis als “tragbar” zu bezeichnen.

    – Markieren Sie Lösegeld

    3. Oktober 2013 um 17:47 Uhr

Benutzeravatar von Croyd
Croyd

Ich schätze, das ist es, was ich zu vermeiden versucht habe, aber ich bin schließlich hingegangen und habe mir das angeschaut C99-Standard mich selbst. 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 vom Typ void) darf in keiner Weise verwendet werden, und auf einen solchen Ausdruck dürfen keine impliziten oder expliziten Konvertierungen (außer in void) angewendet werden. Wenn ein Ausdruck eines anderen Typs als ungültiger Ausdruck ausgewertet wird, wird sein Wert oder Bezeichner verworfen. (Ein void-Ausdruck wird auf seine Nebenwirkungen hin ausgewertet.)

§6.3.2.3 Zeiger

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

Und §3.14

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

§6.5

Auf den gespeicherten Wert eines Objekts darf nur durch einen lvalue-Ausdruck zugegriffen werden, der einen der folgenden Typen hat:
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 enthält
Mitglieder (einschließlich rekursiv eines Mitglieds eines Unteraggregats oder einer enthaltenen Vereinigung) oder
— ein Zeichentyp.

§6.5

Der effektive Typ eines Objekts für einen Zugriff auf seinen gespeicherten Wert ist der deklarierte Typ des
Objekt, ggf. Wenn ein Wert in einem Objekt gespeichert wird, das keinen deklarierten Typ hat, durch einen lvalue, der einen Typ hat, der kein Zeichentyp ist, dann wird der Typ des lvalue zum effektiven Typ des Objekts für diesen Zugriff und für nachfolgende Zugriffe, die das nicht ändern gespeicherter Wert. Wenn ein Wert mit memcpy oder memmove in ein Objekt kopiert wird, das keinen deklarierten Typ hat, oder wenn er als Array eines Zeichentyps kopiert wird, dann 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 vorhanden. Bei allen anderen Zugriffen auf ein Objekt ohne deklarierten Typ ist der effektive Typ des Objekts einfach der Typ des lvalue, der für den Zugriff verwendet wird.

§J.2 Undefiniertes Verhalten

— Es wird versucht, den Wert eines void-Ausdrucks zu verwenden, oder es wird eine implizite oder explizite Konvertierung (außer in void) auf einen void-Ausdruck angewendet (6.3.2.2).

Fazit

Es ist in Ordnung (wohldefiniert), zu und von a zu werfen void*aber nicht in Ordnung, um einen Wert vom Typ zu verwenden void in C99. Daher ist das „Beispiel aus der realen Welt“ kein undefiniertes Verhalten. Daher kann die explizite Casting-Methode mit der folgenden Modifikation verwendet werden, solange Ausrichtung, Polsterung und Byte-Reihenfolge beachtet werden:

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)
        /* ... */
}

Der einzig richtige Weg ist, wie Sie vermutet haben, die Daten aus der zu kopieren char Puffer in Ihre Struktur. Ihre anderen Alternativen verstoßen gegen die strengen Aliasregeln oder die Ein-Mitglied-der-Union-aktiv-Regel.

Ich möchte mir noch einen Moment Zeit nehmen, um Sie daran zu erinnern, dass Sie selbst dann, wenn Sie dies auf einem einzigen Host tun und die Byte-Reihenfolge keine Rolle spielt, immer noch sicherstellen müssen, dass beide Enden der Verbindung mit denselben Optionen aufgebaut werden und dass die struct wird auf die gleiche Weise aufgefüllt, die Typen haben die gleiche Größe usw. Ich schlage vor, dass Sie sich zumindest ein wenig Zeit nehmen, um eine echte Serialisierungsimplementierung in Betracht zu ziehen, damit Sie, wenn Sie jemals eine größere Anzahl von Bedingungen unterstützen müssen, keine haben Großes Update vor Ihnen dann.

  • Ich habe den Beitrag mit der Methode, die Sie meiner Meinung nach beschreiben, und einem Beispiel aus einem Open-Source-Projekt aktualisiert.

    – Croyd

    3. Oktober 2013 um 22:52 Uhr

  • Also habe ich alle Grundlagen in meinem Beitrag behandelt?

    – Croyd

    7. Oktober 2013 um 15:07 Uhr

1386550cookie-checkKorrekte, portable Art, Puffer als Struktur zu interpretieren

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

Privacy policy