簡體   English   中英

如何在不違反嚴格別名規則的情況下合法地使用類型懲罰與聯合在struct sockaddr的變體之間進行投射?

[英]How to legally use type-punning with unions to cast between variations of struct sockaddr without violating the strict aliasing rule?

POSIX打算將struct sockaddrstruct sockaddr指向可轉換的,但是根據C標准的解釋,這可能違反了嚴格的別名規則,因此違反了UB。 (請參閱下面的評論這個答案 。)我至少可以確認gcc可能至少存在問題:此代碼打印Bug! 啟用優化,並且Yay! 禁用優化:

#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;
}

聯機IDE上觀察此行為。

要解決此問題, 此答案建議使用帶有工會的類型懲罰:

/*! 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;

然而,顯然事情仍然不像他們看起來那么簡單......引用@zwol的評論

可以工作,但需要相當多的照顧。 超過我可以適應這個評論框。

需要什么樣的照顧 struct sockaddrstruct sockaddr之間使用類型雙關語進行轉換會有什么陷阱?

我更願意問,而不是碰到UB。

使用這樣的union是安全的,

來自C11§6.5.2.3:

  1. 后綴表達式后跟。 運算符和標識符指定結構或聯合對象的成員。 該值是指定成員的值,95)如果第一個表達式是左值,則它是左值。 如果第一個表達式具有限定類型,則結果具有指定成員類型的限定版本。

95)如果用於讀取union對象內容的成員與上次用於在對象中存儲值的成員不同,則該值的對象表示的適當部分將被重新解釋為新對象表示如6.2.6所述的類型(有時稱為''punning''的過程)。 這可能是陷阱表示。

  1. 為了簡化聯合的使用,我們做了一個特殊的保證: 如果一個聯合包含幾個共享一個共同初始序列的結構 (見下文),並且如果聯合對象當前包含這些結構中的一個, 則允許檢查公共其中任何一個的初始部分都可以看到完整類型的聯合聲明。 如果對應的成員具有一個或多個初始成員的序列的兼容類型 (並且對於位字段,具有相同的寬度),則兩個結構共享共同的初始序列

(突出了我認為最重要的)

通過訪問struct sockaddr成員,您將從 常見的初始部分中讀取。


注意 :這不會使之安全指針傳遞給成員任何地方,並期望編譯器知道他們指的是同一個存儲的對象。 因此,您的示例代碼的文字版本可能仍會中斷,因為在您的test()union是未知的。

例:

#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;
}

這里, test()可能會中斷,但test2()會正確。

鑒於你提出的address_t聯盟

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

和一個聲明為address_t的變量,

address_t addr; 

你可以安全地初始化addr.sa.sa_family ,然后讀取addr.sa_in.sin_family (或任何其他一對別名的_family字段)。 您還可以安全地使用addr來調用recvfromrecvmsgaccept或任何其他帶有struct sockaddr * out-parameter的套接字原語,例如

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
}

你也可以向另一個方向走,

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);

使用malloc分配address_t緩沖區或將其嵌入更大的結構中也是可以的。

什么不安全的是指向一個的單獨的子結構傳遞address_t聯盟,你寫的功能。 例如,你的test功能......

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!
}

...可能不會調用(void *)a等於(void *)b ,即使發生這種情況也是因為調用點傳遞了&addr.sa&addr.sa_in作為參數。 有些人曾經認為,當定義test時, address_t的完整聲明在范圍內時應該允許這樣做,但這對於編譯器開發來說太像“ spukhafte Fernwirkung ”了; 當前一代編譯器采用的“共同初始子序列”規則(引用Felix的答案)的解釋是,它僅適用於聯合類型在特定訪問中靜態和本地參與的情況。 你必須改寫

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;
}

您可能想知道為什么可以通過&addr.saconnect 非常粗略, connect有自己的內部address_t聯合,它以類似的東西開頭

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

此時它可以安全地檢查xaddr.sa.sa_family然后xaddr.sa_in.sin_addr或其他什么。

無論是會好起來的connect ,以只它的addr參數address_t * ,當主叫用戶可能不會使用這種聯盟本身,我不清楚; 我可以從標准的文本中想象兩種方式的論點(在某些關鍵點上與“對象”,“訪問”和“有效類型”這兩個詞的確切含義有關​​,而且我不這樣做)知道編譯器實際會做什么。 實際上, connect無論如何都必須進行復制,因為它是一個系統調用,幾乎所有通過用戶/內核邊界傳遞的內存塊都必須被復制。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM