字节序&位序

字节序

    字节序,又称端序、尾序,英文单词为Endian,该单词来源于于乔纳森·斯威夫特的小说《格列佛游记》,小说中的小人国因为吃鸡蛋的问题而内战,战争开始是由于以下的原因:我们大家都认为,吃鸡蛋前,原始的方法是打破鸡蛋较大的一端。可是当今皇帝的祖父小时候吃鸡蛋,一次按古法打鸡蛋时碰巧将一个手指弄破了,因此他的父亲,当时的皇帝,就下了一道敕令,命令全体臣民吃鸡蛋时打破鸡蛋较小的一端,违令者重罚。老百姓们对这项命令极为反感。历史告诉我们,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位…关于这一争端,曾出版过几百本大部著作,不过大端派的书一直是受禁的,法律也规定该派的任何人不得做官。1980年,Danny Cohen在其著名的论文"On Holy Wars and a Plea for Peace"中,为平息一场关于字节该以什么样的顺序传送的争论,而引用了该词。

        在计算机科学领域中,字节序是指存放多字节数据的字节(byte)的顺序,典型的情况是整数在内存中的存放方式和网络传输的传输顺序。有时候也可以用指位序(bit)。为了更好地理解,先看下面这段小程序,这个程序是把一个包含4位数字的字符串转换为16进制整数来存储,16进制整数的每一个字节存储一位数字字符。比如:”1234”,转换成16进制整数0x01020304。

程序1清单:

#include<stdio.h>

#include<conio.h>

 

int main( )

{

    char input[4] = {0};

    int integer   = 0;

    int i;

    printf("/r/n请输入一个位数,每一位的范围是从到0到9/r/n");

    for(i = 0; i < 4; i++)

    {

        input[i] = getch();

        if(input[i] > '9' || input[i] < '0')

        {

             printf("input error!/r/n");

             return 1;

        }

        putch(input[i]);

    }

   getch();

    putch('/n');

 

    for(i = 0; i < 4; i++)

    {

        input[i] = input[i] - '0';

    }

 

    memcpy((void*)&integer, (void*)input, 4);

 

    printf("转换后的进制数是:0x%08x/r/n", integer);

 

    getch();

 

    return 0;

}

 

现在来分析一下这段代码

首先,定义了一个字符数组input,用来接收用户输入的4个数字字符;

第二,把4个字符数字转换成对应的数字;

第三,把转换的数字复制到整型变量integer中;

最后在屏幕上打印。

如果在PPC或者ARM的机器上编译运行这个程序,那么会在屏幕上打印出结果:0x01020304,这与我们的预期一致;但是在X86的机器上则打印出的结果是:0x04030201。这个令人惊讶的结果正是字节序问题引起。下面来详细谈谈这个问题。

 

    从计算机诞生之后,就有几种不同的字节序,典型的是大端序(big endian)和小端序(little endian),当然还有不常见的混合序(middle endian)。这些用来都是描述多字节数据在内存中的存放方式的。以上面的16进制数0x01020304为例,在计算机中需要用4个字节来保存它,’01’, ’02’,’03’,’04’各占一个字节。按照人类的计数习惯,最左边的’01’,称之为最高有效位(MSB,Most Significant Byte,它具有最高权重);最右边的’04’称之为最低有效位(LSB, Least Significant Byte,他具有最低权重);在计算机中需要用4个字节来保存它,其中’01’, ’02’,’03’,’04’各占一个字节。大端序的计算机保存这个数值时,按照从低地址到高地址的顺序分别保存MSB到LSB四个字节,0x01020304的存储情况如下图所示:

 

而小端序的计算机则以相反的顺序来保存它,如下图所示:

 

 

可以看到,在小端序的计算机中,0x01020304的保存顺序恰好与上面的程序1中相反,这就是最后输出结果为0x04030201的原因;大端序的计算机中两者保存顺序一致,所以打印正确。

 

    我们可以在VC集成环境中来验证上面的分析。在VS2005中调试下面的小程序,在return语句处设置断点,断住后打开内存窗口查看&i处的内容。可以直观的看到在x86(小端序)的机器上整数的存放方式。

程序2清单:

#include<stdio.h>

int main()

{

    int i = 0x01020304;

    printf("i = %#x/r/n",i);

    return 0;

}


 

 

 

 

 

再看看如何才能让程序1在大端序和小端序的机器上都能正确执行呢?一个办法就是利用预编译宏,针对不同的机器定义定义不同的数据结构。下面是一个例子:

程序3清单:

#include<stdio.h>

#include<conio.h>

 

typedefunion

