具有位域的结构的存储器布局

时间:2013-02-28 13:10:50

标签: c unix struct ip bit-fields

我有这个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

但我希望如此:

enter image description here

输出部分正确;在某个地方,字节和半字节似乎是互换的。这里有一些字节序问题吗?位字段是否不适合此目的?我真的不知道。有帮助吗?提前谢谢!

7 个答案:

答案 0 :(得分:7)

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

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

最好的方法可能是在没有位域的情况下实现它,即通过自己进行所需的按位运算。这很烦人,但比某种方式更容易找到解决这个问题的方法。此外,它与平台无关。

将标题定义为16位字的数组,然后您可以轻松地计算校验和。

答案 1 :(得分:2)

C11标准说:

  

实现可以分配任何可寻址的存储单元   足以容纳一个位域。如果剩余足够的空间,那就是一个位域   紧接着结构中的另一个位字段应该被打包   进入同一单元的相邻位。如果剩余空间不足,   是否将不适合的位域放入下一个单元或   重叠相邻单元是实现定义的。的顺序   单位内的位域分配(高位到低位或   低阶到高阶)是实现定义的。

我很确定这是不受欢迎的,因为这意味着您的字段之间可能存在填充,并且您无法控制字段的顺序。不仅如此,您还可以在网络字节顺序方面随心所欲地实现。另外,假设unsigned int只有16位,并且要求在其中插入32位位域:

  

指定位字段宽度的表达式应为a   具有非负值的整数常量表达式   超过将指定类型的对象的宽度   冒号和表达式被省略。

我建议使用unsigned char s数组而不是结构。这样您就可以保证控制填充和网络字节顺序。从您想要结构的位数开始,总计。我假设你在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等定义标识符,以使数组的修改看起来不那么轻松。

答案 2 :(得分: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 * pointer 来遍历和打印值。 它应该工作。

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

答案 3 :(得分:0)

对于中国用户,我认为您可以参考blog以获得更多详细信息,真的很好。

总而言之,由于字节顺序,既有字节顺序又有比特顺序。位顺序是一个字节的每个位如何保存在内存中的顺序。就字节顺序而言,位顺序与字节顺序具有相同的规则。

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

显示每个位的方法是错误的,因为当通过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");
    }
}

答案 4 :(得分:0)

@unwind

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

@布鲁斯

对于 Intel/GCC,我看到一个打包的 LITTLE ENDIAN 位布局,即在 struct ip_dgram 中,字段 ver 由位 0..3 表示,字段 hlen 由位 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;
    }
    // ...
}

最早,当上述测试失败时,您需要考虑编译器编译指示或相应地调整您的结构,分别是。 verifyLayoutModel().

答案 5 :(得分:-1)

我同意放松说的话。位字段取决于编译器。

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

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

答案 6 :(得分:-1)

编译器是否依赖,取决于您是想编写一个非常快的程序,还是想要一个适用于不同编译器的程序。要为快速,紧凑的应用程序编写C,请使用带有位字段/的结构。如果你想要一个缓慢的通用程序,可以长代码。