getterò attorno a sockaddr_storage e sockaddr_in interromperà il rigoroso aliasing

Seguendo la mia domanda precedente, sono davvero curioso di questo codice –

case AF_INET: { struct sockaddr_in * tmp = reinterpret_cast (&addrStruct); tmp->sin_family = AF_INET; tmp->sin_port = htons(port); inet_pton(AF_INET, addr, tmp->sin_addr); } break; 

Prima di fare questa domanda, ho cercato su SO lo stesso argomento e ho avuto risposte miste su questo argomento. Ad esempio, vedi questo , questo e questo post che dicono che è in qualche modo sicuro usare questo tipo di codice. C’è anche un altro post che dice di usare i sindacati per tale compito, ma ancora una volta i commenti sulla risposta accettata cominciano a differire.


La documentazione di Microsoft sulla stessa struttura dice:

Gli sviluppatori di applicazioni normalmente utilizzano solo il membro ss_family di SOCKADDR_STORAGE. I membri rimanenti assicurano che SOCKADDR_STORAGE possa contenere un indirizzo IPv6 o IPv4 e che la struttura sia opportunamente riempita per ottenere l’allineamento a 64 bit. Tale allineamento consente alle strutture dati di indirizzo socket specifiche del protocollo di accedere ai campi all’interno di una struttura SOCKADDR_STORAGE senza problemi di allineamento. Con il suo riempimento, la struttura SOCKADDR_STORAGE ha una lunghezza di 128 byte.

La documentazione di Opengroup afferma:

L’intestazione deve definire la struttura sockaddr_storage. Questa struttura deve essere:

Abbastanza grande da contenere tutte le strutture di indirizzi specifiche del protocollo supportate

Allineati a un confine appropriato in modo che i puntatori a esso possano essere lanciati come puntatori alle strutture di indirizzo specifiche del protocollo e utilizzate per accedere ai campi di quelle strutture senza problemi di allineamento

Anche la pagina man di socket dice lo stesso –

Inoltre, l’API socket fornisce il tipo di dati struct sockaddr_storage. Questo tipo è adatto per ospitare tutte le strutture di indirizzo socket specifiche del dominio supportate; è abbastanza grande ed è allineato correttamente. (In particolare, è abbastanza grande da contenere indirizzi di socket IPv6.)


Ho visto l’implementazione multipla utilizzando tali cast in entrambi i C e C++ in natura e ora non sono sicuro del fatto che uno sia corretto poiché ci sono alcuni post che sono in contraddizione con le affermazioni precedenti: questo e questo .

Quindi qual è il modo sicuro e giusto per riempire una struttura sockaddr_storage ? Questi puntatori sono sicuri? o il metodo sindacale ? Sono anche a conoscenza della chiamata a getaddrinfo() ma sembra un po ‘complicato per il compito di cui sopra di riempire semplicemente le strutture. C’è un altro modo consigliato con memcpy , è sicuro?

I compilatori C e C ++ sono diventati molto più sofisticati nell’ultimo decennio di quanto lo fossero quando sono state progettate le interfacce sockaddr o anche quando è stato scritto C99. Come parte di ciò, lo scopo inteso di “comportamento indefinito” è cambiato. Nel corso della giornata, il comportamento non definito era di solito inteso a coprire il disaccordo tra le implementazioni hardware su cosa fosse la semantica di un’operazione. Ma oggigiorno, grazie in definitiva a un certo numero di organizzazioni che volevano smettere di scrivere FORTRAN e potevano permettersi di pagare gli ingegneri del compilatore per farlo accadere, il comportamento indefinito è una cosa che i compilatori usano per fare inferenze sul codice . Spostamento a sinistra è un buon esempio: letture C99 6.5.7p3,4 (riarrangiate un po ‘per chiarezza)

Il risultato di E1 << E2 è E1 con posizioni E2 spostate a sinistra; i bit vuoti sono pieni di zeri. Se il valore di [ E2 ] è negativo o è maggiore o uguale alla larghezza della promozione [ E1 ], il comportamento non è definito.

