大端模式和小端模式

来源

我觉得这个故事还是挺有意思的

关于大端小端名词的由来,有一个有趣的故事,来自于Jonathan Swift的《格利佛游记》:Lilliput和Blefuscu这两个强国在过去的36个月中一直在苦战。战争的原因:大家都知道,吃鸡蛋的时候,原始的方法是打破鸡蛋较大的一端,可以那时的皇帝的祖父由于小时侯吃鸡蛋,按这种方法把手指弄破了,因此他的父亲,就下令,命令所有的子民吃鸡蛋的时候,必须先打破鸡蛋较小的一端,违令者重罚。然后老百姓对此法令极为反感,期间发生了多次叛乱,其中一个皇帝因此送命,另一个丢了王位,产生叛乱的原因就是另一个国家Blefuscu的国王大臣煽动起来的,叛乱平息后,就逃到这个帝国避难。据估计,先后几次有11000余人情愿死也不肯去打破鸡蛋较小的端吃鸡蛋。这个其实讽刺当时英国和法国之间持续的冲突。Danny Cohen一位网络协议的开创者,第一次使用这两个术语指代字节顺序,后来就被大家广泛接受。

定义

  • 大端Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

  • 小端Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

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


举例

数的储存

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

数组的存储

以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) -- 低位
--------------

低地址


可以看出,大端模式更加符合我们平时的阅读习惯

特点

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

  • 小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。

判断程序

  • 可以编写一个小的测试程序来判断机器的字节序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
bool isBigEndian() {
int a = 0x1234;
char b = *(char *) &a;
if (b == 0x12) {
return true;
}
return false;
}
int main() {
if (isBigEndian()) {
cout << "is BigEndian" << endl;
} else {
cout << "is LittleEndian" << endl;
}
return 0;
}

结果:

  • 联合体union的存放顺序是所有成员都从低地址开始存放,利用该特性可以轻松地获得了CPU对内存采用LittleEndian还是BigEndian模式读写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;
bool checkPattern() {
union Unit {
int a;
char b;
}unit;
unit.a = 0x1234;
if (unit.b == 0x12) {
return true;
}
return false;
}
int main() {
if (checkPattern()) {
cout << "is BigEndian" << endl;
} else {
cout << "is LittleEndian" << endl;
}
return 0;
}

结果:

  • python 版本的大小端判断
1
2
3
4
5
6
7
8
9
10
11
12
def check_sys():
import struct
val = 0x12345678
pk = struct.pack('i', val)
hex_pk = hex(ord(pk[0]))
if hex_pk == '0x78':
print 'little endian'
elif hex_pk == '0x12':
print 'big endian'
check_sys()

结果:

大端模式和小端模式转换

移位运算

1
2
3
4
5
6
7
8
9
10
11
12
//宏定义
//对于32位的数,大端转换为小端
#define BigtoLittle32(A) ( (((uint32_t)(A) & 0xff000000) >> 24) | \
(( (uint32_t)(A) & 0x00ff0000) >> 8) | \
(( (uint32_t)(A) & 0x0000ff00) << 8) | \
(( (uint32_t)(A) & 0x000000ff) << 24))
//函数形式
//对于32位的数,大端转换为小端
uint32_t reversebytes_uint32t(uint32_t value) {
return (value & 0x000000ffu) << 24 | (value & 0x0000ff00u) << 8 | (value & 0x00ff0000u) >> 8 | (value & 0xff000000u) >> 24;
}

1
2
3
4
5
6
7
8
9
10
//对于64位的数,大端转换为小端
// 先将64位的低32位转成小端模式,再将64位的高32位转成小端模式
// 再交换高32位和低32位的位置
uint64_t reversebytes_uint64t(uint64_t value) {
uint32_t high_uint64 = uint64_t(reversebytes_uint32t(uint32_t (value))); //低32位转换为小端
uint32_t low_uint64 = uint64_t(reversebytes_uint32t(uint32_t (value >> 32))); //高32位转换为小端
return (high_uint64 << 32) + low_uint64;
}

依次处理每个字节

首先将输入的uint32_t的变量强制转换成字符类型数组,以便一个字节一个字节的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//对32位的数依次处理
uint32_t changeEndian_uint32t(uint32_t value) {
char* ptr = (char*)(&value);
uint64_t base[4]; //设置基
base[0] = 1;
for (int i = 0; i < 4; i++) {
base[i] = base[i - 1] * 256;
}
uint32_t res = 0;
for (int i = 0; i < sizeof(value); ++i) {
res += uint8_t(ptr[i]) * base[4 - i - 1];
}
return res;
}

理解大端小端

从软件的角度理解端模式

从软件的角度上,不同端模式的处理器进行数据传递时必须要考虑端模式的不同。如进行网络数据传递时,必须要考虑端模式的转换。在Socket接口编程中,以下几个函数用于大小端字节序的转换:
其中
n = network
s = short
l = long

1
2
3
4
#define ntohs(n) //16位数据类型网络字节顺序到主机字节顺序的转换
#define htons(n) //16位数据类型主机字节顺序到网络字节顺序的转换
#define ntohl(n) //32位数据类型网络字节顺序到主机字节顺序的转换
#define htonl(n) //32位数据类型主机字节顺序到网络字节顺序的转换

其中互联网使用的网络字节顺序采用大端模式进行编址,而主机字节顺序根据处理器的不同而不同。

从系统的角度理解端模式

MSB和LSB

MSB: Most Significant Bit ——- 最高有效位
LSB: Least Significant Bit —— 最低有效位

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

运用实例

在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]),之后再进行发送才可以得到正确的数据。

参考

详解Big-Endian和Little-Endian,大端模式和小端模式