跳转到内容

计算机科学:字节顺序

本文将讲述关于编程中可能会遇到并且引起疑惑的字节顺序的相关问题,配合之前关于UTF-8编码的部分一起食用效果更佳。

jskyzero 2017/04/30

字节顺序,又称端序或尾序(英语:Endianness)。在计算机科学领域中,是跨越多字节的程序对象的存储规则。

有仔细看过关于编码的那篇文章的读者应该已经有留意到关于字节顺序的问题(具体来说就是UTF-16的BE/LE两种不同的表示方式)。

这里我们将继续就大小头问题做一个深入的理解,从问题的来源到解决方案。

使用过C语言的读者可能知道,一个类型为int的变量可能占用四个字节,很多其他的类型也是这样,需要多个字节为一个单位,每个字节由于硬件设计的原因位的顺序是一般是固定的,多个字节排布的顺序则有可能不同。

我们一般将最高有效位字节储存在最低的内存地址的叫做大端序,最低位字节储存在最低的内存地址的叫做小端序。举例来说:

// 一个4字节int类型数据的十六进制表示,最高位字节是0x01,最低位是0x04
data = 0x01020304
// 内存地址方向:从左到右为低地址到高地址
低位地址 >> 高位地址
// 大端序(Big-Endian):最高有效字节(0x01)保存在最低内存地址,符合人类阅读习惯
大端序 0x01 | 0x02 | 0x03 | 0x04
// 小端序(Little-Endian):最低有效字节(0x04)保存在最低内存地址,x86等CPU使用此方式
小端序 0x04 | 0x03 | 0x02 | 0x01

根据字节序的定义,我们只要知道了数据在内存中的保存方式,自然可以很容易的判断它是那种字节序列。这里以C语言描述为例。具体的逻辑可以参考实际代码,如下。

#include <stdio.c> // 引入printf()函数,用于格式化输出
int main() {
// main函数:程序的入口点,本程序的目的是检测当前机器的字节序
// 假定int为4byte
// 定义一个int类型变量,其十六进制值为0x01020304,在内存中占用4个字节
int a = 0x01020304;
// 获取首byte地址,并用(也是一个byte的char类型)的指针存储
// 关键步骤:将int指针强制转换为char指针,因为char只占1个字节
// 这样b就指向了a在内存中的最低地址字节,可以逐字节读取
char *b = (char *)&a;
// 依照地址增长,打印每个byte
// 循环4次,每次打印当前字节的地址和字符值
// 如果是大端序:依次打印0x01, 0x02, 0x03, 0x04
// 如果是小端序:依次打印0x04, 0x03, 0x02, 0x01
for (int i = 0; i < 4; i++) {
// %p打印指针地址(内存地址),%c打印该字节对应的字符
printf("%p:%c\n", b, *b);
// 指针向前移动1个字节,指向下一个字节
b++;
}
return 0;
}

以下故事只告诉我们要注意仔细看文档 关于该工具具体的介绍和使用方法请参考man等

使用Hexdump我们可以以字节为单位很方便的查看文件的原貌,但是请注意关于这个工具的默认查看方式

// -x 选项说明:以两字节为一组进行十六进制显示
// 每行显示偏移量,然后是8组、4列、空格分隔的两字节数据(注意:两字节组内顺序受字节序影响)
-x Two-byte hexadecimal display. Display the input offset in hexa‐
decimal, followed by eight, space separated, four column, zero-
filled, two-byte quantities of input data, in hexadecimal, perline.
// 中间内容省略
...
// 重要提醒:如果不指定任何格式参数,hexdump默认使用 -x 的显示方式
If no format strings are specified, the default display is equivalent to
specifying the -x option.

举例来说,你现在有一个文件里面写入的是123(换个表示方法0x313233),假定你的机器是小端序,使用hexdump不加参数查看,你将看到

// 小端序机器上的输出结果:文件内容"123"(0x31 0x32 0x33)被按两字节分组显示
// 第一组:低地址字节0x32在前、高地址字节0x31在后 -> 显示为"3231"(两字节内部翻转了)
// 第二组:第三字节0x33与字符串尾部的空字符0x00 -> 显示为"0033"
0000000 3231 0033
// 偏移地址,表示文件总长度为0x000003字节处结束
0000003

