2017-05-29 2 views
4

POSIX beabsichtigt, Zeiger auf Variationen von struct sockaddr gießbar zu machen, jedoch kann dies je nach Interpretation des C-Standards eine Verletzung der strengen Aliasing-Regel und daher UB sein. (Siehe this answer mit Kommentaren darunter.) Ich kann zumindest bestätigen, dass es zumindest ein Problem mit gcc sein kann: Dieser Code druckt Bug! mit Optimierung aktiviert ist, und Yay! mit Optimierung deaktiviert:Wie legal Typ-Punning mit Unionen zu verwenden, um zwischen Variationen von struct Sockaddr zu übertragen, ohne die strenge Aliasing-Regel zu verletzen?

#include <sys/types.h> 
#include <netinet/in.h> 
#include <stdio.h> 

sa_family_t test(struct sockaddr *a, struct sockaddr_in *b) 
{ 
    a->sa_family = AF_UNSPEC; 
    b->sin_family = AF_INET; 
    return a->sa_family; // AF_INET please! 
} 

int main(void) 
{ 
    struct sockaddr addr; 
    sa_family_t x = test(&addr, (struct sockaddr_in*)&addr); 
    if(x == AF_INET) 
     printf("Yay!\n"); 
    else if(x == AF_UNSPEC) 
     printf("Bug!\n"); 
    return 0; 
} 

dies beachten Verhalten auf einem online IDE.

Um dieses Problem zu umgehen this answer schlägt die Verwendung von Typ punning mit Gewerkschaften:

/*! Multi-family socket end-point address. */ 
typedef union address 
{ 
    struct sockaddr sa; 
    struct sockaddr_in sa_in; 
    struct sockaddr_in6 sa_in6; 
    struct sockaddr_storage sa_stor; 
} 
address_t; 

aber anscheinend sind die Dinge noch nicht so einfach, wie sie aussehen ... this comment von @zwol Zitiert:

Das kann arbeiten, aber braucht ein gutes Stück Sorgfalt. Mehr als ich in dieses Kommentarfeld passen kann.

Welche Art von gerechtes bisschen Sorgfalt dauert es? Was sind die Fallstricke der Verwendung von Typ Punning mit Gewerkschaften zwischen Varianten von struct sockaddr zu werfen?

Ich bevorzuge zu fragen, als in UB zu laufen.

+0

Es ist nicht klar, was Sie eigentlich mit der "Union" prowpem ist. Wie wäre es mit einem [MCVE] und mehr Details über Ihre Bedenken? Warum fragst du nicht zwol was er meint? Wir sind keine Hellseher. – Olaf

+0

@Olaf Warum nicht zwol fragen? Denn wie ich ihn zitierte, hat er schon gesagt, dass er nicht in Kommentaren darüber reden wollte. Was ist mit einem minimalen, vollständigen und verifizierbaren Beispiel? Nun, ich stelle diese Frage genau, weil ich vermeiden will, in eine mir unbekannte Fallgrube zu fallen, die es notwendig machen würde, solch ein minimales, vollständiges und überprüfbares Beispiel zu schaffen. Wenn es um UB in C geht, denke ich, dass der Satz "besser verhindern als heilen" vollständig gilt. – gaazkam

+0

Ich bin nicht sicher, dass Sie alles tun können, ohne die ganze Reihe von Schnittstellen, die sockaddr beinhalten, zu überarbeiten. Was auch immer Sie tun bestehende Funktion immer noch erwarten struct sockaddr * und nicht irgendeine Art von Union. –

Antwort

2

ein union wie diese Verwendung ist sicher,

von C11 §6.5.2.3:

  1. Eine Postfix-Expression durch die gefolgt. Operator und eine Kennung bezeichnen ein Mitglied von eine Struktur oder ein Vereinigungsobjekt. Der Wert ist der des benannten Members, 95) und ist ein Lvalue, wenn der erste Ausdruck ein Lvalue ist. Wenn der erste Ausdruck einen qualifizierten Typ hat, hat das Ergebnis die so qualifizierte Version des Typs des angegebenen Mitglieds.

