对于比特序的理解

原文名称:【假设】关于位序的一点说明

原文地址: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。

如果我们没有针对不同系统定义不用的结构体,而统一用下面的结构题来处理报文:

  1. struct packet  
  2. {  
  3.     uint8_t a:4, b:4;  
  4. };  

如果收到报文【A】,在大端系统上,报文在内存表示为【B】,那么,位域a对应【B】中的低4位,也就是0-3位,为0110b,也就是0x6,结果正确;在小端系统上,报文在内存中表示为【C】,那么,位域对应【C】中的低4位,也就是0-3位,为1000b,也就是0x1,结果错误。可以看到,不同系统的解释结果是不同的。因此,有必要为不同系统定义不同的结构体来处理报文,小端系统上域的定义顺序应该和大端位序(网络位序)刚好相反。针对报文【D】,我们可以为小端系统定义报文结构体如下:

    uint8_t b:4, a:4;  
  • 域内的位序不用管了吗?Yes!不同系统各自以自己的方式去处理这个位域,在发送数据前,后者接收数据后,底层(网卡驱动)统一进行逐字节位序转换。

    最后的一个问题是,如果我们收到一个报文【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】
    

    当然,程序员对存储移位操作的差异是不可见的,也不需要可见,要不然,还让不让人活!

    上述是个人的一点猜测,有待考证!

  • 相关文章

    文章浏览阅读903次。文章主要介绍了收益聚合器Beefy协议在币...
    文章浏览阅读952次。比特币的主要思路是,构建一个无中心、去...
    文章浏览阅读2.5k次。虚拟人从最初的不温不火,到现在步入“...
    文章浏览阅读1.3k次,点赞25次,收藏13次。通过调查和分析用...
    文章浏览阅读1.7k次。这个智能合约安全系列提供了一个广泛的...
    文章浏览阅读1.3k次。本文描述了比特币核心的编译与交互方法...