C ++ struct bit字段无法正确解析数据

时间:2017-12-26 22:25:15

标签: c++ struct network-programming bit-fields

我正在尝试使用压缩结构从VLAN标头中提取字段:

我创建了这个结构:

#pragma pack(push, 1)
struct vlan_header
{
    uint16_t PCP : 3,
             DEI : 1,
             ID : 12;
};
#pragma pack(pop)

当我使用uint8_t数组并尝试从中提取字段时:

uint8_t* data;
vlan_header* vlanHeader;
data = new uint8_t[2];
data[0] = 0;
data[1] = 0x14; // data is 00 14
                // That means PCP is 0, DEI is 0 and vlan id is 20
vlanHeader = (vlan_header*)data;
std::cout << "PCP: " << vlanHeader->PCP << std::endl;
std::cout << "DEI: " << vlanHeader->DEI << std::endl;
std::cout << "ID: " <<  vlanHeader->ID << std::endl;
delete[] data;

输出结果为:

PCP: 0
DEI: 0
ID: 320

显然,我们看到vlan id是320而不是20,这不是我的意思。我认为问题是字节序(我的机器是小端)我不知道如何优雅地解决问题。

也许位字段不适合这项工作?

3 个答案:

答案 0 :(得分:0)

您的id值为0x140而不是0x14,请记住位字段部分已打包到该类型中。你有16位可用。 如果你想要它是0x14,你需要

data[0] = 0x40;
data[1] = 1;

答案 1 :(得分:0)

OP问这个:

  

我认为问题是字节序(我的机器是小端)我不知道如何优雅地解决问题。

     

也许位字段不适合这份工作?

虽然在使用位域或工会时,考虑机器的端序始终是一个很好的考虑因素并且不应该被遗忘。但是,在您目前的情况下,我没有看到端点是任何问题的原因或关注点。至于问题的第二部分,一切都取决于具体的需求。如果要编写的代码专门用于特定的体系结构/ os /平台并且不可能是可移植的,那么如果正确构造了使用位域,则应该没有任何问题。即使您决定移植到其他机器,您仍然可以使用位域,但您必须更加小心,并且可能必须使用预处理器指令或控制开关和放大器编写更多代码。 case语句让代码使用并在一台机器上做一件事,而不是另一台。

当使用位域时,我认为在混合类型时会考虑字节序。

struct Bitfield {
    unsigned a : 10,
             b : 10,
             c : 16;
    int      x : 10,
             y : 10,
             z : 16;
};

像上面这样的东西可能需要考虑到endian。

通过查看您的位域结构,我所看到的是对位域内位的对齐与结构本身对齐的误解。

目前的结构是:

#pragma pack(push, 1)
struct vlan_header {
    // uint16_t = 2bytes: - 16bits to work with
    uint16_t PCP : 3,  // bit(s) 0-2
             DEI : 1,  // bit(s) 3
             ID : 12;  // bit(s) 4-15
};
#pragma pack(pop)

您正在将对齐打包到1 byte的最小可能大小,因此此结构中的边界对于每个边界都在8 bits。没什么大不了的,而且非常自我解释。然后,您使用的uint16_t类型为typedef unsigned short 2 bytes,其大小为16 bitsunsigned short可供使用。 [0,65535]的值范围为PCP

然后在结构体内设置位域成员DEIID&amp; 3分位数为112uint8_t。我在你的结构中添加了注释以显示这种模式。

现在在主函数中声明指向[2]类型的指针,然后创建上面结构的实例,然后为数组大小为{{1的指针创建动态内存}}。此处uint8_ttypedef unsigned char的{​​{1}},其大小为1 byte8 bits可供使用,因为您拥有2个总共有2 bytes16 bits。好的,所以内存的总大小在bitfield structdata[]数组之间匹配。

然后通过索引并使用十六进制值设置指针数组来填充指针数组。然后,通过将其转换为该类型,将array中的值分配给bitfield。但是我认为你假设data[0]应该适用于位域的1 st 2成员,并且data[1]应该适用于最后一个成员ID值。data[0] = 0; data[1] = 0x14; // data is 00 14 然而事实并非如此:

这里发生的是代码的这一部分:

#include <iostream>
#include <fstream>

#pragma pack(push, 1)
struct vlan_header {
    // uint16_t = 2bytes: - 16bits to work with

    uint16_t PCP : 3,  // bit(s) 0-2
             DEI : 1,  // bit(s) 3
             ID : 12;  // bit(s) 4-15
};
#pragma pack(pop)

