字节序转换与结构体位域(bit field)值的读取 Part 2 - 深入理解字节序和结构体位域存储方式
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字节序转换与结构体位域(bit field)值的读取 Part 2 - 深入理解字节序和结构体位域存储方式相关的知识,希望对你有一定的参考价值。
上一篇文章讲解了带位域的结构体,在从大端机(Big Endian)传输到小端机(Little Endian)后如何解析位域值。下面继续深入详解字节序,以及位域存储的方式。
(1) 我们知道,存储数字时,对小端机而言,数字的低位,存在低地址,高位存在高地址。大端机正相反。
(2) 读取的方式,也是一样的。对于小端机,读出的低地址位作为数字的低位。
(3) 此外Big-Endian/Little-Endian存储顺序,不仅仅针对字节,还针对字节内的比特位。对于小端机而言,字节内的8个比特,低地址端比特位,对应二进制数字的低位。
(4) 对于结构体的多个位域,和普通成员一样,编译器同样按照地址由低到高顺序存储,无论是大端机还是小端机。只是位域内的比特顺序有区别罢了。
(5) 表述一个数值,可以使用两种视图 :
第一个是“逻辑视图”,通俗的表述方式,也就是我们平时在书本上看到的,手写数字时的方式。左边为高位,右边为低位。例如 375,-4.1,036,0xAF4D215B
另一个是“内存视图”,即数字在内存中的存储方式,是我们程序员专有的一种表述方式。左边为低地址字节,右边为高地址字节。字节内左边为低地址比特位,右边为高地址比特位。很明显,同一个unsigned int值,在大端机、小端机上,分别有两种不同的“内存视图”。
例如,uint16 0x2A1F,二进制比特位为0010 1010 0001 1111 (显然这一行使用的就是“逻辑视图”)
在小端机上的“内存视图”为:1111 1000 0101 0100 (低地址 -> 高地址)
在大端机上的“内存视图”为:0010 1010 0001 1111 (低地址 -> 高地址)
另外可以看到,大端机的"内存视图"和"逻辑视图"是相同的。在很多相关的文章里,并没有去区分数字的两种表述方式,导致了很多混淆。其次,很多例子使用16进制,只能用于表达字节序,无法精确表达内部的比特顺序。
再举一个上一节使用过的例子:
typedef struct _exam_ { unsigned int tag : 6; unsigned int field1 : 3; unsigned int field2 : 7; unsigned int field3 : 11; unsigned int pad : 5; }Exam; Exam ex; ex.tag = 4; ex.field1 = 2; ex.field2 = 0x3a; ex.field3 = 0x4C1; ex.pad = 0;
变量ex的6个位域的"内存视图",在大端机是000100 010 0111010 10010110001 00000(低地址->高地址),在小端机是001000 010 0101110 10001101001 00000(低地址->高地址)。可见位域顺序是一样的,但是位域内比特位顺序不同。
若按照4位一组,大端机"内存视图"为 0001 0001 0011 1010 1001 0110 0010 0000,如果按照unsigned int的方式读取这块内存,结果是0x113A9620,四个比特位对应一个16进制数,和"逻辑视图"完全一样
在小端机上4位一组排列,"内存视图"为 0010 0001 0010 1110 1000 1101 0010 0000,如果按照unsigned int的方式读取这块内存,就会按照小端机的方式来解析内存。可以先把二进制翻译为"逻辑视图" - 把整个"内存视图"32位颠倒顺序,结果是0x04B17484,注意不是0x212E8D20.
那么这些规则,对位域值的读取有什么影响呢?
字节流在网络上传输是按照网络字节序传输的,也就是大端序。网卡不知道数据的含义(到底是int还是double,还是什么image),只能看到一个个字节,因此它做的就是把每个字节的8个比特位转换为本机的位序。而具体的内容,则由我们的程序处理。比如对于整形等,调用socket接口的ntohl(),htonl()...等函数转换字节序。顺便提一句,对于float/double类型,可以直接memcpy到一个整形里面,之后按照整形正常的处理流程,到了目标机后,再memcpy到一个float/double里。
char,short,int,long等2次幂大小的整形,作为一个单独的整体,经过整个流程梳理是没有任何问题的。但无法保证结构体内的多个位域,按照定义的先后顺序,从低地址到高地址排列。这意味着,无论如何,直接在代码中使用ex.tag的方式,是读不出tag位域的数据的。
细分有如下几种情况:
(1) 主机内部传输无任何影响,毕竟是一样的CPU架构。
(2) 相同字节序的主机间传输,同样没有影响。因为经过二次socket+网卡转换后,码流是相同的。读者可自行验证。
(3) 大端机传输到小端机(上一节所描述的)。下列二进制值如没有特殊说明,都是"内存视图"。
还以上面的位域为例,在大端机的为000100 010 0111010 10010110001 00000(低地址->高地址),按照四比特一组为: 0001 0001 0011 1010 1001 0110 0010 0000
传输到网络中,由于大端序和网络序相同,所以网卡不做转换,字节流按照先后,依然是 0001 0001 0011 1010 1001 0110 0010 0000
传输到小端机,网卡自动转换每个字节的比特序,但字节顺序维持原状, 1000 1000 0101 1100 0110 1001 0000 0100,可见原先跨字节相连的位域被"打散了"。字节内的位域,虽然比特顺序对了,但是从低比特位挪到了高比特位,位置错了。
调用ntohl,比特序不变,转换字节序,0000 0100 0110 1001 0101 1100 1000 1000,效果是跨字节位域再次连通了。位域内存地址顺序,正好和原先相反。如果把大端机的内存视图画到一张纸上,相当于翻到纸的背面。
此时,将这4个字节码流当作unsigned int,得到一个"无符号整形",其"逻辑视图"等于大端机上的“内存视图”。左边恰好是结构体最开始的位域:0001 0001 0011 1010 1001 0110 0010 0000。因此我们将错就错,直接使用位操作符来左移相应的位数(需要计算后边所有位域的总比特数),即可得到对应的位域值。位移操作符等,都是对"逻辑视图"操作的。
(4) 小端机传到大端机。网卡转换+ntohl转换后,依然在内存中得到一个位域顺序和正常顺序相反的"无符号整形"。只是这次使用位运算符要注意,第一个位域在"逻辑视图"的最右边,依次向左类推,和(3)的情形是相反的。
以上是关于字节序转换与结构体位域(bit field)值的读取 Part 2 - 深入理解字节序和结构体位域存储方式的主要内容,如果未能解决你的问题,请参考以下文章