0%

字节序解析

字节序

字节间的顺序。主要有两种:大端序和小端序。当然你也可以规定其他的顺序,但这两种顺序是规则最简单的了。

内存:

1
2
内存:   ------------------------------->
内存地址:0 1 2 3

大端序:从左往右读,最左边为最高位。反映在内存即最低地址为最高位,最高地址为最低位。
小端序:从右往左读,最右边为最高位。反映在内存即最低地址为最低位,最高地址为最高位。
可以看到,大端序和小端序最高位到最低位的顺序刚好相反,是翻转的关系。

为什么会有字节序?
因为一串数字可以从左开始读,也可以从右开始读。如果不规定读的顺序,那么就无法得知正确的信息。
比如:一张纸上写着1234
按大端序读,最左边的1就是最高位,结果就是一千两百三十四
按小端序读,最右边4就是最高位,最左边的1反而是最低位,结果就是四千三百二十一
可以看到对于同一串文本,读的顺序不同,得到的结果也会不同。大部分国家都是从左到右读,但也有的国家是从右到左读的。没有好坏,纯属习惯问题。因此数字“一千两百三十四”,对于从左到右读的国家,书写出来就是1234。而对于从右到左读的国家,书写出来就是4321。大小端只是记法不同,表达的数字还是同一个。

CPU读取内存中的数据时,是从低地址向高地址方向进行读取的。所以一般计算机的内部处理都是小端字节序(目前为止我就还没遇到过采用大端存储的机器)。但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合比如网络传输和文件储存,几乎都是用的大端字节序。

举例:

0x1234
0001 0010 0011 0100 (采用大端序存储)-> 0010 1100 0100 1000(采用小端序存储,根据大端翻转即可)
1234 -> 3412(第一个字节代表的就是0x34,小端从右往左读)

0x01
0000 0000 0000 0000 0000 0000 0000 0001(大端序)
->
1000 0000 0000 0000 0000 0000 0000 0000(小端序)

判断当前机器的端序

可以采用以下代码判断:

1
2
3
4
5
6
7
union
{
unsigned int a;
unsigned char b;
}c;
c.a = 1;
bool isLittleEndian = 1 == c.b;

如果是小端序那么最低位1在最低地址,char占一个字节,c.b就等于1。
如果是大端序那么最低位1在最高地址,char占一个字节,c.b就等于0。

虽然大部分计算机的内部处理都是采用小端字节序,但还是有一些计算机采用的是大端字节序。那么这两台计算机通信时该如何确保正确性呢?
很简单,可以规定网络传输时必须采用大端字节序传输即先传输最高字节依此类推直到最低字节,两端收到后各自自行转换即可。这样就不必增加协议传递字节序信息了。

1
2
3
hton端转换器  <-->  网络传输 <--> ntoh端转换器
|                  |
A电脑                B电脑

通过端转换器,以及规定网络传输时必须采用大端字节序,就可以屏蔽电脑的具体字节序了。所以如果你要发送一个数字给对方,你要先用端转换器将host端转为大端,另一端收到后再将大端转换为host端,这样你们两个理解的才是同一个数字。测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void test_bit_data1(void) {
short a = 0x5678;
NSData *dataA = [[NSData alloc] initWithBytes:&a length:sizeof(short)];
NSLog(@"a = %d, hex:0x%x, data:%@", a, a, dataA); //hex:0x5678 data:0x7856

//传输时将host端序转为大端序
short b = NSSwapHostShortToBig(a);
NSData *dataB = [[NSData alloc] initWithBytes:&b length:sizeof(short)];
NSLog(@"b = %d, hex:0x%x, data:%@", b, b, dataB); //hex:0x7856 data:0x5678

short c;
[dataB getBytes:&c range:NSMakeRange(0, sizeof(short))];
NSData *dataC = [[NSData alloc] initWithBytes:&c length:sizeof(short)];
NSLog(@"c = %d, hex:0x%x, data:%@", c, c, dataC); //hex:0x7856 data:0x5678

//收到后将大端序转为host端序
short d = NSSwapBigShortToHost(c);
NSData *dataD = [[NSData alloc] initWithBytes:&d length:sizeof(short)];
NSLog(@"d = %d, hex:0x%x, data:%@", d, d, dataD); //hex:0x5678 data:0x7856
}

比特序

比特间的顺序。事实上只要有两位bit,就需要规定读顺序了。比特序同样分为大端比特序和小端比特序。通常小端字节序的CPU也采用小端比特序,大端字节序的CPU采用大端比特序。但是这个不绝对,也有大端字节序的CPU采用小端比特序。比特序对于软件层是个黑箱通常不需要关心,不必纠结机器到底是采用哪种比特序存储一个字节的。

