大小端转换定义结构体的技巧

        这两年一直在写协议分析和报文填充相关内容。因为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为单位定义,对于超过一字节又不足两字节的变量,要拆成两部分处理。




你可能感兴趣的:(网络数据处理)