{

    struct{

#ifdef BIG_ENDIAN

    char msb;

    char midb1;

    char midb2;

    char lsb;

#else

    char lsb;

    char midb2;

    char midb1;

    char msb;

#endif

    } bytes;

    int  var;

} INTEGER;

 

int main()

{

    int  i         = 0;

    char input[5]   = {0};

    INTEGER integer = {0};

 

    printf("/r/n请输入一个位数,每一位的范围是从到到/r/n");

    for(i = 0; i < 4; i++)

    {

        input[i] = getch();

        if(input[i] > '9' || input[i] < '0')

        {

             printf("input error!/r/n");

             return 1;

        }

        putch(input[i]);

    }

   getch();

    putch('/n');

 

    integer.bytes.msb   = input[0] - '0';

    integer.bytes.midb1 = input[1] - '0';

    integer.bytes.midb2 = input[2] - '0';

    integer.bytes.lsb   = input[3] - '0';

 

    printf("转换后的进制数是:0x%08x/r/n", integer.var);

 

    getch();

 

    return 0;

}


可以看到,这段代码定义了两套数据结构,通过BIG_ENDIAN这个宏定义来决定使用哪一套数据结构。这是个笨拙却有效的方法。下面这个例子则漂亮一些:

程序4清单

#include<stdio.h>

#include<conio.h>

#include<memory.h>

#include<winsock2.h>

 

int main( )

{

    char input[4] = {0};

    int integer   = 0;

    int i;

    printf("/r/n请输入一个位数,每一位的范围是从到到/r/n");

    for(i = 0; i < 4; i++)

    {

        input[i] = getch();

        if(input[i] > '9' || input[i] < '0')

        {

             printf("input error!/r/n");

             return 1;

        }

        putch(input[i]);

    }

   getch();

    putch('/n');

 

    for(i = 0; i < 4; i++)

    {

        input[i] = input[i] - '0';

    }

 

    memcpy((void*)&integer, (void*)input, 4);

 

    integer = ntohl(integer);

 

    printf("转换后的进制数是:0x%08x/r/n", integer);

 

    getch();

 

    return 0;

}


这个程序利用了大端序与人类书写习惯一致的特点,通过ntohl函数将整数进行转换。这个函数的功能是将网络序转换成主机序,在大端机器上它什么也不做,在小端机器上它会将输入参数的值转换成小端序的值。在windows环境下链接时别忘了将Ws2_32.lib库添加进来。

 

主机序和网络序

主机序就是指主机的端序。

网络字节序(网络序)指多字节数据在网络传输中的顺序,TCP协议规定网络序是大端序,即高字节先发送。因此大端序的机器接受到的数据可直接使用,小端序机器则需要转换后使用。BSD socket API中定义了一组转换函数,用于16和32bit整数在网络序和本机字节序之间的转换。htonl,htons用于本机序转换到网络序;ntohl,ntohs用于网络序转换到本机序。一般来说,为了保证程序的可移植性,编写代码时,发送的数据需要使用htonl、htons转换,接收到的数据要使用ntohl、ntohs转换。

注意:不存在对单字节整数进行转换的函数”ntohc”和”htonc”!

位序

        位序,一般用于描述串行设备的传输顺序。一般说来大部分硬件都是采用小端序(先传低位),因此,对于一个字节数据,大部分机器上收发的顺序都一样,不会有问题,这就是为什么没有针对单字节数据的API接口”ntohc”和”htonc”。当然,也有例外,比如­I2C协议就是采用了大端序。这些细节只有在网络协议的数据链路层底端才会碰到,对一般的程序员来说很少涉及。

        但是在C语言中存在一种特殊的数据结构:位域。它的存在,使得C程序员能方便地进行位操作(比如在网络协议中经常出现1bit或者多bit的标示位,它们不是一个完整的字节)。但同时也引起一些难以察觉的问题,这些问题的根源仍然是前面提到的端序。

 

        与字节序一样,一个字节中的8个bit顺序在不同端序的机器上并不相同。大端机器上从低地址到高地址顺寻分别是msb->lsb,如下图:

 

 

小端序的机器上则正好相反

 

现代计算机的最小存储单位是BYTE,无法对bit寻址,因此我们无法直接观察每个字节内部bit的顺序。但是我们仍然可以通过位域来间接观察字节内部bit顺序,以印证上面的说法。

在C语言中,位域与结构体类似,其语法规定:先声明的成员位于低地址,后声明的成员位于高地址。那么下面的位域中:

typedefstruct OneByte

