1 概述
大小端(endian)会同时涉及byte序和bit序。大端指低地址,高byte,高bit;小端指低地址,低byte,低bit。
为方便讨论,下面以IP地址 A.B.C.D 为例,每个byte中bit值用不同的字母来区分。该IP地址在不同 endian 中内存布局如下所示(从左往右,地址由低到高):
========================================
Big endian (host & network)
========================================
A B C D
abcdefgh ijklmnop qrstuvwx yz123456
========================================
Littleendian (host)
========================================
D C B A
654321zy xwvutsrq ponmlkji hgfedcba
========================================
Littleendian (network)
========================================
A B C D
Hgfedcba ponmlkji xwvutsrq 654321zy
大端系统主机序和网络序完全一致(byte序和bit序),符合人脑逻辑思维。
小端系统的主机序(byte序和bit序)和大端系统的主机序(或网络序)完全相反。
小端系统的网路序中的byte序和大端系统一致,bit序在每个byte范围内和大端系统相反。
2 字节序转换
glibc 提供的主机序和网络序之间的转换函数ntohl& ntohs & htonl & htons 只转换byte序,不转换(一个byte内的)bit序。
3 位域的定义
GCC对位域定义的解释是按照物理内存由低到高(对应上图即由左至右)。
常见的一个byte内的位域定义如下:
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4,
version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4,
ihl:4;
#else
#error "Pleasefix <asm/byteorder.h>"
#endif
如果跨域byte,下面貌似合理的定义,只要对照前面的内存布局,就会发现这样的定义是完全错误的:
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u32 b5:5,
b15:15,
b12:12;
#elif defined (__BIG_ENDIAN_BITFIELD)
__u32 b12:12,
b15:15,
b5:5;
#else
#error "Pleasefix <asm/byteorder.h>"
#endif
所以,位域的定义最好在一个byte之内定义,对于跨越byte之间的位域定义很危险,在小端系统下,这样的定义往往和人脑的逻辑思维不一致。GCC为什么不按逻辑顺序来解释位域的定义呢,个人理解是太复杂,几乎不可能实现。
4 移位操作
GCC对移位操作的解释是逻辑上的。对应上图,右移操作对于大端系统就是向右移,而对于小端系统就是向左移;左移操作反过来就可以了。由于GCC屏蔽了系统之间的差异,所以,本身移位操作不容易出错。但当移位操作和ntoh& hton等一起用的时候,就要清楚的知道当前的操作是针对网络序还是主机序。建议,移位操作最好针对主机序,减少出错几率。如果一定要是网络序,则建议将移位操作限制在一个byte范围之内,byte的定位通过地址的递增和递减来完成。
5 强制类型转换
强制类型转换,尤其是大转小的时候,在小端系统下要小心了,如下:
uint32_t ip = 0x0A0B0C0D;
uint16_t *p = (uint16_t*)&ip;
Printf("0x%x\n",p[0]); //结果是0x0C0D,而不是0x0A0B
如果,转换成网络序后,是不是就没问题了?
ip = htonl(ip);
Printf("0x%x\n",p[0]); //结果是0x0B0A,而不是0x0A0B
因为对于*p, p[0]等的操作,GCC的解释是按逻辑,而不是物理来处理的。所以对于强制类型转换,建议针对网络序处理,同时用移位操作来配合,如下:
uint32_t ip = 0x0A0B0C0D;
uint8_t *p = (uint8_t*)&ip;
Printf("0x%x\n",p[0]<<8 | p[1]); //结果是0x0A0B