int main() {            
    uint8_t* data; // sizeof(uint8_t) = 1byte - 8bits           
    vlan_header* vlanHeader;
    data = new uint8_t[2];

    std::ofstream log;
    log.open( "results.txt" );
    for ( unsigned i = 0; i < 256; i++ ) {
        for ( unsigned j = 0; j < 256; j++ ) {
            data[0] = j;
            data[1] = i;

            std::cout << "data[0] = " << static_cast<unsigned>(data[0]) << " ";
            std::cout << "data[1] = " << static_cast<unsigned>(data[1]) << " ";

            log << "data[0] = " << static_cast<unsigned>(data[0]) << " ";
            log << "data[1] = " << static_cast<unsigned>(data[1]) << " ";

            vlanHeader = reinterpret_cast<vlan_header*>(data);
            std::cout << "PCP: " << std::hex << vlanHeader->PCP << " ";
            std::cout << "DEI: " << std::hex << vlanHeader->DEI << " ";
            std::cout << "ID: " << std::hex << vlanHeader->ID << std::endl;

            log << "PCP: " << std::hex << vlanHeader->PCP << " ";
            log << "DEI: " << std::hex << vlanHeader->DEI << " ";
            log << "ID: " << std::hex << vlanHeader->ID << std::endl;
        }   
    }    
    log.close();

    delete[] data;


    std::cout << "\nPress any key and enter to quit." << std::endl;
    char q;
    std::cin >> q;

    return 0;
}

以上并没有按照你的想法去做。

我制作一张图表只是为了向您展示示例:但是它太大而无法在此显示;所以我能做的就是为你提供一些代码,让你在你的机器上运行,生成一个日志文件供你查看模式。

// Values are represented in hex
// For field member PCP: remember that 3 bits can only hold a max value of 7
// 8-bits     8-bits     3-bits   1-bit   12-bits
// data[0]    data[1]    PCP      DEI     ID  
   0x00       0x00       0        0       0
   0x01       0x00       1        0       0
   0x02       0x00       2        0       0
   0x03       0x00       3        0       0
   0x04       0x00       4        0       0
   0x05       0x00       5        0       0
   0x06       0x00       6        0       0
   0x07       0x00       7        0       0   // PCP at max value since 3 bits only has 2^3 digit combinations
   0x08       0x00       0        1       0
   0x09       0x00       1        1       0
   0x0a       0x00       2        1       0
   0x0b       0x00       3        1       0
   0x0c       0x00       4        1       0
   0x0d       0x00       5        1       0
   0x0e       0x00       6        1       0
   0x0f       0x00       7        1       0  // the next iteration is where the bit carries into ID
   0x10       0x00       0        0       1
   // And this pattern repeats through out until ID has max value. 

如果你看一下模式,就会发生什么变得非常明显。

让我们看一下此处简化的生成文件的前几次迭代。

8 bits

在你的位域内存中发生的事情是1 st 字节或PCP正在消耗DEI4 bits 以及<{em> ID的1 st SoronelHaetir,我认为这是你感到困惑的地方。正如3在其简短回答中所述,如果您希望{0,0,20}位域的值为data array,则需要将data[0] = 0x40设置为data[1] = 0x01 }&amp;分别为data[0]PCP中的位溢出到其他位域成员中,当该成员不再包含足够高的值而不是分配的位数可以支持时。

这基本上意味着3 bits可用2^3 = 8,其最大组合位数为PCP,因此[0,7]可以存储DEI的值{1}}。由于1 bit只有[0,1],因此它可以作为单个位bool标记,只能存储ID的值,最后12 bits具有4,其中1 < sup> st data[0]来自8,最后data[1]来自2^12 = 4096,这会给你[0,4095]组合数字给出范围为FFF的值,以十六进制表示最大值为data[]。这可以在日志或结果文件中看到。

我还会将您的bitfield数组与 First Byte | Second Byte data[0] | data[1] data[n]: ([0][0][0]) ([0])-([0][0][0][0] | [0][0][0][0]-[0][0][0][0]) | PCP DEI ID | bitfield: ([0][0][0]) ([0])-([0][0][0][0] | [0][0][0][0]-[0][0][0][0])

并列显示
data[0]

修改

OP在对这个答案的评论中提到了这些陈述:

  

我没有得到&#34;当该成员不再包含足够高的值时,来自data [0]的位溢出到其他位域成员中#34;我不会看看溢出的地方 - Lior Sharon

  

同样根据答案底部的对齐方式我所做的工作应该有效,因为当ID为20时,位域只使用数据1的第二个字节

我在这里尝试做的是显示data[1]&amp;的位模式。 0x40,其值为0x01Byte 1 Byte 2 data[0] = 0x40 data[1] = 0x01 [0][1][0][0] [0][0][0][0] | [0][0][0][0] [0][0][0][1]

data

