测试用源代码:
#include<stdio.h>
#include<string.h>
#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;
}
分析与结果:
总结:
- csdn上很多文章称“位域不可以跨越字节”,错。正确说是位域不可以跨越变量类型。如图中中间的例子(测试用源代码里用#if #else分别测试了unsigned short 和unsigned char两种情况)。
- 字节对齐与位域对齐的规则网上有很多文章,图中给出了两种例子,其实原则都是一样的:以对齐要求为边界(通常是4字节为边界),能挤就挤。不能挤就再开一个。
- 面试时遇到这类题的解题思路如上图:
1,先从低地址到高地址画出一张图;
2,再把结构体成员按照字节对齐和位域对齐要求填入;
3,再把成员变量的二进制值填入,小端与书写顺序相反(从右往左写值),大端符合书写和阅读习惯(从左往右)
4,根据二进制的值计算出最终的输出。(注意小端低地址存的是数字的低位)
再来说一说网上众说纷纭的MSB,LSB,结合大小端的问题:
网上大多数文章的例子:
大端模式:一个多字节数据的高字节在前,低字节在后,以数据 0x1234ABCD 看例子:
低地址 ---------------------> 高地址
±±±±±±±±±±±±±±±
| 12 | 34 | AB | CD |
±±±±±±±±±±±±±±±
小端模式:一个多字节数据的低字节在前,高字节在后,仍以 0x1234ABCD 看:
低地址 ---------------------> 高地址
±±±±±±±±±±±±±±±
| CD | AB | 34 | 12 |
±±±±±±±±±±±±±±±
这里有一个重大的存在可能误导的地方,就是上面只做了字节序的调整,没有做位序(比特序)的调整。严格的说这只是小端CPU里网络序与主机序的转换,而不是大小端的转换。
如果真要做大小端的转换呢?
我们都知道上面的例子中小端模式十六进制的CD存在低地址。那小端模式十六进制的CD的二进制到底是怎么存的呢?
看了上面的图例我们可以推测出“CD”的二进制形式在小端模式下,仍然是反书写顺序的(即从右往左看才能得到CD)
这里给出一个更直观的大小端对比图:
以上图再导出MSB与LSB:
- MSB 与LSB是数字的高低位的概念,是数字就有最高位和最低位。
- MSB first 与LSB first 是传输或拷贝时的概念,常见于不同协议之间的转换(比如32位传输转换到到8位传输),以上图举例:
如果是LSB first ,这是在告诉我们传输或拷贝时,数据的低位放在前面,即从2的0次方开始传输或拷贝。
为什么htonl()、ntohl()只做了字节转换?
因为在以太网中,字节序我们是按照大端序来发送,但是位序(比特序)却是按照小端序的方式来发送(LSB first)
结合上图,网络发送顺序为:
- 224 225 226 227 228 229 230 231
- 216 217 218 219 220 221 222 223
- 208 209 210 211 212 213 214 215
- 200 201 202 203 204 205 206 207
这里解释了为什么小端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 |
脚->下半身->上半身->头 | 上半身->头,脚->下半身 | 上半身->头,脚->下半身 | 头<-上半身<-下半身<-脚 |
- 小端CPU要做字节转换,然后让以太网协议去传输,传输完成,最终的数据是一致的(大小端只是方向反了而已)。
- 可见从大端机到网络也是有“转换”的。只是编写上层的程序时不用考虑。驱动或硬件要考虑。
为什么小端机不做成驱动/硬件自动字节转换?
字节转换有长度问题,比如两个字节一转?还是四个字节一转?还是八个字节一转?
而位转换只有一种,就是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 <asm/byteorder.h> 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 <asm/byteorder.h>"
#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 <asm/byteorder.h>"
#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.