很多人讨厌碰到字节序问题,跟它打交道就像走迷宫,每次都要牺牲不少脑细胞。即使这一次似乎搞清楚了,下次碰到还是要重新在大脑里构建和模拟。这里尽量做一个字节序问的完整备忘记录。
主机字节序
多字节数据在内存中的字节排列顺序称为主机字节序,主机字节序基本由CPU硬件决定,某些CPU如X86、Z80等为little-endian;有些如moto6800、sparc等为big-endian;而有些CPU可通过寄存器设置支持不同字节序,如ARM、MIPS等,这类平台上不同OS可能配置了不同字节序。
理解字节序需要知道两个名词:MSB (Most Significant Byte),代表一个数中权值最大的字节;LSB(Least Significant Byte),代表一个数中权值最小的字节。比如十进制数1234,1权值为千位,最大,相当于MSB;4为个位,权值最小,相当于LSB。那么真正以字节为单位考虑, 4字节数0x12345678里那个是MSB/LSB呢?
主机字节序分Big-Endian和Little-Endian两种,定义为:
a. Little-Endian,又称低字节优先,是把LSB放在内存低地址端,MSB在内存高地址端。举例,4字节数0x12345678在little-endian体系的内存中存放(从地址0x1000开始)为:
b. Big-Endian,又称高字节优先,把MSB放在内存低地址端,LSB放在内存高地址端。0x12345678此时存放为:
big-endian沿地址增长方向先放高权位数字的方式恰好符合一般习惯(按照千,百,十,个位来书写数字)。这也是为什么初学者变换endian时总觉得big-endian比较正常。
字节序问题有时会被扩大化,有人一碰到奇怪问题就怀疑字节序。其实它只存在于多字节数据在内存中的解析,注意:1)多字节数据;2)内存中的排列。
a. 多字节数据是指诸如long int short等需要多个字节存储的数据类型,而象char等用一个字节表示的类型永远不会有字节序问题。
b. CPU通用寄存器都是整体操作,不存在单字节地址以及地址增长方向的概念,因此也没有MSB/LSB在前或在后的问题,只有内存中的多字节数据存储才会有这两种差异。从硬件角度看,CPU是直接通过数据总线连接内存和CPU寄存器,这中间没有额外字节序转换的硬件开销,只是不同总线连接方式导致了不同字节序,如下图(from wiki):
信息交换中的字节序
当不同类型平台通过某种媒介交换信息时需要考虑字节序问题,常见介质主要指文件和网络。通过文件或网络读取来自其它主机的多字节数据时,要警惕字节序问题。如x86和moto6800主机通过文件交换信息时,如果不转换,数据处理就不匹配。在x86 VC下运行如下代码:
void main( )
{
FILE* fp;
short a = 0x3132; //为ASIIC码’12’
fp = fopen ("c:test.txt", "wb")
fwrite(&a, sizeof(short), 1, fp);
fclose(fp);
}
代码中a的值0x3132即'12’,由于x86为little endian体系,内存中多字节LSB(这里即0x32)在前,所以运行后打开test.txt文件,内容是'21'。把test.txt文件复制到moto6800机器上,再用fread把文件内容读到short变量里,得到的就不是0x3132而是0x3231了,数据就此被颠倒,后续处理完全错误。
假设我们制定了介质层字节序标准(如big-endian),要通过软件屏蔽不同endian体系的差异,需要两步操作,一判断endian类型:
typedef union {
u16 a;
u8 b[2];
}ENDIAN;
/* judge cpu is big endian(0) or small endian(1) */
bool judge_endian(void)
{
ENDIAN t;
t.a = 0x0001U;
return (bool)(t.b[0] == 1);
}
想想指针怎么做?
判断完两端主机的endian之后,要根据情况做相应endian交换,即发送和接收任何一方只要不符合媒介层标准(big-endian),就要通过移位和位操作,在主机序和媒介序之间转换。一方发送数据前要先将内存数据由主机字节序转换为传输介质层的字节序,再发送出去,接收方收到数据后,要转换为本地的主机字节序,再做后续处理。
比如TCP/IP协议的socket通信,基于socket通讯的双方一般选择Big-Endian为标准,又称网络字节序。socket定义了一组转换函数,用于多字节数在网络字节序和主机字节序之间的转换。htonl,htons用于主机序转换到网络序(如主机本身big endian,函数实际啥也不做);ntohl,ntohs把网络序转换到本机序(同样)。因此傻瓜式做法,无论发送方主机字节序是什么,发送前都用htons或htonl将多字节数据转换为网络字节序;同样无论接收方主机字节序是什么,都用ntohs或ntohl把接收的网络数据转换为本地主机字节序。这样有了网络字节序标准,socket通信时,只需套用这几个函数就能方便地抹平主机间的字节序差异。
问题在于如果双方主机都与网络字节序相反,本来是可以直接通信的,却用htonl/ntohl等转过去又转回来,有点自找麻烦。这就要看软件的预期运行环境,以及怎样平衡软件效率和多平台通用性。
补充SOC内部的字节序
soc中除主CPU外,一些外设中(如硬件媒体编解码器)还包含内部MCU,它们之间一般通过高速总线共享外部DDR,通过低速总线共享外设寄存器。这时如果双方的endian属性不匹配,对host cpu驱动及MCU上的固件编写会造成一定困扰。
总结
真正理解大小端的本质可能要从硬件系统角度,涉及CPU指令集、寄存器、总线以及硬件外设等。但对程序员来说,能够了解字节序问题的存在范围以及如何实现移植性更高的代码,这就足够了。
Tip:对于ARM等可配置的CPU,其编译工具中会有big/little endian的选项,程序移植时也需要注意,如果跟系统设置不匹配,编译得到的目标程序就无法运行。