这是0s在将其转换为位域结构之前的位模式应该是什么样的。现在让我们在演员之前查看所有PCP的位域,然后让我们看看与位域成员相关的十六进制值以及它们可以存储的值。我已经声明[0-7]可以存储来自DEI的值,[0,1]可以存储值[0-4095],而ID可以存储来自PCP的十进制值。您将十六进制值分配给两个字节或16位内存。你想要DEI&amp; 0的值为ID20的值为0x00(十进制)。您认为第一个字节的PCP将同时提供DEI&amp; 0 0x14的值ID 20应该0x14 ID的值。那样不行。对于十六进制值,12 bits表示内存中的一个字节,但1.5 bytes要存储PCP7。如果您参考上面的图表,则成员data[0]只有3位可存储,因此如果我们将PCP的值添加到[1][1][1] data[1],则如下所示:{{ 1}}二进制。在没有使用DEI字节的情况下,我们可以将值推送到ID和&amp; Byte1 = Byte 2 = ============================|========================== PCP DEI ID 0x00 0x00 [0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x01 0x00 [0][0][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x02 0x00 [0][1][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x03 0x00 [0][1][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x04 0x00 [1][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x05 | 0x00 [1][0][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x06 0x00 [1][1][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x07 0x00 [1][1][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x08 0x00 [0][0][0] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x09 0x00 [0][0][1] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x0A 0x00 [0][1][0] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x0B 0x00 [0][1][1] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x0C 0x00 [1][0][0] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x0D 0x00 [1][0][1] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x0E 0x00 [1][1][0] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0] 0x0F 0x00 [1][1][1] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0] // When we increment the hex value from 0x0F to 0x10 with a decimal value of 16 // this is where the overflow into the ID member happens and as of right now // PCP has a max value of 7 and DEI has a max value of 1 where all bits are full. // Watch what happens on the next iteration. Also note that we never gave any values // to data[1] or byte 2 we only gave values to byte 1. This next value will // populate a result into the bitfield's member ID. 0x10 0x00 [0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][1] // then for the next iteration it'll be like this and so on... 0x11 0x00 [0][0][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][1] 0x12 0x00 [0][1][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][1] // while this pattern continues we seen that `0x10` gave us a bit at the right end // of member ID so lets look at values 0x20, 0x30 & 0x40 in the first byte // if 0x10 = [0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][1] // then 0x20 should be [0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][1][0] // and 0x30 should be [0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][1][1] // finally 0x40 should be [0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][1][0][0] // This is all without touching byte. // Remember we want both PCP & DEI to have values of 0 but we // need a value of 0x16 or 20 in decimal in ID. Because of this // overflow of bits due to the nature of bit fields, we can not // just set the bytes directly with regular hex values as normal // because member PCP only has 3 bits, member DEI has only 1, and // the rest belong to ID. In order to get to the value we want // we would have to iterate 0x40 all the way up to 0xFF before we would // ever use byte 2 making it have a value of 0x01 // Another words: 0xFF 0x00 comes before 0x00 0x01 in this sequence // bit patterns, but since we have the value of 0x40 already in the first // byte of data[n] giving us a bit pattern of [0][0][0] [0] [0][0][0] | [0][0][0][0] [0][1][0][0] // what does making byte 2 with a value of 0x01 do to this pattern? // It does this: [0][0][0] [0] [0][0][0] | [0][0][0][1] [0][1][0][0] // Okay so data[0] = 0x40 and data[1] = 0x01 so how does this // give us the values of {0,0,20} or {0x00,0x00,0x14} ? // Let's see from the far left going right the first 3 bits // are PCP and all bits are 0 giving it a value of 0 // Next is the single bit for DEI which has a value of 0. // Finally the next 12 bits are for ID and when we look at this 12 bit // pattern we have [0][0][0][0] | [0][0][0][1] [0][1][0][0] // Let's ignore the left 4 since they are all 0s or padding at this moment // So we can see that [0][0][0][1] [0][1][0][0] = 0x14 in hex with a // a decimal value of 20. 名成员。

{{1}}

现在唯一的问题是:我在运行Win7 x64家庭高级版的英特尔四核处理器上的MS Visual Studio 2017 CE中执行此操作,并将其编译为x86应用程序。实际存储位的位置也会因编译器,操作系统和体系结构而异。我只是从左到右显示了纯数学位表示,大多数机器将以从右到左的顺序存储它们的位。如果您在小端机器上运行并使用Visual Studio编译器,您应该得到类似的结果。

以下是关于位域的一篇写得很好的文章;如果我遇到更多,我会在这里发布:

答案 2 :(得分:0)

使用std::bitset

这样的事情怎么样?
#include <iostream>
#include <bitset>

class VLANHeader
{

private:

    // 000      0       000000000000
    // PCP      DEI     ID
    std::bitset<16> bin;

public:

    VLANHeader(uint8_t byte1, uint8_t byte2) : bin(byte1 << 8 | byte2) {}
    unsigned long getPCP() const { return (bin >> 13).to_ulong(); }
    unsigned long getDEI() const { return ((bin >> 12) & std::bitset<16>(0x1)).to_ulong(); }
    unsigned long getID() const { return (bin & std::bitset<16>(0xFFF)).to_ulong(); }
};


int main()
{

    VLANHeader vh(0x00, 0x14);
    std::cout << "PCP: " << vh.getPCP() << std::endl;
    std::cout << "DEI: " << vh.getDEI() << std::endl;
    std::cout << "ID: " << vh.getID() << std::endl;

    system("pause");
    return 0;
}

0x000x14字节对将转换为二进制0b0000000000010100,因此如果标头格式为uint16_t PCP : 3, DEI : 1, ID : 12;,则这就是我们想要的PCP =&gt; 000,DEI =&gt; 0,ID =&gt;这可以通过按照上述代码进行掩蔽和移位来提取。甚至可能适用于不同的endian系统。

enter image description here