关于大端小端名词的由来,有一个有趣的故事,来自于
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
开始存放)为:
|
|
|
|
|
|
|
|
|
32bit
宽的数
0x12345678
在
Little-endian
模式以及
Big-endian
模式)
CPU
内存中的存放方式(假设从地址
0x4000
开始存放)为:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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;
}
一般操作系统都是小端,而通讯协议是大端的。
Big Endian : PowerPC
、
IBM
、
Sun
Little Endian : x86
、
DEC
ARM
既可以工作在大端模式,也可以工作在小端模式。
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
程序,它可以测试不同平台上的字节序。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
在
80X86CPU
平台上,执行该程序得到如下结果:
[0]:0x78
[1]:0x56
[2]:0x34
[3]:0x12
[0]:0x12
[1]:0x34
[2]:0x56
[3]:0x78
分析结果,在
80X86
平台上,系统将多字节中的低位存储在变量起始地址,使用小端法。
htonl
将
i_num
转换成网络字节序,可见网络字节序是大端法。
总结点:
80X86
使用小端法,网络字节序使用大端法。