Quindi, per esempio, 1u << 33 è UB su una piattaforma in cui unsigned int larghezza di 32 bit. Il comitato lo ha reso indefinito perché le istruzioni sullo spostamento a sinistra dell'architettura delle CPU diverse fanno cose diverse in questo caso: alcune producono lo zero in modo coerente, alcune riducono il numero di turni modulo la larghezza del tipo (x86), alcune riducono il numero di turni modulo un numero maggiore (ARM), e almeno un'architettura storicamente comune si intrappolerebbe (non so quale, ma è per questo che è indefinita e non specificata). Ma al giorno d'oggi, se scrivi

 unsigned int left_shift(unsigned int x, unsigned int y) { return x << y; } 

su una piattaforma con unsigned int a 32 bit, il compilatore, conoscendo la regola UB sopra, dedurrà che y deve avere un valore compreso tra 0 e 32 quando viene chiamata la funzione. Inserirà quell'intervallo in analisi interprocedurale e lo userà per fare cose come rimuovere controlli di intervallo non necessari nei chiamanti. Se il programmatore ha motivo di pensare che non siano inutili, beh, ora inizi a capire perché questo argomento è una tale lattina di vermi.

Per ulteriori informazioni su questo cambiamento nello scopo di un comportamento non definito, consultare il saggio in tre parti delle persone LLVM sull'argomento ( 1 2 3 ).


Ora che lo capisci, posso effettivamente rispondere alla tua domanda.

Queste sono le definizioni di struct sockaddr , struct sockaddr_in e struct sockaddr_storage , dopo aver eliminato alcune complicazioni irrilevanti:

 struct sockaddr { uint16_t sa_family; }; struct sockaddr_in { uint16_t sin_family; uint16_t sin_port; uint32_t sin_addr; }; struct sockaddr_storage { uint16_t ss_family; char __ss_storage[128 - (sizeof(uint16_t) + sizeof(unsigned long))]; unsigned long int __ss_force_alignment; }; 

Questa è la sottoclass dei poveri. È un idioma onnipresente in C. Definisci un insieme di strutture che hanno tutte lo stesso campo iniziale, che è un numero di codice che ti dice quale struttura hai effettivamente passato. Nel passato, tutti si aspettavano che se si assegnasse e si compilasse una struct sockaddr_in , la si aggiornasse per struct sockaddr e la si passasse ad es. Connect, l'implementazione di connect poteva dereferenziare il puntatore struct sockaddr modo sicuro per recuperare il campo sa_family , imparare che guardava un sockaddr_in , lo sockaddr_in e procedeva. Lo standard C ha sempre detto che il dereferenziamento del puntatore struct sockaddr triggers un comportamento indefinito - quelle regole sono invariate dal C89 - ma tutti si aspettavano che sarebbe stato sicuro in questo caso perché sarebbe stata la stessa istruzione "carica 16 bit" indipendentemente dalla struttura tu stavi davvero lavorando. Ecco perché POSIX e la documentazione di Windows parlano di allineamento; le persone che hanno scritto quelle specifiche, negli anni '90, pensavano che il modo principale in cui questo potesse effettivamente essere un problema era se si finiva per rilasciare un accesso alla memoria disallineato.

Ma il testo dello standard non dice nulla sulle istruzioni di caricamento, né sull'allineamento. Questo è quello che dice (C99 §6.5p7 + nota in calce):

Un object deve avere il suo valore memorizzato accessibile solo da un'espressione lvalue che ha uno dei seguenti tipi: 73)

  • un tipo compatibile con il tipo effettivo dell'object,
  • una versione qualificata di un tipo compatibile con il tipo effettivo dell'object,
  • un tipo che è il tipo firmato o non firmato corrispondente al tipo effettivo dell'object,
  • un tipo che è il tipo firmato o senza segno corrispondente a una versione qualificata del tipo effettivo dell'object,
  • un tipo aggregato o sindacale che include uno dei tipi sopra menzionati tra i suoi membri (incluso, in modo ricorsivo, un membro di un'unione subaggregata o contenuta), o
  • un tipo di personaggio.

73) L'intento di questa lista è di specificare quelle circostanze in cui un object può o non può essere aliasato.

struct tipi di struct sono "compatibili" solo con se stessi e il "tipo effettivo" di una variabile dichiarata è il suo tipo dichiarato. Quindi il codice che hai mostrato ...

 struct sockaddr_storage addrStruct; /* ... */ case AF_INET: { struct sockaddr_in * tmp = (struct sockaddr_in *)&addrStruct; tmp->sin_family = AF_INET; tmp->sin_port = htons(port); inet_pton(AF_INET, addr, tmp->sin_addr); } break; 