对于一个字节的整型值机器到底是采用哪种比特序存储的,我曾经花了很长的时间去查资料,但是说啥的都有。最后还是自己写了个程序验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void test_bit_order_in_byte(void) {

union {
unsigned char a;

struct bs {
unsigned char b0: 1,
b1: 1,
b2: 1,
b3: 1,
b4: 1,
b5: 1,
b6: 1,
b7: 1;
}data;

struct bs1 {
unsigned char b0_1: 2,
b2_3: 2,
b4_5: 2,
b6_7: 2;
}data1;

struct bs2 {
unsigned char b0_3: 4,
b4_7: 4;
}data2;
}ua;
/**
0x34
大端:0011 0100
小端:0010 1100

//0010 1100
//0 1 3 0
//4 3
*/
ua.a = 0x34;

printf("a:%d\n", ua.a);

printf("b0:%d\n", ua.data.b0);
printf("b1:%d\n", ua.data.b1);
printf("b2:%d\n", ua.data.b2);
printf("b3:%d\n", ua.data.b3);
printf("b4:%d\n", ua.data.b4);
printf("b5:%d\n", ua.data.b5);
printf("b6:%d\n", ua.data.b6);
printf("b7:%d\n", ua.data.b7);

printf("b0_1:%d\n", ua.data1.b0_1);
printf("b2_3:%d\n", ua.data1.b2_3);
printf("b4_5:%d\n", ua.data1.b4_5);
printf("b6_7:%d\n", ua.data1.b6_7);

printf("b0_3:%d\n", ua.data2.b0_3);
printf("b4_7:%d\n", ua.data2.b4_7);
}

小端字节序机器运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a:52
b0:0
b1:0
b2:1
b3:0
b4:1
b5:1
b6:0
b7:0
b0_1:0
b2_3:1
b4_5:3
b6_7:0
b0_3:4
b4_7:3

可以看到小端字节序机器,对于0x34,它的比特顺序是0010 1100,还是小端。如果小端字节序的CPU采用大端比特序这不疯了吗。所以我更倾向于:小端字节序的CPU也采用小端比特序,大端字节序的CPU采用大端比特序。

但是网络传输的时候还是一个bit接一个bit发送的,那比特顺序不一样的两台机器收发不会出问题吗?比如发一个unsigned char 整型值0x34。从实际情况来看,确实没有问题,怎么做到的?查了很多资料,比较可信的说法:
在以太网(Ethernet)中,发送一个字节是从最低有效比特位到最高有效比特位的发送顺序,也就是最低有效比特位首先发送。接收方接收到的数据顺序和发送方发送的bit顺序一致,收到后网卡会把接收到的比特序转换成主机的比特序。这一过程对CPU、软件都是不可见的,所以一般不需要关心比特间的顺序,只需要关心字节间的顺序。比如单字节0x34,不管发送方是大端还是小端,对端收到后肯定还是0x34。

1
2
3
4
5
6
7
发送端大端: 0011 0100      
网络传输: 0010 1100
接收端小端: 0010 1100

发送端小端: 0010 1100
网络传输: 0010 1100
接收端大端: 0011 0100

单个字节不需要端序转换,两端也能正确理解,两端中的比特顺序可能不一致,但表达的是同一个数字。

问题

Q1:真机查看一下整数、字符串在内存里的二进制表示?

测试代码:

1
2
3
4
5
void test_number_and_char(void) {
int a = 0x1234;
char *p = "1234"; //字符1对应的ascii码就是0x31
printf("a = 0x%x, ch = %s\n", a, p);
}

小端机器运行

int整型0x1234

数字在内存里是按小端字节序存储的。

字符串”1234”

可以看到小端机器,字符串在内存里是按大端字节序存储的。

Q2:查看文本文件的字节序?

作为一个文本文件,个人觉得是要说明编码规则和字节序的。当然有的编码不需要特别说明字节序比如utf-8。(有空再研究吧)

Q3:一段字符串从A电脑网络传输到B电脑需要考虑字节序吗?

不需要,因为字符串无论在大端还是小端机器都是以大端字节序存储的,网络传输时也是按大端字节序传输的,对端收到后同样是大端字节顺序,所以两端理解的是同一个东西。至于单字节里的比特序可能会不一样。

参考

字节序: 一个不是很重要的概念

Bit numbering

一文带你秒懂 字节序(byte order),比特序(bit order),位域(bit field)

Ethernet下字节序和bit序的总结 这两篇文章bit发送顺序的观点刚好相反。

MAC帧的八位字节发送顺序 优先发送最低位。Each octet of the MAC frame, with the exception of the FCS, is transmitted least significant bit first.

字节序(byte order)和位序(bit order) 3⭐️

理解字节序

How to teach endian

C语言打印数据的二进制格式-原理解析与编程实现

【字符编码】彻底理解字符编码

觉得文章有帮助可以打赏一下哦!