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

问题描述 投票:1回答:3

我正在尝试使用压缩结构从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,这不是我的意思。我认为问题是字节序(我的机器是小端)我不知道如何优雅地解决问题。

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

c++ struct network-programming bit-fields
3个回答
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,它是typedefunsigned short,它的大小是2 bytes或者是16 bitsunsigned short的值范围从[0,65535]

然后在结构体内,您将位域成员PCPDEIID设置为具有位数:3112。我在你的结构中添加了注释以显示这种模式。

现在,在主函数中声明指向uint8_t类型的指针,然后创建上述结构的实例,然后为数组大小为[2]的指针创建动态内存。这里uint8_ttypedefunsigned char,其大小是1 byte8 bits可以使用,因为你有2,你总共有2 bytes16 bits。好的,所以内存的总大小在你的bitfield structdata[]数组之间匹配。

然后,通过索引并使用十六进制值设置指针数组来填充指针数组。然后通过将array的值转换为该类型,将其分配给bitfield。然而,我认为你假设的是data[0]应该适用于位域的第一个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正在消耗PCPDEI以及4 bits的第一个ID,我认为这是你感到困惑的地方。正如SoronelHaetir在他们的简短回答中所说的那样,如果你想让你的3位域的值为{0,0,20},那么你需要将你的data array分别设置为data[0] = 0x40data[1] = 0x01。来自data[0]的位会溢出到其他位域成员中,当该成员不再包含足够高的值而不是分配的位数可以支持时。

这基本上意味着PCP3可用bits,它的最大组合位数是2^3 = 8所以PCP可以存储来自[0,7]的值。由于DEI只有1 bit这个作为一个单位bool标志,只能存储[0,1]的值,最后ID12 bits可用的第一个4来自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])

编辑

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

我没有得到“当数据[0]中的位在该成员不再包含足够高的值时溢出到其他位域成员”时,我看不到溢出发生的位置--Lior Sharon

另外根据你在答案底部的对齐我所做的应该工作,因为当ID为20时,只有data1的第二个字节被位域使用

我在这里尝试做的是显示data[0]data[1]的位模式,其值为0x400x01

Byte 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在你将它转换为你的bitfield结构之前的位模式。现在让我们在演员表之前查看所有0s的位域,然后让我们看一下与位域成员相关的十六进制值以及它们可以存储的值。我已经说过PCP可以存储来自[0-7]的值,DEI可以存储值[0,1],ID可以存储来自[0-4095]的十进制值。您将十六进制值分配给两个字节或16位内存。你希望PCPDEI的值为0,而ID的值为20(十进制)。你认为第一个字节的0x00将给PCPDEI 0的值,而0x14应该给ID 20的值。那样不行。 0x14为十六进制值表示内存中的一个字节,但ID12 bits1.5 bytes来存储。如果您参考上面的图表,成员PCP只有3位存储,所以如果我们将7的值添加到data[0]中,PCP将如下所示:[1][1][1] in binary。即使没有使用data[1]的字节,我们也可以将值推送到DEIID成员。

  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.

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


这里有一些关于位域的精美文章;如果我遇到更多,我会在这里发布:


0
投票

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

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

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 => 000,DEI => 0,ID => 000000010100。这可以通过屏蔽和移位来提取按照上面的代码。甚至可能适用于不同的endian系统。

enter image description here

© www.soinside.com 2019 - 2024. All rights reserved.