打印
[ZLG-ARM]

深入浅出大端和小端

[复制链接]
1879|1
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
arminfo|  楼主 | 2009-6-1 13:17 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
**一: 
端模式(Endian)的这个词出自Jonathan Swift书写的《格列佛游记》。这本书根据将**蛋敲开的方法不同将所有的人分为两类,从圆头开始将**蛋敲开的人被归为Big Endian,从尖头开始将**蛋敲开的人被归为Littile Endian。小人国的内战就源于吃**蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。在计算机业Big Endian和Little Endian也几乎引起一场战争。在计算机业界,Endian表示数据在存储器中的存放顺序。下文举例说明在计算机中大小端模式的区别。
如果将一个32位的整数0x12345678存放到一个整型变量(int)中,这个整型变量采用大端或者小端模式在内存中的存储由下表所示。为简单起见,本书使用OP0表示一个32位数据的最高字节MSB(Most Significant Byte),使用OP3表示一个32位数据最低字节LSB(Least Significant Byte)。


地址偏移
 大端模式
 小端模式
 
0x00
 12(OP0)
 78(OP3)
 
0x01
 34(OP1)
 56(OP2)
 
0x02
 56(OP2)
 34(OP1)
 
0x03
 78(OP3)
 12(OP0)
 


如果将一个16位的整数0x1234存放到一个短整型变量(short)中。这个短整型变量在内存中的存储在大小端模式由下表所示。

地址偏移
 大端模式
 小端模式
 
0x00
 12(OP0)
 34(OP1)
 
0x01
 34(OP1)
 12(OP0)
 


由上表所知,采用大小模式对数据进行存放的主要区别在于在存放的字节顺序,大端方式将高位存放在低地址,小端方式将高位存放在高地址。采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。到目前为止,采用大端或者小端进行数据存放,其孰优孰劣也没有定论。
有的处理器系统采用了小端方式进行数据存放,如Intel的奔腾。有的处理器系统采用了大端方式进行数据存放,如IBM半导体和Freescale的PowerPC处理器。不仅对于处理器,一些外设的设计中也存在着使用大端或者小端进行数据存放的选择。
因此在一个处理器系统中,有可能存在大端和小端模式同时存在的现象。这一现象为系统的软硬件设计带来了不小的麻烦,这要求系统设计工程师,必须深入理解大端和小端模式的差别。大端与小端模式的差别体现在一个处理器的寄存器,指令集,系统总线等各个层次中。
1.1.1        从软件的角度理解端模式从软件的角度上,不同端模式的处理器进行数据传递时必须要考虑端模式的不同。如进行网络数据传递时,必须要考虑端模式的转换。有过Socket接口编程经验的程序员一定使用过以下几个函数用于大小端字节序的转换。
¨          #define ntohs(n)     //16位数据类型网络字节顺序到主机字节顺序的转换
¨          #define htons(n)     //16位数据类型主机字节顺序到网络字节顺序的转换
¨          #define ntohl(n)      //32位数据类型网络字节顺序到主机字节顺序的转换
¨          #define htonl(n)      //32位数据类型主机字节顺序到网络字节顺序的转换
其中互联网使用的网络字节顺序采用大端模式进行编址,而主机字节顺序根据处理器的不同而不同,如PowerPC处理器使用大端模式,而Pentuim处理器使用小端模式。
大端模式处理器的字节序到网络字节序不需要转换,此时ntohs(n)=n,ntohl = n;而小端模式处理器的字节序到网络字节必须要进行转换,此时ntohs(n) = __swab16(n),ntohl = __swab32(n)。__swab16与__swab32函数定义如下所示。
#define ___swab16(x)
{
            __u16 __x = (x);
            ((__u16)(
                        (((__u16)(__x) & (__u16)0x00ffU) << 8) |
                        (((__u16)(__x) & (__u16)0xff00U) >> 8) ));
}
#define ___swab32(x)
{
            __u32 __x = (x);
            ((__u32)(
                        (((__u32)(__x) & (__u32)0x000000ffUL) << 24) |
                        (((__u32)(__x) & (__u32)0x0000ff00UL) << 8) |
                        (((__u32)(__x) & (__u32)0x00ff0000UL) >> 8) |
                        (((__u32)(__x) & (__u32)0xff000000UL) >> 24) ));
}

 