95) Wenn das Mitglied den Inhalt einer Union Objekt zu lesen, verwendet nicht das gleiche wie das Element ist zuletzt Speicher ein Wert in dem Objekt verwendet wird, den entsprechenden Teil der Objektdarstellung des Wertes neu interpretiert als Objektdarstellung in dem neuen Typ, wie in 6.2.6 beschrieben (ein Prozess, der manchmal '' punning '' genannt wird). Dies könnte eine Trap-Repräsentation sein.

und

  1. Eine besondere Garantie wird gemacht, um die Verwendung von Gewerkschaften zu vereinfachen: wenn eine Vereinigung mehrere Strukturen enthält, die eine gemeinsame Anfangssequenz teilen (siehe unten), und wenn die Vereinigung Objekt zur Zeit ein dieser Strukturen enthält, es erlaubt ist, den gemeinsamen Anfangsteil eines von ihnen zu untersuchen überall, dass eine Erklärung der fertiggestellten Art der Vereinigung ist sichtbar. zwei Strukturen teilen eine gemeinsame Anfangsfolge, wenn entsprechende Elemente kompatible Typen (und, für Bitfelder, die gleiche Breite) für eine Folge von einem oder mehreren Anfangs Mitglieder

(hervorgehoben, was ich denken, ist am wichtigsten)

mit dem struct sockaddr Mitglied Zugriff auf Sie wird Lesen aus dem gemeinsamen Anfangsteil.


Hinweis: Dies wird nicht macht es sicher Zeiger auf die Mitglieder um überall zu passieren und erwartet, dass der Compiler weiß, dass sie auf das gleiche gespeicherte Objekt beziehen. Daher kann die literale Version Ihres Beispielcodes möglicherweise weiterhin beschädigt werden, da in Ihrer test() die union nicht bekannt ist.

Beispiel:

#include <stdio.h> 

struct foo 
{ 
    int fooid; 
    char x; 
}; 

struct bar 
{ 
    int barid; 
    double y; 
}; 

union foobar 
{ 
    struct foo a; 
    struct bar b; 
}; 

int test(struct foo *a, struct bar *b) 
{ 
    a->fooid = 23; 
    b->barid = 42; 
    return a->fooid; 
} 

int test2(union foobar *a, union foobar *b) 
{ 
    a->a.fooid = 23; 
    b->b.barid = 42; 
    return a->a.fooid; 
} 

int main(void) 
{ 
    union foobar fb; 
    int result = test(&fb.a, &fb.b); 
    printf("%d\n", result); 
    result = test2(&fb, &fb); 
    printf("%d\n", result); 
    return 0; 
} 

Hier test() könnte brechen, aber test2() richtig sein.

+0

Aus dem angezeigten Code zu schließen, scheint OP nicht auf den gemeinsamen Teil zuzugreifen (was identische Namen ** und Typen ** bedeuten würde). Es ist nicht klar, wie OP beabsichtigt, mit der "Gewerkschaft" umzugehen, nur ein "Gewerkschafts" -Objekt zu haben bedeutet nicht, dass Hinweise auf die Mitglieder für Punning sicher sind. – Olaf

+0

Wie ich den Standard lese, müssen nur die Typen kompatibel sein (nicht unbedingt identisch) und die Namen spielen keine Rolle. Aber siehe meine bearbeitete Notiz: Es ist wirklich wichtig, dass die Vereinigung bekannt ist. –

+0

Was ich will, ist in der Lage sein, die IP-Adresse von der 'struct sockaddr' von' recvfrom' oder 'accept' abrufen, ohne UB auszulösen, und weniger wichtig, eine manuell gefüllte' struct sockaddr_in' zu 'sendto' oder zu übergeben 'connect', erneut ohne UB auszulösen. Ohne diese "Union" müsste ich ein paar Pointer-Castings machen, und IIUC I könnte UB auslösen, sobald ich etwas wie "(struct sockaddr *) (& sa_in)" oder "(struct sockaddr_in *) (& sa)" schreibe.Ich hoffe, ich werde zumindest in der Lage sein, den Gewerkschaftsmitgliedern auf Funktionen wie 'sento',' recvfrom', 'accept',' connect' usw. zu verweisen. – gaazkam

1

die address_t Vereinigung Sie

