安全地取消引用C中结构联合的指针?

时间:2011-08-16 03:30:09

标签: c pointers unions

我正在尝试在C中为SCTP协议编写数据包解码器,并在解除引用指向结构联合(表示SCTP块)的指针时遇到一些问题。

不是每个人都熟悉SCTP(流控制传输协议),所以这里有一些简短的引物:

Stream Control Transmission Protocol
SCTP packet structure
RFC 4960

在坚果壳中,SCTP Common标题之后是一系列一个或多个“块”。大多数情况下,每个SCTP数据包只有一个块类型,但您可以使用块捆绑,其中某些块类型可以捆绑在一起形成一个数据包。由于这种捆绑,我不能只在我的SCTP头结构中定义一个联合并将其称为一天。

所以这就是我对struct sctp_header所拥有的:

struct sctp_header {
    uint16_t srcport;
    uint16_t dstport;
    uint32_t vtag;
    uint32_t chksum;
    uint8_t ctype;
    uint8_t cflags;
    uint16_t clength;
    void *chunks;
};


对于union sctp_chunks(此问题仅截断为两个块类型):

struct sctp_data {
    uint32_t tsn;
    uint16_t stream_id;
    uint16_t stream_seq;
    uint32_t pp_id;
    void *data;
};

struct sctp_init {
    uint32_t initate_tag;
    uint32_t a_rwnd;
    uint16_t num_out_streams;
    uint16_t num_in_streams;
    uint32_t tsn;
    void *params;
};

union sctp_chunks {
    struct sctp_data *data;
    struct sctp_init *init;
};



现在,我将sctp_chunks覆盖到sctp_header->chunks上(一旦我完成了所有其他必要的检查以确保我坐在SCTP数据包上)。然后我在switch语句中阅读了sctp_header->ctype,基于此,我知道我是否可以访问sctp_header->chunks->data->tsnsctp_header->chunks->init->initate_tag(在转换为(sctp_chunks *)之后),依此类推其他块类型。稍后,我将进行数学运算并检查剩余的块,并将sctp_chunks联合重新覆盖到剩余数据上,直到我处理完所有块。现在,我只是在第一个块上运行。

问题是,尝试访问data->tsninit->initiate_tag(或联盟的任何其他成员)会导致SIGSEGV。我没有GDB方便(我只是机器上的用户正在编码),所以我很难弄清楚为什么我的程序是segfaulting。我相信我对结构/联合的使用是合理的,但因此是C的本质,它可能是一个非常微妙的东西,在这里挂钩我。

打印chunks->datachunks->init的指针地址显示我看起来不是空指针的地址,并且我没有收到gcc中的任何重大错误或警告,所以我有点难住了。

有什么不同寻常的,或者有更好的方法来解决这个问题吗?

2 个答案:

答案 0 :(得分:3)

当你说你在数据包上“覆盖”你的数据结构时(至少就是你所说的那样),你void*指针数据的四个或八个字节中出现的值-member(取决于您的平台)很可能是sctp_initscpt_data块内的值,而不是指向该数据块的实际指针。这很可能是为什么你的指针不是NULL,而是在解引用指针中的值时你的seg-faulting。

其次,在没有编译指示和/或编译器指令的情况下将结构覆盖在序列化数据上可能是危险的...有时你认为编译器可能分配/填充结构的方式不是它实际上最终如何做它,然后结构与数据包的实际格式不匹配,你最终会得到无效的值。

所以我现在假设你正试图通过用scpt_header结构直接覆盖数据包的前N个字节来做这样的事情:

|scpt_header .........|void*|
|packet information ..|scpt_chunk.................|

这不是非常便携,并且可能会引起很多麻烦,特别是当您将网络字节顺序问题插入到这种情况中时。您真正想要的是将SCPT数据包的内容复制到内部数据结构中,如链接列表,然后您可以正确操作。

一种方式可能如下所示:

#include <arpa/inet.h>
unsigned char* packet_buffer = malloc(sizeof(PACKET_SIZE));

//... proceed to copy into the buffer however you are reading your packet

//now fill in your structure
unsigned char* temp = packet_buffer;
struct sctp_header header_data;

header_data.src_port = ntohs(*((uint16_t*)temp));
temp += sizeof(uint16_t);
header_data.dstport = ntohs(*((uint16_t*)temp));
temp += sizeof(uint16_t);
header_data.vtag = ntohl(*((uint32_t*)temp));
temp += sizeof(uint32_t);
//... keep going for all data members

