qinghuan
← 回到知识探索

计算机趣闻 / 动手实验

同样四个字节,为什么读出两个数字?

把一个整数拆成字节,再亲手用相反顺序读回来,认识大小端。

展开本文目录
  1. 先动手看看
  2. 先把数字拆成四份
  3. 只改读取方式,结果就变了
  4. 先问文件规定,别先问电脑型号
  5. 小端不等于把文字全部倒过来
  6. 记住这三点
  7. 别把例子用过头
  8. 自己试一小步
  9. 查原始资料
  10. 找一份资料继续练
开始前,会这些就够了
  • 知道一个字节有 8 位
  • 能读懂简单 JavaScript
遇到不熟悉的词,先看这 6 个解释
位
位是表示二选一状态的基本单位,通常写成零或一。许多位按约定组合起来,才能表达更大的数字、文字或图片,单独一位并不会告诉你它属于哪种数据。
字节
在这里介绍的现代文件和网络格式中,一个字节由八个位组成,常用来计量数据大小。它有二百五十六种位组合,但这些组合代表什么仍由格式规定。
字符编码
字符编码是把文字与可存储的数据对应起来的规则。写入时按规则产生数据,读取时按相应规则还原文字;双方使用不同规则,就可能看到乱码或替代符号。
UTF-8
它是把统一字符编号写成字节序列的一种编码方式,长度并不固定。一个可编码的字符编号会使用一到四个字节,所以文字数量与文件字节数通常不能直接画等号。
字节序
字节序规定一个数拆成多个字节后,按什么顺序保存或传输。大端把较高位的字节放在前面,小端把较低位的放在前面;读取时必须采用相同约定才能还原数值。
内存
这里的内存主要指程序运行时使用的随机存取内存,用来放当前要处理的代码和数据。它适合频繁读写,普通电脑中的这类内存通常不能在断电后保留内容。
打开完整术语小抄 ↗

假设你收到四个字节:01、02、03、04。你以为它表示一个很小的编号,程序却读出了另一个大数。数据可能没有丢,也没有被加密;双方只是把哪一端当作数字的开头这件事理解得不一样。

这种分歧叫字节序。它处理的是一个多字节数怎样排在存储空间里。先看一个明确的四字节整数,比背“大端”和“小端”两个词更容易理解。

四个字节,为什么有两种排列?

数值:305419896(十进制)

大端:高位字节在低地址
  1. 12
    地址 +0
  2. 34
    地址 +1
  3. 56
    地址 +2
  4. 78
    地址 +3
小端:低位字节在低地址
  1. 78
    地址 +0
  2. 56
    地址 +1
  3. 34
    地址 +2
  4. 12
    地址 +3

这是无符号 32 位整数的字节顺序示意。从左到右表示地址由低到高,不是把每个字节里的二进制位反过来;两种顺序表示的数值相同。

先把数字拆成四份

十六进制的 0x01020304 可以拆成 01、02、03、04 四个字节。每向左移动一个字节,所代表的位权就乘以 256。因此 01 是最高有效字节,04 是最低有效字节;“有效”在这里说的是对数值大小的贡献,不是说某个字节更有用。

现在给它准备四个连续位置,从低地址往高地址看。大端把最高有效字节放在前面,得到 01 02 03 04;小端把最低有效字节放在前面,得到 04 03 02 01。整数本身没有变,改变的是它在这四个位置里的摆法。

只改读取方式,结果就变了

下面先明确按大端写入,再把同一块空间分别按大端和小端读取。DataView 允许直接指定字节序;false 表示大端,true 表示小端。这样不用猜自己的电脑采用什么顺序,也不会让实验结果依赖处理器型号。

第二种读法把原本较小位权的字节放到了较大位权的位置,所以得到不同的整数。注意输出使用十六进制,开头的零不会自动保留。数字 1020304 与写入时的 01020304 是同一个数,少的是显示填充,不是数据。

小实验 · javascript
const bytes = new ArrayBuffer(4);
const view = new DataView(bytes);
view.setUint32(0, 0x01020304, false);
console.log([...new Uint8Array(bytes)].map(x => x.toString(16).padStart(2, "0")).join(" "));
console.log(view.getUint32(0, false).toString(16));
console.log(view.getUint32(0, true).toString(16));

怎样运行:在现代浏览器控制台或 Node.js 中运行;只创建四字节内存,不读写文件。

运行后对照结果
01 02 03 04
1020304
4030201

先问文件规定,别先问电脑型号

假设一个文件格式规定,前四字节是按小端保存的图片宽度。即使读取它的机器本身偏爱大端,也必须遵守文件格式。网络消息同样如此:两台机器可以习惯不同,只要写入方和读取方按同一份约定转换,就能正确交换数据。

所以排查二进制文件时,可以先记下原始字节、字段宽度、是否带符号和规定字节序,再计算解释结果。只看最终显示的十进制数字,很容易把顺序错误误判成传输损坏。一次交换两个字节或读取错位置,又是另外的错误。例如长度字段写错顺序时,程序可能以为后面还有大量数据,却实际上已经到达文件末尾。先核对这个字段,往往比继续猜测后面的内容更有帮助。

小端不等于把文字全部倒过来

字节序不是“整个文件从右向左读”。若文件依次保存宽度和高度,应分别按两个字段的规则解释,不能把两个字段一起翻转。一个字节内部的位怎样编号,也与多个字节怎样排列是不同问题,不要把两种顺序混为一谈。

文本还需要另看字符编码。UTF-8 按自身规则安排字节,不能因为电脑是小端就反转一句话。接触 UTF-16、图片头或网络整数时,字节序才会以对应格式规定的方式出现。遇到陌生数据,先找格式说明,再选择读取函数。

记住这三点

  • 大小端决定多字节数的字节排列方式,不改变那个数原本的数学值。
  • 读取数据应服从文件或协议规定,并明确字段宽度和是否带符号。
  • DataView 可以显式选择顺序,适合观察同一串字节的不同解释。

别把例子用过头

  • 不能把整个文件倒序当成通用的大小端转换。
  • 这个实验没有测量处理器的原生字节序;它主动指定了读写规则。

自己试一小步

怎样构造一个“顺序读错了,但结果碰巧没变”的四字节整数?

  1. 先在纸上写出四个十六进制字节,预测反转后的排列。
  2. 替换示例中的整数,并保留两种读取方式。
  3. 再只改其中一个字节,观察原先的巧合是否仍成立。

做到这些,就算完成

  • 能把预测字节与实际输出对应起来。
  • 能解释一次结果相同为什么不足以证明读取规则正确。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。