这两年一直在写协议分析和报文填充相关内容。因为PC机是小端(Little Endian),网络序是大端(Big Endian),在写代码的时候必须考虑到大小端转换的问题,否则网卡或者网络设备会解析错误。网上的总结分析甚少,大部分都还处于纠结于大小尾分辨的层次。今天我就来深度分析一下大小尾问题。
首先,不管是哪一种数据存放方式,对于一个单字节的数据,存储方式都是一样的。比如uint8_t类,char类在大端和小端都可以不经过转换直接读取。
而对于两字节及以上字节大小的数据,大小端的存储方式就有区别了。大端(网络序)的存储方式与我们的阅读逻辑是相同的。大端的存储方式是高位保存在低地址中,小端(常见的PC机)的存储方式则是高位存高地址。
举个例子就明白了,比如有个数字0x12345678,存储到内存至少需要4字节,按照大端的存储方式,则是0x12 0x34 0x56 0x78,与我们的阅读逻辑一致;而小端的存储方式则是0x78 0x56 0x34 0x12。记住大端存储方式(网络序)与我们的阅读逻辑相同就可以了。
那么根据上面的分析,我们就会发现,大小端基于内存的颠倒是以字节为单位的,不是以每一位为单位的,这一点在具体的代码实现中尤其重要。
假定我们有一个uint16_t类型的数据,在小端机器上面处理(现在的pc机基本都是小端机)需要转换成大端怎么转换呢?前一字节与后一字节位置换。
假定我们有一个uint32_t类型的数据,在小端机器上面处理(现在的pc机基本都是小端机)需要转换成大端怎么转换呢?前一字节与后一字节置换,中间两个字节也互相置换。
那么,再进一步,如果有一个结构体,大小32位(4字节),定义如下:
struct test_u32{
uint8_t a;
uint8_t b;
uint8_t c;
uint8_t d;
};
在大小端转换时,自然是a和d互换,b和c互换。因此,完全可以在小端定义结构体时,就定义成如下结构:
struct test_u32{
uint8_t d;
uint8_t c;
uint8_t b;
uint8_t a;
};
这样在进行网络通信时就能省去了大小端转换这个步骤,在大小端转换较多的情况下,通过改变结构体定义的方式尽可能省去大小端转换的步骤,对于数据分析和网络传输的速率提升是明显的。那么接下来再考虑一下情况,如果上述结构体在大端机定义如下:
struct test_u32_2{
uint8_t a;
uint16_t b;
uint8_t c;
};
如果只是在小端机定义成如下形式,会有什么影响吗?struct test_u32_2{
uint8_t c;
uint16_t b;
uint8_t a;
};
明显的,a和c由于他们的定义不大于1字节,因此a和c的数据不会有错。而由于b大于1字节了,所以仍然需要进行大小端转换。这里就是一个陷阱。另外在实际的网络私有协议定义中,不可能所有的数据都是基于8位(1字节)对齐,有些数据可能只占1位,有些数据可能占35位,遇到这种情况该如何处理呢?
首先,如果是如下情况,n个变量瓜分一个字节:
struct test_u8{
uint8_t a:1;
uint8_t b:3;
uint8_t c:4;
};
此时变量a占1位,变量b占3位,变量c占4位,他们共享一个字节。一个字节是8位,在内存中的存储方式是“abbbcccc”(每个字符代表该位存储着哪个变量的数据)
对于这种情况,可以直接在小端定义中进行置换:
struct test_u8{
uint8_t c:4;
uint8_t b:3;
uint8_t a:1;
};
在小端如此定义,内存中的数据存储方式依然是abbbcccc。
struct test_u16{
uint16_t a:3;
uint16_t b:1;
uint16_t c:12;
};
这个时候我会想当然的认为可以置换成如下形式,但是打印结果告诉我出错了。struct test_u16{
uint16_t c:12;
uint16_t b:1;
uint16_t a:3;
};
比如我在置换后的结构体中,令a=1,然后按照uint16_t类型输出该结构体数据,按照预想情况应该是0x2000,但是实际打印数据是0x0100。根据打印结果推原因,出错理由如下。1.在大端中,数据保存方式应当如下:aaabcccc cccccccc。那么如果a=1,b=0,c=0,数据当是0010000 00000000,也就是0x2000。但是现在存储内容成了00000001 00000000,变成了0x0100,那么此时在内存中,数据保存形式为xxxxxaaa xxxxxxxx (x表示未知是b还是c的存储位),经过测试,如此错误定义结构体,使得本来的数据存储方式变为了ccccbaaa cccccccc 。为什么?这个需要自己画图想一下,注意大小端的转换是以字节为单位转换而不是以位为单位转换的。
因此需要修改定义为:
struct test_u16{
uint8_t c_1:4;
uint8_t b:1;
uint8_t a:3;
uint8_t c_2;
};
这时令a=1,b=0,c_1=0,c_2=0,结构体安装uint16_t输出便是0x2000。 结论:在大小端处理时遇到不是整自己的变量,定义结构体以uint8_t为单位定义,对于超过一字节又不足两字节的变量,要拆成两部分处理。