unix 具有位字段的结构的存储器布局

9w11ddsr  于 8个月前  发布在  Unix
关注(0)|答案(7)|浏览(72)

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

但我希望这样:

输出部分正确;在某个地方,字节和半字节似乎互换了。这里是否有一些endianness问题?比特域不适合这个目的吗?我真的不知道.任何帮助?提前感谢!

t40tm48m

t40tm48m1#

不,位域不适合此目的。布局依赖于编译器。
对于想要控制结果布局的数据,使用位域通常不是一个好主意,除非您有(特定于编译器的)方法,例如#pragma s。
最好的方法可能是不使用位域来实现它,即。通过自己做所需的逐位操作。这是恼人的,但方式比以某种方式挖掘一种方法来解决这个问题容易。它是平台独立的。
将头定义为一个16位字的数组,然后就可以很容易地计算校验和。

lnlaulya

lnlaulya2#

C11标准说:
实现可以分配足够大以保持位字段的任何可寻址存储单元。如果剩余足够的空间,则结构中紧接在另一位字段之后的位字段应被打包到同一单元的相邻位中。如果剩余的空间不足,则不适合的位字段是否被放入下一个单元或与相邻单元重叠是实现定义的。单元内的位字段的分配顺序(高阶到低阶或低阶到高阶)是实现定义的。
我很确定这是不可取的,因为这意味着字段之间可能有填充,并且您无法控制字段的顺序。不仅如此,在网络字节顺序方面,您还可以随心所欲地实现。此外,假设unsigned int只有16位,你要求在其中放入一个32位的位域:
指定位字段宽度的表达式应为整数常量表达式,其非负值不超过在省略冒号和表达式的情况下指定的类型的对象的宽度。
我建议使用unsigned char s数组而不是结构体。这样就保证了对填充和网络字节顺序的控制。从你希望你的结构整体的位数大小开始。我假设你是在一个常量中声明的,比如IP_PACKET_BITOWN: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等定义标识符,以使数组的修改看起来不那么轻松。

8hhllhi2

8hhllhi23#

虽然这个问题已经问了很久了,但是没有答案来解释你的结果。我会回答的,希望对别人有用。
我将使用数据结构的前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来遍历和打印值。应该可以的

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

inn6fuwd

inn6fuwd4#

对于中国用户,我想你可以参考blog了解更多细节,真的很好。
总之,由于字节顺序,存在字节顺序和位顺序。位顺序是一个字节的每一位在内存中保存的顺序。在字节序问题上,位序与字节序有相同的规则。
对于你的图片,它是按照网络顺序设计的,也就是大端。所以你的struct定义实际上是用于big endian的。根据你的输出,你的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");
    }
}
guykilcj

guykilcj5#

@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;
    }
    // ...
}

最早,当上述测试失败时,您需要考虑编译器杂注或相应地调整结构。getString().

0vvn1miw

0vvn1miw6#

我同意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
   }
}
oyt4ldly

oyt4ldly7#

这取决于你是想写一个非常快的程序,还是想写一个能在不同编译器下工作的程序。要为C编写一个快速、紧凑的应用程序,请使用带有位字段/的结构。如果你想要一个慢的通用程序,长代码。

相关问题