原文名称:【假设】关于位序的一点说明
原文地址:http://www.paulchan.tk/?p=435
【高位低位 高地址低地址 高字节低字节】
一个字节(octet)的数据有8个位,可以表示2^8=256种不同的值。比如我们要表示一个字母'a',ASCII码值为97,十六进制表示为0x61,那么二进制表示为01100001b,那么左边的位为(数据)高位,右边的就是(数据)低位。
一个字节的存储空间也有8个位,也有高低位之分,习惯的,我们表示一个字节的存储空间时,规定左边的为(存储)低位,右边的是(存储)高位。
内存可以存储很多个字节,计算机是如何访问到特定的字节的呢?我们都知道,计算机通过内存地址去定位内存里数据,每一个字节的存储空间都有自己的一个地址。内存地址在32位机器上用一个32位的无符号整数表示,表示范围是0x00000000-0xffffffff(当然,主存不一定有这么多),这里0x00000000是最低地址,0xffffffff是最高地址。
一个多字节的数据,有高字节部分,和低字节部分之分。比如一个32位的整数0x12345678,要用4个字节才能表示。那么0x12就是数据的最高字节,0x78就是数据的最低字节。
【字节序】
计算机如何表示信息?在不同的系统,所用方法是有差异的。比如,我们要保存一个32位的整数0x12345678,总过需要4个字节,那么怎么安排这4个字节呢?可以这样考虑:把0x12345678分成四部分,0x12,0x34,0x56,0x78,分别放入4个字节,有多少种方法?根据全排列,可知答案是,4!=24种。当然,计算机世界没有这么蛋疼(没有像这样的方法:先放0x56,然后0x34,然后0x78,最后0x12)。世界上只有两种系统,一种是大端系统,另一种是小端系统。大端系统把数据的高位部分保存在低地址的字节中,以上面0x12345678为例,把0x12存在第一个字节,0x34存在第二个字节,以此类推。小端系统刚好相反,把数据的低位部分保存在低地址的内存中,同样以上面0x12345678为例,把0x78放在第一个字节,把0x56放在第二个字节,一次类推。眼尖的读者可能已经注到这种差异带来的问题:如果一个大端系统发一个数字0x12345678给一个小端系统,小端系统也确实收到了这个数据,但是在它看来,收到的是0x78563412,因为它用一套不同的方法解释这个数据!因此,在网络编程中,必须要小心对待字节序问题。所以在发二进制数据之前,我们先统一把字节序转换成网络字节序(大端);同样的,接收数据之后,也把收到的网络字节序数据先转换成本系统所用的字节序,然后再去使用这个数据。站在巨人的肩膀上,我们有hton和ntoh系列函数可用,不足为虑。
【字节位序】
字节间的顺序问题是需要注意的,那么一个字节的位之间呢?答案是肯定的,不信你看看TCP协议头的C语言定义,像URG,ACK,PSH,RST,SYS,FIN这些标志位在在不同字节序系统上的定义顺序是刚好相反的。大端系统里面,数据的高位存在字节低位;小端系统刚好相反。那么,我们在平时网络编程,发送一个字母'a',ASCII码值为0x61,为什么我们在发送数据是不用先转换位序?而且,hton和ntoh系列函数也没有针对一个字节的版本。下面的解释出于假设,还没有找到相关资料证实。
比如,我们发送一个字母'a',根据ASCII码值0x61,我们要发送的二进制数据是01100001b。根据网络协议RFC(RFC791APPENDIX B: Data Transmission Order)的规定,报文从左到右逐位发送,而且一个字节左边的位是高位(大端)。那么字母'a',用报文表示如下:
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |0 1 1 0 0 0 0 1| +-+-+-+-+-+-+-+-+ 【A】
在大端系统中,字母'a'的表示如下(从0为最低位,7为最高位):
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |0 1 1 0 0 0 0 1| +-+-+-+-+-+-+-+-+ 【B】
小端系统刚好相反(同样的,0为最低位,7为最高位):
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |1 0 0 0 0 1 1 0| +-+-+-+-+-+-+-+-+ 【C】
我们在发送以字节为单位的数据时,为什么不用进行转换呢?原因可能在于,所有发送的数据都要进行转换,所以放在网卡驱动或者其他相对比较底层的模块统一做转化,这样可以做到对高层透明,降低高层软件的复杂度。比如一个小端系统接受到一个报文【A】,网卡把这个报文拷贝到内存前先把它转化成【C】,当系统处理报文时,根据小端位序,所看到的也就是字母'a'了。因此,我们在发送前不用转换位序,接收后也不用转换位序,底层(猜测是网卡驱动)已经为我们做好这个转换了。
那么,回到另一个问题,为什么TCP协议头在定义上面提到的标志位时需要考虑顺序?原因可能在于,编译器在定义位域时,先定义的域放在字节低位,而后定义的域放在字节高位。比如,我们定义一个一个字节长的报文,包含两个域,每一个域刚好4个位,名字分别是a,b:
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ | a | b | +-+-+-+-+-+-+-+-+ 【D】
如果我们收到一个报文A,那么,对应的a应该等于0110b,也就是0x6;b应该等于0001b,也就是0x1。
如果我们没有针对不同系统定义不用的结构体,而统一用下面的结构题来处理报文:
- struct packet
- {
- uint8_t a:4, b:4;
- };
如果收到报文【A】,在大端系统上,报文在内存表示为【B】,那么,位域a对应【B】中的低4位,也就是0-3位,为0110b,也就是0x6,结果正确;在小端系统上,报文在内存中表示为【C】,那么,位域对应【C】中的低4位,也就是0-3位,为1000b,也就是0x1,结果错误。可以看到,不同系统的解释结果是不同的。因此,有必要为不同系统定义不同的结构体来处理报文,小端系统上域的定义顺序应该和大端位序(网络位序)刚好相反。针对报文【D】,我们可以为小端系统定义报文结构体如下:
最后的一个问题是,如果我们收到一个报文【A】,存在字节变量packet里面,那么,不论对于何种系统,packet & 0x0f 取到的都是数据低4位,也就是域b。为什么会这样一致呢?原因在于,在程序员看来,我存的数据是报文【A】,我不在乎系统内部是怎么表示,【B】或者【C】,我只需要编译器保证,packet & ox0f取到的是数据的低4位就好了。因此,编译器实现的位操作是针对数据的,而不是针对存储的,换句话说是存储无关的。针对数据相同的位操作,到了底层,编译器转化成针对存储的操作,在不同系统,是有区别的(刚好相反),但是这种区别对程序员是不可见的。
举个例子,我们把01100001b这个值存在字节变量packet里面,packet >>= 1的结果应该是0x30。
在大端系统,packet的在存储中表示为【B】,数据向右(数据低位)移1位,那么在存储中,必须让【B】向存储高位移1位,结果就是下面的【E】,在大端系统中,【E】就是0x30。
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |0 0 1 1 0 0 0 0| +-+-+-+-+-+-+-+-+ 【E】
在小端系统中,apcket在存储中表示为【C】,数据向右(数据低位)移1位,那么在存储中,必须让【C】向存储低位移1位,结果就是下面的【F】, 在小端系统中,【F】就是0x30。
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |0 0 0 0 1 1 0 0| +-+-+-+-+-+-+-+-+ 【F】
当然,程序员对存储移位操作的差异是不可见的,也不需要可见,要不然,还让不让人活!
上述是个人的一点猜测,有待考证!
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。