【问题标题】:Safely dereferencing pointers to unions of structs in C?在 C 中安全地取消引用指向结构联合的指针?
【发布时间】:2011-08-16 03:30:09
【问题描述】:

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

可能不是每个人都熟悉 SCTP(流控制传输协议),所以这里有一些简短的入门知识:

Stream Control Transmission Protocol
SCTP packet structure
RFC 4960

简而言之,在 SCTP 公共标头之后是一系列一个或多个“块”。大多数情况下,每个 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(我只是正在编码的机器上的用户),所以我很难弄清楚为什么我的程序会出现段错误。我相信我对结构/联合的使用是合理的,但这就是 C 的本质,它可能是非常微妙的东西让我着迷。

打印chunks->datachunks->init 的指针地址显示的地址似乎不是NULL 指针,而且我没有从gcc 中得到任何重大错误或警告,所以我有点难过。

有什么不寻常的地方,还是有更好的方法来解决这个问题?

【问题讨论】:

  • 您可以打印简短的内存转储,而不仅仅是指针值,并检查内容是否有意义。结构是否正确对齐(#pragma pack 等)?
  • 如果没有gdb,那就用肮脏的方式,即很多printfs。
  • @Eugene:没有尝试过#pragma pack,但我做了一些修改(将块类型、长度、标志移动到 sctp_chunks 中),一旦我停止让它们成为指针,就可以取消引用这些成员。但是现在我偏移了四个字节,这是奇怪

标签: c pointers unions


【解决方案1】:

当你说你在数据包上“覆盖”你的数据结构时(至少你看起来是这么说的),出现在你的 void* 指针数据成员的四个或八个字节中的值(取决于您的平台)很可能是 asctp_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
    }
}

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

【讨论】:

  • 我认为你在做某事。我按照 Eugene 的想法研究了#pragma pack,现在正在读取正确偏移量的位。但是我又遇到了血腥的字节序问题(我正在成为大字节序的支持者......)。不过,这对我来说还是新鲜事。当我编写内核驱动程序时,我没有遇到任何问题。但我想在那里,我将一个结构覆盖到一个未序列化的缓冲区上(即,一些内核函数已经隔离了数据)并且一切正常,因为我从第一块数据开始。
  • 你能提供一个小代码示例来演示复制到缓冲区然后覆盖它吗?这可能更符合我在内核驱动程序中处理的内容。
  • 这看起来确实很麻烦。我知道我见过其他情况,其中数据从void * 复制到新缓冲区(并 malloc'ed),然后覆盖一个结构及其成员访问(尽管通过-&gt;.,我忘记)。我用其他语言做过类似的技巧,但是那些,我手边有一个很好的调试器,所以我可以看到我在做什么。除非我进入位域,否则字节顺序不应该成为真正的问题,然后我将使用#ifdef 并定义特定的 LE 和 BE 版本。一旦我的结构正确对齐,ntohsntohl 就可以处理特定变量。
  • PS,我只读取数据包。不写修改过的或新的。
  • 好的,我已经在答案中为您提供了更新,地址只是将您的结构覆盖在数据包数据上。
【解决方案2】:

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

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

并且sctp_header 中的chunks 应该是union sctp_chunks chunks[1](如果您知道数据是有效的,您可以安全地索引超过 1 的块。

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

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

您必须为每个结构都这样做。

【讨论】:

  • __attribute__ 是 GCC 特定的扩展吗?我无权访问 MSVC++ 来检查它是否理解该指令。
  • 另外,为什么chunks[1]?我认为 C 中的数组是零索引的?
  • 刮掉第一条评论。 __attribute__ 是特定于 GCC 的,根据 this question。我想使用#pragma pack 更安全,因为它看起来更便携。
  • 我想说 gcc 不使用 #pragma(因为在我们的代码中,我们对待 gcc 和 MSVC++ 的方式不同),但快速的 Google 显示 gcc 尊重 MSVC++ #pragma。跨度>
  • 数组索引为零,但在这种情况下,[1] 是一个计数。我很确定 gcc 会允许 [0] 的大小(仅适用于您拥有未知数量的块的情况),但我使用 [1] 是为了与其他编译器兼容。除非您使用 sizeof() 或为结构分配内存,否则数字无关紧要。我假设您有一个要转换为结构的字节指针,这就是为什么您不知道您将拥有多少块。
猜你喜欢
  • 2023-04-07
  • 2013-01-04
  • 1970-01-01
  • 2013-12-22
  • 2015-05-21
  • 2013-07-01
  • 2017-06-17
  • 2011-02-04
  • 2014-02-01
相关资源
最近更新 更多