9

我有这个 C 结构:(代表一个 IP 数据报)

struct ip_dgram
{
    unsigned int ver   : 4;
    unsigned int hlen  : 4;
    unsigned int stype : 8;
    unsigned int tlen  : 16;
    unsigned int fid   : 16;
    unsigned int flags : 3;
    unsigned int foff  : 13;
    unsigned int ttl   : 8;
    unsigned int pcol  : 8;
    unsigned int chksm : 16;
    unsigned int src   : 32;
    unsigned int des   : 32;
    unsigned char opt[40];
};

我给它赋值,然后用 16 位字打印它的内存布局,如下所示:

//prints 16 bits at a time
void print_dgram(struct ip_dgram dgram)
{
    unsigned short int* ptr = (unsigned short int*)&dgram;
    int i,j;
    //print only 10 words
    for(i=0 ; i<10 ; i++)
    {
        for(j=15 ; j>=0 ; j--)
        {
            if( (*ptr) & (1<<j) ) printf("1");
            else printf("0");
            if(j%8==0)printf(" ");
        }
        ptr++;
        printf("\n");
    }
}

int main()
{
    struct ip_dgram dgram;

    dgram.ver   = 4;
    dgram.hlen  = 5;
    dgram.stype = 0;
    dgram.tlen  = 28;
    dgram.fid   = 1;
    dgram.flags = 0;
    dgram.foff  = 0;
    dgram.ttl   = 4;
    dgram.pcol  = 17;
    dgram.chksm = 0;
    dgram.src   = (unsigned int)htonl(inet_addr("10.12.14.5"));
    dgram.des   = (unsigned int)htonl(inet_addr("12.6.7.9"));

    print_dgram(dgram);

    return 0;
}

我得到这个输出:

00000000 01010100 
00000000 00011100 
00000000 00000001 
00000000 00000000 
00010001 00000100 
00000000 00000000 
00001110 00000101 
00001010 00001100 
00000111 00001001 
00001100 00000110

但我希望这样:

在此处输入图像描述

输出部分正确;在某处,字节和半字节似乎被互换了。这里有一些字节顺序问题吗?位域不适合这个目的吗?我真的不知道。有什么帮助吗?提前致谢!

4

7 回答 7

11

不,位域不适用于此目的。布局依赖于编译器。

对想要控制结果布局的数据使用位域通常不是一个好主意,除非您有(特定于编译器的)方法,例如#pragmas,可以这样做。

最好的方法可能是在没有位域的情况下实现这一点,即自己进行所需的按位运算。这很烦人,但比想办法解决这个问题要容易得多。此外,它与平台无关。

将标头定义为一个 16 位字的数组,然后您可以很容易地计算校验和。

于 2013-02-28T13:12:58.487 回答
3

C11 标准说:

实现可以分配任何大到足以容纳位域的可寻址存储单元。如果有足够的空间剩余,紧跟在结构中另一个位域之后的位域将被打包到同一单元的相邻位中。如果剩余空间不足,则将不适合的位域放入下一个单元还是与相邻单元重叠是实现定义的。单元内位域的分配顺序(高位到低位或低位到高位)是实现定义的。

我很确定这是不可取的,因为这意味着您的字段之间可能存在填充,并且您无法控制字段的顺序。不仅如此,您还可以在网络字节顺序方面随心所欲地实施。此外,想象一下如果 anunsigned int只有 16 位,并且您要求将 32 位位域放入其中:

指定位域宽度的表达式应是一个整数常量表达式,其非负值不超过在省略冒号和表达式时将指定的类型的对象的宽度。

我建议使用 s 数组unsigned char而不是结构体。这样,您就可以保证控制填充和网络字节顺序。从您希望结构的总大小开始。我假设您在 IP_PACKET_BITCOUNT 等常量中声明它:typedef unsigned char ip_packet[(IP_PACKET_BITCOUNT / CHAR_BIT) + (IP_PACKET_BITCOUNT % CHAR_BIT > 0)];

编写一个函数,void set_bits(ip_packet p, size_t bitfield_offset, size_t bitfield_width, unsigned char *value) { ... }该函数允许您将从位开始的p[bitfield_offset / CHAR_BIT]位设置bitfield_offset % CHARBIT为在值中找到的位,最长为bitfield_width位。这将是您任务中最复杂的部分。

然后,您可以为 VER_OFFSET 0 和 VER_WIDTH 4、HLEN_OFFSET 4 和 HLEN_WIDTH 4 等定义标识符,以使修改数组看起来不那么痛苦。

于 2013-02-28T16:42:54.707 回答
1

尽管很久以前就提出了问题,但没有解释您的结果的答案。我会回答它,希望它对某人有用。

我将使用数据结构的前 16 位来说明这个错误。

请注意:此解释仅适用于您的处理器和编译器集。如果这些变化中的任何一个发生变化,行为可能会发生变化。

领域:

unsigned int ver   : 4;
unsigned int hlen  : 4;
unsigned int stype : 8;

分配给:

dgram.ver   = 4;
dgram.hlen  = 5;
dgram.stype = 0;

编译器开始分配从偏移量 0 开始的位域。这意味着数据结构的第一个字节存储在内存中:

Bit offset: 7     4     0
            -------------
            |  5  |  4  |
            -------------

