【问题标题】:C++ struct bit field doesn't parse data correctlyC++ 结构位字段无法正确解析数据
【发布时间】:2017-12-26 22:25:15
【问题描述】:

我正在尝试使用打包结构从 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


    【解决方案1】:

    OP 这么问:

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

    也许位域不是适合这项工作的工具?

    尽管在使用位域或联合时,将机器的字节序考虑在内始终是一个很好的考虑因素,并且不应该忘记这一点。但是,在您目前的情况下,我看不出字节序是导致任何问题的原因或问题。至于问题的第二部分,这完全取决于具体的需求。如果要写入的代码专门用于特定的架构/操作系统/平台并且不太可能是可移植的,那么如果正确构造位域,则使用位域应该没有任何问题。即使您决定移植到其他机器,您仍然可以使用位域,但您必须更加小心,并且可能必须使用预处理器指令或控制 switch 和 case 语句编写更多代码才能使用代码并做一件事在一台机器上而不是另一台机器上。

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

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

    可能需要将字节序考虑在内。


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

    你当前的结构是:

    #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。我将 cmets 添加到您的结构中以显示此模式。

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

    然后您通过索引和设置十六进制值来填充您的指针数组。然后,通过将其转换为该类型,将 array 中的值分配给您的 bitfield。但是,我认为您的假设是 data[0] 应该适用于位域的 1st2 成员,而 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. 
    

    您的位域在内存中发生的情况是第 1st 字节或 8 bits 正在同时消耗 PCPDEI 以及ID 的第一个st4 bits,我认为这就是你感到困惑的地方。正如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,这充当了一个单比特布尔标志,它只能存储[0,1] 的值,最后ID12 bits 可用,其中1st4来自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 在对此答案的评论中提到了这些陈述:

    我没有得到“当该成员不能再包含足够高的值时,来自 data[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 在将其转换为位域结构之前的位模式。现在让我们看看在转换之前所有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] 二进制。即使不使用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 编译器,您应该会得到类似的结果。


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

    【讨论】:

    • 我没有得到“当该成员不能再包含足够高的值时,来自 data[0] 的位溢出到其他位域成员中”,我看不到溢出的位置正在发生
    • 也根据您在答案底部的对齐方式,我所做的应该可以工作,因为当 ID 为 20 时,位域仅使用数据 [1] 的第二个字节
    • @Lior Sharon 将指针数组转换为位域结构时会发生溢出。您使用 pragma 将位域“打包”为单字节对齐。您没有明确地为您的位域分配值,例如:vlanHeader.PCP = 0; vlanHeader.DEI = 0; vlanHeader.ID = 20; 您从指针数组中转换了它。正在打包各个位。运行我在答案中间部分提供的代码以生成日志文件。当您将值设置为 data[0] 而不对 data[1] 执行任何操作时,请注意当您将值从 0x00 增加到 0x10 时会发生什么。
    【解决方案2】:

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

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

    【讨论】:

    • 我真的不明白,我从问题中写入的格式(0x00 0x14)接收来自套接字的VLAN数据,有没有办法将数据从网络字节顺序转换为主机字节顺序?
    【解决方案3】:

    这样的事情怎么样,使用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;
    }
    

    0x00,0x14这对字节会被转换成二进制为0b0000000000010100,所以如果头部格式是uint16_t PCP : 3, DEI : 1, ID : 12;这就是我们想要的PCP => 000, DEI = > 0, ID = > 000000010100。这可以按照上面的代码通过掩码和移位来提取。甚至可能适用于不同的字节序系统。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多