今天在写一个协议分析程序时,使用了位域,因为协议的一个数据包有的参数并不是占据n个字节(bytes),而是占据n位(bits)。
比如有1个字节包含了4个参数,分别占据的位数为3,1,1,2,1,我就在我的数据包结构体中定义了如下位域
struct Packet
{
// ...
uint8_t a : 3;
uint8_t b : 1;
uint8_t c : 1;
uint8_t d : 2;
uint8_t e : 1;
// ...
};
调试的时候发现结果不对,然后写了个测试
struct Widget
{
uint8_t a : 4;
uint8_t b : 3;
uint8_t c : 1;
void show()
{
printf("%d, %d, %d\n", a, b, c);
}
};
void func()
{
uint8_t x = 0b11100110;
auto p = (Widget*)&x;
p->show(); // output: 6, 6, 1
}
a表示前4位,b表示中间3位,c表示后面1位,直观地来看,a是1110(14),b是011(3),c是0。但结果并非直观看到的那样。
问题出在内存布局方面,windows系统是小端布局,即低地址存放低字节,也就是位域的顺序是反过来的,即a是0110(6),b是110(6),c是1。
需要注意的是,大端小端是以字节为单位的,所以在内存中,x并非存储为
0 1 1 0 0 1 1 1 (从左向右地址依次增加)
而是针对位域而言,低地址的成员(a)对应的是字节的后半部分。
引用《C++ Primer》第5版19.8.1的说明
当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
位域在内存中的布局是与机器相关的。
其中19.8这一小节的标题是固有的不可移植的特性
因此使用位域的时候,必须明确程序运行的机器是大端还是小端,再在代码中定义各位域的具体顺序。像Linux中/usr/include/netinet/tcp.h中定义TCP包结构时就使用了位域
# if __BYTE_ORDER == __LITTLE_ENDIAN
u_int16_t res1:4;
u_int16_t doff:4;
u_int16_t fin:1;
u_int16_t syn:1;
u_int16_t rst:1;
u_int16_t psh:1;
u_int16_t ack:1;
u_int16_t urg:1;
u_int16_t res2:2;
# elif __BYTE_ORDER == __BIG_ENDIAN
u_int16_t doff:4;
u_int16_t res1:4;
u_int16_t res2:2;
u_int16_t urg:1;
u_int16_t ack:1;
u_int16_t psh:1;
u_int16_t rst:1;
u_int16_t syn:1;
u_int16_t fin:1;
# else
可以看到它是用系统定义的宏来区分大端还是小端。通过代码跳转可以找到__BYTE_ORDER宏的定义
#define __BYTE_ORDER __LITTLE_ENDIAN
或者,在结构体内不使用位域,而是使用字节来存储(比如8位),再通过位运算来计算出字节具体若干位对应的值,比如刚才的Widget类可以改写成下面这样来避免机器大小端影响。
struct Widget
{
uint8_t x;
uint8_t a() const { return (x & 0xf0) >> 4; }
uint8_t b() const { return (x & 0x0e) >> 1; }
uint8_t c() const { return (x & 0x01); }
void show()
{
printf("%d, %d, %d\n", a(), b(), c());
}
};
void func()
{
uint8_t x = 0b11100110;
auto p = (Widget*)&x;
p->show(); // output: 14, 3, 0
}