使用更新版本的gcc时,UDP校验和计算不起作用。

3
下面包含的代码是一个生成给定固定大小负载的UDP数据包并发送它的函数的简化实现。
在切换到更新版本的gcc后,该代码突然显示出错误:UDP校验和计算不正确,并且这可以追溯到以下行:
pseudoHeader->protocol = IPPROTO_UDP;

如果使用至少-O2优化,编译器似乎不会生成指令。
以下解决方法可以解决该问题(每个建议都是独立的,即您不必同时应用所有建议!):
  • 将提到的代码行移动到两个inet_pton调用之前
  • 在校验和计算后删除对memset(ipHeader, 0, sizeof(struct ip))的调用
  • ip_checksum()作为外部函数放在此翻译单元之外
由于代码频繁使用强制类型转换以及错误仅出现在-O2或更高版本中,并且解决方法的性质几乎要求这是代码中的别名错误。是否存在实际错误,如果有,如何修复?
#include <string.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <netpacket/packet.h>

#define UDP_PORT 2345
#define REPLY_PAYLOAD_SIZE 360

typedef struct UDPPseudoHeader
{
    unsigned long int source_ip;
    unsigned long int dest_ip;
    unsigned char reserved;
    unsigned char protocol;
    unsigned short int udp_length;
} UDPPseudoHeader;

void sendPacket(unsigned char* packet, int len);

static unsigned short ip_checksum(unsigned short *ptr, int len)
{
    int sum = 0;
    unsigned short answer = 0;
    unsigned short *w = ptr;
    int nleft = len;

    while(nleft > 1) {
        sum += *w++;
        nleft -= 2;
    }

    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);
    answer = ~sum;
    return(answer);
}

void sendBroadcastPacket(uint16_t destPort, char* packet) {
    unsigned char buffer[REPLY_PAYLOAD_SIZE + sizeof(struct ip) + sizeof(struct udphdr)];
    int bufferLen = REPLY_PAYLOAD_SIZE + sizeof(struct ip) + sizeof(struct udphdr);

    /* initialize header pointers */
    struct udphdr* udpHeader = (struct udphdr*)(buffer + sizeof(struct ip));
    UDPPseudoHeader* pseudoHeader = (UDPPseudoHeader*)(buffer + sizeof(struct ip) - sizeof(UDPPseudoHeader));
    struct ip* ipHeader = (struct ip*)(buffer);

    memset(buffer, 0, bufferLen);

    /* copy user data */
    memcpy(buffer + sizeof(struct ip) + sizeof(struct udphdr), packet, REPLY_PAYLOAD_SIZE);

    /* fill in UDP header */
    udpHeader->source = htons(UDP_PORT);
    udpHeader->dest = htons(destPort);
    udpHeader->len = htons(sizeof(struct udphdr) + REPLY_PAYLOAD_SIZE);
    udpHeader->check = 0;

    /* create UDP pseudo header for checksum calculation */
    inet_pton(AF_INET, "0.0.0.0", &pseudoHeader->source_ip);
    inet_pton(AF_INET, "255.255.255.255", &pseudoHeader->dest_ip);
    pseudoHeader->reserved = 0;
    pseudoHeader->protocol = IPPROTO_UDP;
    pseudoHeader->udp_length = htons(sizeof(struct udphdr) + REPLY_PAYLOAD_SIZE);

    /* calculate UDP checksum */
    udpHeader->check = ip_checksum((unsigned short*) pseudoHeader, bufferLen - sizeof(struct ip) + sizeof(UDPPseudoHeader));

    /* fill in IP header */
    memset(ipHeader, 0, sizeof(struct ip));
    ipHeader->ip_v = 4;
    ipHeader->ip_hl = 5;
    ipHeader->ip_tos = IPTOS_LOWDELAY;
    ipHeader->ip_len = htons(bufferLen);
    ipHeader->ip_off = htons(IP_DF);
    ipHeader->ip_id = 0;
    ipHeader->ip_ttl = 16;
    ipHeader->ip_p = IPPROTO_UDP;
    inet_pton(AF_INET, "0.0.0.0", &ipHeader->ip_src);
    inet_pton(AF_INET, "255.255.255.255", &ipHeader->ip_dst);
    ipHeader->ip_sum = 0;

    /* calculate IP checksum */
    ipHeader->ip_sum = ip_checksum((unsigned short*) ipHeader, ipHeader->ip_hl * 4);

    sendPacket(buffer, bufferLen);
}

这是一些非常脆弱的代码...理想情况下,它应该能够在原始字节流之间进行复制,并复制到/从结构中(并在此过程中修复字节序),而不需要使用肮脏的转换。在现有代码中,所有内容都被复制,因此不会影响性能。 - Lundin
是的,我知道。这已经使用了将近十年了,所以我很惊讶它突然间不起作用了。嗯,结果证明我之前只是幸运而已。在写下问题时,我显然是朝着正确的方向搜索,最终自己找到了答案。 - Christoph Freundl
@ChristophFreundl 在 ip_checksum() 函数中,将 int sum = 0; 改为 unsigned sum = 0; 可以避免在 len 较大时出现问题。对于 sum >> 16 中的符号位移是一个关注点。 - chux - Reinstate Monica
2个回答

3

这段代码确实违反了严格别名规则。编译器假设调用ip_checksum()与结构体成员reservedprotocol的赋值无关,因为它们修改了char类型,而ip_checksum()是在unsigned short数组上计算的。因此,由于后续对memset()的调用会覆盖内存,这些赋值都被完全优化掉了。

一个可能的解决方案是将伪头声明为

typedef union {
    struct {
        unsigned long int source_ip;
        unsigned long int dest_ip;
        unsigned char reserved;
        unsigned char protocol;
        unsigned short int udp_length;
    } hdr;
    unsigned short as_short[6];
} UDPPseudoHeader;

并通过替换伪标题的生成和校验和计算来实现。
/* create UDP pseudo header for checksum calculation */
inet_pton(AF_INET, "0.0.0.0", &pseudoHeader->hdr.source_ip);
inet_pton(AF_INET, "255.255.255.255", &pseudoHeader->hdr.dest_ip);
pseudoHeader->hdr.reserved = 0;
pseudoHeader->hdr.protocol = IPPROTO_UDP;
pseudoHeader->hdr.udp_length = htons(sizeof(struct udphdr) + REPLY_PAYLOAD_SIZE);

/* calculate UDP checksum */
udpHeader->check = ip_checksum(pseudoHeader->as_short, bufferLen - sizeof(struct ip) + sizeof(UDPPseudoHeader));

6
我强烈建议使用stdint.h。一些奇怪的编译器端口会使64位计算机上的long变为8个字节,这样你就会得到膨胀的开销字节。 - Lundin

2
另一个问题:
对齐
unsigned char buffer[REPLY_PAYLOAD_SIZE + sizeof(struct ip) + sizeof(struct udphdr)];
...
struct ip* ipHeader = (struct ip*)(buffer);

buffer没有为struct ip正确对齐。


这是一个很好的评论。我将扩展创建适当的结构体/联合体的方法到整个缓冲区,这应该能够处理正确的对齐方式,同时也可以通过这种方式摆脱几乎所有的强制转换。 - Christoph Freundl
@ChristophFreundl 一个_cast_通常是做错事的标志,最好避免使用 - 当然也有例外情况。 - chux - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接