大小端序存储

大小端序是一个常见的, 但又不可忽视的知识, 弄清楚端序问题对于计算机的内存布局理解更加深刻

端序概念

端序(Endianness)主要定义了多字节数据在内存地址中的存放顺序.

  • 大端模式 (Big-Endian): 数据的高位字节(MSB)存放在内存的低地址端.

    • 符合人类从左到右的阅读习惯.
  • 小端模式 (Little-Endian): 数据的低位字节(LSB)存放在内存的低地址端.

    • 符合计算机逻辑运算的处理习惯.

记忆口诀:

  • 端: 位在
  • 端: 位在

存储示例

以十进制数字 44033 为例, 其十六进制值为 0x0000AC01(32位整数, 4字节).

假设内存地址从左向右依次增大(低地址 -> 高地址):

模式 内存布局 (地址: 低 -> 高) 说明
大端存储 00 00 AC 01 01是最低位, 放在最高地址
小端存储 01 AC 00 00 01是最低位, 放在最低地址

常见标准与应用场景

大小端模式的存在, 主要是因为不同 CPU 架构的设计理念不同. 为了在不同的软硬件环境中准确地传递和解析数据, 形成了以下几个层面的标准与应用规范:

物理硬件与操作系统 (系统端序)

这一层决定了数据在物理内存条中真实的摆放顺序.

  • 小端序 (Little-Endian) - 当前绝对主流:

    • 架构: Intel/AMD 的 x86/x64 架构, 以及大部分移动设备使用的 ARM 架构.
    • 操作系统: Windows, Linux, Android, iOS, 以及现代的 macOS (Intel 和 Apple Silicon 芯片).
    • 优势: CPU 从低地址读取低位字节, 方便在读取的同时进行进位运算, 硬件电路设计相对简单高效.
  • 大端序 (Big-Endian) - 历史与特定设备:

    • 架构: 早期的 PowerPC 架构, SPARC 架构, 以及 IBM 大型机.
    • 操作系统: 早期的 Mac OS (基于 PowerPC 芯片时代).

编程语言层面的处理 (语言端序)

不同编程语言对底层硬件的抽象程度不同, 导致它们对待端序的策略也不同:

  • C / C++ (贴近硬件的 Native 语言):

    • 策略: 完全映射底层硬件. 硬件是小端, 程序内存里就是小端.
    • 特点: 允许通过指针直接访问物理内存布局, 因此可以用 C 语言代码准确检测出当前系统的端序.
  • Java (自带虚拟机的跨平台语言):

    • 策略: 强制统一标准, 屏蔽硬件差异.
    • 特点: Java 虚拟机 (JVM) 规范要求其类文件结构, 字节流解析默认统一使用 大端序 (Big-Endian) . 底层运行时, JVM 会自动将宿主机的小端数据翻转为大端供 Java 程序使用. 因此, Java 程序员通常不需要感知底层硬件的端序.
  • Go / Rust: 类似于 C, 默认采用宿主机的字节序(通常是小端), 但在标准库中提供了显式处理大端/小端的包(如 Go 的 encoding/binary), 方便编写网络协议.

  • Python: 纯解释型语言. 整数在 Python 内部是一个复杂的对象(PyObject), 根本看不到它底层的字节序. 只有当使用 struct 模块把数字转成二进制流时, 才需要指定 < (小端) 或 > (大端).

网络数据传输 (TCP/IP 协议标准)

这是端序转换最核心的应用场景.

  • 痛点: 如果一个运行在 x86 系统 (小端) 的电脑, 给一个运行在老式 IBM 主机 (大端) 的设备发送数据, 由于双方解析字节流的顺序相反, 数据就会出错.

  • 统一标准 (网络字节序): 为了解决异构平台的通信问题, TCP/IP 协议强制规定: 在网络上传输的多字节数据, 必须统一采用大端模式 (Big-Endian) .

  • 转换规则:

    • 单字节数据 (char): 只占 1 个字节, 不存在顺序问题, 直接传输.

    • 多字节数据 (int, short, float等):

      • 发送端: 必须先将数据从主机字节序转换为网络字节序 (大端) 后, 再发送到网络上.
      • 接收端: 收到网络数据后, 必须将其从网络字节序转换回当前环境对应的主机字节序, 然后再进行处理.

在实际工作中, 我们在定消息协议时, 可以提前定义好字节序与对齐方式, 之后网络两端的程序都按照规定的方式解析

比如某个消息按照二进制传输, 约定字节按照1字节对齐(无空白字节填充), 小端序传输, 只要保证所有网络端的程序都按照相同约定的方式处理数据即可


字节端序源码示例

以下代码演示了 Python 中如何使用大端或小端打包数据, 并按字节输出, 与之前的结论示例对应

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import struct

# 数字 44033 (0x0000AC01)

print("--- 小端模式 (<) ---")
# pack('<i'): 小端打包成 int
# unpack('<BBBB'): 按小端解包成 4 个 unsigned char
result = struct.unpack('<BBBB', struct.pack('<i', 44033))
print('0x', end='')
for b in result:
    print(f'{b:02X}', end='')
# 输出: 0X01AC0000 (低位 01 在前)

print("\n\n--- 大端模式 (>) ---")
# pack('>i'): 大端打包成 int
result = struct.unpack('>BBBB', struct.pack('>i', 44033))
print('0x', end='')
for b in result:
    print(f'{b:02X}', end='')
# 输出: 0X0000AC01 (高位 00 在前)

使用C语言检测系统端序

可以通过检查变量在内存中的首字节来判断当前环境的端序

方法一: 指针强制转换法

原理: 将 int 的地址强制转换为 char*, 只读取内存的第一个字节(低地址).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

int test1_endian() {
    int i = 1; // 十六进制 0x00000001
    char *a = (char *)&i; // 获取低地址的值

    // 如果低地址存的是 1, 说明低位在低址 -> 小端
    if (*a == 1)
         printf("当前系统为: 小端\n");
    else
         printf("当前系统为: 大端\n");
    return 0;
}

方法二: 联合体法

原理: Union 的所有成员共用同一块内存首地址.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int test2_endian() {
     union {
            int i;
            char c;
     } un;

     un.i = 1; // 写入 0x00000001

     // 读取 c, 相当于读取 int 的低地址字节
     if(un.c == 1) {
            printf("当前系统为: 小端\n");
     }
     else {
            printf("当前系统为: 大端\n");
     }
     return 0;
}
hugo + stack 构建
使用 Hugo 构建
主题 StackJimmy 设计