详解大端模式和小端模式

  • Post author:
  • Post category:其他






详解大端模式和小端模式







一、大端模式和小端模式的起源






关于大端小端名词的由来,有一个有趣的故事,来自于


Jonathan Swift


的《格利佛游记》:


Lilliput





Blefuscu


这两个强国在过去的


36


个月中一直在苦战。战争的原因:大家都知道,吃鸡蛋的时候,原始的方法是打破鸡蛋较大的一端,可以那时的皇帝的祖父由于小时侯吃鸡蛋,按这种方法把手指弄破了,因此他的父亲,就下令,命令所有的子民吃鸡蛋的时候,必须先打破鸡蛋较小的一端,违令者重罚。然后老百姓对此法令极为反感,期间发生了多次叛乱,其中一个皇帝因此送命,另一个丢了王位,产生叛乱的原因就是另一个国家


Blefuscu


的国王大臣煽动起来的,叛乱平息后,就逃到这个帝国避难。据估计,先后几次有


11000


余人情愿死也不肯去打破鸡蛋较小的端吃鸡蛋。这个其实讽刺当时英国和法国之间持续的冲突。


Danny Cohen


一位网络协议的开创者,第一次使用这两个术语指代字节顺序,后来就被大家广泛接受。







二、什么是大端和小端




Big-Endian





Little-Endian


的定义如下:



1) Little-Endian


就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。



2) Big-Endian


就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。





举一个例子,比如数字


0x12 34 56 78


在内存中的表示形式为:




1)


大端模式:




低地址


—————–>


高地址



0x12  |  0x34  |  0x56  |  0x78



2)


小端模式:




低地址


——————>


高地址



0x78  |  0x56  |  0x34  |  0x12


可见,大端模式和字符串的存储模式类似。




3)


下面是两个具体例子:




16bit


宽的数


0x1234





Little-endian


模式(以及


Big-endian


模式)


CPU


内存中的存放方式(假设从地址


0x4000


开始存放)为:




内存地址



小端模式存放内容



大端模式存放内容



0x4000


0x34


0x12


0x4001


0x12


0x34


32bit


宽的数


0x12345678





Little-endian


模式以及


Big-endian


模式)


CPU


内存中的存放方式(假设从地址


0x4000


开始存放)为:



内存地址



小端模式存放内容



大端模式存放内容



0x4000


0x78


0x12


0x4001


0x56


0x34


0x4002


0x34


0x56


0x4003


0x12


0x78




4)


大端小端没有谁优谁劣,各自优势便是对方劣势:




小端模式




:强制转换数据不需要调整字节内容,


1





2





4


字节的存储方式一样。





大端模式




:符号位的判定固定为第一个字节,容易判断正负。






三、数组在大端小端情况下的存储:







unsigned int value = 0x12345678


为例,分别看看在两种字节序下其存储情况,我们可以用


unsigned char buf[4]


来表示


value










Big-Endian:


低地址存放高位,如下:





高地址



—————

buf[3] (0x78) —


低位



buf[2] (0x56)

buf[1] (0x34)

buf[0] (0x12) —


高位



—————



低地址



Little-Endian:


低地址存放低位,如下:





高地址



—————

buf[3] (0x12) —


高位



buf[2] (0x34)

buf[1] (0x56)

buf[0] (0x78) —


低位



————–



低地址







四、为什么会有大小端模式之分呢?






这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为


8bit


。但是在


C


语言中除了


8bit





char


之外,还有


16bit





short


型,


32bit





long


型(要看具体的编译器),另外,对于位数大于


8


位的处理器,例如


16


位或者


32


位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个


16bit





short





x


,在内存中的地址为


0x0010





x


的值为


0x1122


,那么


0x11


为高字节,


0x22


为低字节。对于大端模式,就将


0x11


放在低地址中,即


0x0010


中,


0x22


放在高地址中,即


0x0011


中。小端模式,刚好相反。我们常用的


X86


结构是小端模式,而


KEIL C51


则为大端模式。很多的


ARM





DSP


都为小端模式。有些


ARM


处理器还可以由硬件来选择是大端模式还是小端模式。







五、如何判断机器的字节序




可以编写一个小的测试程序来判断机器的字节序:




[cpp]





view plain




copy




print




?




1.






BOOL



IsBigEndian()



2.





{



3.







int


a = 0x1234;



4.







char


b =  *(


char


*)&a;  //


通过将


int


强制类型转换成


char


单字节,通过判断起始存储位置。即等于







b


等于


a


的低地址部分




5.





if( b == 0x12)



6.





{



7.





return TRUE;



8.





}



9.





return FALSE;



10.





}<SPAN style=”BACKGROUND-COLOR: rgb(255,255,255); FONT-FAMILY: Arial, Verdana, sans-serif; WHITE-SPACE: normal”> </SPAN>


BOOL IsBigEndian()