赋值后的前 16 位如下所示:

Bit offset:     15   12    8     4     0
                -------------------------
                |  5  |  4  |  0  |  0  |
                -------------------------
Memory Address: 100          101

您正在使用无符号 16 指针来取消引用内存地址 100。因此,地址 100 被视为 16 位数的 LSB。并且 101 被视为 16 位数字的 MSB。

如果您以十六进制打印 *ptr,您将看到:

*ptr = 0x0054

您的循环在这个 16 位值上运行,因此您得到:

00000000 0101 0100
-------- ---- ----
   0       5    4

解决方案: 将元素的顺序更改为

unsigned int hlen  : 4;
unsigned int ver   : 4;
unsigned int stype : 8;

并使用 unsigned char * 指针来遍历和打印值。它应该工作。

请注意,正如其他人所说,这种行为是特定于平台和编译器的。如果有任何这些更改,您需要验证数据结构的内存布局是否正确。

于 2016-12-23T16:24:50.483 回答
0

对于中国用户,我认为您可以参考博客了解更多详细信息,非常好。

总之,由于字节序,有字节顺序和位顺序。位顺序是一个字节的每个位如何保存在内存中的顺序。在字节序问题的意义上,位顺序与字节顺序具有相同的规则。

对于您的图片,它是按照大端的网络顺序设计的。所以你的结构定义实际上是针对大端的。根据您的输出,您的 PC 是 little endian,因此您需要在使用时更改 struct 字段顺序。

显示每个位的方式是不正确的,因为当通过 char 获取时,位顺序已从机器顺序(在您的情况下为小端序)更改为我们人类使用的正常顺序。您可以根据参考的博客将其更改为以下内容。

void
dump_native_bits_storage_layout(unsigned char *p, int bytes_num)
{

    union flag_t {
        unsigned char c;
        struct base_flag_t {
            unsigned int p7:1,
                        p6:1,
                        p5:1,
                        p4:1,
                        p3:1,
                        p2:1,
                        p1:1,
                        p0:1;
        } base;
    } f;

    for (int i = 0; i < bytes_num; i++) {
        f.c = *(p + i);
        printf("%d%d%d%d %d%d%d%d ",
                        f.base.p7,
                        f.base.p6, 
                        f.base.p5, 
                        f.base.p4, 
                        f.base.p3,
                        f.base.p2, 
                        f.base.p1, 
                        f.base.p0);
    }
    printf("\n");
}

//prints 16 bits at a time
void print_dgram(struct ip_dgram dgram)
{
    unsigned char* ptr = (unsigned short int*)&dgram;
    int i,j;
    //print only 10 words
    for(i=0 ; i<10 ; i++)
    {
        dump_native_bits_storage_layout(ptr, 1);
        /* for(j=7 ; j>=0 ; j--)
        {
            if( (*ptr) & (1<<j) ) printf("1");
            else printf("0");
            if(j%8==0)printf(" ");
        }*/
        ptr++;
        //printf("\n");
    }
}
于 2020-03-27T09:37:48.847 回答
0

@放松

位域的一个典型用例是解释/模拟具有给定布局的字节码或 CPU 指令。“不要使用它,因为你无法控制它”是孩子们的答案。

@布鲁斯

对于 Intel/GCC,我看到了一个打包的 LITTLE ENDIAN 位布局,即struct ip_dgram 字段ver由位 0..3hlen表示,字段由位 4..7 表示......

为了操作的正确性,需要在运行时根据您的设计验证内存布局。

struct ModelIndicator
{
    int a:4;
    int b:4;
    int c:4;
};

union UModelIndicator
{
    ModelIndicator i;
    int v;
};

// test packed little endian
static bool verifyLayoutModel()
{
    UModelIndicator um;
    um.v = 0;
    um.i.a = 2; // 0..3
    um.i.b = 3; // 4..7
    um.i.c = 9; // 8..11
    return um.v = (9 << 8) + (3 << 4) + 2;
}

int main()
{
    if (!verifyLayoutModel())
    {
        std::cerr << "Invalid memory layout" << std::endl; 
        return -1;
    }
    // ...
}

最早,当上述测试失败时,您需要考虑编译器编译指示或相应地调整您的结构,分别。验证布局模型()。

于 2021-02-03T15:48:43.907 回答
-1

是否依赖于编译器,这取决于您是要编写一个非常快的程序还是要一个可以与不同编译器一起使用的程序。要为 C 编写一个快速、紧凑的应用程序,请使用带有位域/的结构。如果您想要一个慢速的通用程序,请对其进行长代码。

于 2014-01-04T22:49:31.397 回答
-2

我同意 unwind 所说的。位字段取决于编译器。

如果您需要位按特定顺序排列,请将数据打包到指向字符数组的指针中。将缓冲区增加被打包元素的大小。打包下一个元素。

pack( char** buffer )
{
   if ( buffer & *buffer )
   {
     //pack ver
     //assign first 4 bits to 4. 
     *((UInt4*) *buffer ) = 4;
     *buffer += sizeof(UInt4); 

     //assign next 4 bits to 5
     *((UInt4*) *buffer ) = 5;
     *buffer += sizeof(UInt4); 

     ... continue packing
   }
}
于 2013-02-28T15:55:59.797 回答