//allocate memory for the first chunk (we'll assume you checked and it's a data chunk)
header_data.chunks = malloc(sizeof(sctp_data));
scpt_data* temp_chunk_ptr = header_data.chunks;

//copy the rest of the packet chunks into your linked-list data-structure
while (temp < (packet_buffer + PACKET_SIZE))
{
    temp_chunk_ptr->tsn = ntohl(*((uint32_t*)temp));
    temp += sizeof(uint32_t);
    //... keep going for the rest of this data chunk

    //allocate memory in your linked list for the next data-chunk
    temp_chunk_ptr->data = malloc(sizeof(scpt_data));
    temp_chunk_ptr = temp_chunk_ptr->data;
}

//free the packet buffer when you're done since you now have copied the data into a linked
//list data-structure in memory
free(packet_buffer);

这种方法可能看起来很麻烦,但不幸的是,如果您要处理字节序和网络字节顺序问题,那么您将无法简单地将数据结构覆盖在数据包数据中。平台可移植的方式,即使您将其声明为打包数据结构。声明打包的数据结构将正确对齐字节,但它不会纠正字节序问题,这就像ntohsntohl之类的函数一样。

另外,不要让scpt_header超出范围而不破坏它“拥有”的链接列表,否则最终会导致内存泄漏。

更新:如果您仍想要覆盖路由,请首先确保使用编译器指令打包结构,以便不添加填充。在gcc中,这将是

typdef struct sctp_header {
    uint16_t srcport;
    uint16_t dstport;
    uint32_t vtag;
    uint32_t chksum;
} __attribute__((packed)) sctp_header;

typedef struct sctp_data 
{
    uint8_t ctype;
    uint8_t cflags;
    uint16_t clength;
    uint32_t tsn;
    uint16_t stream_id;
    uint16_t stream_seq;
    uint32_t pp_id;
} __attribute__((packed)) sctp_data;

typedef struct sctp_init 
{
    uint8_t ctype;
    uint8_t cflags;
    uint16_t clength;
    uint32_t initate_tag;
    uint32_t a_rwnd;
    uint16_t num_out_streams;
    uint16_t num_in_streams;
    uint32_t tsn;
} __attribute__((packed)) sctp_init;

但在其他编译器中也会出现其他问题。另请注意,我已经改变了一些结构,以更好地反映内存中SCTP数据包实际表示的结果。因为两种不同数据包类型的大小不同,我们实际上无法在内存中进行联合和覆盖...工具在技术上将是最大块类型成员的大小,并且这会产生问题我们试图创建数组等。我也正在摆脱指针...我看到你正在尝试用它们做什么,但同样,因为你想在数据包数据上覆盖这些数据结构,因为你实际上试图“转移”数据,这又会引起问题。对原始数据结构的修改实际上反映了数据在内存中的布局方式,而没有任何花哨的移位或指针转换。由于每个数据包的类型由unsigned char表示,我们现在可以执行以下操作:

enum chunk_type { DATA = 0, INIT = 1 };

unsigned char* packet_buffer = malloc(sizeof(PACKET_SIZE));
//... copy the packet into your buffer

unsigned char* temp = packet_buffer;

sctp_header* header_ptr = temp;
temp += sizeof(sctp_header);

//... do something with your header

//now read the rest of the packets
while (temp < (packet_buffer + PACKET_SIZE))
{
    switch(*temp)
    {
        case DATA:
            sctp_data* data_ptr = temp;
            //... do something with the data
            temp += data_ptr->clength;
            break;

        case INIT:
            sctp_init* init_ptr = temp;
            // ... do something with your init type
            temp += init_ptr->clength;
            break;

        default:
            //do some error correction here
    }
}

再次请记住,此方法仅纠正对齐问题...它不能纠正字节顺序,因此在读取多字节数据类型中的任何值时要小心。

答案 1 :(得分:1)

问题是你根本不应该使用指针。 sctp_chunks应该是:

union sctp_chunks {
    struct sctp_data data;
    struct sctp_init init;
};

chunks中的sctp_header应为union sctp_chunks chunks[1](如果您知道数据有效,则可以安全地将块编入索引超过1。

我刚看到Jason的答案,他对结构包装是正确的。如果您正在使用gcc,请定义如下结构:

struct sctp_header __attribute__ ((packed)) {
  ...
};

你必须为每个结构做到这一点。