... ha un comportamento indefinito, ei compilatori possono ricavarne inferenze, anche se la generazione di codice ingenuo si comporterebbe come previsto. Ciò che un moderno compilatore può dedurre da questo è che il case AF_INET non può mai essere eseguito . Cancellerà l'intero blocco come codice morto e ne deriverà l'ilarità.


Quindi, come lavori con sockaddr modo sicuro? La risposta più breve è "usa solo getaddrinfo e getnameinfo ". Si occupano di questo problema per te.

Ma forse hai bisogno di lavorare con una famiglia di indirizzi, come AF_UNIX , che getaddrinfo non gestisce. Nella maggior parte dei casi è sufficiente dichiarare una variabile del tipo corretto per la famiglia di indirizzi e lanciarla solo quando si chiamano funzioni che prendono una struct sockaddr *

 int connect_to_unix_socket(const char *path, int type) { struct sockaddr_un sun; size_t plen = strlen(path); if (plen >= sizeof(sun.sun_path)) { errno = ENAMETOOLONG; return -1; } sun.sun_family = AF_UNIX; memcpy(sun.sun_path, path, plen+1); int sock = socket(AF_UNIX, type, 0); if (sock == -1) return -1; if (connect(sock, (struct sockaddr *)&sun, offsetof(struct sockaddr_un, sun_path) + plen)) { int save_errno = errno; close(sock); errno = save_errno; return -1; } return sock; } 

L' implementazione del connect deve passare attraverso alcuni cerchi per renderlo sicuro, ma questo non è il tuo problema.

Contro l'altra risposta, c'è un caso in cui si potrebbe voler usare sockaddr_storage ; in combinazione con getpeername e getnameinfo , in un server che deve gestire entrambi gli indirizzi IPv4 e IPv6. È un modo conveniente per sapere quanto grande deve essere il buffer da allocare.

 #ifndef NI_IDN #define NI_IDN 0 #endif char *get_peer_hostname(int sock) { char addrbuf[sizeof(struct sockaddr_storage)]; socklen_t addrlen = sizeof addrbuf; if (getpeername(sock, (struct sockaddr *)addrbuf, &addrlen)) return 0; char *peer_hostname = malloc(MAX_HOSTNAME_LEN+1); if (!peer_hostname) return 0; if (getnameinfo((struct sockaddr *)addrbuf, addrlen, peer_hostname, MAX_HOSTNAME_LEN+1, 0, 0, NI_IDN) { free(peer_hostname); return 0; } return peer_hostname; } 

(Avrei potuto anche scrivere struct sockaddr_storage addrbuf , ma volevo sottolineare che non ho mai realmente bisogno di accedere direttamente ai contenuti di addrbuf .)

Un'ultima nota: se la gente di BSD avesse definito le strutture sockaddr solo un po 'diversamente ...

 struct sockaddr { uint16_t sa_family; }; struct sockaddr_in { struct sockaddr sin_base; uint16_t sin_port; uint32_t sin_addr; }; struct sockaddr_storage { struct sockaddr ss_base; char __ss_storage[128 - (sizeof(uint16_t) + sizeof(unsigned long))]; unsigned long int __ss_force_alignment; }; 

... gli up-up e i downcast sarebbero stati perfettamente definiti, grazie alla regola "aggregato o unione che include uno dei tipi sopra citati". Se ti stai chiedendo come dovresti affrontare questo problema con il nuovo codice C, ecco qui.

Sì, è una violazione di aliasing per fare questo. Quindi non farlo. Non è necessario utilizzare mai sockaddr_storage ; è stato un errore storico. Ma ci sono alcuni modi sicuri per usarlo:

  1. malloc(sizeof(struct sockaddr_storage)) . In questo caso, la memoria puntata non ha un tipo effettivo finché non si memorizza qualcosa.
  2. Come parte di un sindacato, accedendo esplicitamente al membro che desideri. Ma in questo caso basta inserire i tipi di sockaddr si desidera ( in e in6 e forse un ) nel sindacato anziché in sockaddr_storage .

Ovviamente nella programmazione moderna non dovrebbe mai essere necessario creare oggetti di tipo struct sockaddr_* affatto . È sufficiente utilizzare getaddrinfo e getnameinfo per tradurre gli indirizzi tra le rappresentazioni di stringa e gli oggetti sockaddr e considerare questi ultimi come oggetti completamente opachi .