PowerPC处理器提供了lwbrx,lhbrx,stwbrx,sthbrx四条指令用于处理字节序的转换以优化__swab16和__swap32这类函数。此外PowerPC处理器中的rlwimi指令也可以用来实现__swab16和__swap32这类函数。在Linux PowerPC中,定义了一系列有关字节序转换的函数,其详细定义在./include/asm-powerpc/byteorder.h文件中。
程序员在对普通文件进行处理也需要考虑端模式问题。在大端模式的处理器下对文件的32,16位读写操作所得到的结果与小端模式的处理器不同。读者单纯从软件的角度理解上远远不能真正理解大小端模式的区别。事实上,真正的理解大小端模式的区别,必须要从系统的角度,从指令集,寄存器和数据总线上深入理解,大小端模式的区别。
1.1.2        从系统的角度理解端模式除了4.2.1节中,软件上对不同端模式编程上的差异,处理器在硬件上也由于端模式问题在设计中有所不同。从系统的角度上看,端模式问题对软件和硬件的设计带来了不同的影响,当一个处理器系统中大小端模式同时存在时,必须要对这些不同端模式的访问进行特殊的处理。
PowerPC处理器主导网络市场,可以说绝大多数的通信设备都使用PowerPC处理器进行协议处理和其他控制信息的处理,这也可能也是在网络上的绝大多数协议都采用大端编址方式的原因。因此在有关网络协议的软件设计中,使用小端方式的处理器需要在软件中处理端模式的转变。而Pentium主导个人机市场,因此多数用于个人机的外设都采用小端模式,包括一些在网络设备中使用的PCI总线,Flash等设备,这也要求硬件工程师在硬件设计中注意端模式的转换。
本书中的小端外设是指这种外设中的寄存器以小端方式进行存储,如PCI设备的配置空间,NOR FLASH中的寄存器等等。
对于有些设备,如DDR颗粒,没有以小端方式存储的寄存器,因此从逻辑上讲并不需要对端模式进行转换。在设计中,只需要将双方数据总线进行一一对应的互连,而不需要进行数据总线的转换。
如果从实际应用的角度说,采用小端模式的处理器需要在软件中处理端模式的转换,因为采用小端模式的处理器在与小端外设互连时,不需要任何转换。
而采用大端模式的处理器需要在硬件设计时处理端模式的转换。大端模式处理器需要在寄存器,指令集,数据总线及数据总线与小端外设的连接等等多个方面进行处理,以解决与小端外设连接时的端模式转换问题。
在寄存器和数据总线的位序定义上,基于大小端模式的处理器有所不同。
一个采用大端模式的32位处理器,如基于E500内核的MPC8541,将其寄存器的最高位msb(most significant bit)定义为0,最低位lsb(lease significant bit)定义为31;而小端模式的32位处理器,将其寄存器的最高位定义为31,低位地址定义为0。
与此向对应,采用大端模式的32位处理器数据总线的最高位为0,最高位为31;采用小端模式的32位处理器的数据总线的最高位为31,最低位为0。如图4.5所示。


OP0

 
OP1

 
OP2

 
OP3

 
OP0

 
OP1

 
OP2

 
OP3

 
31

 
0

 
31

 
0

 
图4.5大小端模式处理器的寄存器的定义

 
大端模式处理器寄存器位序定义

 
小端模式处理器寄存器位序定义

 