{




int a = 0x1234;




char b =


*(char *)&a;


//


通过将

int

强制类型转换成

char

单字节,通过判断起始存储位置。即等于 取

b

等于

a

的低地址部分





if( b == 0x12)




{




return TRUE;




}




return FALSE;


}





联合体


union


的存放顺序是所有成员都从低地址开始存放,利用该特性可以轻松地获得了


CPU


对内存采用


Little-endian


还是


Big-endian


模式读写:




[cpp]





view plain




copy




print




?




1.






BOOL



IsBigEndian()



2.





{



3.





union NUM



4.





{



5.







int


a;



6.







char


b;



7.





}num;



8.





num.a = 0x1234;



9.





if( num.b == 0x12 )



10.





{



11.





return TRUE;



12.





}



13.





return FALSE;



14.





}<SPAN style=”BACKGROUND-COLOR: rgb(255,255,255); FONT-FAMILY: Arial, Verdana, sans-serif; WHITE-SPACE: normal”> </SPAN>


BOOL IsBigEndian()


{




union NUM




{




int a;




char b;




}num;




num.a = 0x1234;




if( num.b == 0x12 )




{




return TRUE;




}




return FALSE;


}








六、常见的字节序




一般操作系统都是小端,而通讯协议是大端的。






4.1


常见


CPU


的字节序




Big Endian : PowerPC





IBM





Sun

Little Endian : x86





DEC

ARM


既可以工作在大端模式,也可以工作在小端模式。







4.2


常见文件的字节序




Adobe PS – Big Endian

BMP – Little Endian

DXF(AutoCAD) – Variable

GIF – Little Endian

JPEG – Big Endian

MacPaint – Big Endian

RTF – Little Endian



另外,


Java


和所有的网络通讯协议都是使用


Big-Endian


的编码。







七、如何进行转换




对于字数据(


16


位):




[cpp]





view plain




copy




print




?




1.





#define BigtoLittle16(A)   (( ((uint16)(A) & 0xff00) >> 8)    | \



2.





(( (uint16)(A) & 0x00ff) << 8))


#define BigtoLittle16(A)


(( ((uint16)(A) & 0xff00) >> 8)


| \




(( (uint16)(A) & 0x00ff) << 8))


对于双字数据(


32


位):




[cpp]





view plain




copy




print




?




1.





#define BigtoLittle32(A)   ((( (uint32)(A) & 0xff000000) >> 24) | \



2.





(( (uint32)(A) & 0x00ff0000) >> 8)   | \



3.





(( (uint32)(A) & 0x0000ff00) << 8)   | \



4.





(( (uint32)(A) & 0x000000ff) << 24))


#define BigtoLittle32(A)


((( (uint32)(A) & 0xff000000) >> 24) | \






(( (uint32)(A) & 0x00ff0000) >> 8)


| \




(( (uint32)(A) & 0x0000ff00) << 8)


| \




(( (uint32)(A) & 0x000000ff) << 24))





八、从软件的角度理解端模式






从软件的角度上,不同端模式的处理器进行数据传递时必须要考虑端模式的不同。如进行网络数据传递时,必须要考虑端模式的转换。在


Socket


接口编程中,以下几个函数用于大小端字节序的转换。




[cpp]





view plain




copy




print




?




1.





#define ntohs(n)     //16


位数据类型网络字节顺序到主机字节顺序的转换




2.





#define htons(n)     //16


位数据类型主机字节顺序到网络字节顺序的转换




3.





#define ntohl(n)      //32


位数据类型网络字节顺序到主机字节顺序的转换




4.





#define htonl(n)      //32


位数据类型主机字节顺序到网络字节顺序的转换



#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


函数定义如下所示。




[cpp]





view plain




copy




print




?




1.





#define ___swab16(x)



2.





{



3.





__u16 __x = (x);



4.





((__u16)(



5.





(((__u16)(__x) & (__u16)0x00ffU) << 8) |



6.





(((__u16)(__x) & (__u16)0xff00U) >> 8) ));



7.





}



8.







9.







10.





#define ___swab32(x)



11.





{



12.





__u32 __x = (x);



13.





((__u32)(



14.





(((__u32)(__x) & (__u32)0x000000ffUL) << 24) |



15.





(((__u32)(__x) & (__u32)0x0000ff00UL) << 8) |



16.





(((__u32)(__x) & (__u32)0x00ff0000UL) >> 8) |



17.





(((__u32)(__x) & (__u32)0xff000000UL) >> 24) ));



18.





}


#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


这类函数。





在对普通文件进行处理也需要考虑端模式问题。在大端模式的处理器下对文件的


32





16


位读写操作所得到的结果与小端模式的处理器不同。单纯从软件的角度理解上远远不能真正理解大小端模式的区别。事实上,真正的理解大小端模式的区别,必须要从系统的角度,从指令集,寄存器和数据总线上深入理解,大小端模式的区别。






九、从系统的角度理解端模式




先补充两个关键词,


MSB





LSB










MSB:MoST Significant Bit ——-


