2017-05-29 22 views
4

POSIX打算指向struct sockaddr的变体,但是根据C标准的解释,这可能违反了严格的别名规则,因此违反了UB。 (见this answer与它下面的评论。)我可以,至少,在确认不存在至少可以用gcc的一个问题:这个代码打印Bug!与优化启用,Yay!与优化禁用:如何在不违反严格别名规则的情况下合法地使用类型捣破与工会在struct sockaddr的变体之间进行投射?

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

观察这行为在online IDE

要解决这一问题this answer提出与工会的使用类型双关的:

/*! 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报价this comment

可以工作,但需要一定的照顾。超过我可以适应这个评论框。

什么样的公平的照顾这是否需要?使用类型捻与工会在struct sockaddr变体之间施加什么缺陷?

我更愿意问,而不是碰到UB。

+0

目前还不清楚你的“联盟”的实际问题是什么。 [mcve]和你关心的更多细节如何?你为什么不问zwol他的意思?我们不是千里眼。 – Olaf

+0

@Olaf为什么不问zwol?因为正如我引用他的话,他已经表示,他不想在评论中谈论这一点。一个最小,完整和可验证的例子呢?嗯,我正在问这个问题,因为我想避免陷入我未知的陷阱,这将使我有必要制作这样一个最小化,完整和可验证的示例。当谈到C的UB时,我认为“更好的防止而非治疗”这个词完全成立。 – gaazkam

+0

我不确定如果不修改涉及sockaddr的所有接口,就可以做任何事情。不管你现在的函数是什么,仍然期望struct sockaddr *而不是任何类型的联合。 –

回答

2

使用union这样是安全的,

从C11§6.5.2.3

  • 后缀表达式接着。运算符和标识符指定结构或联合对象的成员。该值是指定成员的值,95),如果第一个表达式是左值,则为左值。如果第一个表达式具有限定类型,则结果具有 指定成员类型的合格版本。
  • 95)如果用于读取联合对象的内容的部件是不一样的最后用来 存储的值在对象中的构件,该值的对象表示的适当部分被重新解释 作为6.2.6中描述的新类型的对象表示(有时称为''类型 punning'')。这可能是一个陷阱表示。

  • 一个特殊保证是为了简化使用工会制成:如果联合包含 共享共同初始序列几个结构(见下文),如果工会 对象当前包含这些结构之一,它是被允许检查共同的 其中任何一个的初始部分任何地方的工会的完成类型的声明是可见的。 两个结构共享共同的初始序列,如果对应的构件 具有兼容的类型(和,对于位字段,相同的宽度),用于一个或多个 初始成员
  • 的序列(高亮我认为是最重要的)

    随着访问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()将是正确的。

    +0

    从所示的代码判断,OP似乎不能访问公共部分(这意味着相同的名称**和类型**)。目前尚不清楚OP如何处理'union'只是有一个'union'对象并不意味着指向成员是安全的。 – Olaf

    +0

    在我阅读标准时,只有类型必须兼容(不一定完全相同),名称无关紧要。但请参阅我编辑过的说明:联盟知道其实很重要。 –

    +0

    我想要的是能够从'recvfrom'或'accept'返回的'struct sockaddr'中检索IP地址而不触发UB,更重要的是将手动填充的'struct sockaddr_in'传递给'sendto'或者'connect',再次触发UB。如果没有这个'union',我不得不做一些指针转换,而且我可能会像'(struct sockaddr *)(&sa_in)'或'(struct sockaddr_in *)(&sa)'一样写入某种东西,我可能会触发UB。我希望我至少能够将指针传递给工会成员,例如'sento','recvfrom','accept','connect'等。 – gaazkam

    1

    鉴于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字段)。您还可以在recvfrom,recvmsg,accept的调用中安全地使用addr,或者采用struct sockaddr *输出参数的任何其他套接字基元。

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

    这也没关系分配address_t缓冲区与malloc,或将其嵌入到更大的结构。

    什么安全是指向一个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的定义范围内时,应该允许这样做,但对于编译器开发人员来说,这太类似于“spukhafte Fernwirkung”;当代编译器所采用的“共同初始子序列”规则(在菲利克斯的答案中引用)的解释是它只适用于静态和本地参与特定访问的联合类型。为什么它的好来传递&addr.saconnect那么你必须写,而不是

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

    你可能想知道。大致来说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有反正做一个副本,因为它是一个系统调用和整个用户/内核边界传递几乎所有的内存块必须被复制。

    相关问题