#include
#include
#if 1
struct Test
{
unsigned short a:2;
unsigned short b:3;
unsigned short c:5;
unsigned short d:8;
};
#else
struct Test
{
unsigned char a:2;
unsigned char b:3;
unsigned char c:5;
unsigned char d:8;
};
#endif
int main(void)
{
struct Test t;
memset(&t, 0x00,sizeof(t));
t.a = 1;
t.b = 1;
t.c = 1;
t.d = 1;
printf("%08X\n", *(unsigned int *)&t);
return 0;
}
网上大多数文章的例子:
大端模式:一个多字节数据的高字节在前,低字节在后,以数据 0x1234ABCD 看例子:
低地址 ---------------------> 高地址
±±±±±±±±±±±±±±±
| 12 | 34 | AB | CD |
±±±±±±±±±±±±±±±
小端模式:一个多字节数据的低字节在前,高字节在后,仍以 0x1234ABCD 看:
低地址 ---------------------> 高地址
±±±±±±±±±±±±±±±
| CD | AB | 34 | 12 |
±±±±±±±±±±±±±±±
这里有一个重大的存在可能误导的地方,就是上面只做了字节序的调整,没有做位序(比特序)的调整。严格的说这只是小端CPU里网络序与主机序的转换,而不是大小端的转换。
我们都知道上面的例子中小端模式十六进制的CD存在低地址。那小端模式十六进制的CD的二进制到底是怎么存的呢?
看了上面的图例我们可以推测出“CD”的二进制形式在小端模式下,仍然是反书写顺序的(即从右往左看才能得到CD)
这里给出一个更直观的大小端对比图:
因为在以太网中,字节序我们是按照大端序来发送,但是位序(比特序)却是按照小端序的方式来发送(LSB first)
结合上图,网络发送顺序为:
这里解释了为什么小端CPU的网络序与主机序的转换是0x12345678,变成0x78563412.(即比特序在一个字节内是没有变化的)
是的,大小端之间传送不用做位序(比特序)的转换。重复上面的话,LSB first, MSB first是协议约定,约定好了,之后自然再按约定还原即可。打个比方,快递一套家具,先要拆分,再打包,再发送,到了之后再组装还原。这个过程就是协议做的事情。对用户(读写程序)来说,看到的一直是一套完整的家具(数据)。
大端CPU不用做字节转换,发送时的位序(比特序)的转换是协议的事情,当然发送的位序与内存里的位序是不一样的。怎么发送,是协议的事情。
小端机内存中(低地址到高地址) | 字节转换后 | 以太网中 | 大端机内存中(低地址到高地址) |
---|---|---|---|
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
208 209 210 211 212 213 214 215 200 201 202 203 204 205 206 207 |
208 209 210 211 212 213 214 215 200 201 202 203 204 205 206 207 |
215 214 213 212 211 210 209 208 207 206 205 204 203 202 201 200 |
脚->下半身->上半身->头 | 上半身->头,脚->下半身 | 上半身->头,脚->下半身 | 头<-上半身<-下半身<-脚 |
字节转换有长度问题,比如两个字节一转?还是四个字节一转?还是八个字节一转?
而位转换只有一种,就是8个位一转。
所以小端CPU要调用字节转换函数。
先看几个例子:
//linux-3.4/include/linux/netfilter/nf_conntrack_proto_gre.h
struct gre_hdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 rec:3,
srr:1,
seq:1,
key:1,
routing:1,
csum:1,
version:3,
reserved:4,
ack:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 csum:1,
routing:1,
key:1,
seq:1,
srr:1,
rec:3,
ack:1,
reserved:4,
version:3;
#else
#error "Adjust your defines"
#endif
__be16 protocol;
};
//linux-3.4/include/linux/icmpv6.h
struct icmpv6_nd_advt {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u32 reserved:5,
override:1,
solicited:1,
router:1,
reserved2:24;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u32 router:1,
solicited:1,
override:1,
reserved:29;
#else
#error "Please fix "
#endif
} u_nd_advt;
struct icmpv6_nd_ra {
__u8 hop_limit;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 reserved:3,
router_pref:2,
home_agent:1,
other:1,
managed:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u8 managed:1,
other:1,
home_agent:1,
router_pref:2,
reserved:3;
#else
#error "Please fix "
#endif
__be16 rt_lifetime;
} u_nd_ra;
由上面三个内核源代码里的结构体可以看出规律:
1. 以一个字节为单位,字节内的位域位置反转。
2. 大端结构体里位域字段顺序与网络序相同,小端相反。
有没有跨字节的例子?有802.1Q协议里的vlan字段就跨了字节。
Type | PRI | CFI | Vlan ID |
---|---|---|---|
16bits | 3bits | 1bits | 12bits |
//linux-3.4/include/linux/if_vlan.h
/**
* struct vlan_ethhdr - vlan ethernet header (ethhdr + vlan_hdr)
* @h_dest: destination ethernet address
* @h_source: source ethernet address
* @h_vlan_proto: ethernet protocol (always 0x8100)
* @h_vlan_TCI: priority and VLAN ID
* @h_vlan_encapsulated_proto: packet type ID or len
*/
struct vlan_ethhdr {
unsigned char h_dest[ETH_ALEN];
unsigned char h_source[ETH_ALEN];
__be16 h_vlan_proto;
__be16 h_vlan_TCI;
__be16 h_vlan_encapsulated_proto;
};
使用位移或位运算处理,占2个字节的h_vlan_TCI在网络序转主机序后,去掉高位的4位就是vlan ID.
参考资料:https://www.linuxjournal.com/article/6788