最高有效位



LSB:Least Significant Bit ——-


最低有效位






处理器在硬件上由于端模式问题在设计中有所不同。从系统的角度上看,端模式问题对软件和硬件的设计带来了不同的影响,当一个处理器系统中大小端模式同时存在时,必须要对这些不同端模式的访问进行特殊的处理。



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








大小端模式处理器外部总线的位序也遵循着同样的规律,根据所采用的数据总线是


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


位宽度的数据总线的定义不同,因此需要分别进行讨论在系统级别上如何处理端模式转换。在一个大端处理器系统中,需要处理大端处理器对小端外设的访问。






十、实际中的例子






虽然很多时候,字节序的工作已由编译器完成了,但是在一些小的细节上,仍然需要去仔细揣摩考虑,尤其是在以太网通讯、


MODBUS


通讯、软件移植性方面。这里,举一个


MODBUS


通讯的例子。在


MODBUS


中,数据需要组织成数据报文,该报文中的数据都是大端模式,即低地址存高位,高地址存低位。假设有一


16


位缓冲区


m_RegMW[256]


,因为是在


x86


平台上,所以内存中的数据为小端模式:


m_RegMW[0].low





m_RegMW[0].high





m_RegMW[1].low





m_RegMW[1].high……



为了方便讨论,假设


m_RegMW[0] = 0x3456;


在内存中为


0x56





0x34








现要将该数据发出,如果不进行数据转换直接发送,此时发送的数据为


0x56,0x34


。而


Modbus


是大端的,会将该数据解释为


0x5634


而非原数据


0x3456


,此时就会发生灾难性的错误。所以,在此之前,需要将小端数据转换成大端的,即进行高字节和低字节的交换,此时可以调用步骤五中的函数


BigtoLittle16(m_RegMW[0])


,之后再进行发送才可以得到正确的数据。








大端法、小端法、网络字节序















关于字节序


(


大端法、小端法


)


的定义






UNXI


网络编程》定义:术语





小端











大端





表示多字节值的哪一端


(


小端或大端


)


存储在该值的起始地址。


小端存在起始地址,即是小端字节序;大端存在起始地址,即是大端字节序。







也可以说:





1.



小端法



(Little-Endian)



就是低位字节排放在内存的低地址端即该值的起始地址,高位字节排放在内存的高地址端。






2.



大端法



(Big-Endian)



就是高位字节排放在内存的低地址端即该值的起始地址,低位字节排放在内存的高地址端。







举个简单的例子,对于整形



0x12345678



。它在大端法和小端法的系统内中,分别如图



1



所示的方式存放。

















网络字节序




我们知道网络上的数据流是字节流,对于一个多字节数值,在进行网络传输的时候,先传递哪个字节?也就是说,当接收端收到第一个字节的时候,它是将这个字节作为高位还是低位来处理呢?








网络字节序定义:


收到的第一个字节被当作高位看待,这就要求发送端发送的第一个字节应当是高位。而在发送端发送数据时,发送的第一个字节是该数字在内存中起始地址对应的字节。可见多字节数值在发送前,在内存中数值应该以大端法存放。









网络字节序说是大端字节序。









比如我们经过网络发送



0x12345678



这个整形,在



80X86



平台中,它是以小端法存放的,在发送前需要使用系统提供的



htonl



将其转换成大端法存放,如图



2



所示。

















字节序测试程序



不同


cpu


平台上字节序通常也不一样,下面写个简单的


C


程序,它可以测试不同平台上的字节序。



1



#include




<


stdio.h


>



2



#include




<


netinet/in.h


>



3



int


main()



4



{




5





int


i_num =


0x12345678


;



6





printf


(


“[0]:0x%x\n”


, *((


char


*)&i_num +


0


));



7





printf


(


“[1]:0x%x\n”


, *((


char


*)&i_num +


1


));



8





printf


(


“[2]:0x%x\n”


, *((


char


*)&i_num +


2


));



9





printf


(


“[3]:0x%x\n”


, *((


char


*)&i_num +


3


));



10






11



i_num = htonl(i_num);



12





printf


(


“[0]:0x%x\n”


, *((


char


*)&i_num +


0


));



13





printf


(


“[1]:0x%x\n”


, *((


char


*)&i_num +


1


));



14





printf


(


“[2]:0x%x\n”


, *((


char


*)&i_num +


2


));



15





printf


(


“[3]:0x%x\n”


, *((


char


*)&i_num +


3


));



16






17





return




0


;



18



}














80X86CPU



平台上,执行该程序得到如下结果:






[0]:0x78

[1]:0x56

[2]:0x34

[3]:0x12



[0]:0x12

[1]:0x34

[2]:0x56

[3]:0x78




分析结果,在



80X86



平台上,系统将多字节中的低位存储在变量起始地址,使用小端法。



htonl







i_num



转换成网络字节序,可见网络字节序是大端法。







总结点:


80X86


使用小端法,网络字节序使用大端法。