[英]will casting around sockaddr_storage and sockaddr_in break strict aliasing
按照我之前的問題 ,我對這段代碼非常好奇 -
case AF_INET:
{
struct sockaddr_in * tmp =
reinterpret_cast<struct sockaddr_in *> (&addrStruct);
tmp->sin_family = AF_INET;
tmp->sin_port = htons(port);
inet_pton(AF_INET, addr, tmp->sin_addr);
}
break;
在提出這個問題之前,我已經搜索了關於同一主題的SO,並對此主題進行了混合回答。 例如,看到這個 , 這個和這個帖子說,使用這種代碼在某種程度上是安全的。 還有另一篇文章說使用工會來完成這樣的任務,但是對接受的答案的評論再次提出不同意見。
微軟關於相同結構的文檔說 -
應用程序開發人員通常只使用SOCKADDR_STORAGE的ss_family成員。 其余成員確保SOCKADDR_STORAGE可以包含IPv6或IPv4地址,並且適當填充結構以實現64位對齊。 這種對齊使協議特定的套接字地址數據結構能夠訪問SOCKADDR_STORAGE結構中的字段而不會出現對齊問題。 通過填充,SOCKADDR_STORAGE結構的長度為128個字節。
Opengroup的文件說明 -
標題應定義sockaddr_storage結構。 該結構應為:
足夠大以容納所有支持的協議特定的地址結構
在適當的邊界對齊,以便指向它的指針可以作為指向協議特定地址結構的指針,並用於訪問這些結構的字段而沒有對齊問題
socket的 man頁面也說同樣 -
此外,套接字API提供數據類型struct sockaddr_storage。 此類型適用於容納所有受支持的特定於域的套接字地址結構; 它足夠大並且正確對齊。 (特別是,它足以容納IPv6套接字地址。)
我已經看到了在C
和C++
語言中使用這種演員的多種實現,現在我不確定哪一個是正確的,因為有些帖子與上述聲明相矛盾 - 這就是 這個 。
那么哪一個是填充sockaddr_storage
結構的安全正確的方法? 這些指針轉換是否安全? 還是工會方法 ? 我也知道getaddrinfo()
調用,但對於剛剛填充結構的上述任務來說,這似乎有點復雜。 memcpy還有另外一種推薦的方法 ,這樣安全嗎?
在過去十年中,C和C ++編譯器已經變得比設計sockaddr
界面時更加復雜,甚至在編寫C99時也是如此。 作為其中的一部分,“未定義行為”的理解目的已經改變。 在當天,未定義的行為通常旨在涵蓋硬件實現之間關於操作的語義是什么的不一致。 但是現在,最終要感謝許多想要停止編寫FORTRAN且能夠支付編譯工程師來實現這一目標的組織,未定義的行為是編譯器用來推斷代碼的事情。 左移是一個很好的例子:C99 6.5.7p3,4(為了清晰而重新排列)讀取
E1 << E2
的結果是E1
左移E2
位位置; 騰出的位用零填充。 如果[E2
]的值為負或大於或等於提升的[E1
]的寬度,則行為未定義。
因此,例如, 1u << 33
是unsigned int
為32位寬的平台上的UB。 委員會對此進行了定義,因為不同的CPU體系結構的左移指令在這種情況下做了不同的事情:一些產生零一致,一些減少移位計數模數的寬度(x86),一些減少移位數模數一些更大的數字(ARM),至少有一個歷史上常見的架構會陷阱(我不知道哪一個,但這就是為什么它是未定義的而不是未指定的)。 但是現在,如果你寫的話
unsigned int left_shift(unsigned int x, unsigned int y)
{ return x << y; }
在具有32位unsigned int
的平台上,編譯器知道上述UB規則,將推斷在調用函數時y
必須具有0到32范圍內的值 。 它會將該范圍提供給過程間分析,並使用它來執行諸如在調用者中刪除不必要的范圍檢查之類的操作。 如果程序員有理由認為它們不是不必要的,那么現在你就開始明白為什么這個主題就是這樣一種蠕蟲。
有關未定義行為目的的更改,請參閱LLVM人員關於該主題的三篇文章( 1 2 3 )。
既然你明白了,我實際上可以回答你的問題。
在消除了一些不相關的復雜問題之后,這些是struct sockaddr
, struct sockaddr_in
和struct sockaddr_storage
的定義:
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;
};
這是窮人的子類。 它是C中無處不在的習語。你定義了一組結構,它們都具有相同的初始字段,這是一個代碼編號,告訴你實際上已經傳遞了哪個結構。 早在一天,大家都預計,如果您分配並裝入struct sockaddr_in
,上溯造型它struct sockaddr
,並把它遞給如connect
,執行connect
可提領的struct sockaddr
指針安全地檢索sa_family
領域,得知它正在看着一個sockaddr_in
,把它sockaddr_in
,然后繼續。 C標准總是說取消引用struct sockaddr
指針觸發未定義的行為 - 這些規則自C89以來沒有改變 - 但是每個人都認為在這種情況下它是安全的,因為無論哪種結構它都是相同的“加載16位”指令你真的和我一起工作。 這就是POSIX和Windows文檔談論對齊的原因; 早在20世紀90年代,編寫這些規范的人認為, 實際上可能遇到麻煩的主要方式是,如果你最后發布一個錯位的內存訪問。
但是標准的文本沒有說明加載指令,也沒有對齊。 這就是它所說的(C99§6.5p7+腳注):
對象的存儲值只能由具有以下類型之一的左值表達式訪問: 73)
- 與對象的有效類型兼容的類型,
- 與對象的有效類型兼容的類型的限定版本,
- 與對象的有效類型對應的有符號或無符號類型的類型,
- 與有效類型的對象的限定版本對應的有符號或無符號類型的類型,
- 聚合或聯合類型,包括其成員中的上述類型之一(包括遞歸地,子聚合或包含聯合的成員),或者
- 一個字符類型。
73)此列表的目的是指定對象可能或可能不具有別名的情況。
struct
類型只與自身“兼容”,聲明變量的“有效類型”是其聲明的類型。 所以你展示的代碼......
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;
...具有未定義的行為,編譯器可以從中做出推論, 即使天真的代碼生成將按預期運行。 現代編譯器可能從中推斷出case AF_INET
永遠不會被執行 。 它將刪除整個塊作為死代碼,並且隨之而來的是歡鬧。
那么你如何安全地使用sockaddr
? 最簡單的答案是“只使用getaddrinfo
和getnameinfo
。 他們為你處理這個問題。
但也許您需要使用getaddrinfo
無法處理的地址系列,例如AF_UNIX
。 在大多數情況下,您只需為地址族聲明一個正確類型的變量,並僅在調用帶有struct sockaddr *
的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;
}
connect
的實現必須跳過一些箍以使其安全,但這不是你的問題。
魂斗羅對方的回答, 有一個情況下,你可能想使用sockaddr_storage
; 與getpeername
和getnameinfo
一起,在需要同時處理IPv4和IPv6地址的服務器中。 這是一種了解分配緩沖區大小的便捷方法。
#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;
}
(我也可以編寫struct sockaddr_storage addrbuf
,但我想強調一下,我實際上並不需要直接訪問addrbuf
的內容。)
最后要注意的:如果BSD人已經確定sockaddr結構只是一點點不同...
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;
};
由於“包含上述類型之一的聚合或聯合”規則,...向上和向下傾斜將完全明確定義。 如果您想知道如何在新的C代碼中處理這個問題,那么就去吧。
是的,執行此操作違反了別名。 所以不要。 有沒有必要曾經使用sockaddr_storage
; 這是一個歷史錯誤。 但是有一些安全的方法可以使用它:
malloc(sizeof(struct sockaddr_storage))
。 在這種情況下,指向內存在您存儲內容之前沒有有效類型。 sockaddr
類型( in
和in6
以及un
)放入union中,而不是sockaddr_storage
。 在現代編程當然,你不應該需要創建一個類型的對象struct sockaddr_*
在所有 。 只需使用getaddrinfo
和getnameinfo
在字符串表示和sockaddr
對象之間轉換地址,並將后者視為完全不透明的對象 。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.