{

    bt0 : 1;

    bt1 : 1;

    bt2 : 1;

    bt3 : 1;

    bt4 : 1;

    bt5 : 1;

    bt6 : 1;

    bt7 : 1;

}


成员bt0就位于一个字节中最低地址bit0处,成员bt7就位于一个字节的最地址bit7处。

 

我们看看下面的程序。

#include<stdio.h>

 

typedefstruct OneByte

{

    char bt0 : 1;

    char bt1 : 1;

    char bt2 : 1;

    char bt3 : 1;

    char bt4 : 1;

    char bt5 : 1;

    char bt6 : 1;

    char bt7 : 1;

} ONE_BYTE;

 

int main()

{

    ONE_BYTE onebyte = {0};

    

    onebyte.bt7 = 1;

 

    printf("onebyte = %#x/r/n", *((unsignedchar *)&onebyte));

 

    return 0;

}


 

当bt7赋值为1后,onebyte在内存中是这个样子的:

 

而在VC2005中编译运行的结果如下:

 

0x80转换成二进制是1000 0000。由于在X86(小端序)中,高地址bit7是msb,因此onebyte的值是0x80了;这就证实了前面的说法。相应的如果是在大端序计算机中,bit7是lsb,则onebyte的值是0x01。




字节序与位序切实是统一的;

依据计算机设计的架构形式不同,等闲CPU(和一些外设)判别为大端序和小端序,另外也有所谓混杂序的装备存在,这种装备在这里不做琢磨

所谓大端序,即便当机器沿着地址从低地址向高地址读的时候,第一个读取到的字节byte或位bit

都是从最高的一位(MSB:Most Significant Bit,最高管用位)或最高的一个byte;

(本文所描写内容均不琢磨补码导致的切实存储数据改变)

如下所示

地址低位————>地址增长方向 ————>地址高位

  数据高位 | 1 0 0 0 1 1 1 1 | 0 1 0 1 0 0 0 0 | 数据低位

              |    byte 1                 byte 2

              |

             /|/

              pointer

指针指在第一个bit处,当 读取的时候顺次读入1000111101010000 (顺次读入 0x8f, 0x50)这个16位的数即便0x8f50

 

而低端序对应的16位数0x8f50 的存储措施为:

地址低位————>地址增长方向 ————>地址高位

数据低位  | 0 0 0 0 1 0 1 0 | 1 1 1 1 0 0 0 1| 数据高位

              |    byte 1                 byte 2

              |

             /|/

              pointer

读取的时候顺次读入0 0 0 0 1 0 1 0 1 1 1 1 0 0 0 1;

 

从上能够看出从大端序到小端序,发生了两个改变:

1、字节序列编排措施的改变 ;高低互换;

2、位序发生了改变,高低互换;

测验过程如下:

#include <stdlib.h>
#include <stdio.h>

int main () {

    int a = 0x12345678;

    char *pos = (char *) &a;

    printf("sizeof int : %d/r/n", sizeof(int));

    int i = 0;

    while (i < sizeof(int)) {
        printf("%x - %x ", pos, *pos);

        i++;

        pos++;
    }


 

小端序机器上运行收获(x86):

sizeof int : 4
bfcfd470 - 78 bfcfd471 - 56 bfcfd472 - 34 bfcfd473 – 12

地址是添置的,输出时从第四字节(起码)到第一字节(最高)

 

大端序机器上运行收获(powerpc 51xx):

sizeof int : 4
2ff22c30 - 12 2ff22c31 - 34 2ff22c32 - 56 2ff22c33 - 78

 

另字节序和位序的联系是很紧凑的;参见如下的关于网络协议中IP头部的数据构造定义:


typedef struct _ip
{
#if BYTE_ORDER == LITTLE_ENDIAN
        uchar   ip_hl:4,                /* header length */
                        ip_v:4;                 /* version */
#endif
#if BYTE_ORDER == BIG_ENDIAN
        uchar   ip_v:4,                 /* version */
                        ip_hl:4;                /* header length */
#endif
        uchar   ip_tos;                 /* type of service */
        ushort  ip_len;                 /* total length */
        ushort  ip_id;                  /* identification */
        ushort  ip_off;                 /* fragment offset field */
#define IP_DF 0x4000                    /* dont fragment flag */
#define IP_MF 0x2000                    /* more fragments flag */
#define IP_OFFMASK 0x1fff               /* mask for fragmenting bits */
        uchar   ip_ttl;                 /* time to live */
        uchar   ip_p;                   /* protocol */
        ushort  ip_sum;                 /* checksum */
        IPADDRTYPE ip_src;              /* source address */
        IPADDRTYPE ip_dst;              /* dest address */
}IP; 



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值