大小端模式处理器外部总线的位序也遵循着同样的规律,根据所采用的数据总线是32位,16位和8位,大小端处理器外部总线的位序有所不同。
¨          大端模式下32位数据总线的msb是第0位,MSB是数据总线的第0~7的字段;而lsb是第31位,LSB是第24~31字段。小端模式下32位总线的msb是第31位,MSB是数据总线的第31~24位,lsb是第0位,LSB是7~0字段。
¨          大端模式下16位数据总线的msb是第0位,MSB是数据总线的第0~7的字段;而lsb是第15位,LSB是第8~15字段。小端模式下16位总线的msb是第15位,MSB是数据总线的第15~7位,lsb是第0位,LSB是7~0字段。
¨          大端模式下8位数据总线的msb是第0位,MSB是数据总线的第0~7的字段;而lsb是第7位,LSB是第0~7字段。小端模式下8位总线的msb是第7位,MSB是数据总线的第7~0位,lsb是第0位,LSB是7~0字段。
由上分析,我们可以得知对于8位,16位和32位宽度的数据总线,采用大端模式时数据总线的msb和MSB的位置都不会发生变化,而采用小端模式时数据总线的lsb和LSB位置也不会发生变化。
为此,大端模式的处理器对8位,16位和32位的内存访问(包括外设的访问)一般都包含第0~7字段,即MSB。小端模式的处理器对8位,16位和32位的内存访问都包含第7~0位,小端方式的第7~0字段,即LSB。
由于大小端处理器的数据总线其8位,16位和32位宽度的数据总线的定义不同,因此需要分别进行讨论在系统级别上如何处理端模式转换。
在一个大端处理器系统中,需要处理大端处理器对小端外设的访问。
1.1.2.1    大端处理器对32位小端外设进行访问大端处理器采用32位总线与小端外设进行访问时,大端处理器的32位数据总线的第0~7位用来处理OP0,第8~15位用来处理OP1,第16~23位用来处理OP2,第24~31位用来处理OP3。而32位的小端设备使用数据总线的第31~24位用来处理OP0,第23~16位用来处理OP1,第15~8位用来处理OP2,第7~0位用来处理OP3。
大端处理器,如MPC8541,使用stw,sth,stb和lwz,lhz,lbz指令对32位的外部设备进行访问。在这些指令结束后,存放在外部设备的数据将被读入MPC8541的通用寄存器中。为保证软件的一致性,当访问结束后,存放在通用寄存器的字节序,即OP0,OP1,OP2和OP3必须要和存放在小端外设的字节序一致。此时在使用大端处理器的数据总线连接小端外设时必须要进行一定的处理,按照某种拓扑结构连接以保证软件的一致性。大端处理器数据总线与小端外设进行连接的拓扑结构如图4.6所示。

OP0

 
OP1

 
OP2

 
OP3

 
31

 
31

 
0

 
7

 
8

 
15

 
16

 
23

 
24

 
24

 
23

 
16

 
15

 
8

 
7

 
0

 
大端处理器的32位数据总线

 
小端设备的32位总线接口

 
图4.6 大端处理器与小端外设的32位连接

 
OP0

 
OP1

 
OP2

 
OP3

 

如图4.6所示,采用大端处理器访问小端设备时,将各自的OP0~OP3字段直接相连。在大端处理器的32位数据总线的最高位为0,最低位为31;而小端设备的最高位为31,最低位为0。因此硬件工程师在进行信号连接时需要将采用大端处理器的0~31位分别与小端设备的31~0位一一对应,进行互连。
1.1.2.2    大端处理器对8,16位小端外设进行访问大端处理器使用8位,16位数据总线对8位,16位的小端外设进行连接。对于32位处理器,用来连接外设的总线一般是32位。因此体系结构工程师在进行大端处理器总线设计时有两种选择,是采用32位总线的高端部分(第0~15字段)还是低端部分(第16~31字段)连接小端设备。PowerPC处理器使用32位总线的高端部分,即数据总线的第0~15位连接16位的小端设备,使用0~7位连接8位的小端设备。
PowerPC处理器采用16位总线与16位的小端外设进行访问时,PowerPC处理器的16位数据总线的第0~7位用来处理OP0,第8~15位用来处理OP1。而16位的小端设备使用数据总线的第15~8位用来处理OP0,第7~0位用来处理OP1。
PowerPC处理器采用8位总线与8位的小端外设进行访问时,PowerPC处理器的8位数据总线的第0~7字段用来处理OP0。而8位的小端设备使用数据总线的第7~0位用来处理OP1。大端处理器与小端外设的连接关系如图4.7所示。 
OP0

 
OP1

 
OP0

 
OP1

 
15

 
0

 
7

 
8

 
15

 
8

 
7

 
0

 
大端处理器的8/16位数据总线

 
小端设备的8/16位总线接口

 
图4.7 大端处理器与小端外设的8/16位连接

 
OP0

 
OP0

 
7

 
0

 
7

 
0

 