typedef union address 
{ 
    struct sockaddr sa; 
    struct sockaddr_in sa_in; 
    struct sockaddr_in6 sa_in6; 
    struct sockaddr_storage sa_stor; 
} 
address_t; 

und eine variable als address_t,

address_t addr; 

Sie sicher addr.sa.sa_family erklärt schlagen Gegeben initialisieren und lesen dann addr.sa_in.sin_family (oder jedes andere Paar _family aliased Felder). Sie können auch sicher addr in einem Anruf zu recvfrom, recvmsg, accept oder jedem anderen Socket-Grundelement verwenden, das einen struct sockaddr * Out-Parameter, z.

bytes_read = recvfrom(sockfd, buf, sizeof buf, &addr.sa, sizeof addr); 
if (bytes_read < 0) goto recv_error; 
switch (addr.sa.sa_family) { 
    case AF_INET: 
    printf("Datagram from %s:%d, %zu bytes\n", 
      inet_ntoa(addr.sa_in.sin_addr), addr.sa_in.sin_port, 
      (size_t) bytes_read); 
    break; 
    case AF_INET6: 
    // etc 
} 

Und Sie können auch in der anderen Richtung gehen,

memset(&addr, 0, sizeof addr); 
addr.sa_in.sin_family = AF_INET; 
addr.sa_in.sin_port = port; 
inet_aton(address, &addr.sa_in.sin_addr); 
connect(sockfd, &addr.sa, sizeof addr.sa_in); 

Es ist auch address_t Puffer mit malloc zuzuteilen in Ordnung, oder betten sie in einer größeren Struktur.

Was ist nicht sicher ist, Zeiger auf einzelne Unterstrukturen einer address_t Vereinigung zu Funktionen, die Sie schreiben, übergeben. Zum Beispiel Ihre test Funktion ...

sa_family_t test(struct sockaddr *a, struct sockaddr_in *b) 
{ 
    a->sa_family = AF_UNSPEC; 
    b->sin_family = AF_INET; 
    return a->sa_family; // AF_INET please! 
} 

... kann nicht mit (void *)a gleich (void *)b genannt werden, auch wenn dies geschieht, weil die callsite &addr.sa und &addr.sa_in als Argumente übergeben. Einige Leute haben argumentiert, dass dies erlaubt sein sollte, wenn eine vollständige Deklaration von address_t im Geltungsbereich war, wenn test definiert wurde, aber das ist zu viel wie "spukhafte Fernwirkung" für den Compiler-Entwickler; Die von der aktuellen Compilergeneration übernommene Interpretation der Regel "Common Initial Subsequence" (die in der Antwort von Felix zitiert wird) gilt nur dann, wenn der Unionstyp statisch und lokal an einem bestimmten Zugriff beteiligt ist. Sie müssen stattdessen

sa_family_t test2(address_t *x) 
{ 
    x->sa.sa_family = AF_UNSPEC; 
    x->sa_in.sa_family = AF_INET; 
    return x->sa.sa_family; 
} 

schreiben Sie fragen sich vielleicht, warum es okay ist, &addr.sa-connect dann zu übergeben. Sehr grob, hat connect seine eigene interne address_t Vereinigung, und es beginnt mit so etwas wie

int connect(int sock, struct sockaddr *addr, socklen_t len) 
{ 
    address_t xaddr; 
    memcpy(xaddr, addr, len); 

an welcher Stelle es sicher xaddr.sa.sa_family inspizieren und dann xaddr.sa_in.sin_addr oder was auch immer.

Ob es in Ordnung wäre, connect nur Guss seine addr Argument address_t *, wenn der Anrufer möglicherweise nicht solche Vereinigung selbst verwendet haben, mir ist unklar; Ich kann mir Argumente sowohl aus dem Text des Standards vorstellen (der in bestimmten Schlüsselpunkten nicht eindeutig ist und mit den genauen Bedeutungen der Wörter "Objekt", "Zugriff" und "effektiver Typ" zu tun hat), und ich nicht wissen, was Compiler eigentlich machen würden. In der Praxis muss connect trotzdem eine Kopie machen, da es sich um einen Systemaufruf handelt und fast alle Speicherblöcke, die über die Benutzer/Kernel-Grenze laufen, kopiert werden müssen.

Verwandte Themen