当然我们直接用hd指令就挺好的

// 使用 hd(等于 hexdump -C)的规范显示:每个字节独立显示,附带ASCII字符对照
// 格式:偏移地址 + 16个字节的十六进制值 + ASCII可视字符(用 | | 包围)
00000000 31 32 33 |123|
// 数据在偏移地址0x00000003处结束,共3个字节
00000003

毕竟

// -C 选项说明:以规范格式显示,包含十六进制与ASCII字符对照(推荐使用)
// 每行格式:偏移地址 + 最多16个字节的十六进制值(每字节一列) + ASCII表示(| | 包围)
-C Canonical hex+ASCII display. Display the input offset in hexa-
decimal, followed by sixteen space-separated, two column, hexa-
decimal bytes, followed by the same sixteen bytes in %_p format
enclosed in ``|'' characters.
// 直接使用 hd 命令等同于 hexdump -C,推荐日常使用
Calling the command hd implies this option.

BOM大概就是个概念,在编码上还挺容易被烦到,以下取自某不靠谱的百科

字节顺序标记(英语:byte-order mark,BOM)是位于码点U+FEFF的统一码字符的名称。当以UTF-16或UTF-32来将UCS/统一码字符所组成的字符串编码时,这个字符被用来标示其字节序。它常被用来当做标示文件是以UTF-8、UTF-16或UTF-32编码的记号。

字符U+FEFF如果出现在字节流的开头,则用来标识该字节流的字节序,是高位在前还是低位在前。如果它出现在字节流的中间,则表达零宽度非换行空格的意义,用户看起来就是一个空格。从Unicode3.2开始,U+FEFF只能出现在字节流的开头,只能用于标识字节序,就如它的名称——字节序标记——所表示的一样;除此以外的用法已被舍弃。取而代之的是,使用U+2060来表达零宽度无断空白。

许多视窗程序(包含记事本)会添加字节顺序标记到UTF-8文件。然而,在类Unix系统(大量使用文本文件,用于文件格式,用于进程间通信)中,这种作法则不被建议采用。因为它会妨碍到如解译器脚本开头的Shebang等的一些重要的码的正确处理。它亦会影响到无法识别它的编程语言。如gcc会报告源码档开头有无法识别的字符。而在PHP中,如果没有激活输出缓冲(output buffering),它会使得页面内容开始被送往浏览器(即:用户头文件已被提交),这使PHP脚本无法指定用户头文件(HTTP Header)。字节顺序标记在UTF-8中被表示为序列EF BB BF,对大部分未准备好处理UTF-8的文本编辑器及网页浏览器而言,在ISO-8859-1的环境中则会显示。

如果之前有遇到相关情况的,大概看了上面的说明就明白了。

换行(英语:newline,Line break,end-of-line(EOL), Line Feed(LF)),在计算机领域中是一种加在文字最后位置的特殊字元,在换行字元的下一个字元将会出现在下一,实际上换行字元根据不同的硬件平台或操作系统平台会有不同的编码方式。

换行字符可以看作是行的结束符,也可以看作行之间的分隔符,这两种处理方式之间存在一些歧义。如果换行字符被当作分隔符,那么文件的最后一行就不需要再有换行字符。但是多数系统的做法是在最后一行的后面也加上一个换行字符,也就是把换行字符看作是行的结束符。这样的程序在处理末行没有换行字符的文件时,可能会存在问题。相反地,有的程序把换行符看作分隔符,就会把最末尾的换行字符看作是新行的开始,也就是多出了一个空行。

ASCII为基础的或相容的字元集使用分别LF(Line feed,0Ah)或CR(Carriage Return,0Dh)或CR+LF;下面列出各系统换行字元编码的列表

  • LF:在Unix或Unix相容系统(GNU/Linux,AIX,Xenix,Mac OS X,…)、BeOS、Amiga、RISC OS
  • CR+LF:MS-DOS微软视窗操作系统(Microsoft Windows)、大部分非Unix的系统
  • CR:Apple II家族,Mac OS至版本9

请尤其注意,某些语言对文件的以文本文件打开的方式是根据平台不同而修改过写入的换行符的,这样将会在试图以直接按字节写入文件(二进制处理方式)时带来很多不必要的麻烦,所以后者一定要以二进制方式打开文件。具体可以参考下面的参考。