与32位总线接口类似,PowerPC处理器可以使用stw,sth,stb和lwz,lhz,lbz指令对32位的外部设备进行访问,并将数据存放在相应的通用寄存器中。当访问结束后,存放在通用寄存器的字节序,即OP0,OP1必须要和存放在小端外设的字节序一致。
PowerPC处理器对8位的小端外设进行访问时,一个总线周期只能访问8位数据,如果处理器使用stw或者lwz指令访问8位的小端设备内的32位数据时,在数据总线上将OP0,OP1,OP2和OP3依次传递到PowerPC的通用寄存器中。
PowerPC处理器对16位的小端外设进行访问时,一个总线周期只能访问16位数据,如果处理器使用stw或者lwz指令访问16位的小端设备内的32位数据时,在数据总线上将OP0~1和OP2~3依次传递到PowerPC的通用寄存器中。
PowerPC处理器使用sth或者lhz指令访问16位的小端设备时,16位的小端设备将数据的第15~0位,传递到PowerPC处理器的总线的第0~15位,然后再将数据最终传递给相应的通用寄存器。这里有许多读者会感到困惑,因为为了保证软件的一致性,PowerPC处理器使用lhz指令访问16位的小端设备的16位寄存器时,需要将结果保存在通用寄存器的第16~31位,而不是0~15位。究竟PowerPC处理器是如何将系统总线中0~15位的数据搬移到寄存器的第16~31位中的呢?为此我们需要对lhz指令进行分析。
lhz rD,d(rA)
if rA = 0 then b ← 0
else b ← (rA)
EA ← b + EXTS(d)
rD ← (24)0 || MEM(EA, 1)

 

由lhz指令的以上描述得知lhz指令将来自数据总线上的OP0与OP1直接存入寄存器的第16~31位,而将第0~15位直接清零。
PowerPC处理器使用stb或者lbz指令访问8位的小端设备时,8位的小端设备将数据的第7~0位,传递到PowerPC处理器的总线的第0~7位,然后再将数据最终传递给相应的通用寄存器,lbz指令的描述如下所示。
lbz rD,d(rA)
if rA = 0 then b ← 0
else b ← (rA)
EA ← b + EXTS(d)
rD ← (24)0 || MEM(EA, 1)

 

由lhz指令的以上描述得知lhz指令将来自数据总线上的OP0直接存入寄存器的第24~31位,而将第0~23位清零。
本节分别描述了大端处理器的32位,16位及8位数据总线与32位,16位和8位的小端设备进行连接。如果大端处理器的数据总线需要同时支持小端设备的32位,16位及8位的数据传送方式,端模式的处理将会更加复杂。IC的设计人员在设计PCI总线桥片的时候将会遇到这一类问题,此时设计人员将使用多路总线开关来解决这一问题。端模式问题的解决需要软硬件协调处理,并在指令集上加以支持。对于小端处理器而言,需要使用软件转换的方法实现大小端模式的匹配;对于大端处理器而言,在外部数据总线与小端外设的连接时必须要考虑数据总线连接的拓扑结构。

 
   
**二:

大端(big-endian)和小端(little-endian)<转>
2007-12-07 20:36
补:x86机是小端(修改分区表时要注意),单片机一般为大端

    今天碰一个关于字节顺序的问题,虽然看起来很简单,但一直都没怎么完全明白这个东西,索性就找了下资料,把它弄清楚. 
    因为现行的计算机都是以八位一个字节为存储单位,那么一个16位的整数,也就是C语言中的short,在内存中可能有两种存储顺序big-endian和 litte-endian.考虑一个short整数0x3132(0x32是低位,0x31是高位),把它赋值给一个short变量,那么它在内存中的存 储可能有如下两种情况:
大端字节(Big-endian):
----------------->>>>>>>>内存地址增大方向
short变量地址
       0x1000                  0x1001
___________________________________
|     0x31       |      0x32       |
|_______________ | ________________|
高位字节在低位字节的前面,也就是高位在内存地址低的一端.可以这样记住(大端->高位->在前->正常的逻辑顺序)
小端字节(little-endian):
----------------->>>>>>>>内存地址增大方向
short变量地址
       0x1000                  0x1001
___________________________________
|     0x32       |      0x31       |
|_______________ | ________________|

低位字节在高位字节的前面,也就是低位在内存地址低的一端.可以这样记住(小端->低位->在前->与正常逻辑顺序相反)
可以做个实验
在windows上下如下程序
#include 
#include 
void main( void )
{
        short test;
        FILE* fp;
         
        test = 0x3132; //(31ASIIC码的’1’,32ASIIC码的’2’)
        if ((fp = fopen ('c:\test.txt', 'wb')) == NULL)
              assert(0);
        fwrite(&test, sizeof(short), 1, fp);
        fclose(fp);
}
    然后在C盘下打开test.txt文件,可以看见内容是21,而test等于0x3132,可以明显的看出来x86的字节顺序是低位在前.如果我们把这段 同样的代码放到(big-endian)的机器上执行,那么打出来的文件就是12.这在本机中使用是没有问题的.但当你把这个文件从一个big- endian机器复制到一个little-endian机器上时就出现问题了.
    如上述例子,我们在big-endian的机器上创建了这个test文件,把其复制到little-endian的机器上再用fread读到一个 short里面,我们得到的就不再是0x3132而是0x3231了,这样读到的数据就是错误的,所以在两个字节顺序不一样的机器上传输数据时需要特别小 心字节顺序,理解了字节顺序在可以帮助我们写出移植行更高的代码.
正因为有字节顺序的差别,所以在网络传输的时候定义了所有字节顺序相关的数据都使用big-endian,BSD的代码中定义了四个宏来处理:
#define ntohs(n)     //网络字节顺序到主机字节顺序 n代表net, h代表host, s代表short
#define htons(n)     //主机字节顺序到网络字节顺序 n代表net, h代表host, s代表short
#define ntohl(n)      //网络字节顺序到主机字节顺序 n代表net, h代表host, l代表 long
#define htonl(n)      //主机字节顺序到网络字节顺序 n代表net, h代表host, l代表 long
举例说明下这其中一个宏的实现:
#define sw16(x) 
    ((short)( 
        (((short)(x) & (short)0x00ffU) << 8) | 
        (((short)(x) & (short)0xff00U) >> 8) ))
这里实现的是一个交换两个字节顺序.其他几个宏类似.

我们改写一下上面的程序
#include 
#include 

#define sw16(x) 
    ((short)( 
        (((short)(x) & (short)0x00ffU) << 8) | 
        (((short)(x) & (short)0xff00U) >> 8) ))

// 因为x86下面是低位在前,需要交换一下变成网络字节顺序
#define htons(x) sw16(x)
void main( void )
{
        short test;
        FILE* fp;
        
        test = htons(0x3132); //(31ASIIC码的’1’,32ASIIC码的’2’)
        if ((fp = fopen ('c:\test.txt', 'wb')) == NULL)
              assert(0);
        fwrite(&test, sizeof(short), 1, fp);
        fclose(fp);
}
    如果在高字节在前的机器上,由于与网络字节顺序一致,所以我们什么都不干就可以了,只需要把#define htons(x) sw16(x)宏替换为 #define htons(x) (x).
    一开始我在理解这个问题时,总在想为什么其他数据不用交换字节顺序?比如说我们write一块buffer到文件,最后终于想明白了,因为都是unsigned char类型一个字节一个字节的写进去,这个顺序是固定的,不存在字节顺序的问题,够笨啊..
 
   

相关帖子

沙发
lpczcy| | 2009-6-1 13:22 | 只看该作者

啊!好东东……

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

17

主题

47

帖子

0

粉丝