开篇

关于字节序很多书都有讲到,去网上搜字节序或者大端序、小端序,有很多介绍的文章,
但是这些文章基本只讲到大端序或者小端序中字节层面数据是如何存储的,没有讲到
位序是如何的。之前也没想过位序的问题,直到前段时间看到linux kernel中linux/tcp.h的定义:

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

struct tcphdr {
__u16 source;
__u16 dest;
__u32 seq;
__u32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__u16 window;
__u16 check;
__u16 urg_ptr;
};

可以看到头文件使用两个宏(__LITTLE_ENDIAN_BITFIELD和__BIG_ENDIAN_BITFIELD)。
我们知道tcp头的结构如下:
中文版:
对比图片和上面定义的头文件可以看出,针对大端和小端系统,进行了不同的定义,说明在不同字节序中位序也是不同的。
具体可以看这两篇参考:

https://www.fileformat.info/mirror/egff/ch06_04.htm
https://www.linuxjournal.com/article/6788
https://en.wikipedia.org/wiki/Endianness

结论

大端系统中的位序也是"大端",小端系统中的位序也是"小端"
即对于0x0a0b0c0d这个四字节的整数,在内存中是如下存储的:
大端:

1
2
3
4
5
6
7
8
9
+-------------------------------------------------------+  
| 低地址 ----> 高地址 |
|-------------------------------------------------------|
| 0 | 1 | 2 | 3 |
|-------------------------------------------------------|
|bit0 --> bit7|bit0 --> bit7|bit0 --> bit7|bit0 --> bit7|
|-------------------------------------------------------|
| 00001010 | 00001011 | 00001100 | 00001101 |
+-------------------------------------------------------+

小端:

1
2
3
4
5
6
7
8
9
+-------------------------------------------------------+  
| 低地址 ----> 高地址 |
|-------------------------------------------------------|
| 0 | 1 | 2 | 3 |
|-------------------------------------------------------|
|bit0 --> bit7|bit0 --> bit7|bit0 --> bit7|bit0 --> bit7|
|-------------------------------------------------------|
| 10110000 | 00110000 | 11010000 | 01010000 |
+-------------------------------------------------------+

验证大端系统中的位序

我们现在接触到的大部分系统是小端系统,小端系统很好验证,大端系统就比较麻烦,为了验证大端系统,得通过qemu装大端的虚拟机来验证。
参考:

https://stackoverflow.com/questions/2839087/how-to-test-your-code-on-a-machine-with-big-endian-architecture
https://stackoverflow.com/questions/3337896/imitate-emulate-a-big-endian-behavior-in-c

0x00. 安装qemu

https://www.qemu.org/download/#windows 这个页面下载windows下的qemu,并安装,安装完后记得添加环境变量

0x01. 下载模拟mips的debian镜像

mips芯片是大端的,所以我们模拟这个芯片去验证。
https://people.debian.org/~aurel32/qemu/mips/下载vmlinux-3.2.0-4-5kc-maltadebian_wheezy_mips_standard.qcow2
根据页面的说明,打开cmd输入

1
qemu-system-mips64 -M malta -kernel vmlinux-3.2.0-4-5kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" -m 256

来启动虚拟机,这里怕内存不够,加了-m 256的启动参数,测试发现即使使用|-m 512实际使用的内存也只有256M。

0x02. 启动后发现没法联网

启动后想安装gcc,发现没法联网,这是第一次使用qemu,联网搞了半天。
找到以下解决方法,

在 Windows 上使用 qemu 虚拟机,通过此配置,可以使 qemu 中的虚拟机能连接互联网,并且也可以和 Windows 主机通信。此方式类似于 Vmware 和 VitrualBox 中的桥接网卡。配置方法如下:

  1. 在 Windows 主机上安装 TAP 网卡驱动: 可下载 openvpn 客户端软件,只安装其中的 TAP 驱动;在网络连接中,会看到一个新的网卡,属性类似于 TAP-Win32 Adapter…,将其名称修改为 tap0。
  2. 将 tap0 虚拟网卡和 Windows 上连接互联网的真实网卡桥接: 选中这两块网卡,右键,桥接。此时,Windows 主机将不能连接互联网,需要在网桥上配置 IP 地址和域名等信息,才能使 Windows 主机连接互联网。
  3. qemu 配置: 在虚拟机启动命令行添加以下参数:–net nic -net tap,ifname=tap0;启动虚拟机,并配置虚拟机中的网卡,则虚拟机也可以和 Windows 主机一样,连接互联网和 Windows 主机。

参考:https://my.oschina.net/Czl6BQ6SEmYt/blog/164308
我把TAP网卡重命名为my-tap,所以我的启动参数改为:

1
qemu-system-mips64 -M malta -kernel vmlinux-3.2.0-4-5kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" -m 256 -net nic -net tap,ifname=my-tap

注意tap和ifname之间只有一个逗号,不要加空格,否则报错

0x03. 换软件源

终于能联网了,发现官方源失效了
于是找到阿里云的镜像站中wheezy(Debian7.x)的源替换。
备份官方源

