大小端,字节序,位序,字节对齐,位域对齐,一文看懂

测试用源代码:

#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;
}

分析与结果:

大小端,字节序,位序,字节对齐,位域对齐,一文看懂_第1张图片

总结:

  • 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)
这里给出一个更直观的大小端对比图:
大小端,字节序,位序,字节对齐,位域对齐,一文看懂_第2张图片

以上图再导出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  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

你可能感兴趣的:(大小端,字节序,位序,字节对齐,位域对齐,一文看懂)