1
root@debian-mips:~# cp /etc/apt/sources.list /etc/apt/sources.list.backup

创建新文件

1
root@debian-mips:~# vi /etc/apt/sources.list

写入

1
2
3
4
5
# deb http://ftp.debian.org/debian wheezy main
deb http://mirrors.aliyun.com/debian-archive/debian/ wheezy main non-free contrib
deb http://mirrors.aliyun.com/debian-archive/debian/ wheezy-proposed-updates main non-free contrib
deb-src http://mirrors.aliyun.com/debian-archive/debian/ wheezy main non-free contrib
deb-src http://mirrors.aliyun.com/debian-archive/debian/ wheezy-proposed-updates main non-free contrib

更新apt缓存,安装gcc

1
2
root@debian-mips:~# apt-get update
root@debian-mips:~# apt-get install gcc

0x04. 开始验证

测试代码如下:

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
#include <stdio.h>
#include <stdlib.h>

struct A {
unsigned int a:3;
unsigned int b:1;
unsigned int c:4;
unsigned int d:5;
};

struct B {
unsigned int a:3;
unsigned int b:2;
unsigned int c:5;
unsigned int d:6;
};

struct C {
int a;
int b;
int c;
};

void foo(int *p)
{
int a1 = 1;
int a2 = 2;
printf("&a1:%p, &a2:%p\n", &a1, &a2);
return;
}

int main(void)
{
int x[]={0b10101010110010101000101110110011, 0xbbbbbbbb};
int h, i, j;
printf("&h:%p, &i:%p, &j:%p\n", &h, &i ,&j);
char y[] = {0b11010, 0b11011, 0b11100, 0b11101};
int* p = (int*)y;
printf("*p = %#x\n", *p);
struct A* m = (struct A*)&x;
struct B* n = (struct B*)&x;
printf("m:%p\n", m);
printf("n:%p\n", n);
printf("m:%#x, %#x,%#x, %#x,%#x\n", m->a,m->b, m->c, m->d);
printf("n:%#x, %#x,%#x, %#x,%#x\n", n->a,n->b, n->c, n->d);
struct C c;
printf("&c.a:%p, &c.b:%p, &c.c:%p\n", &c.a, &c.b, &c.c);
foo(p);
int * po1 = (int*)malloc(16);
int * po2 = (int*)malloc(16);
printf("po1:%p, po2%p\n", po1, po2);
free(po1);
free(po2);
return 0;
}

运行结果

1
2
3
4
5
6
7
8
9
10
root@debian-mips:~# ./a.out
&h:0x7fba3df4, &i:0x7fba3df8, &j:0x7fba3dfc
*p = 0x1a1b1c1d
m:0x7fba3dec
n:0x7fba3dec
m:0x5, 0, 0xa, 0x19,0x77667000
n:0x5, 0x1, 0xb, 0xa,0x77667000
&c.a:0x7fba3e04, &c.b:0x7fba3e08, &c.c:0x7fba3e0c
&a1:0x7fba3da8, &a2:0x7fba3dac
po1:0x81f008, po20x81f020

因为使用位序的结构体成员不能通过&符号取到地址,所以只能再使用一个struct C来验证结构体内的成员地址是怎么分配的,从c.a,c.b,c.c的地址是递增的,说明在大端系统中,结构体内的成员的地址按照书写顺序递增的。
m->a占3bit,数值是0x5 —–> 10101010110010101000101110110011
m->b占1bit,数值是0x0 —–> 10101010110010101000101110110011
m->c占4bit,数值是0xa —–> 10101010110010101000101110110011
m->d占5bit,数值是0x19 —-> 10101010110010101000101110110011
从以上可以看出,m中a,b,c,d四个成员变量的地址应该是递增,它们对应的数值在二进制也是依次从高位往低位排。这说明,在内存中,二进制数10101010110010101000101110110011的高位数值在低地址的低bit位。即

在小端系统中的结果:

1
2
3
4
5
6
7
8
9
10
11
●●●E:\workspace\code\alg_practice\test*master>$ .\a.exe
&h:000000a1089ffb48, &i:000000a1089ffb44, &j:000000a1089ffb40
sizeof(struct A):4
m:000000a1089ffb50
n:000000a1089ffb50
m:0x3, 0, 0xb, 0xb
n:0x3, 0x2, 0x1d, 0x22
*p:0x1d1c1b1a
000000a1089ffb4c, 000000a1089ffb4d, 000000a1089ffb4e, 000000a1089ffb4f&c.a:000000a1089ffb34, &c.b:000000a1089ffb38, &c.c:000000a1089ffb3c
&a1:000000a1089ffaec, &a2:000000a1089ffae8
po1:000002858a656cd0, po2000002858a656cf0

m->a占3bit,数值是0x3 —–> 10101010110010101000101110110011
m->b占1bit,数值是0x0 —–> 10101010110010101000101110110011
m->c占4bit,数值是0xb —–> 10101010110010101000101110110011
m->d占5bit,数值是0xb —–> 10101010110010101000101110110011
说明小端字节序的位序也是小端的。